Merge branch 'develop' into fix/postform-footer-button-overflow
|
@ -2,3 +2,4 @@
|
||||||
POSTGRES_PASSWORD=example-misskey-pass
|
POSTGRES_PASSWORD=example-misskey-pass
|
||||||
POSTGRES_USER=example-misskey-user
|
POSTGRES_USER=example-misskey-user
|
||||||
POSTGRES_DB=misskey
|
POSTGRES_DB=misskey
|
||||||
|
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||||
|
|
2
.github/dependabot.yml
vendored
|
@ -17,7 +17,7 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 10
|
||||||
# List dependencies required to be updated together, sharing the same version numbers.
|
# List dependencies required to be updated together, sharing the same version numbers.
|
||||||
# Those who simply have the common owner (e.g. @fastify) don't need to be listed.
|
# Those who simply have the common owner (e.g. @fastify) don't need to be listed.
|
||||||
groups:
|
groups:
|
||||||
|
|
13
CHANGELOG.md
|
@ -14,8 +14,21 @@
|
||||||
|
|
||||||
## 202x.x.x (Unreleased)
|
## 202x.x.x (Unreleased)
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
|
||||||
|
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: 新しいゲームを追加
|
||||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||||
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
|
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||||
|
- Enhance: クリップをエクスポートできるように
|
||||||
|
|
||||||
## 2023.12.2
|
## 2023.12.2
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ Also, the later tasks are more indefinite and are subject to change as developme
|
||||||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
||||||
|
|
||||||
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
|
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
|
||||||
|
- Make the number of type errors zero (frontend)
|
||||||
- Improve CI
|
- Improve CI
|
||||||
- ~~Fix tests~~ → Done ✔️
|
- ~~Fix tests~~ → Done ✔️
|
||||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
||||||
|
|
|
@ -7,6 +7,7 @@ services:
|
||||||
links:
|
links:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
# - mcaptcha
|
||||||
# - meilisearch
|
# - meilisearch
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
@ -48,6 +49,36 @@ services:
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
|
# mcaptcha:
|
||||||
|
# restart: always
|
||||||
|
# image: mcaptcha/mcaptcha:latest
|
||||||
|
# networks:
|
||||||
|
# internal_network:
|
||||||
|
# external_network:
|
||||||
|
# aliases:
|
||||||
|
# - localhost
|
||||||
|
# ports:
|
||||||
|
# - 7493:7493
|
||||||
|
# env_file:
|
||||||
|
# - .config/docker.env
|
||||||
|
# environment:
|
||||||
|
# PORT: 7493
|
||||||
|
# MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
|
||||||
|
# depends_on:
|
||||||
|
# db:
|
||||||
|
# condition: service_healthy
|
||||||
|
# mcaptcha_redis:
|
||||||
|
# condition: service_healthy
|
||||||
|
#
|
||||||
|
# mcaptcha_redis:
|
||||||
|
# image: mcaptcha/cache:latest
|
||||||
|
# networks:
|
||||||
|
# - internal_network
|
||||||
|
# healthcheck:
|
||||||
|
# test: "redis-cli ping"
|
||||||
|
# interval: 5s
|
||||||
|
# retries: 20
|
||||||
|
|
||||||
# meilisearch:
|
# meilisearch:
|
||||||
# restart: always
|
# restart: always
|
||||||
# image: getmeili/meilisearch:v1.3.4
|
# image: getmeili/meilisearch:v1.3.4
|
||||||
|
|
17
locales/index.d.ts
vendored
|
@ -382,6 +382,11 @@ export interface Locale {
|
||||||
"enableHcaptcha": string;
|
"enableHcaptcha": string;
|
||||||
"hcaptchaSiteKey": string;
|
"hcaptchaSiteKey": string;
|
||||||
"hcaptchaSecretKey": string;
|
"hcaptchaSecretKey": string;
|
||||||
|
"mcaptcha": string;
|
||||||
|
"enableMcaptcha": string;
|
||||||
|
"mcaptchaSiteKey": string;
|
||||||
|
"mcaptchaSecretKey": string;
|
||||||
|
"mcaptchaInstanceUrl": string;
|
||||||
"recaptcha": string;
|
"recaptcha": string;
|
||||||
"enableRecaptcha": string;
|
"enableRecaptcha": string;
|
||||||
"recaptchaSiteKey": string;
|
"recaptchaSiteKey": string;
|
||||||
|
@ -672,6 +677,7 @@ export interface Locale {
|
||||||
"other": string;
|
"other": string;
|
||||||
"regenerateLoginToken": string;
|
"regenerateLoginToken": string;
|
||||||
"regenerateLoginTokenDescription": string;
|
"regenerateLoginTokenDescription": string;
|
||||||
|
"theKeywordWhenSearchingForCustomEmoji": string;
|
||||||
"setMultipleBySeparatingWithSpace": string;
|
"setMultipleBySeparatingWithSpace": string;
|
||||||
"fileIdOrUrl": string;
|
"fileIdOrUrl": string;
|
||||||
"behavior": string;
|
"behavior": string;
|
||||||
|
@ -1186,6 +1192,7 @@ export interface Locale {
|
||||||
"decorate": string;
|
"decorate": string;
|
||||||
"addMfmFunction": string;
|
"addMfmFunction": string;
|
||||||
"enableQuickAddMfmFunction": string;
|
"enableQuickAddMfmFunction": string;
|
||||||
|
"bubbleGame": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -1650,6 +1657,15 @@ export interface Locale {
|
||||||
"title": string;
|
"title": string;
|
||||||
"description": string;
|
"description": string;
|
||||||
};
|
};
|
||||||
|
"_bubbleGameExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
};
|
||||||
|
"_bubbleGameDoubleExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
"flavor": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_role": {
|
"_role": {
|
||||||
|
@ -2250,6 +2266,7 @@ export interface Locale {
|
||||||
"_exportOrImport": {
|
"_exportOrImport": {
|
||||||
"allNotes": string;
|
"allNotes": string;
|
||||||
"favoritedNotes": string;
|
"favoritedNotes": string;
|
||||||
|
"clips": string;
|
||||||
"followingList": string;
|
"followingList": string;
|
||||||
"muteList": string;
|
"muteList": string;
|
||||||
"blockingList": string;
|
"blockingList": string;
|
||||||
|
|
|
@ -379,6 +379,11 @@ hcaptcha: "hCaptcha"
|
||||||
enableHcaptcha: "hCaptchaを有効にする"
|
enableHcaptcha: "hCaptchaを有効にする"
|
||||||
hcaptchaSiteKey: "サイトキー"
|
hcaptchaSiteKey: "サイトキー"
|
||||||
hcaptchaSecretKey: "シークレットキー"
|
hcaptchaSecretKey: "シークレットキー"
|
||||||
|
mcaptcha: "mCaptcha"
|
||||||
|
enableMcaptcha: "mCaptchaを有効にする"
|
||||||
|
mcaptchaSiteKey: "サイトキー"
|
||||||
|
mcaptchaSecretKey: "シークレットキー"
|
||||||
|
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
|
||||||
recaptcha: "reCAPTCHA"
|
recaptcha: "reCAPTCHA"
|
||||||
enableRecaptcha: "reCAPTCHAを有効にする"
|
enableRecaptcha: "reCAPTCHAを有効にする"
|
||||||
recaptchaSiteKey: "サイトキー"
|
recaptchaSiteKey: "サイトキー"
|
||||||
|
@ -669,6 +674,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
|
||||||
other: "その他"
|
other: "その他"
|
||||||
regenerateLoginToken: "ログイントークンを再生成"
|
regenerateLoginToken: "ログイントークンを再生成"
|
||||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
||||||
|
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
|
||||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
||||||
fileIdOrUrl: "ファイルIDまたはURL"
|
fileIdOrUrl: "ファイルIDまたはURL"
|
||||||
behavior: "動作"
|
behavior: "動作"
|
||||||
|
@ -1183,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
||||||
decorate: "デコる"
|
decorate: "デコる"
|
||||||
addMfmFunction: "装飾を追加"
|
addMfmFunction: "装飾を追加"
|
||||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||||
|
bubbleGame: "バブルゲーム"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -1561,6 +1568,13 @@ _achievements:
|
||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
title: "Misskey初心者講座 修了証"
|
title: "Misskey初心者講座 修了証"
|
||||||
description: "チュートリアルを完了した"
|
description: "チュートリアルを完了した"
|
||||||
|
_bubbleGameExplodingHead:
|
||||||
|
title: "🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを出した"
|
||||||
|
_bubbleGameDoubleExplodingHead:
|
||||||
|
title: "ダブル🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||||
|
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||||
|
|
||||||
_role:
|
_role:
|
||||||
new: "ロールの作成"
|
new: "ロールの作成"
|
||||||
|
@ -2153,6 +2167,7 @@ _profile:
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
favoritedNotes: "お気に入りにしたノート"
|
favoritedNotes: "お気に入りにしたノート"
|
||||||
|
clips: "クリップ"
|
||||||
followingList: "フォロー"
|
followingList: "フォロー"
|
||||||
muteList: "ミュート"
|
muteList: "ミュート"
|
||||||
blockingList: "ブロック"
|
blockingList: "ブロック"
|
||||||
|
|
22
packages/backend/migration/1704373210054-support-mcaptcha.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SupportMcaptcha1704373210054 {
|
||||||
|
name = 'SupportMcaptcha1704373210054'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Global, Inject, Module } from '@nestjs/common';
|
import { Global, Inject, Module } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
||||||
import { Config, loadConfig } from './config.js';
|
import { Config, loadConfig } from './config.js';
|
||||||
import { createPostgresDataSource } from './postgres.js';
|
import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
|
import { allSettled } from './misc/promise-tracker.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const $config: Provider = {
|
const $config: Provider = {
|
||||||
|
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
||||||
useFactory: (config: Config) => {
|
useFactory: (config: Config) => {
|
||||||
if (config.meilisearch) {
|
if (config.meilisearch) {
|
||||||
return new MeiliSearch({
|
return new MeiliSearch({
|
||||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||||
apiKey: config.meilisearch.apiKey,
|
apiKey: config.meilisearch.apiKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential DB queries
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then disconnect from DB
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.db.destroy(),
|
this.db.destroy(),
|
||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
|
|
|
@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
||||||
'brainDiver',
|
'brainDiver',
|
||||||
'smashTestNotificationButton',
|
'smashTestNotificationButton',
|
||||||
'tutorialCompleted',
|
'tutorialCompleted',
|
||||||
|
'bubbleGameExplodingHead',
|
||||||
|
'bubbleGameDoubleExplodingHead',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -73,6 +73,37 @@ export class CaptchaService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go
|
||||||
|
@bindThis
|
||||||
|
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
|
||||||
|
if (response == null) {
|
||||||
|
throw new Error('mcaptcha-failed: no response provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
|
||||||
|
const result = await this.httpRequestService.send(endpointUrl.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
key: siteKey,
|
||||||
|
secret: secret,
|
||||||
|
token: response,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 200) {
|
||||||
|
throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = (await result.json()) as { valid: boolean };
|
||||||
|
|
||||||
|
if (!resp.valid) {
|
||||||
|
throw new Error('mcaptcha-request-failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -676,7 +677,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.relayService.deliverToRelays(user, noteActivity);
|
this.relayService.deliverToRelays(user, noteActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isMentioned: true,
|
isMentioned: true,
|
||||||
}).then(mentionsCount => {
|
}).then(mentionsCount => {
|
||||||
|
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: true,
|
isSpecified: true,
|
||||||
}).then(specifiedCount => {
|
}).then(specifiedCount => {
|
||||||
|
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserListService } from '@/core/UserListService.js';
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
import type { FilterUnionByProperty } from '@/types.js';
|
import type { FilterUnionByProperty } from '@/types.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
|
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNotification<T extends MiNotification['type']>(
|
public createNotification<T extends MiNotification['type']>(
|
||||||
|
notifieeId: MiUser['id'],
|
||||||
|
type: T,
|
||||||
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
|
notifierId?: MiUser['id'] | null,
|
||||||
|
) {
|
||||||
|
trackPromise(
|
||||||
|
this.#createNotificationInternal(notifieeId, type, data, notifierId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #createNotificationInternal<T extends MiNotification['type']>(
|
||||||
notifieeId: MiUser['id'],
|
notifieeId: MiUser['id'],
|
||||||
type: T,
|
type: T,
|
||||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||||
|
import { allSettled } from '@/misc/promise-tracker.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||||
|
|
||||||
|
@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential queue jobs
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then close all queues
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.systemQueue.close(),
|
this.systemQueue.close(),
|
||||||
this.endedPollNotificationQueue.close(),
|
this.endedPollNotificationQueue.close(),
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
|
||||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
|
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -74,11 +75,15 @@ export class QueueService {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
|
const contentBody = JSON.stringify(content);
|
||||||
|
const digest = ApRequestCreator.createDigest(contentBody);
|
||||||
|
|
||||||
const data: DeliverJobData = {
|
const data: DeliverJobData = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
content,
|
content: contentBody,
|
||||||
|
digest,
|
||||||
to,
|
to,
|
||||||
isSharedInbox,
|
isSharedInbox,
|
||||||
};
|
};
|
||||||
|
@ -103,6 +108,8 @@ export class QueueService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
|
const contentBody = JSON.stringify(content);
|
||||||
|
const digest = ApRequestCreator.createDigest(contentBody);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||||
|
@ -117,7 +124,8 @@ export class QueueService {
|
||||||
name: d[0],
|
name: d[0],
|
||||||
data: {
|
data: {
|
||||||
user,
|
user,
|
||||||
content,
|
content: contentBody,
|
||||||
|
digest,
|
||||||
to: d[0],
|
to: d[0],
|
||||||
isSharedInbox: d[1],
|
isSharedInbox: d[1],
|
||||||
} as DeliverJobData,
|
} as DeliverJobData,
|
||||||
|
@ -174,6 +182,16 @@ export class QueueService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createExportClipsJob(user: ThinUser) {
|
||||||
|
return this.dbQueue.add('exportClips', {
|
||||||
|
user: { id: user.id },
|
||||||
|
}, {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createExportFavoritesJob(user: ThinUser) {
|
public createExportFavoritesJob(user: ThinUser) {
|
||||||
return this.dbQueue.add('exportFavorites', {
|
return this.dbQueue.add('exportFavorites', {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||||
|
@ -268,7 +269,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
@ -316,7 +317,7 @@ export class ReactionService {
|
||||||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||||
}
|
}
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ class DeliverManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliver
|
// deliver
|
||||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,8 @@ export class ApInboxService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +258,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
|
|
||||||
this.announceNote(actor, activity, targetUri);
|
await this.announceNote(actor, activity, targetUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -288,7 +290,7 @@ export class ApInboxService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (err.isClientError) {
|
if (!err.isRetryable) {
|
||||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +375,7 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(object)) {
|
if (isPost(object)) {
|
||||||
this.createNote(resolver, actor, object, false, activity);
|
await this.createNote(resolver, actor, object, false, activity);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
@ -404,7 +406,7 @@ export class ApInboxService {
|
||||||
await this.apNoteService.createNote(note, resolver, silent);
|
await this.apNoteService.createNote(note, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && err.isClientError) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
return `skip ${err.statusCode}`;
|
return `skip ${err.statusCode}`;
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -34,9 +34,9 @@ type PrivateKey = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ApRequestCreator {
|
export class ApRequestCreator {
|
||||||
static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
|
static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
|
||||||
const u = new URL(args.url);
|
const u = new URL(args.url);
|
||||||
const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
|
const digestHeader = args.digest ?? this.createDigest(args.body);
|
||||||
|
|
||||||
const request: Request = {
|
const request: Request = {
|
||||||
url: u.href,
|
url: u.href,
|
||||||
|
@ -59,6 +59,10 @@ export class ApRequestCreator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createDigest(body: string) {
|
||||||
|
return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
|
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
|
||||||
const u = new URL(args.url);
|
const u = new URL(args.url);
|
||||||
|
|
||||||
|
@ -145,8 +149,8 @@ export class ApRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
|
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
|
||||||
const body = JSON.stringify(object);
|
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
|
@ -157,6 +161,7 @@ export class ApRequestService {
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
|
digest,
|
||||||
additionalHeaders: {
|
additionalHeaders: {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -216,7 +216,7 @@ export class ApNoteService {
|
||||||
return { status: 'ok', res };
|
return { status: 'ok', res };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -351,6 +351,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
color: channel.color,
|
color: channel.color,
|
||||||
isSensitive: channel.isSensitive,
|
isSensitive: channel.isSensitive,
|
||||||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||||
|
userId: channel.userId,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||||
const log = [] as any[];
|
const log = [] as any[];
|
||||||
|
|
||||||
ev.on('requestServerStatsLog', x => {
|
ev.on('requestServerStatsLog', x => {
|
||||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
|
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
|
||||||
});
|
});
|
||||||
|
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
|
|
23
packages/backend/src/misc/promise-tracker.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This tracks promises that other modules decided not to wait for,
|
||||||
|
* and makes sure they are all settled before fully closing down the server.
|
||||||
|
*/
|
||||||
|
export function trackPromise(promise: Promise<unknown>) {
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ref = new WeakRef(promise);
|
||||||
|
promiseRefs.add(ref);
|
||||||
|
promise.finally(() => promiseRefs.delete(ref));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function allSettled(): Promise<void> {
|
||||||
|
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ export class StatusError extends Error {
|
||||||
public statusCode: number;
|
public statusCode: number;
|
||||||
public statusMessage?: string;
|
public statusMessage?: string;
|
||||||
public isClientError: boolean;
|
public isClientError: boolean;
|
||||||
|
public isRetryable: boolean;
|
||||||
|
|
||||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
|
@ -14,5 +15,6 @@ export class StatusError extends Error {
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
this.statusMessage = statusMessage;
|
this.statusMessage = statusMessage;
|
||||||
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
||||||
|
this.isRetryable = !this.isClientError || this.statusCode === 429;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,6 +191,29 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public hcaptchaSecretKey: string | null;
|
public hcaptchaSecretKey: string | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableMcaptcha: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public mcaptchaSitekey: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public mcaptchaSecretKey: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public mcaptchaInstanceUrl: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -148,6 +148,10 @@ export const packedNoteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
localOnly: {
|
localOnly: {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
|
||||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||||
|
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
||||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
||||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
||||||
|
@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
DeleteDriveFilesProcessorService,
|
DeleteDriveFilesProcessorService,
|
||||||
ExportCustomEmojisProcessorService,
|
ExportCustomEmojisProcessorService,
|
||||||
ExportNotesProcessorService,
|
ExportNotesProcessorService,
|
||||||
|
ExportClipsProcessorService,
|
||||||
ExportFavoritesProcessorService,
|
ExportFavoritesProcessorService,
|
||||||
ExportFollowingProcessorService,
|
ExportFollowingProcessorService,
|
||||||
ExportMutingProcessorService,
|
ExportMutingProcessorService,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
||||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
||||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||||
|
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
||||||
|
@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||||
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
||||||
private exportNotesProcessorService: ExportNotesProcessorService,
|
private exportNotesProcessorService: ExportNotesProcessorService,
|
||||||
|
private exportClipsProcessorService: ExportClipsProcessorService,
|
||||||
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
||||||
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
||||||
private exportMutingProcessorService: ExportMutingProcessorService,
|
private exportMutingProcessorService: ExportMutingProcessorService,
|
||||||
|
@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||||
|
case 'exportClips': return this.exportClipsProcessorService.process(job);
|
||||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class DeliverProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
|
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.fetch(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
|
@ -111,7 +111,7 @@ export class DeliverProcessorService {
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||||
this.federatedInstanceService.fetch(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import { Writable } from 'node:stream';
|
||||||
|
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
|
||||||
|
import { MoreThan } from 'typeorm';
|
||||||
|
import { format as dateFormat } from 'date-fns';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
|
import type { MiPoll } from '@/models/Poll.js';
|
||||||
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
import type * as Bull from 'bullmq';
|
||||||
|
import type { DbJobDataWithUser } from '../types.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExportClipsProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.pollsRepository)
|
||||||
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipsRepository)
|
||||||
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipNotesRepository)
|
||||||
|
private clipNotesRepository: ClipNotesRepository,
|
||||||
|
|
||||||
|
private driveService: DriveService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
private idService: IdService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
|
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
|
if (user == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temp file
|
||||||
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
|
this.logger.info(`Temp file is ${path}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
|
||||||
|
const writer = stream.getWriter();
|
||||||
|
writer.closed.catch(this.logger.error);
|
||||||
|
|
||||||
|
await writer.write('[');
|
||||||
|
|
||||||
|
await this.processClips(writer, user, job);
|
||||||
|
|
||||||
|
await writer.write(']');
|
||||||
|
await writer.close();
|
||||||
|
|
||||||
|
this.logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||||
|
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||||
|
|
||||||
|
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
|
||||||
|
let exportedClipsCount = 0;
|
||||||
|
let cursor: MiClip['id'] | null = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const clips = await this.clipsRepository.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clips.length === 0) {
|
||||||
|
job.updateProgress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = clips.at(-1)?.id ?? null;
|
||||||
|
|
||||||
|
for (const clip of clips) {
|
||||||
|
// Stringify but remove the last `]}`
|
||||||
|
const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
|
||||||
|
const isFirst = exportedClipsCount === 0;
|
||||||
|
await writer.write(isFirst ? content : ',\n' + content);
|
||||||
|
|
||||||
|
await this.processClipNotes(writer, clip.id);
|
||||||
|
|
||||||
|
await writer.write(']}');
|
||||||
|
exportedClipsCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await this.clipsRepository.countBy({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
job.updateProgress(exportedClipsCount / total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
|
||||||
|
let exportedClipNotesCount = 0;
|
||||||
|
let cursor: MiClipNote['id'] | null = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const clipNotes = await this.clipNotesRepository.find({
|
||||||
|
where: {
|
||||||
|
clipId,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
relations: ['note', 'note.user'],
|
||||||
|
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||||
|
|
||||||
|
if (clipNotes.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = clipNotes.at(-1)?.id ?? null;
|
||||||
|
|
||||||
|
for (const clipNote of clipNotes) {
|
||||||
|
let poll: MiPoll | undefined;
|
||||||
|
if (clipNote.note.hasPoll) {
|
||||||
|
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
|
||||||
|
}
|
||||||
|
const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
|
||||||
|
const isFirst = exportedClipNotesCount === 0;
|
||||||
|
await writer.write(isFirst ? content : ',\n' + content);
|
||||||
|
|
||||||
|
exportedClipNotesCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeClip(clip: MiClip): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: clip.id,
|
||||||
|
name: clip.name,
|
||||||
|
description: clip.description,
|
||||||
|
lastClippedAt: clip.lastClippedAt?.toISOString(),
|
||||||
|
clipNotes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: clip.id,
|
||||||
|
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
||||||
|
note: {
|
||||||
|
id: clip.note.id,
|
||||||
|
text: clip.note.text,
|
||||||
|
createdAt: this.idService.parse(clip.note.id).date.toISOString(),
|
||||||
|
fileIds: clip.note.fileIds,
|
||||||
|
replyId: clip.note.replyId,
|
||||||
|
renoteId: clip.note.renoteId,
|
||||||
|
poll: poll,
|
||||||
|
cw: clip.note.cw,
|
||||||
|
visibility: clip.note.visibility,
|
||||||
|
visibleUserIds: clip.note.visibleUserIds,
|
||||||
|
localOnly: clip.note.localOnly,
|
||||||
|
reactionAcceptance: clip.note.reactionAcceptance,
|
||||||
|
uri: clip.note.uri,
|
||||||
|
url: clip.note.url,
|
||||||
|
user: {
|
||||||
|
id: clip.note.user.id,
|
||||||
|
name: clip.note.user.name,
|
||||||
|
username: clip.note.user.username,
|
||||||
|
host: clip.note.user.host,
|
||||||
|
uri: clip.note.user.uri,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,7 +85,7 @@ export class InboxProcessorService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (err.isClientError) {
|
if (!err.isRetryable) {
|
||||||
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService {
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@ export type DeliverJobData = {
|
||||||
/** Actor */
|
/** Actor */
|
||||||
user: ThinUser;
|
user: ThinUser;
|
||||||
/** Activity */
|
/** Activity */
|
||||||
content: unknown;
|
content: string;
|
||||||
|
/** Digest header */
|
||||||
|
digest: string;
|
||||||
/** inbox URL to deliver */
|
/** inbox URL to deliver */
|
||||||
to: string;
|
to: string;
|
||||||
/** whether it is sharedInbox */
|
/** whether it is sharedInbox */
|
||||||
|
|
|
@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||||
|
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||||
|
@ -569,6 +570,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass:
|
||||||
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
|
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
|
||||||
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
|
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
|
||||||
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
|
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
|
||||||
|
const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
|
||||||
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
|
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
|
||||||
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
|
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
|
||||||
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
|
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
|
||||||
|
@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_exportFollowing,
|
$i_exportFollowing,
|
||||||
$i_exportMute,
|
$i_exportMute,
|
||||||
$i_exportNotes,
|
$i_exportNotes,
|
||||||
|
$i_exportClips,
|
||||||
$i_exportFavorites,
|
$i_exportFavorites,
|
||||||
$i_exportUserLists,
|
$i_exportUserLists,
|
||||||
$i_exportAntennas,
|
$i_exportAntennas,
|
||||||
|
@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_exportFollowing,
|
$i_exportFollowing,
|
||||||
$i_exportMute,
|
$i_exportMute,
|
||||||
$i_exportNotes,
|
$i_exportNotes,
|
||||||
|
$i_exportClips,
|
||||||
$i_exportFavorites,
|
$i_exportFavorites,
|
||||||
$i_exportUserLists,
|
$i_exportUserLists,
|
||||||
$i_exportAntennas,
|
$i_exportAntennas,
|
||||||
|
|
|
@ -65,6 +65,7 @@ export class SignupApiService {
|
||||||
'hcaptcha-response'?: string;
|
'hcaptcha-response'?: string;
|
||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
|
'm-captcha-response'?: string;
|
||||||
}
|
}
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
|
@ -82,6 +83,12 @@ export class SignupApiService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
|
||||||
|
await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Schema } from '@/misc/json-schema.js';
|
|
||||||
import { permissions } from 'misskey-js';
|
import { permissions } from 'misskey-js';
|
||||||
|
import type { Schema } from '@/misc/json-schema.js';
|
||||||
import { RolePolicies } from '@/core/RoleService.js';
|
import { RolePolicies } from '@/core/RoleService.js';
|
||||||
|
|
||||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||||
|
@ -209,6 +209,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||||
|
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||||
|
@ -568,6 +569,7 @@ const eps = [
|
||||||
['i/export-following', ep___i_exportFollowing],
|
['i/export-following', ep___i_exportFollowing],
|
||||||
['i/export-mute', ep___i_exportMute],
|
['i/export-mute', ep___i_exportMute],
|
||||||
['i/export-notes', ep___i_exportNotes],
|
['i/export-notes', ep___i_exportNotes],
|
||||||
|
['i/export-clips', ep___i_exportClips],
|
||||||
['i/export-favorites', ep___i_exportFavorites],
|
['i/export-favorites', ep___i_exportFavorites],
|
||||||
['i/export-user-lists', ep___i_exportUserLists],
|
['i/export-user-lists', ep___i_exportUserLists],
|
||||||
['i/export-antennas', ep___i_exportAntennas],
|
['i/export-antennas', ep___i_exportAntennas],
|
||||||
|
|
|
@ -41,6 +41,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
enableMcaptcha: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
mcaptchaSiteKey: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
mcaptchaInstanceUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
enableRecaptcha: {
|
enableRecaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -163,6 +175,10 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
mcaptchaSecretKey: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
recaptchaSecretKey: {
|
recaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -468,6 +484,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||||
|
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
|
@ -498,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
sensitiveWords: instance.sensitiveWords,
|
sensitiveWords: instance.sensitiveWords,
|
||||||
preservedUsernames: instance.preservedUsernames,
|
preservedUsernames: instance.preservedUsernames,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
|
mcaptchaSecretKey: instance.mcaptchaSecretKey,
|
||||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||||
turnstileSecretKey: instance.turnstileSecretKey,
|
turnstileSecretKey: instance.turnstileSecretKey,
|
||||||
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
||||||
|
|
|
@ -63,6 +63,10 @@ export const paramDef = {
|
||||||
enableHcaptcha: { type: 'boolean' },
|
enableHcaptcha: { type: 'boolean' },
|
||||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
hcaptchaSecretKey: { type: 'string', nullable: true },
|
hcaptchaSecretKey: { type: 'string', nullable: true },
|
||||||
|
enableMcaptcha: { type: 'boolean' },
|
||||||
|
mcaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
|
mcaptchaInstanceUrl: { type: 'string', nullable: true },
|
||||||
|
mcaptchaSecretKey: { type: 'string', nullable: true },
|
||||||
enableRecaptcha: { type: 'boolean' },
|
enableRecaptcha: { type: 'boolean' },
|
||||||
recaptchaSiteKey: { type: 'string', nullable: true },
|
recaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
recaptchaSecretKey: { type: 'string', nullable: true },
|
recaptchaSecretKey: { type: 'string', nullable: true },
|
||||||
|
@ -269,6 +273,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
|
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableMcaptcha !== undefined) {
|
||||||
|
set.enableMcaptcha = ps.enableMcaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.mcaptchaSiteKey !== undefined) {
|
||||||
|
set.mcaptchaSitekey = ps.mcaptchaSiteKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.mcaptchaInstanceUrl !== undefined) {
|
||||||
|
set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.mcaptchaSecretKey !== undefined) {
|
||||||
|
set.mcaptchaSecretKey = ps.mcaptchaSecretKey;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableRecaptcha !== undefined) {
|
if (ps.enableRecaptcha !== undefined) {
|
||||||
set.enableRecaptcha = ps.enableRecaptcha;
|
set.enableRecaptcha = ps.enableRecaptcha;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
antenna.isActive = true;
|
antenna.isActive = true;
|
||||||
antenna.lastUsedAt = new Date();
|
antenna.lastUsedAt = new Date();
|
||||||
this.antennasRepository.update(antenna.id, antenna);
|
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||||
|
|
||||||
if (needPublishEvent) {
|
if (needPublishEvent) {
|
||||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||||
|
|
35
packages/backend/src/server/api/endpoints/i/export-clips.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import ms from 'ms';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
secure: true,
|
||||||
|
requireCredential: true,
|
||||||
|
limit: {
|
||||||
|
duration: ms('1day'),
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
private queueService: QueueService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
this.queueService.createExportClipsJob(me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,6 +108,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
enableMcaptcha: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
mcaptchaSiteKey: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
mcaptchaInstanceUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
enableRecaptcha: {
|
enableRecaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -351,6 +363,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||||
|
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
|
|
|
@ -21,6 +21,7 @@ class UserListChannel extends Channel {
|
||||||
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||||
private listUsersClock: NodeJS.Timeout;
|
private listUsersClock: NodeJS.Timeout;
|
||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
@ -39,6 +40,7 @@ class UserListChannel extends Channel {
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
this.listId = params.listId as string;
|
this.listId = params.listId as string;
|
||||||
this.withFiles = params.withFiles ?? false;
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Check existence and owner
|
// Check existence and owner
|
||||||
const listExist = await this.userListsRepository.exist({
|
const listExist = await this.userListsRepository.exist({
|
||||||
|
@ -104,6 +106,8 @@ class UserListChannel extends Channel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
|
|
|
@ -24,7 +24,7 @@ import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('2要素認証', () => {
|
describe('2要素認証', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const password = 'test';
|
const password = 'test';
|
||||||
|
|
|
@ -37,7 +37,7 @@ describe('アンテナ', () => {
|
||||||
// - srcのenumにgroupが残っている
|
// - srcのenumにgroupが残っている
|
||||||
// - userGroupIdが残っている, isActiveがない
|
// - userGroupIdが残っている, isActiveがない
|
||||||
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
||||||
type User = misskey.entities.MeSignup;
|
type User = misskey.entities.SignupResponse;
|
||||||
type Note = misskey.entities.Note;
|
type Note = misskey.entities.Note;
|
||||||
|
|
||||||
// アンテナを作成できる最小のパラメタ
|
// アンテナを作成できる最小のパラメタ
|
||||||
|
|
|
@ -24,15 +24,15 @@ describe('API visibility', () => {
|
||||||
describe('Note visibility', () => {
|
describe('Note visibility', () => {
|
||||||
//#region vars
|
//#region vars
|
||||||
/** ヒロイン */
|
/** ヒロイン */
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
/** フォロワー */
|
/** フォロワー */
|
||||||
let follower: misskey.entities.MeSignup;
|
let follower: misskey.entities.SignupResponse;
|
||||||
/** 非フォロワー */
|
/** 非フォロワー */
|
||||||
let other: misskey.entities.MeSignup;
|
let other: misskey.entities.SignupResponse;
|
||||||
/** 非フォロワーでもリプライやメンションをされた人 */
|
/** 非フォロワーでもリプライやメンションをされた人 */
|
||||||
let target: misskey.entities.MeSignup;
|
let target: misskey.entities.SignupResponse;
|
||||||
/** specified mentionでmentionを飛ばされる人 */
|
/** specified mentionでmentionを飛ばされる人 */
|
||||||
let target2: misskey.entities.MeSignup;
|
let target2: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
/** public-post */
|
/** public-post */
|
||||||
let pub: any;
|
let pub: any;
|
||||||
|
|
|
@ -13,9 +13,9 @@ import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('API', () => {
|
describe('API', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -14,9 +14,9 @@ describe('Block', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
// alice blocks bob
|
// alice blocks bob
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -17,10 +17,10 @@ import type * as misskey from 'misskey-js';
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let dave: misskey.entities.MeSignup;
|
let dave: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
194
packages/backend/test/e2e/exports.ts
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { signup, api, startServer, startJobQueue, port, post } from '../utils.js';
|
||||||
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
|
describe('export-clips', () => {
|
||||||
|
let app: INestApplicationContext;
|
||||||
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
|
// XXX: Any better way to get the result?
|
||||||
|
async function pollFirstDriveFile() {
|
||||||
|
while (true) {
|
||||||
|
const files = (await api('/drive/files', {}, alice)).body;
|
||||||
|
if (!files.length) {
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (files.length > 1) {
|
||||||
|
throw new Error('Too many files?');
|
||||||
|
}
|
||||||
|
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
|
||||||
|
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await startServer();
|
||||||
|
await startJobQueue();
|
||||||
|
alice = await signup({ username: 'alice' });
|
||||||
|
bob = await signup({ username: 'bob' });
|
||||||
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Clean all clips and files of alice
|
||||||
|
const clips = (await api('/clips/list', {}, alice)).body;
|
||||||
|
for (const clip of clips) {
|
||||||
|
const res = await api('/clips/delete', { clipId: clip.id }, alice);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
throw new Error('Failed to delete clip');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const files = (await api('/drive/files', {}, alice)).body;
|
||||||
|
for (const file of files) {
|
||||||
|
const res = await api('/drive/files/delete', { fileId: file.id }, alice);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
throw new Error('Failed to delete file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('basic export', async () => {
|
||||||
|
let res = await api('/clips/create', {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'bar',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
|
||||||
|
res = await api('/i/export-clips', {}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
const exported = await pollFirstDriveFile();
|
||||||
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
assert.strictEqual(exported[0].description, 'bar');
|
||||||
|
assert.strictEqual(exported[0].clipNotes.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('export with notes', async () => {
|
||||||
|
let res = await api('/clips/create', {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'bar',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const clip = res.body;
|
||||||
|
|
||||||
|
const note1 = await post(alice, {
|
||||||
|
text: 'baz1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const note2 = await post(alice, {
|
||||||
|
text: 'baz2',
|
||||||
|
poll: {
|
||||||
|
choices: ['sakura', 'izumi', 'ako'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const note of [note1, note2]) {
|
||||||
|
res = await api('/clips/add-note', {
|
||||||
|
clipId: clip.id,
|
||||||
|
noteId: note.id,
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
res = await api('/i/export-clips', {}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
const exported = await pollFirstDriveFile();
|
||||||
|
assert.strictEqual(exported[0].name, 'foo');
|
||||||
|
assert.strictEqual(exported[0].description, 'bar');
|
||||||
|
assert.strictEqual(exported[0].clipNotes.length, 2);
|
||||||
|
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
|
||||||
|
assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2');
|
||||||
|
assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple clips', async () => {
|
||||||
|
let res = await api('/clips/create', {
|
||||||
|
name: 'kawaii',
|
||||||
|
description: 'kawaii',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const clip1 = res.body;
|
||||||
|
|
||||||
|
res = await api('/clips/create', {
|
||||||
|
name: 'yuri',
|
||||||
|
description: 'yuri',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const clip2 = res.body;
|
||||||
|
|
||||||
|
const note1 = await post(alice, {
|
||||||
|
text: 'baz1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const note2 = await post(alice, {
|
||||||
|
text: 'baz2',
|
||||||
|
});
|
||||||
|
|
||||||
|
res = await api('/clips/add-note', {
|
||||||
|
clipId: clip1.id,
|
||||||
|
noteId: note1.id,
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
res = await api('/clips/add-note', {
|
||||||
|
clipId: clip2.id,
|
||||||
|
noteId: note2.id,
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
res = await api('/i/export-clips', {}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
const exported = await pollFirstDriveFile();
|
||||||
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
assert.strictEqual(exported[0].clipNotes.length, 1);
|
||||||
|
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
|
||||||
|
assert.strictEqual(exported[1].name, 'yuri');
|
||||||
|
assert.strictEqual(exported[1].clipNotes.length, 1);
|
||||||
|
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Clipping other user\'s note', async () => {
|
||||||
|
let res = await api('/clips/create', {
|
||||||
|
name: 'kawaii',
|
||||||
|
description: 'kawaii',
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
const clip = res.body;
|
||||||
|
|
||||||
|
const note = await post(bob, {
|
||||||
|
text: 'baz',
|
||||||
|
visibility: 'followers',
|
||||||
|
});
|
||||||
|
|
||||||
|
res = await api('/clips/add-note', {
|
||||||
|
clipId: clip.id,
|
||||||
|
noteId: note.id,
|
||||||
|
}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
res = await api('/i/export-clips', {}, alice);
|
||||||
|
assert.strictEqual(res.status, 204);
|
||||||
|
|
||||||
|
const exported = await pollFirstDriveFile();
|
||||||
|
assert.strictEqual(exported[0].name, 'kawaii');
|
||||||
|
assert.strictEqual(exported[0].clipNotes.length, 1);
|
||||||
|
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz');
|
||||||
|
assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob');
|
||||||
|
});
|
||||||
|
});
|
|
@ -25,7 +25,7 @@ const JSON_UTF8 = 'application/json; charset=utf-8';
|
||||||
describe('Webリソース', () => {
|
describe('Webリソース', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let aliceUploadedFile: any;
|
let aliceUploadedFile: any;
|
||||||
let alicesPost: any;
|
let alicesPost: any;
|
||||||
let alicePage: any;
|
let alicePage: any;
|
||||||
|
@ -34,7 +34,7 @@ describe('Webリソース', () => {
|
||||||
let aliceGalleryPost: any;
|
let aliceGalleryPost: any;
|
||||||
let aliceChannel: any;
|
let aliceChannel: any;
|
||||||
|
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
path: string,
|
path: string,
|
||||||
|
|
|
@ -13,8 +13,8 @@ import type * as misskey from 'misskey-js';
|
||||||
describe('FF visibility', () => {
|
describe('FF visibility', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -20,12 +20,12 @@ describe('Account Move', () => {
|
||||||
let url: URL;
|
let url: URL;
|
||||||
|
|
||||||
let root: any;
|
let root: any;
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let dave: misskey.entities.MeSignup;
|
let dave: misskey.entities.SignupResponse;
|
||||||
let eve: misskey.entities.MeSignup;
|
let eve: misskey.entities.SignupResponse;
|
||||||
let frank: misskey.entities.MeSignup;
|
let frank: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let Users: UsersRepository;
|
let Users: UsersRepository;
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ describe('Mute', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
// alice mutes carol
|
// alice mutes carol
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -16,8 +16,8 @@ describe('Note', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
let Notes: any;
|
let Notes: any;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -75,7 +75,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName:
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
||||||
return fetch(new URL('/oauth/decision', host), {
|
return fetch(new URL('/oauth/decision', host), {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
|
@ -90,14 +90,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
||||||
const { transactionId } = getMeta(await response.text());
|
const { transactionId } = getMeta(await response.text());
|
||||||
assert.ok(transactionId);
|
assert.ok(transactionId);
|
||||||
|
|
||||||
return await fetchDecision(transactionId, user, { cancel });
|
return await fetchDecision(transactionId, user, { cancel });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
|
async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
|
||||||
const client = new AuthorizationCode(clientConfig);
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
const response = await fetch(client.authorizeURL({
|
const response = await fetch(client.authorizeURL({
|
||||||
|
@ -150,8 +150,8 @@ describe('OAuth', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
let fastify: FastifyInstance;
|
let fastify: FastifyInstance;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let sender: (reply: FastifyReply) => void;
|
let sender: (reply: FastifyReply) => void;
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ describe('Renote Mute', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
// alice mutes carol
|
// alice mutes carol
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -32,15 +32,15 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
describe('Streaming', () => {
|
describe('Streaming', () => {
|
||||||
// Local users
|
// Local users
|
||||||
let ayano: misskey.entities.MeSignup;
|
let ayano: misskey.entities.SignupResponse;
|
||||||
let kyoko: misskey.entities.MeSignup;
|
let kyoko: misskey.entities.SignupResponse;
|
||||||
let chitose: misskey.entities.MeSignup;
|
let chitose: misskey.entities.SignupResponse;
|
||||||
let kanako: misskey.entities.MeSignup;
|
let kanako: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
// Remote users
|
// Remote users
|
||||||
let akari: misskey.entities.MeSignup;
|
let akari: misskey.entities.SignupResponse;
|
||||||
let chinatsu: misskey.entities.MeSignup;
|
let chinatsu: misskey.entities.SignupResponse;
|
||||||
let takumi: misskey.entities.MeSignup;
|
let takumi: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let kyokoNote: any;
|
let kyokoNote: any;
|
||||||
let kanakoNote: any;
|
let kanakoNote: any;
|
||||||
|
|
|
@ -13,9 +13,9 @@ import type * as misskey from 'misskey-js';
|
||||||
describe('Note thread mute', () => {
|
describe('Note thread mute', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
|
|
|
@ -13,7 +13,7 @@ import type * as misskey from 'misskey-js';
|
||||||
describe('users/notes', () => {
|
describe('users/notes', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let jpgNote: any;
|
let jpgNote: any;
|
||||||
let pngNote: any;
|
let pngNote: any;
|
||||||
let jpgPngNote: any;
|
let jpgPngNote: any;
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js';
|
||||||
import { loadConfig } from '../src/config.js';
|
import { loadConfig } from '../src/config.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
export { server as startServer } from '@/boot/common.js';
|
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
||||||
|
|
||||||
interface UserToken {
|
interface UserToken {
|
||||||
token: string;
|
token: string;
|
||||||
|
|
BIN
packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/bubble2.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/cold_face.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
packages/frontend/assets/drop-and-fusion/drop-arrow.svg
Normal file
After Width: | Height: | Size: 646 B |
BIN
packages/frontend/assets/drop-and-fusion/dropper.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/exploding_head.png
Normal file
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 39 KiB |
BIN
packages/frontend/assets/drop-and-fusion/frame-dark.svg
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
packages/frontend/assets/drop-and-fusion/frame-light.svg
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
packages/frontend/assets/drop-and-fusion/gameover.png
Normal file
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 40 KiB |
BIN
packages/frontend/assets/drop-and-fusion/heart_suit.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_1.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_10.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_2.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_3.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_4.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_5.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_6.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_7.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_8.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/keycap_9.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packages/frontend/assets/drop-and-fusion/logo.png
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
packages/frontend/assets/drop-and-fusion/pleading_face.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
packages/frontend/assets/drop-and-fusion/poi1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/poi2.mp3
Normal file
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 47 KiB |
BIN
packages/frontend/assets/drop-and-fusion/zany_face.png
Normal file
After Width: | Height: | Size: 44 KiB |
|
@ -4,7 +4,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite",
|
"watch": "vite",
|
||||||
"dev": "vite --config vite.config.local-dev.ts",
|
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "15.0.2",
|
"@discordapp/twemoji": "15.0.2",
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
|
"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
|
|
|
@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { MenuButton } from '@/types/menu.js';
|
import { MenuButton } from '@/types/menu.js';
|
||||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@/config.js';
|
||||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os.js';
|
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
@ -23,9 +24,14 @@ const accountData = miLocalStorage.getItem('account');
|
||||||
// TODO: 外部からはreadonlyに
|
// TODO: 外部からはreadonlyに
|
||||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||||
|
|
||||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
|
||||||
export const iAmAdmin = $i != null && $i.isAdmin;
|
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||||
|
|
||||||
|
export function signinRequired() {
|
||||||
|
if ($i == null) throw new Error('signin required');
|
||||||
|
return $i;
|
||||||
|
}
|
||||||
|
|
||||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||||
export function incNotesCount() {
|
export function incNotesCount() {
|
||||||
notesCount++;
|
notesCount++;
|
||||||
|
@ -246,7 +252,7 @@ export async function openAccountMenu(opts: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
|
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
|
||||||
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
|
const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||||
|
|
||||||
function createItem(account: Misskey.entities.UserDetailed) {
|
function createItem(account: Misskey.entities.UserDetailed) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
|
import { setupRouter } from '@/global/router/definition.js';
|
||||||
|
|
||||||
export async function common(createVue: () => App<Element>) {
|
export async function common(createVue: () => App<Element>) {
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
|
@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
|
||||||
|
|
||||||
const app = createVue();
|
const app = createVue();
|
||||||
|
|
||||||
|
setupRouter(app);
|
||||||
|
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
app.config.performance = true;
|
app.config.performance = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,23 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createApp, markRaw, defineAsyncComponent } from 'vue';
|
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||||
import { common } from './common.js';
|
import { common } from './common.js';
|
||||||
import { ui } from '@/config.js';
|
import { ui } from '@/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { $i, updateAccount, signout } from '@/account.js';
|
import { $i, signout, updateAccount } from '@/account.js';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||||
import { mainRouter } from '@/router.js';
|
|
||||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
|
import { mainRouter } from '@/global/router/main.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { Cache } from '@/scripts/cache.js';
|
import { Cache } from '@/scripts/cache.js';
|
||||||
import { api } from '@/os.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => api('clips/list'));
|
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
|
||||||
export const rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list'));
|
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
|
||||||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => api('users/lists/list'));
|
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
|
||||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => api('antennas/list'));
|
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
|
||||||
|
|
|
@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import MkMention from './MkMention.vue';
|
import MkMention from './MkMention.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { host as localHost } from '@/config.js';
|
import { host as localHost } from '@/config.js';
|
||||||
import { api } from '@/os.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
const user = ref<Misskey.entities.UserLite>();
|
const user = ref<Misskey.entities.UserLite>();
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ const props = defineProps<{
|
||||||
movedTo: string; // user id
|
movedTo: string; // user id
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
api('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { onMounted, ref, computed } from 'vue';
|
import { onMounted, ref, computed } from 'vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
|
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
|
||||||
|
|
||||||
|
@ -71,7 +72,7 @@ const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null
|
||||||
const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
|
const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
|
||||||
|
|
||||||
function fetch() {
|
function fetch() {
|
||||||
os.api('users/achievements', { userId: props.user.id }).then(res => {
|
misskeyApi('users/achievements', { userId: props.user.id }).then(res => {
|
||||||
achievements.value = [];
|
achievements.value = [];
|
||||||
for (const t of ACHIEVEMENT_TYPES) {
|
for (const t of ACHIEVEMENT_TYPES) {
|
||||||
const a = res.find(x => x.name === t);
|
const a = res.find(x => x.name === t);
|
||||||
|
|
|
@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { onMounted, shallowRef } from 'vue';
|
import { onMounted, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -49,7 +50,7 @@ async function ok() {
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.value.close();
|
modal.value.close();
|
||||||
os.api('i/read-announcement', { announcementId: props.announcement.id });
|
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||||
updateAccount({
|
updateAccount({
|
||||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,7 @@ import contains from '@/scripts/contains.js';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||||
import { acct } from '@/filters/user.js';
|
import { acct } from '@/filters/user.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -201,7 +202,7 @@ function exec() {
|
||||||
users.value = JSON.parse(cache);
|
users.value = JSON.parse(cache);
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
os.api('users/search-by-username-and-host', {
|
misskeyApi('users/search-by-username-and-host', {
|
||||||
username: props.q,
|
username: props.q,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
detail: false,
|
detail: false,
|
||||||
|
@ -224,7 +225,7 @@ function exec() {
|
||||||
hashtags.value = hashtags;
|
hashtags.value = hashtags;
|
||||||
fetching.value = false;
|
fetching.value = false;
|
||||||
} else {
|
} else {
|
||||||
os.api('hashtags/search', {
|
misskeyApi('hashtags/search', {
|
||||||
query: props.q,
|
query: props.q,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
}).then(searchedHashtags => {
|
}).then(searchedHashtags => {
|
||||||
|
|