Merge branch 'develop' into fix/postform-footer-button-overflow
|
@ -2,3 +2,4 @@
|
|||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
POSTGRES_USER=example-misskey-user
|
||||
POSTGRES_DB=misskey
|
||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||
|
|
|
@ -17,7 +17,7 @@ updates:
|
|||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 5
|
||||
open-pull-requests-limit: 10
|
||||
# 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.
|
||||
groups:
|
||||
|
|
13
CHANGELOG.md
|
@ -14,8 +14,21 @@
|
|||
|
||||
## 202x.x.x (Unreleased)
|
||||
|
||||
### General
|
||||
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
|
||||
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
|
||||
|
||||
### Client
|
||||
- Feat: 新しいゲームを追加
|
||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||
- Enhance: クリップをエクスポートできるように
|
||||
|
||||
## 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.
|
||||
|
||||
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
|
||||
- Make the number of type errors zero (frontend)
|
||||
- Improve CI
|
||||
- ~~Fix tests~~ → Done ✔️
|
||||
- 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:
|
||||
- db
|
||||
- redis
|
||||
# - mcaptcha
|
||||
# - meilisearch
|
||||
depends_on:
|
||||
db:
|
||||
|
@ -48,6 +49,36 @@ services:
|
|||
interval: 5s
|
||||
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:
|
||||
# restart: always
|
||||
# image: getmeili/meilisearch:v1.3.4
|
||||
|
|
|
@ -382,6 +382,11 @@ export interface Locale {
|
|||
"enableHcaptcha": string;
|
||||
"hcaptchaSiteKey": string;
|
||||
"hcaptchaSecretKey": string;
|
||||
"mcaptcha": string;
|
||||
"enableMcaptcha": string;
|
||||
"mcaptchaSiteKey": string;
|
||||
"mcaptchaSecretKey": string;
|
||||
"mcaptchaInstanceUrl": string;
|
||||
"recaptcha": string;
|
||||
"enableRecaptcha": string;
|
||||
"recaptchaSiteKey": string;
|
||||
|
@ -672,6 +677,7 @@ export interface Locale {
|
|||
"other": string;
|
||||
"regenerateLoginToken": string;
|
||||
"regenerateLoginTokenDescription": string;
|
||||
"theKeywordWhenSearchingForCustomEmoji": string;
|
||||
"setMultipleBySeparatingWithSpace": string;
|
||||
"fileIdOrUrl": string;
|
||||
"behavior": string;
|
||||
|
@ -1186,6 +1192,7 @@ export interface Locale {
|
|||
"decorate": string;
|
||||
"addMfmFunction": string;
|
||||
"enableQuickAddMfmFunction": string;
|
||||
"bubbleGame": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1650,6 +1657,15 @@ export interface Locale {
|
|||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameDoubleExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
"flavor": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
"_role": {
|
||||
|
@ -2250,6 +2266,7 @@ export interface Locale {
|
|||
"_exportOrImport": {
|
||||
"allNotes": string;
|
||||
"favoritedNotes": string;
|
||||
"clips": string;
|
||||
"followingList": string;
|
||||
"muteList": string;
|
||||
"blockingList": string;
|
||||
|
|
|
@ -379,6 +379,11 @@ hcaptcha: "hCaptcha"
|
|||
enableHcaptcha: "hCaptchaを有効にする"
|
||||
hcaptchaSiteKey: "サイトキー"
|
||||
hcaptchaSecretKey: "シークレットキー"
|
||||
mcaptcha: "mCaptcha"
|
||||
enableMcaptcha: "mCaptchaを有効にする"
|
||||
mcaptchaSiteKey: "サイトキー"
|
||||
mcaptchaSecretKey: "シークレットキー"
|
||||
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "reCAPTCHAを有効にする"
|
||||
recaptchaSiteKey: "サイトキー"
|
||||
|
@ -669,6 +674,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
|
|||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
||||
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
|
||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
||||
fileIdOrUrl: "ファイルIDまたはURL"
|
||||
behavior: "動作"
|
||||
|
@ -1183,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
|||
decorate: "デコる"
|
||||
addMfmFunction: "装飾を追加"
|
||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||
bubbleGame: "バブルゲーム"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -1561,6 +1568,13 @@ _achievements:
|
|||
_tutorialCompleted:
|
||||
title: "Misskey初心者講座 修了証"
|
||||
description: "チュートリアルを完了した"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "バブルゲームで最も大きいモノを出した"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "ダブル🤯"
|
||||
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
|
@ -2153,6 +2167,7 @@ _profile:
|
|||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
favoritedNotes: "お気に入りにしたノート"
|
||||
clips: "クリップ"
|
||||
followingList: "フォロー"
|
||||
muteList: "ミュート"
|
||||
blockingList: "ブロック"
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
|||
import { Config, loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import { allSettled } from './misc/promise-tracker.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const $config: Provider = {
|
||||
|
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
|||
useFactory: (config: Config) => {
|
||||
if (config.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,
|
||||
});
|
||||
} else {
|
||||
|
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential DB queries
|
||||
await allSettled();
|
||||
// And then disconnect from DB
|
||||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
|
|
|
@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
@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
|
||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
|
|
|
@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -676,7 +677,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.relayService.deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
|
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
|
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
|
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import type { FilterUnionByProperty } from '@/types.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
|
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@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'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
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> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential queue jobs
|
||||
await allSettled();
|
||||
// And then close all queues
|
||||
await Promise.all([
|
||||
this.systemQueue.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 httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
|
@ -74,11 +75,15 @@ export class QueueService {
|
|||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
|
||||
const contentBody = JSON.stringify(content);
|
||||
const digest = ApRequestCreator.createDigest(contentBody);
|
||||
|
||||
const data: DeliverJobData = {
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
content,
|
||||
content: contentBody,
|
||||
digest,
|
||||
to,
|
||||
isSharedInbox,
|
||||
};
|
||||
|
@ -103,6 +108,8 @@ export class QueueService {
|
|||
@bindThis
|
||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||
if (content == null) return null;
|
||||
const contentBody = JSON.stringify(content);
|
||||
const digest = ApRequestCreator.createDigest(contentBody);
|
||||
|
||||
const opts = {
|
||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||
|
@ -117,7 +124,8 @@ export class QueueService {
|
|||
name: d[0],
|
||||
data: {
|
||||
user,
|
||||
content,
|
||||
content: contentBody,
|
||||
digest,
|
||||
to: d[0],
|
||||
isSharedInbox: d[1],
|
||||
} 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
|
||||
public createExportFavoritesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
|||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
@ -268,7 +269,7 @@ export class ReactionService {
|
|||
}
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
@ -316,7 +317,7 @@ export class ReactionService {
|
|||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||
}
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ class DeliverManager {
|
|||
}
|
||||
|
||||
// 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) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
this.logger.error(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +258,7 @@ export class ApInboxService {
|
|||
|
||||
const targetUri = getApId(activity.object);
|
||||
|
||||
this.announceNote(actor, activity, targetUri);
|
||||
await this.announceNote(actor, activity, targetUri);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -288,7 +290,7 @@ export class ApInboxService {
|
|||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
if (err instanceof StatusError) {
|
||||
if (err.isClientError) {
|
||||
if (!err.isRetryable) {
|
||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||
return;
|
||||
}
|
||||
|
@ -373,7 +375,7 @@ export class ApInboxService {
|
|||
});
|
||||
|
||||
if (isPost(object)) {
|
||||
this.createNote(resolver, actor, object, false, activity);
|
||||
await this.createNote(resolver, actor, object, false, activity);
|
||||
} else {
|
||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||
}
|
||||
|
@ -404,7 +406,7 @@ export class ApInboxService {
|
|||
await this.apNoteService.createNote(note, resolver, silent);
|
||||
return 'ok';
|
||||
} catch (err) {
|
||||
if (err instanceof StatusError && err.isClientError) {
|
||||
if (err instanceof StatusError && !err.isRetryable) {
|
||||
return `skip ${err.statusCode}`;
|
||||
} else {
|
||||
throw err;
|
||||
|
|
|
@ -34,9 +34,9 @@ type PrivateKey = {
|
|||
};
|
||||
|
||||
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 digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
|
||||
const digestHeader = args.digest ?? this.createDigest(args.body);
|
||||
|
||||
const request: Request = {
|
||||
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 {
|
||||
const u = new URL(args.url);
|
||||
|
||||
|
@ -145,8 +149,8 @@ export class ApRequestService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
|
||||
const body = JSON.stringify(object);
|
||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
|
||||
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
||||
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
|
@ -157,6 +161,7 @@ export class ApRequestService {
|
|||
},
|
||||
url,
|
||||
body,
|
||||
digest,
|
||||
additionalHeaders: {
|
||||
},
|
||||
});
|
||||
|
|
|
@ -216,7 +216,7 @@ export class ApNoteService {
|
|||
return { status: 'ok', res };
|
||||
} catch (e) {
|
||||
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,
|
||||
isSensitive: channel.isSensitive,
|
||||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||
userId: channel.userId,
|
||||
} : undefined,
|
||||
mentions: note.mentions.length > 0 ? note.mentions : undefined,
|
||||
uri: note.uri ?? undefined,
|
||||
|
|
|
@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||
const log = [] as any[];
|
||||
|
||||
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 () => {
|
||||
|
|
|
@ -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 statusMessage?: string;
|
||||
public isClientError: boolean;
|
||||
public isRetryable: boolean;
|
||||
|
||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||
super(message);
|
||||
|
@ -14,5 +15,6 @@ export class StatusError extends Error {
|
|||
this.statusCode = statusCode;
|
||||
this.statusMessage = statusMessage;
|
||||
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;
|
||||
|
||||
@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', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -148,6 +148,10 @@ export const packedNoteSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
localOnly: {
|
||||
|
|
|
@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
|
|||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
||||
|
@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||
DeleteDriveFilesProcessorService,
|
||||
ExportCustomEmojisProcessorService,
|
||||
ExportNotesProcessorService,
|
||||
ExportClipsProcessorService,
|
||||
ExportFavoritesProcessorService,
|
||||
ExportFollowingProcessorService,
|
||||
ExportMutingProcessorService,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
|||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
||||
|
@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
||||
private exportNotesProcessorService: ExportNotesProcessorService,
|
||||
private exportClipsProcessorService: ExportClipsProcessorService,
|
||||
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
||||
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
||||
private exportMutingProcessorService: ExportMutingProcessorService,
|
||||
|
@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||
case 'exportClips': return this.exportClipsProcessorService.process(job);
|
||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||
|
|
|
@ -72,7 +72,7 @@ export class DeliverProcessorService {
|
|||
}
|
||||
|
||||
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
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
|
@ -111,7 +111,7 @@ export class DeliverProcessorService {
|
|||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
if (!res.isRetryable) {
|
||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||
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) {
|
||||
// 対象が4xxならスキップ
|
||||
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 Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
|
||||
|
|
|
@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService {
|
|||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
if (!res.isRetryable) {
|
||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ export type DeliverJobData = {
|
|||
/** Actor */
|
||||
user: ThinUser;
|
||||
/** Activity */
|
||||
content: unknown;
|
||||
content: string;
|
||||
/** Digest header */
|
||||
digest: string;
|
||||
/** inbox URL to deliver */
|
||||
to: string;
|
||||
/** 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_exportMute from './endpoints/i/export-mute.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_exportUserLists from './endpoints/i/export-user-lists.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_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_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_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 };
|
||||
|
@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
|
@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
|
|
|
@ -65,6 +65,7 @@ export class SignupApiService {
|
|||
'hcaptcha-response'?: string;
|
||||
'g-recaptcha-response'?: string;
|
||||
'turnstile-response'?: string;
|
||||
'm-captcha-response'?: string;
|
||||
}
|
||||
}>,
|
||||
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) {
|
||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||
throw new FastifyReplyError(400, err);
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { permissions } from 'misskey-js';
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { RolePolicies } from '@/core/RoleService.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_exportMute from './endpoints/i/export-mute.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_exportUserLists from './endpoints/i/export-user-lists.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-mute', ep___i_exportMute],
|
||||
['i/export-notes', ep___i_exportNotes],
|
||||
['i/export-clips', ep___i_exportClips],
|
||||
['i/export-favorites', ep___i_exportFavorites],
|
||||
['i/export-user-lists', ep___i_exportUserLists],
|
||||
['i/export-antennas', ep___i_exportAntennas],
|
||||
|
|
|
@ -41,6 +41,18 @@ export const meta = {
|
|||
type: 'string',
|
||||
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: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -163,6 +175,10 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
mcaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
recaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
@ -468,6 +484,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
enableTurnstile: instance.enableTurnstile,
|
||||
|
@ -498,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
sensitiveWords: instance.sensitiveWords,
|
||||
preservedUsernames: instance.preservedUsernames,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
mcaptchaSecretKey: instance.mcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
turnstileSecretKey: instance.turnstileSecretKey,
|
||||
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
||||
|
|
|
@ -63,6 +63,10 @@ export const paramDef = {
|
|||
enableHcaptcha: { type: 'boolean' },
|
||||
hcaptchaSiteKey: { 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' },
|
||||
recaptchaSiteKey: { 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;
|
||||
}
|
||||
|
||||
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) {
|
||||
set.enableRecaptcha = ps.enableRecaptcha;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
antenna.isActive = true;
|
||||
antenna.lastUsedAt = new Date();
|
||||
this.antennasRepository.update(antenna.id, antenna);
|
||||
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||
|
||||
if (needPublishEvent) {
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||
|
|
|
@ -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',
|
||||
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: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -351,6 +363,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||
enableRecaptcha: instance.enableRecaptcha,
|
||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||
enableTurnstile: instance.enableTurnstile,
|
||||
|
|
|
@ -21,6 +21,7 @@ class UserListChannel extends Channel {
|
|||
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
private withFiles: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
@ -39,6 +40,7 @@ class UserListChannel extends Channel {
|
|||
public async init(params: any) {
|
||||
this.listId = params.listId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Check existence and owner
|
||||
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がミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -24,7 +24,7 @@ import type * as misskey from 'misskey-js';
|
|||
|
||||
describe('2要素認証', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
|
||||
const config = loadConfig();
|
||||
const password = 'test';
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('アンテナ', () => {
|
|||
// - srcのenumにgroupが残っている
|
||||
// - userGroupIdが残っている, isActiveがない
|
||||
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
||||
type User = misskey.entities.MeSignup;
|
||||
type User = misskey.entities.SignupResponse;
|
||||
type Note = misskey.entities.Note;
|
||||
|
||||
// アンテナを作成できる最小のパラメタ
|
||||
|
|
|
@ -24,15 +24,15 @@ describe('API visibility', () => {
|
|||
describe('Note visibility', () => {
|
||||
//#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を飛ばされる人 */
|
||||
let target2: misskey.entities.MeSignup;
|
||||
let target2: misskey.entities.SignupResponse;
|
||||
|
||||
/** public-post */
|
||||
let pub: any;
|
||||
|
|
|
@ -13,9 +13,9 @@ import type * as misskey from 'misskey-js';
|
|||
|
||||
describe('API', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('Block', () => {
|
|||
let app: INestApplicationContext;
|
||||
|
||||
// alice blocks bob
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -17,10 +17,10 @@ import type * as misskey from 'misskey-js';
|
|||
describe('Endpoints', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let dave: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
let dave: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -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リソース', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let aliceUploadedFile: any;
|
||||
let alicesPost: any;
|
||||
let alicePage: any;
|
||||
|
@ -34,7 +34,7 @@ describe('Webリソース', () => {
|
|||
let aliceGalleryPost: any;
|
||||
let aliceChannel: any;
|
||||
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
type Request = {
|
||||
path: string,
|
||||
|
|
|
@ -13,8 +13,8 @@ import type * as misskey from 'misskey-js';
|
|||
describe('FF visibility', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -20,12 +20,12 @@ describe('Account Move', () => {
|
|||
let url: URL;
|
||||
|
||||
let root: any;
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let dave: misskey.entities.MeSignup;
|
||||
let eve: misskey.entities.MeSignup;
|
||||
let frank: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
let dave: misskey.entities.SignupResponse;
|
||||
let eve: misskey.entities.SignupResponse;
|
||||
let frank: misskey.entities.SignupResponse;
|
||||
|
||||
let Users: UsersRepository;
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('Mute', () => {
|
|||
let app: INestApplicationContext;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -16,8 +16,8 @@ describe('Note', () => {
|
|||
let app: INestApplicationContext;
|
||||
let Notes: any;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
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), {
|
||||
method: 'post',
|
||||
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());
|
||||
assert.ok(transactionId);
|
||||
|
||||
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 response = await fetch(client.authorizeURL({
|
||||
|
@ -150,8 +150,8 @@ describe('OAuth', () => {
|
|||
let app: INestApplicationContext;
|
||||
let fastify: FastifyInstance;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
let sender: (reply: FastifyReply) => void;
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('Renote Mute', () => {
|
|||
let app: INestApplicationContext;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -32,15 +32,15 @@ describe('Streaming', () => {
|
|||
|
||||
describe('Streaming', () => {
|
||||
// Local users
|
||||
let ayano: misskey.entities.MeSignup;
|
||||
let kyoko: misskey.entities.MeSignup;
|
||||
let chitose: misskey.entities.MeSignup;
|
||||
let kanako: misskey.entities.MeSignup;
|
||||
let ayano: misskey.entities.SignupResponse;
|
||||
let kyoko: misskey.entities.SignupResponse;
|
||||
let chitose: misskey.entities.SignupResponse;
|
||||
let kanako: misskey.entities.SignupResponse;
|
||||
|
||||
// Remote users
|
||||
let akari: misskey.entities.MeSignup;
|
||||
let chinatsu: misskey.entities.MeSignup;
|
||||
let takumi: misskey.entities.MeSignup;
|
||||
let akari: misskey.entities.SignupResponse;
|
||||
let chinatsu: misskey.entities.SignupResponse;
|
||||
let takumi: misskey.entities.SignupResponse;
|
||||
|
||||
let kyokoNote: any;
|
||||
let kanakoNote: any;
|
||||
|
|
|
@ -13,9 +13,9 @@ import type * as misskey from 'misskey-js';
|
|||
describe('Note thread mute', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let bob: misskey.entities.MeSignup;
|
||||
let carol: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
|
|
@ -13,7 +13,7 @@ import type * as misskey from 'misskey-js';
|
|||
describe('users/notes', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.MeSignup;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let jpgNote: any;
|
||||
let pngNote: any;
|
||||
let jpgPngNote: any;
|
||||
|
|
|
@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js';
|
|||
import { loadConfig } from '../src/config.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 {
|
||||
token: string;
|
||||
|
|
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 646 B |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 248 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 44 KiB |
|
@ -4,7 +4,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"dev": "vite --config vite.config.local-dev.ts",
|
||||
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||
"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\"",
|
||||
"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": {
|
||||
"@discordapp/twemoji": "15.0.2",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
|
|
|
@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js';
|
|||
import { MenuButton } from '@/types/menu.js';
|
||||
import { del, get, set } from '@/scripts/idb-proxy.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';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
@ -23,9 +24,14 @@ const accountData = miLocalStorage.getItem('account');
|
|||
// TODO: 外部からはreadonlyに
|
||||
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 function signinRequired() {
|
||||
if ($i == null) throw new Error('signin required');
|
||||
return $i;
|
||||
}
|
||||
|
||||
export let notesCount = $i == null ? 0 : $i.notesCount;
|
||||
export function incNotesCount() {
|
||||
notesCount++;
|
||||
|
@ -246,7 +252,7 @@ export async function openAccountMenu(opts: {
|
|||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
|||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { setupRouter } from '@/global/router/definition.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
|
@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
|
|||
|
||||
const app = createVue();
|
||||
|
||||
setupRouter(app);
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
}
|
||||
|
|
|
@ -3,23 +3,23 @@
|
|||
* 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 { ui } from '@/config.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 * as sound from '@/scripts/sound.js';
|
||||
import { $i, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { $i, signout, updateAccount } from '@/account.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
import * as Misskey from 'misskey-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 rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list'));
|
||||
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => api('users/lists/list'));
|
||||
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => api('antennas/list'));
|
||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
|
||||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/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, () => misskeyApi('antennas/list'));
|
||||
|
|
|
@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
|
|||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n.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>();
|
||||
|
||||
|
@ -25,7 +25,7 @@ const props = defineProps<{
|
|||
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>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.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)));
|
||||
|
||||
function fetch() {
|
||||
os.api('users/achievements', { userId: props.user.id }).then(res => {
|
||||
misskeyApi('users/achievements', { userId: props.user.id }).then(res => {
|
||||
achievements.value = [];
|
||||
for (const t of ACHIEVEMENT_TYPES) {
|
||||
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 * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -49,7 +50,7 @@ async function ok() {
|
|||
}
|
||||
|
||||
modal.value.close();
|
||||
os.api('i/read-announcement', { announcementId: props.announcement.id });
|
||||
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||
updateAccount({
|
||||
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 { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -201,7 +202,7 @@ function exec() {
|
|||
users.value = JSON.parse(cache);
|
||||
fetching.value = false;
|
||||
} else {
|
||||
os.api('users/search-by-username-and-host', {
|
||||
misskeyApi('users/search-by-username-and-host', {
|
||||
username: props.q,
|
||||
limit: 10,
|
||||
detail: false,
|
||||
|
@ -224,7 +225,7 @@ function exec() {
|
|||
hashtags.value = hashtags;
|
||||
fetching.value = false;
|
||||
} else {
|
||||
os.api('hashtags/search', {
|
||||
misskeyApi('hashtags/search', {
|
||||
query: props.q,
|
||||
limit: 30,
|
||||
}).then(searchedHashtags => {
|
||||
|
|