summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-09 09:17:34 +0900
committerGitHub <noreply@github.com>2023-05-09 09:17:34 +0900
commit94690c835e3179e3fd616758ad00a8b66d844a0a (patch)
tree3171356ca8298aa6caae7c95df7232844163f913 /packages/backend/src
parentMerge pull request #10608 from misskey-dev/develop (diff)
parent[ci skip] 13.12.0 (diff)
downloadmisskey-94690c835e3179e3fd616758ad00a8b66d844a0a.tar.gz
misskey-94690c835e3179e3fd616758ad00a8b66d844a0a.tar.bz2
misskey-94690c835e3179e3fd616758ad00a8b66d844a0a.zip
Merge pull request #10774 from misskey-dev/develop
Release: 13.12.0
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/GlobalModule.ts28
-rw-r--r--packages/backend/src/boot/common.ts2
-rw-r--r--packages/backend/src/config.ts20
-rw-r--r--packages/backend/src/const.ts5
-rw-r--r--packages/backend/src/core/AccountMoveService.ts327
-rw-r--r--packages/backend/src/core/AchievementService.ts1
-rw-r--r--packages/backend/src/core/AntennaService.ts2
-rw-r--r--packages/backend/src/core/AppLockService.ts2
-rw-r--r--packages/backend/src/core/CacheService.ts2
-rw-r--r--packages/backend/src/core/CoreModule.ts7
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts18
-rw-r--r--packages/backend/src/core/DownloadService.ts10
-rw-r--r--packages/backend/src/core/DriveService.ts46
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts27
-rw-r--r--packages/backend/src/core/FetchInstanceMetadataService.ts4
-rw-r--r--packages/backend/src/core/FileInfoService.ts25
-rw-r--r--packages/backend/src/core/GlobalEventService.ts2
-rw-r--r--packages/backend/src/core/MetaService.ts2
-rw-r--r--packages/backend/src/core/NoteCreateService.ts18
-rw-r--r--packages/backend/src/core/NotificationService.ts4
-rw-r--r--packages/backend/src/core/PushNotificationService.ts2
-rw-r--r--packages/backend/src/core/QueueModule.ts2
-rw-r--r--packages/backend/src/core/QueueService.ts33
-rw-r--r--packages/backend/src/core/RemoteUserResolveService.ts10
-rw-r--r--packages/backend/src/core/RoleService.ts5
-rw-r--r--packages/backend/src/core/SearchService.ts178
-rw-r--r--packages/backend/src/core/SignupService.ts23
-rw-r--r--packages/backend/src/core/UserFollowingService.ts207
-rw-r--r--packages/backend/src/core/UserKeypairService.ts2
-rw-r--r--packages/backend/src/core/UserSuspendService.ts4
-rw-r--r--packages/backend/src/core/WebfingerService.ts3
-rw-r--r--packages/backend/src/core/WebhookService.ts2
-rw-r--r--packages/backend/src/core/activitypub/ApDbResolverService.ts8
-rw-r--r--packages/backend/src/core/activitypub/ApInboxService.ts60
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts66
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts4
-rw-r--r--packages/backend/src/core/activitypub/models/ApImageService.ts5
-rw-r--r--packages/backend/src/core/activitypub/models/ApNoteService.ts16
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts145
-rw-r--r--packages/backend/src/core/entities/ChannelEntityService.ts2
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts14
-rw-r--r--packages/backend/src/core/entities/RoleEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts58
-rw-r--r--packages/backend/src/daemons/ServerStatsService.ts2
-rw-r--r--packages/backend/src/di-symbols.ts2
-rw-r--r--packages/backend/src/misc/cache.ts6
-rw-r--r--packages/backend/src/misc/check-https.ts4
-rw-r--r--packages/backend/src/misc/check-word-mute.ts20
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/entities/Channel.ts12
-rw-r--r--packages/backend/src/models/entities/Meta.ts12
-rw-r--r--packages/backend/src/models/entities/Role.ts5
-rw-r--r--packages/backend/src/models/entities/User.ts18
-rw-r--r--packages/backend/src/models/entities/UserMemo.ts42
-rw-r--r--packages/backend/src/models/index.ts3
-rw-r--r--packages/backend/src/models/json-schema/channel.ts8
-rw-r--r--packages/backend/src/models/json-schema/user.ts13
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/queue/DbQueueProcessorsService.ts6
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts4
-rw-r--r--packages/backend/src/queue/RelationshipQueueProcessorsService.ts2
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts15
-rw-r--r--packages/backend/src/queue/processors/ExportAntennasProcessorService.ts103
-rw-r--r--packages/backend/src/queue/processors/ExportBlockingProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ExportFollowingProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ExportMutingProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ExportNotesProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ExportUserListsProcessorService.ts2
-rw-r--r--packages/backend/src/queue/processors/ImportAntennasProcessorService.ts96
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts11
-rw-r--r--packages/backend/src/queue/processors/RelationshipProcessorService.ts3
-rw-r--r--packages/backend/src/queue/types.ts12
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts6
-rw-r--r--packages/backend/src/server/FileServerService.ts6
-rw-r--r--packages/backend/src/server/WellKnownServerService.ts4
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts11
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts20
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts2
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts7
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts2
-rw-r--r--packages/backend/src/server/api/endpoints.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts37
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/server-info.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/channels/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/channels/favorite.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/channels/featured.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/channels/follow.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/channels/owned.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/channels/search.ts19
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/channels/unfavorite.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/channels/unfollow.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/channels/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/clips/add-note.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/clips/favorite.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/remove-note.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/unfavorite.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/flash/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/flash/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/flash/unlike.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/flash/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/following/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/hashtags/users.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/export-antennas.ts30
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-antennas.ts84
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-blocking.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-following.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-muting.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-user-lists.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/i/known-as.ts92
-rw-r--r--packages/backend/src/server/api/endpoints/i/move.ts70
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/pin.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts85
-rw-r--r--packages/backend/src/server/api/endpoints/meta.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/mute/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/vote.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/pages/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/pages/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/pages/unlike.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/pages/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/create.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/reset-db.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/username/available.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/pull.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/lists/push.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/update-memo.ts85
-rw-r--r--packages/backend/src/server/web/UrlPreviewService.ts4
-rw-r--r--packages/backend/src/server/web/views/base.pug2
160 files changed, 2072 insertions, 620 deletions
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 174d0d8beb..5fb4e8ef3c 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -1,7 +1,8 @@
import { setTimeout } from 'node:timers/promises';
import { Global, Inject, Module } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DataSource } from 'typeorm';
+import { MeiliSearch } from 'meilisearch';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js';
@@ -22,10 +23,25 @@ const $db: Provider = {
inject: [DI.config],
};
+const $meilisearch: Provider = {
+ provide: DI.meilisearch,
+ useFactory: (config) => {
+ if (config.meilisearch) {
+ return new MeiliSearch({
+ host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
+ apiKey: config.meilisearch.apiKey,
+ });
+ } else {
+ return null;
+ }
+ },
+ inject: [DI.config],
+};
+
const $redis: Provider = {
provide: DI.redis,
useFactory: (config) => {
- return new Redis({
+ return new Redis.Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
@@ -40,7 +56,7 @@ const $redis: Provider = {
const $redisForPub: Provider = {
provide: DI.redisForPub,
useFactory: (config) => {
- const redis = new Redis({
+ const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
@@ -56,7 +72,7 @@ const $redisForPub: Provider = {
const $redisForSub: Provider = {
provide: DI.redisForSub,
useFactory: (config) => {
- const redis = new Redis({
+ const redis = new Redis.Redis({
port: config.redisForPubsub.port,
host: config.redisForPubsub.host,
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
@@ -73,8 +89,8 @@ const $redisForSub: Provider = {
@Global()
@Module({
imports: [RepositoryModule],
- providers: [$config, $db, $redis, $redisForPub, $redisForSub],
- exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
+ providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
+ exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts
index 279a1fe59d..45ded5495c 100644
--- a/packages/backend/src/boot/common.ts
+++ b/packages/backend/src/boot/common.ts
@@ -34,4 +34,6 @@ export async function jobQueue() {
jobQueue.get(QueueProcessorService).start();
jobQueue.get(ChartManagementService).start();
+
+ return jobQueue;
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index e4f7601fa9..b41fb603bb 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -4,7 +4,7 @@
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
-import { dirname } from 'node:path';
+import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
/**
@@ -57,13 +57,11 @@ export type Source = {
db?: number;
prefix?: string;
};
- elasticsearch: {
+ meilisearch?: {
host: string;
- port: number;
+ port: string;
+ apiKey: string;
ssl?: boolean;
- user?: string;
- pass?: string;
- index?: string;
};
proxy?: string;
@@ -84,8 +82,10 @@ export type Source = {
deliverJobConcurrency?: number;
inboxJobConcurrency?: number;
+ relashionshipJobConcurrency?: number;
deliverJobPerSec?: number;
inboxJobPerSec?: number;
+ relashionshipJobPerSec?: number;
deliverJobMaxAttempts?: number;
inboxJobMaxAttempts?: number;
@@ -132,9 +132,11 @@ const dir = `${_dirname}/../../../.config`;
/**
* Path of configuration file
*/
-const path = process.env.NODE_ENV === 'test'
- ? `${dir}/test.yml`
- : `${dir}/default.yml`;
+const path = process.env.MISSKEY_CONFIG_YML
+ ? resolve(dir, process.env.MISSKEY_CONFIG_YML)
+ : process.env.NODE_ENV === 'test'
+ ? resolve(dir, 'test.yml')
+ : resolve(dir, 'default.yml');
export function loadConfig() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index 6c7f214214..ee1a9a3093 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -56,6 +56,11 @@ export const FILE_TYPE_BROWSERSAFE = [
'audio/webm',
'audio/aac',
+
+ // see https://github.com/misskey-dev/misskey/pull/10686
+ 'audio/flac',
+ 'audio/wav',
+ // backward compatibility
'audio/x-flac',
'audio/vnd.wave',
];
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 3f2a19b771..ab11785e28 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -1,55 +1,90 @@
import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
+import { IsNull, In, MoreThan, Not } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
-import type { LocalUser } from '@/models/entities/User.js';
-import { User } from '@/models/entities/User.js';
-import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
+import type { Config } from '@/config.js';
+import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
+import type { BlockingsRepository, FollowingsRepository, InstancesRepository, Muting, MutingsRepository, UserListJoiningsRepository, UsersRepository } from '@/models/index.js';
+import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
+import type { User } from '@/models/entities/User.js';
+import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { UserFollowingService } from '@/core/UserFollowingService.js';
+import { QueueService } from '@/core/QueueService.js';
+import { RelayService } from '@/core/RelayService.js';
+import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { AccountUpdateService } from '@/core/AccountUpdateService.js';
-import { RelayService } from '@/core/RelayService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ProxyAccountService } from '@/core/ProxyAccountService.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
+import { MetaService } from '@/core/MetaService.js';
+import InstanceChart from '@/core/chart/charts/instance.js';
+import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@Injectable()
export class AccountMoveService {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
+ @Inject(DI.blockingsRepository)
+ private blockingsRepository: BlockingsRepository,
+
+ @Inject(DI.mutingsRepository)
+ private mutingsRepository: MutingsRepository,
+
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
+
+ @Inject(DI.instancesRepository)
+ private instancesRepository: InstancesRepository,
+
private userEntityService: UserEntityService,
+ private idService: IdService,
+ private apPersonService: ApPersonService,
private apRendererService: ApRendererService,
private apDeliverManagerService: ApDeliverManagerService,
private globalEventService: GlobalEventService,
- private userFollowingService: UserFollowingService,
- private accountUpdateService: AccountUpdateService,
+ private proxyAccountService: ProxyAccountService,
+ private perUserFollowingChart: PerUserFollowingChart,
+ private federatedInstanceService: FederatedInstanceService,
+ private instanceChart: InstanceChart,
+ private metaService: MetaService,
private relayService: RelayService,
+ private cacheService: CacheService,
+ private queueService: QueueService,
) {
}
/**
- * Move a local account to a remote account.
+ * Move a local account to a new account.
*
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
*/
@bindThis
- public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
- // Make sure that the destination is a remote account.
- if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
- if (!dst.uri) throw new Error('destination uri is empty');
+ public async moveFromLocal(src: LocalUser, dst: LocalUser | RemoteUser): Promise<unknown> {
+ const srcUri = this.userEntityService.getUserUri(src);
+ const dstUri = this.userEntityService.getUserUri(dst);
// add movedToUri to indicate that the user has moved
- const update = {} as Partial<User>;
- update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
- update.movedToUri = dst.uri;
+ const update = {} as Partial<LocalUser>;
+ update.alsoKnownAs = src.alsoKnownAs?.includes(dstUri) ? src.alsoKnownAs : src.alsoKnownAs?.concat([dstUri]) ?? [dstUri];
+ update.movedToUri = dstUri;
+ update.movedAt = new Date();
await this.usersRepository.update(src.id, update);
+ Object.assign(src, update);
+
+ // Update cache
+ this.cacheService.uriPersonCache.set(srcUri, src);
const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
@@ -64,51 +99,249 @@ export class AccountMoveService {
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
- // follow the new account and unfollow the old one
- const followings = await this.followingsRepository.find({
- relations: {
- follower: true,
- },
+ // Unfollow after 24 hours
+ const followings = await this.followingsRepository.findBy({
+ followerId: src.id,
+ });
+ this.queueService.createDelayedUnfollowJob(followings.map(following => ({
+ from: { id: src.id },
+ to: { id: following.followeeId },
+ })), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
+
+ await this.postMoveProcess(src, dst);
+
+ return iObj;
+ }
+
+ @bindThis
+ public async postMoveProcess(src: User, dst: User): Promise<void> {
+ // Copy blockings and mutings, and update lists
+ try {
+ await Promise.all([
+ this.copyBlocking(src, dst),
+ this.copyMutings(src, dst),
+ this.updateLists(src, dst),
+ ]);
+ } catch {
+ /* skip if any error happens */
+ }
+
+ // follow the new account
+ const proxy = await this.proxyAccountService.fetch();
+ const followings = await this.followingsRepository.findBy({
+ followeeId: src.id,
+ followerHost: IsNull(), // follower is local
+ followerId: proxy ? Not(proxy.id) : undefined,
+ });
+ const followJobs = followings.map(following => ({
+ from: { id: following.followerId },
+ to: { id: dst.id },
+ })) as RelationshipJobData[];
+
+ // Decrease following count instead of unfollowing.
+ try {
+ await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src);
+ } catch {
+ /* skip if any error happens */
+ }
+
+ // Should be queued because this can cause a number of follow per one move.
+ this.queueService.createFollowJob(followJobs);
+ }
+
+ @bindThis
+ public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> {
+ // Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving.
+ // So block the destination account here.
+ const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id });
+ const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id });
+ const blockerIds = dstBlockings.map(blocking => blocking.blockerId);
+ // reblock the destination account
+ const blockJobs: RelationshipJobData[] = [];
+ for (const blocking of srcBlockings) {
+ if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked
+ blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } });
+ }
+ // no need to unblock the old account because it may be still functional
+ this.queueService.createBlockJob(blockJobs);
+ }
+
+ @bindThis
+ public async copyMutings(src: ThinUser, dst: ThinUser): Promise<void> {
+ // Insert new mutings with the same values except mutee
+ const oldMutings = await this.mutingsRepository.findBy([
+ { muteeId: src.id, expiresAt: IsNull() },
+ { muteeId: src.id, expiresAt: MoreThan(new Date()) },
+ ]);
+ if (oldMutings.length === 0) return;
+
+ // Check if the destination account is already indefinitely muted by the muter
+ const existingMutingsMuterUserIds = await this.mutingsRepository.findBy(
+ { muteeId: dst.id, expiresAt: IsNull() },
+ ).then(mutings => mutings.map(muting => muting.muterId));
+
+ const newMutings: Map<string, { muterId: string; muteeId: string; createdAt: Date; expiresAt: Date | null; }> = new Map();
+
+ // 重複しないようにIDを生成
+ const genId = (): string => {
+ let id: string;
+ do {
+ id = this.idService.genId();
+ } while (newMutings.has(id));
+ return id;
+ };
+ for (const muting of oldMutings) {
+ if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely
+ newMutings.set(genId(), {
+ ...muting,
+ createdAt: new Date(),
+ muteeId: dst.id,
+ });
+ }
+
+ const arrayToInsert = Array.from(newMutings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
+ await this.mutingsRepository.insert(arrayToInsert);
+ }
+
+ /**
+ * Update lists while moving accounts.
+ * - No removal of the old account from the lists
+ * - Users number limit is not checked
+ *
+ * @param src ThinUser (old account)
+ * @param dst User (new account)
+ * @returns Promise<void>
+ */
+ @bindThis
+ public async updateLists(src: ThinUser, dst: User): Promise<void> {
+ // Return if there is no list to be updated.
+ const oldJoinings = await this.userListJoiningsRepository.find({
where: {
- followeeId: src.id,
- followerHost: IsNull(), // follower is local
+ userId: src.id,
},
});
- for (const following of followings) {
- if (!following.follower) continue;
- try {
- await this.userFollowingService.follow(following.follower, dst);
- await this.userFollowingService.unfollow(following.follower, src);
- } catch {
- /* empty */
+ if (oldJoinings.length === 0) return;
+
+ const existingUserListIds = await this.userListJoiningsRepository.find({
+ where: {
+ userId: dst.id,
+ },
+ }).then(joinings => joinings.map(joining => joining.userListId));
+
+ const newJoinings: Map<string, { createdAt: Date; userId: string; userListId: string; }> = new Map();
+
+ // 重複しないようにIDを生成
+ const genId = (): string => {
+ let id: string;
+ do {
+ id = this.idService.genId();
+ } while (newJoinings.has(id));
+ return id;
+ };
+ for (const joining of oldJoinings) {
+ if (existingUserListIds.includes(joining.userListId)) continue; // skip if dst exists in this user's list
+ newJoinings.set(genId(), {
+ createdAt: new Date(),
+ userId: dst.id,
+ userListId: joining.userListId,
+ });
+ }
+
+ const arrayToInsert = Array.from(newJoinings.entries()).map(entry => ({ ...entry[1], id: entry[0] }));
+ await this.userListJoiningsRepository.insert(arrayToInsert);
+
+ // Have the proxy account follow the new account in the same way as UserListService.push
+ if (this.userEntityService.isRemoteUser(dst)) {
+ const proxy = await this.proxyAccountService.fetch();
+ if (proxy) {
+ this.queueService.createFollowJob([{ from: { id: proxy.id }, to: { id: dst.id } }]);
}
}
+ }
- return iObj;
+ @bindThis
+ private async adjustFollowingCounts(localFollowerIds: string[], oldAccount: User): Promise<void> {
+ if (localFollowerIds.length === 0) return;
+
+ // Set the old account's following and followers counts to 0.
+ await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 });
+
+ // Decrease following counts of local followers by 1.
+ await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
+
+ // Decrease follower counts of local followees by 1.
+ const oldFollowings = await this.followingsRepository.findBy({ followerId: oldAccount.id });
+ if (oldFollowings.length > 0) {
+ await this.usersRepository.decrement({ id: In(oldFollowings.map(following => following.followeeId)) }, 'followersCount', 1);
+ }
+
+ // Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
+ if (this.userEntityService.isRemoteUser(oldAccount)) {
+ this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
+ if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, false);
+ }
+ });
+ }
+
+ // FIXME: expensive?
+ for (const followerId of localFollowerIds) {
+ this.perUserFollowingChart.update({ id: followerId, host: null }, oldAccount, false);
+ }
}
/**
- * Create an alias of an old remote account.
+ * dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
*
- * The user's new profile will be published to the followers.
+ * @param dst movedToUrlを指定するユーザー
+ * @param check
+ * @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
+ * @returns Promise<LocalUser | RemoteUser | null>
*/
@bindThis
- public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
- await this.usersRepository.update(me.id, updates);
+ public async validateAlsoKnownAs(
+ dst: LocalUser | RemoteUser,
+ check: (oldUser: LocalUser | RemoteUser | null, newUser: LocalUser | RemoteUser) => boolean | Promise<boolean> = () => true,
+ instant = false,
+ ): Promise<LocalUser | RemoteUser | null> {
+ let resultUser: LocalUser | RemoteUser | null = null;
- // Publish meUpdated event
- const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
- detail: true,
- includeSecrets: true,
- });
- this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
-
- if (me.isLocked === false) {
- await this.userFollowingService.acceptAllFollowRequests(me);
+ if (this.userEntityService.isRemoteUser(dst)) {
+ if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
+ await this.apPersonService.updatePerson(dst.uri);
+ }
+ dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
}
- this.accountUpdateService.publishToFollowers(me.id);
+ if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) return null;
- return iObj;
+ const dstUri = this.userEntityService.getUserUri(dst);
+
+ for (const srcUri of dst.alsoKnownAs) {
+ try {
+ let src = await this.apPersonService.fetchPerson(srcUri);
+ if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
+
+ if (this.userEntityService.isRemoteUser(dst)) {
+ if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
+ await this.apPersonService.updatePerson(srcUri);
+ }
+
+ src = await this.apPersonService.fetchPerson(srcUri) ?? src;
+ }
+
+ if (src.movedToUri === dstUri) {
+ if (await check(resultUser, src)) {
+ resultUser = src;
+ }
+ if (instant && resultUser) return resultUser;
+ }
+ } catch {
+ /* skip if any error happens */
+ }
+ }
+
+ return resultUser;
}
}
diff --git a/packages/backend/src/core/AchievementService.ts b/packages/backend/src/core/AchievementService.ts
index 1ca38d8bb0..9e223f1492 100644
--- a/packages/backend/src/core/AchievementService.ts
+++ b/packages/backend/src/core/AchievementService.ts
@@ -64,6 +64,7 @@ export const ACHIEVEMENT_TYPES = [
'iLoveMisskey',
'foundTreasure',
'client30min',
+ 'client60min',
'noteDeletedWithin1min',
'postedAtLateNight',
'postedAt0min0sec',
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 166c78f479..2d4226a32d 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { Antenna } from '@/models/entities/Antenna.js';
import type { Note } from '@/models/entities/Note.js';
import type { User } from '@/models/entities/User.js';
diff --git a/packages/backend/src/core/AppLockService.ts b/packages/backend/src/core/AppLockService.ts
index ee179b7f01..8dd805552b 100644
--- a/packages/backend/src/core/AppLockService.ts
+++ b/packages/backend/src/core/AppLockService.ts
@@ -1,7 +1,7 @@
import { promisify } from 'node:util';
import { Inject, Injectable } from '@nestjs/common';
import redisLock from 'redis-lock';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index 561face5c3..cf1e81ffc8 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { LocalUser, User } from '@/models/entities/User.js';
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 8775536e4a..d3a1b1b024 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -50,6 +50,7 @@ import { WebhookService } from './WebhookService.js';
import { ProxyAccountService } from './ProxyAccountService.js';
import { UtilityService } from './UtilityService.js';
import { FileInfoService } from './FileInfoService.js';
+import { SearchService } from './SearchService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js';
@@ -171,6 +172,8 @@ const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', u
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
+
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart };
@@ -295,6 +298,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookService,
UtilityService,
FileInfoService,
+ SearchService,
ChartLoggerService,
FederationChart,
NotesChart,
@@ -413,6 +417,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookService,
$UtilityService,
$FileInfoService,
+ $SearchService,
$ChartLoggerService,
$FederationChart,
$NotesChart,
@@ -532,6 +537,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookService,
UtilityService,
FileInfoService,
+ SearchService,
FederationChart,
NotesChart,
UsersChart,
@@ -649,6 +655,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookService,
$UtilityService,
$FileInfoService,
+ $SearchService,
$FederationChart,
$NotesChart,
$UsersChart,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 185171dee2..93557ce617 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
@@ -197,6 +197,22 @@ export class CustomEmojiService {
emojis: await this.emojiEntityService.packDetailedMany(ids),
});
}
+
+ @bindThis
+ public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
+ await this.emojisRepository.update({
+ id: In(ids),
+ }, {
+ updatedAt: new Date(),
+ license: license,
+ });
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packDetailedMany(ids),
+ });
+ }
@bindThis
public async delete(id: Emoji['id']) {
diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts
index bd999c67da..bd535c6032 100644
--- a/packages/backend/src/core/DownloadService.ts
+++ b/packages/backend/src/core/DownloadService.ts
@@ -86,9 +86,13 @@ export class DownloadService {
const contentDisposition = res.headers['content-disposition'];
if (contentDisposition != null) {
- const parsed = parse(contentDisposition);
- if (parsed.parameters.filename) {
- filename = parsed.parameters.filename;
+ try {
+ const parsed = parse(contentDisposition);
+ if (parsed.parameters.filename) {
+ filename = parsed.parameters.filename;
+ }
+ } catch (e) {
+ this.logger.warn(`Failed to parse content-disposition: ${contentDisposition}`, { stack: e });
}
}
}).on('downloadProgress', (progress: Got.Progress) => {
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index c6258474ec..1483b55469 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -59,6 +59,8 @@ type AddFileArgs = {
uri?: string | null;
/** Mark file as sensitive */
sensitive?: boolean | null;
+ /** Extension to force */
+ ext?: string | null;
requestIp?: string | null;
requestHeaders?: Record<string, string> | null;
@@ -125,7 +127,7 @@ export class DriveService {
/***
* Save file
* @param path Path for original
- * @param name Name for original
+ * @param name Name for original (should be extention corrected)
* @param type Content-Type for original
* @param hash Hash for original
* @param size Size for original
@@ -151,7 +153,7 @@ export class DriveService {
}
// 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、
- // 許可されているファイル形式でしか拡張子をつけない
+ // 許可されているファイル形式でしかURLに拡張子をつけない
if (!FILE_TYPE_BROWSERSAFE.includes(type)) {
ext = '';
}
@@ -173,7 +175,7 @@ export class DriveService {
//#region Uploads
this.registerLogger.info(`uploading original: ${key}`);
const uploads = [
- this.upload(key, fs.createReadStream(path), type, ext, name),
+ this.upload(key, fs.createReadStream(path), type, null, name),
];
if (alts.webpublic) {
@@ -189,7 +191,7 @@ export class DriveService {
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
- uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext));
+ uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext, `${name}.thumbnail`));
}
await Promise.all(uploads);
@@ -396,8 +398,9 @@ export class DriveService {
);
}
+ // Expire oldest file (without avatar or banner) of remote user
@bindThis
- private async deleteOldFile(user: RemoteUser) {
+ private async expireOldFile(user: RemoteUser, driveCapacity: number) {
const q = this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id })
.andWhere('file.isLink = FALSE');
@@ -410,12 +413,17 @@ export class DriveService {
q.andWhere('file.id != :bannerId', { bannerId: user.bannerId });
}
+ //This selete is hard coded, be careful if change database schema
+ q.addSelect('SUM("file"."size") OVER (ORDER BY "file"."id" DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)', 'acc_usage');
q.orderBy('file.id', 'ASC');
- const oldFile = await q.getOne();
+ const fileList = await q.getRawMany();
+ const exceedFileIds = fileList.filter((x: any) => x.acc_usage > driveCapacity).map((x: any) => x.file_id);
- if (oldFile) {
- this.deleteFile(oldFile, true);
+ for (const fileId of exceedFileIds) {
+ const file = await this.driveFilesRepository.findOneBy({ id: fileId });
+ if (file == null) continue;
+ this.deleteFile(file, true);
}
}
@@ -437,10 +445,16 @@ export class DriveService {
sensitive = null,
requestIp = null,
requestHeaders = null,
+ ext = null,
}: AddFileArgs): Promise<DriveFile> {
let skipNsfwCheck = false;
const instance = await this.metaService.fetch();
- if (user == null) skipNsfwCheck = true;
+ const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
+ if (user == null) {
+ skipNsfwCheck = true;
+ } else if (userRoleNSFW) {
+ skipNsfwCheck = true;
+ }
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
@@ -468,7 +482,7 @@ export class DriveService {
// DriveFile.nameは256文字, validateFileNameは200文字制限であるため、
// extを付加してデータベースの文字数制限に当たることはまずない
(name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled',
- info.type.ext,
+ ext ?? info.type.ext,
);
if (user && !force) {
@@ -489,22 +503,19 @@ export class DriveService {
//#region Check drive usage
if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
+ const isLocalUser = this.userEntityService.isLocalUser(user);
const policies = await this.roleService.getUserPolicies(user.id);
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
- this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
-
// If usage limit exceeded
- if (usage + info.size > driveCapacity) {
- if (this.userEntityService.isLocalUser(user)) {
+ if (driveCapacity < usage + info.size) {
+ if (isLocalUser) {
throw new IdentifiableError('c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6', 'No free space.');
- } else {
- // (アバターまたはバナーを含まず)最も古いファイルを削除する
- this.deleteOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser);
}
+ await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as RemoteUser, driveCapacity - info.size);
}
}
//#endregion
@@ -565,6 +576,7 @@ export class DriveService {
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
+ if (userRoleNSFW) file.isSensitive = true;
if (url !== null) {
file.src = url;
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 56660ae0d0..2049bd4c60 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { InstancesRepository } from '@/models/index.js';
import type { Instance } from '@/models/entities/Instance.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
@@ -23,8 +23,8 @@ export class FederatedInstanceService {
private idService: IdService,
) {
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
- lifetime: 1000 * 60 * 60 * 24, // 24h
- memoryCacheLifetime: 1000 * 60 * 30, // 30m
+ lifetime: 1000 * 60 * 30, // 30m
+ memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => {
@@ -65,15 +65,18 @@ export class FederatedInstanceService {
}
@bindThis
- public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
- host = this.utilityService.toPuny(host);
-
- const cached = await this.federatedInstanceCache.get(host);
- if (cached == null) return;
+ public async update(id: Instance['id'], data: Partial<Instance>): Promise<void> {
+ const result = await this.instancesRepository.createQueryBuilder().update()
+ .set(data)
+ .where('id = :id', { id })
+ .returning('*')
+ .execute()
+ .then((response) => {
+ return response.raw[0];
+ });
+
+ const updated = result.raw[0];
- this.federatedInstanceCache.set(host, {
- ...cached,
- ...data,
- });
+ this.federatedInstanceCache.set(updated.host, updated);
}
}
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index bbc8b4332e..8103d5afe9 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { LoggerService } from '@/core/LoggerService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import type { DOMWindow } from 'jsdom';
type NodeInfo = {
@@ -42,6 +43,7 @@ export class FetchInstanceMetadataService {
private appLockService: AppLockService,
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
+ private federatedInstanceService: FederatedInstanceService,
) {
this.logger = this.loggerService.getLogger('metadata', 'cyan');
}
@@ -96,7 +98,7 @@ export class FetchInstanceMetadataService {
if (favicon) updates.faviconUrl = favicon;
if (themeColor) updates.themeColor = themeColor;
- await this.instancesRepository.update(instance.id, updates);
+ await this.federatedInstanceService.update(instance.id, updates);
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
} catch (e) {
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index e39b134b7e..b6cae5ea75 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -5,7 +5,7 @@ import * as stream from 'node:stream';
import * as util from 'node:util';
import { Injectable } from '@nestjs/common';
import { FSWatcher } from 'chokidar';
-import { fileTypeFromFile } from 'file-type';
+import * as fileType from 'file-type';
import FFmpeg from 'fluent-ffmpeg';
import isSvg from 'is-svg';
import probeImageSize from 'probe-image-size';
@@ -301,21 +301,34 @@ export class FileInfoService {
return fs.promises.access(path).then(() => true, () => false);
}
+ @bindThis
+ public fixMime(mime: string | fileType.MimeType): string {
+ // see https://github.com/misskey-dev/misskey/pull/10686
+ if (mime === "audio/x-flac") {
+ return "audio/flac";
+ }
+ if (mime === "audio/vnd.wave") {
+ return "audio/wav";
+ }
+
+ return mime;
+ }
+
/**
* Detect MIME Type and extension
*/
@bindThis
public async detectType(path: string): Promise<{
- mime: string;
- ext: string | null;
-}> {
+ mime: string;
+ ext: string | null;
+ }> {
// Check 0 byte
const fileSize = await this.getFileSize(path);
if (fileSize === 0) {
return TYPE_OCTET_STREAM;
}
- const type = await fileTypeFromFile(path);
+ const type = await fileType.fileTypeFromFile(path);
if (type) {
// XMLはSVGかもしれない
@@ -324,7 +337,7 @@ export class FileInfoService {
}
return {
- mime: type.mime,
+ mime: this.fixMime(type.mime),
ext: type.ext,
};
}
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 2c2687a90c..0ed5241148 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
import type { UserList } from '@/models/entities/UserList.js';
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index 1322927c2c..0b861be8d0 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { Meta } from '@/models/entities/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 79629cb2a8..364976e4a7 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -1,7 +1,7 @@
import { setImmediate } from 'node:timers/promises';
import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@@ -46,6 +46,7 @@ import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
+import { SearchService } from '@/core/SearchService.js';
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
@@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private apRendererService: ApRendererService,
private roleService: RoleService,
private metaService: MetaService,
+ private searchService: SearchService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart,
@@ -728,17 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private index(note: Note) {
- if (note.text == null || this.config.elasticsearch == null) return;
- /*
- es!.index({
- index: this.config.elasticsearch.index ?? 'misskey_note',
- id: note.id.toString(),
- body: {
- text: normalizeForSearch(note.text),
- userId: note.userId,
- userHost: note.userHost,
- },
- });*/
+ if (note.text == null && note.cw == null) return;
+
+ this.searchService.indexNote(note);
}
@bindThis
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 6691c42836..a245908c98 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -1,5 +1,5 @@
import { setTimeout } from 'node:timers/promises';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
@@ -111,7 +111,7 @@ export class NotificationService implements OnApplicationShutdown {
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
- if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
+ if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 9b44cf6413..a4c569bdec 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import push from 'web-push';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema';
diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts
index bac85d7a15..d4905a5f88 100644
--- a/packages/backend/src/core/QueueModule.ts
+++ b/packages/backend/src/core/QueueModule.ts
@@ -78,7 +78,7 @@ const $db: Provider = {
const $relationship: Provider = {
provide: 'queue:relationship',
- useFactory: (config: Config) => q(config, 'relationship'),
+ useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64),
inject: [DI.config],
};
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 375ac49911..b4ffffecc0 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
+import Bull from 'bull';
import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
+import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
import type httpSignature from '@peertube/http-signature';
-import Bull from 'bull';
@Injectable()
export class QueueService {
@@ -153,6 +154,16 @@ export class QueueService {
}
@bindThis
+ public createExportAntennasJob(user: ThinUser) {
+ return this.dbQueue.add('exportAntennas', {
+ user: { id: user.id },
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ @bindThis
public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
return this.dbQueue.add('importFollowing', {
user: { id: user.id },
@@ -236,6 +247,17 @@ export class QueueService {
}
@bindThis
+ public createImportAntennasJob(user: ThinUser, antenna: Antenna) {
+ return this.dbQueue.add('importAntennas', {
+ user: { id: user.id },
+ antenna,
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true,
+ });
+ }
+
+ @bindThis
public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
return this.dbQueue.add('deleteAccount', {
user: { id: user.id },
@@ -259,6 +281,12 @@ export class QueueService {
}
@bindThis
+ public createDelayedUnfollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string }[], delay: number) {
+ const jobs = followings.map(rel => this.generateRelationshipJobData('unfollow', rel, { delay }));
+ return this.relationshipQueue.addBulk(jobs);
+ }
+
+ @bindThis
public createBlockJob(blockings: { from: ThinUser, to: ThinUser, silent?: boolean }[]) {
const jobs = blockings.map(rel => this.generateRelationshipJobData('block', rel));
return this.relationshipQueue.addBulk(jobs);
@@ -271,7 +299,7 @@ export class QueueService {
}
@bindThis
- private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData): {
+ private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): {
name: string,
data: RelationshipJobData,
opts: Bull.JobOptions,
@@ -287,6 +315,7 @@ export class QueueService {
opts: {
removeOnComplete: true,
removeOnFail: true,
+ ...opts,
},
};
}
diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts
index b72dce5180..ff68c24219 100644
--- a/packages/backend/src/core/RemoteUserResolveService.ts
+++ b/packages/backend/src/core/RemoteUserResolveService.ts
@@ -4,7 +4,7 @@ import chalk from 'chalk';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/index.js';
-import type { RemoteUser, User } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { UtilityService } from '@/core/UtilityService.js';
@@ -33,7 +33,7 @@ export class RemoteUserResolveService {
}
@bindThis
- public async resolveUser(username: string, host: string | null): Promise<User> {
+ public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
const usernameLower = username.toLowerCase();
if (host == null) {
@@ -44,7 +44,7 @@ export class RemoteUserResolveService {
} else {
return u;
}
- });
+ }) as LocalUser;
}
host = this.utilityService.toPuny(host);
@@ -57,7 +57,7 @@ export class RemoteUserResolveService {
} else {
return u;
}
- });
+ }) as LocalUser;
}
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
@@ -109,7 +109,7 @@ export class RemoteUserResolveService {
if (u == null) {
throw new Error('user not found');
} else {
- return u;
+ return u as LocalUser | RemoteUser;
}
});
}
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 2a4271aa98..68087ccc3b 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
@@ -25,6 +25,7 @@ export type RolePolicies = {
canSearchNotes: boolean;
canHideAds: boolean;
driveCapacityMb: number;
+ alwaysMarkNsfw: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
@@ -45,6 +46,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canSearchNotes: false,
canHideAds: false,
driveCapacityMb: 100,
+ alwaysMarkNsfw: false,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
@@ -279,6 +281,7 @@ export class RoleService implements OnApplicationShutdown {
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
+ alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
new file mode 100644
index 0000000000..e68fde088d
--- /dev/null
+++ b/packages/backend/src/core/SearchService.ts
@@ -0,0 +1,178 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { bindThis } from '@/decorators.js';
+import { Note } from '@/models/entities/Note.js';
+import { User } from '@/models/index.js';
+import type { NotesRepository } from '@/models/index.js';
+import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { QueryService } from '@/core/QueryService.js';
+import { IdService } from '@/core/IdService.js';
+import type { Index, MeiliSearch } from 'meilisearch';
+
+type K = string;
+type V = string | number | boolean;
+type Q =
+ { op: '=', k: K, v: V } |
+ { op: '!=', k: K, v: V } |
+ { op: '>', k: K, v: number } |
+ { op: '<', k: K, v: number } |
+ { op: '>=', k: K, v: number } |
+ { op: '<=', k: K, v: number } |
+ { op: 'and', qs: Q[] } |
+ { op: 'or', qs: Q[] } |
+ { op: 'not', q: Q };
+
+function compileValue(value: V): string {
+ if (typeof value === 'string') {
+ return `'${value}'`; // TODO: escape
+ } else if (typeof value === 'number') {
+ return value.toString();
+ } else if (typeof value === 'boolean') {
+ return value.toString();
+ }
+ throw new Error('unrecognized value');
+}
+
+function compileQuery(q: Q): string {
+ switch (q.op) {
+ case '=': return `(${q.k} = ${compileValue(q.v)})`;
+ case '!=': return `(${q.k} != ${compileValue(q.v)})`;
+ case '>': return `(${q.k} > ${compileValue(q.v)})`;
+ case '<': return `(${q.k} < ${compileValue(q.v)})`;
+ case '>=': return `(${q.k} >= ${compileValue(q.v)})`;
+ case '<=': return `(${q.k} <= ${compileValue(q.v)})`;
+ case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`;
+ case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`;
+ case 'not': return `(NOT ${compileQuery(q.q)})`;
+ default: throw new Error('unrecognized query operator');
+ }
+}
+
+@Injectable()
+export class SearchService {
+ private meilisearchNoteIndex: Index | null = null;
+
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.meilisearch)
+ private meilisearch: MeiliSearch | null,
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private queryService: QueryService,
+ private idService: IdService,
+ ) {
+ if (meilisearch) {
+ this.meilisearchNoteIndex = meilisearch.index('notes');
+ this.meilisearchNoteIndex.updateSettings({
+ searchableAttributes: [
+ 'text',
+ 'cw',
+ ],
+ sortableAttributes: [
+ 'createdAt',
+ ],
+ filterableAttributes: [
+ 'createdAt',
+ 'userId',
+ 'userHost',
+ 'channelId',
+ ],
+ typoTolerance: {
+ enabled: false,
+ },
+ pagination: {
+ maxTotalHits: 10000,
+ },
+ });
+ }
+ }
+
+ @bindThis
+ public async indexNote(note: Note): Promise<void> {
+ if (note.text == null && note.cw == null) return;
+ if (!['home', 'public'].includes(note.visibility)) return;
+
+ if (this.meilisearch) {
+ this.meilisearchNoteIndex!.addDocuments([{
+ id: note.id,
+ createdAt: note.createdAt.getTime(),
+ userId: note.userId,
+ userHost: note.userHost,
+ channelId: note.channelId,
+ cw: note.cw,
+ text: note.text,
+ }], {
+ primaryKey: 'id',
+ });
+ }
+ }
+
+ @bindThis
+ public async searchNote(q: string, me: User | null, opts: {
+ userId?: Note['userId'] | null;
+ channelId?: Note['channelId'] | null;
+ host?: string | null;
+ }, pagination: {
+ untilId?: Note['id'];
+ sinceId?: Note['id'];
+ limit?: number;
+ }): Promise<Note[]> {
+ if (this.meilisearch) {
+ const filter: Q = {
+ op: 'and',
+ qs: [],
+ };
+ if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() });
+ if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
+ if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
+ if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
+ if (opts.host) {
+ if (opts.host === '.') {
+ // TODO: Meilisearchが2023/05/07現在値がNULLかどうかのクエリが書けない
+ } else {
+ filter.qs.push({ op: '=', k: 'userHost', v: opts.host });
+ }
+ }
+ const res = await this.meilisearchNoteIndex!.search(q, {
+ sort: ['createdAt:desc'],
+ matchingStrategy: 'all',
+ attributesToRetrieve: ['id', 'createdAt'],
+ filter: compileQuery(filter),
+ limit: pagination.limit,
+ });
+ if (res.hits.length === 0) return [];
+ const notes = await this.notesRepository.findBy({
+ id: In(res.hits.map(x => x.id)),
+ });
+ return notes.sort((a, b) => a.id > b.id ? -1 : 1);
+ } else {
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
+
+ if (opts.userId) {
+ query.andWhere('note.userId = :userId', { userId: opts.userId });
+ } else if (opts.channelId) {
+ query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
+ }
+
+ query
+ .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
+
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) this.queryService.generateMutedUserQuery(query, me);
+ if (me) this.queryService.generateBlockedUserQuery(query, me);
+
+ return await query.take(pagination.limit).getMany();
+ }
+ }
+}
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index d7bc05b8bd..29eb65fda4 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -13,8 +13,9 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
-import UsersChart from './chart/charts/users.js';
-import { UtilityService } from './UtilityService.js';
+import UsersChart from '@/core/chart/charts/users.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { MetaService } from '@/core/MetaService.js';
@Injectable()
export class SignupService {
@@ -34,6 +35,7 @@ export class SignupService {
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
+ private metaService: MetaService,
private usersChart: UsersChart,
) {
}
@@ -44,6 +46,7 @@ export class SignupService {
password?: string | null;
passwordHash?: UserProfile['password'] | null;
host?: string | null;
+ ignorePreservedUsernames?: boolean;
}) {
const { username, password, passwordHash, host } = opts;
let hash = passwordHash;
@@ -76,7 +79,17 @@ export class SignupService {
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
throw new Error('USED_USERNAME');
}
-
+
+ const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
+
+ if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
+ const instance = await this.metaService.fetch(true);
+ const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+ if (isPreserved) {
+ throw new Error('USED_USERNAME');
+ }
+ }
+
const keyPair = await new Promise<string[]>((res, rej) =>
generateKeyPair('rsa', {
modulusLength: 4096,
@@ -112,9 +125,7 @@ export class SignupService {
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
- isRoot: (await this.usersRepository.countBy({
- host: IsNull(),
- })) === 0,
+ isRoot: isTheFirstUser,
}));
await transactionalEntityManager.save(new UserKeypair({
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index a8eded6733..7d90bc2c08 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
-import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
+import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
@@ -22,6 +22,8 @@ import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import Logger from '../logger.js';
+import { IsNull } from 'typeorm';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
const logger = new Logger('following/create');
@@ -73,6 +75,7 @@ export class UserFollowingService implements OnModuleInit {
private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService,
private apRendererService: ApRendererService,
+ private accountMoveService: AccountMoveService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@@ -87,7 +90,7 @@ export class UserFollowingService implements OnModuleInit {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }),
- ]);
+ ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
// check blocking
const [blocking, blocked] = await Promise.all([
@@ -137,6 +140,20 @@ export class UserFollowingService implements OnModuleInit {
if (followed) autoAccept = true;
}
+ // Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
+ if (followee.isLocked && !autoAccept) {
+ autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
+ follower,
+ (oldSrc, newSrc) => this.followingsRepository.exist({
+ where: {
+ followeeId: followee.id,
+ followerId: newSrc.id,
+ },
+ }),
+ true,
+ ));
+ }
+
if (!autoAccept) {
await this.createFollowRequest(follower, followee, requestId);
return;
@@ -210,32 +227,40 @@ export class UserFollowingService implements OnModuleInit {
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
- //#region Increment counts
- await Promise.all([
- this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
- this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
+ const [followeeUser, followerUser] = await Promise.all([
+ this.usersRepository.findOneByOrFail({ id: followee.id }),
+ this.usersRepository.findOneByOrFail({ id: follower.id }),
]);
- //#endregion
- //#region Update instance stats
- if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
- this.federatedInstanceService.fetch(follower.host).then(async i => {
- this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowing(i.host, true);
- }
- });
- } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- this.federatedInstanceService.fetch(followee.host).then(async i => {
- this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowers(i.host, true);
- }
- });
- }
- //#endregion
+ // Neither followee nor follower has moved.
+ if (!followeeUser.movedToUri && !followerUser.movedToUri) {
+ //#region Increment counts
+ await Promise.all([
+ this.usersRepository.increment({ id: follower.id }, 'followingCount', 1),
+ this.usersRepository.increment({ id: followee.id }, 'followersCount', 1),
+ ]);
+ //#endregion
+
+ //#region Update instance stats
+ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+ this.federatedInstanceService.fetch(follower.host).then(async i => {
+ this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
+ if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowing(i.host, true);
+ }
+ });
+ } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+ this.federatedInstanceService.fetch(followee.host).then(async i => {
+ this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
+ if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, true);
+ }
+ });
+ }
+ //#endregion
- this.perUserFollowingChart.update(follower, followee, true);
+ this.perUserFollowingChart.update(follower, followee, true);
+ }
// Publish follow event
if (this.userEntityService.isLocalUser(follower) && !silent) {
@@ -283,12 +308,18 @@ export class UserFollowingService implements OnModuleInit {
},
silent = false,
): Promise<void> {
- const following = await this.followingsRepository.findOneBy({
- followerId: follower.id,
- followeeId: followee.id,
+ const following = await this.followingsRepository.findOne({
+ relations: {
+ follower: true,
+ followee: true,
+ },
+ where: {
+ followerId: follower.id,
+ followeeId: followee.id,
+ }
});
- if (following == null) {
+ if (following === null || !following.follower || !following.followee) {
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return;
}
@@ -297,7 +328,7 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id);
- this.decrementFollowing(follower, followee);
+ this.decrementFollowing(following.follower, following.followee);
// Publish unfollow event
if (!silent && this.userEntityService.isLocalUser(follower)) {
@@ -316,50 +347,87 @@ export class UserFollowingService implements OnModuleInit {
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
+ const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser), follower));
this.queueService.deliver(follower, content, followee.inbox, false);
}
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
// local user has null host
- const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
+ const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as PartialRemoteUser, followee as PartialLocalUser), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
}
@bindThis
private async decrementFollowing(
- follower: { id: User['id']; host: User['host']; },
- followee: { id: User['id']; host: User['host']; },
+ follower: User,
+ followee: User,
): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
- //#region Decrement following / followers counts
- await Promise.all([
- this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
- this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
- ]);
- //#endregion
+ // Neither followee nor follower has moved.
+ if (!follower.movedToUri && !followee.movedToUri) {
+ //#region Decrement following / followers counts
+ await Promise.all([
+ this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
+ this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
+ ]);
+ //#endregion
- //#region Update instance stats
- if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
- this.federatedInstanceService.fetch(follower.host).then(async i => {
- this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowing(i.host, false);
- }
- });
- } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- this.federatedInstanceService.fetch(followee.host).then(async i => {
- this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
- if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowers(i.host, false);
- }
- });
- }
- //#endregion
+ //#region Update instance stats
+ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+ this.federatedInstanceService.fetch(follower.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
+ if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowing(i.host, false);
+ }
+ });
+ } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+ this.federatedInstanceService.fetch(followee.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
+ if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, false);
+ }
+ });
+ }
+ //#endregion
- this.perUserFollowingChart.update(follower, followee, false);
+ this.perUserFollowingChart.update(follower, followee, false);
+ } else {
+ // Adjust following/followers counts
+ for (const user of [follower, followee]) {
+ if (user.movedToUri) continue; // No need to update if the user has already moved.
+
+ const nonMovedFollowees = await this.followingsRepository.count({
+ relations: {
+ followee: true,
+ },
+ where: {
+ followerId: user.id,
+ followee: {
+ movedToUri: IsNull(),
+ }
+ }
+ });
+ const nonMovedFollowers = await this.followingsRepository.count({
+ relations: {
+ follower: true,
+ },
+ where: {
+ followeeId: user.id,
+ follower: {
+ movedToUri: IsNull(),
+ }
+ }
+ });
+ await this.usersRepository.update(
+ { id: user.id },
+ { followingCount: nonMovedFollowees, followersCount: nonMovedFollowers },
+ );
+ }
+
+ // TODO: adjust charts
+ }
}
@bindThis
@@ -415,7 +483,7 @@ export class UserFollowingService implements OnModuleInit {
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
+ const content = this.apRendererService.addContext(this.apRendererService.renderFollow(follower as PartialLocalUser, followee as PartialRemoteUser, requestId ?? `${this.config.url}/follows/${followRequest.id}`));
this.queueService.deliver(follower, content, followee.inbox, false);
}
}
@@ -430,7 +498,7 @@ export class UserFollowingService implements OnModuleInit {
},
): Promise<void> {
if (this.userEntityService.isRemoteUser(followee)) {
- const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
+ const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower as PartialLocalUser | PartialRemoteUser, followee as PartialRemoteUser), follower));
if (this.userEntityService.isLocalUser(follower)) { // 本来このチェックは不要だけどTSに怒られるので
this.queueService.deliver(follower, content, followee.inbox, false);
@@ -475,7 +543,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
- const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, request.requestId!), followee));
+ const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as PartialLocalUser, request.requestId!), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
@@ -562,15 +630,22 @@ export class UserFollowingService implements OnModuleInit {
*/
@bindThis
private async removeFollow(followee: Both, follower: Both): Promise<void> {
- const following = await this.followingsRepository.findOneBy({
- followeeId: followee.id,
- followerId: follower.id,
+ const following = await this.followingsRepository.findOne({
+ relations: {
+ followee: true,
+ follower: true,
+ },
+ where: {
+ followeeId: followee.id,
+ followerId: follower.id,
+ }
});
- if (!following) return;
+ if (!following || !following.followee || !following.follower) return;
await this.followingsRepository.delete(following.id);
- this.decrementFollowing(follower, followee);
+
+ this.decrementFollowing(following.follower, following.followee);
}
/**
diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts
index 22a9fb2b8e..72c35c529c 100644
--- a/packages/backend/src/core/UserKeypairService.ts
+++ b/packages/backend/src/core/UserKeypairService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { UserKeypairsRepository } from '@/models/index.js';
import { RedisKVCache } from '@/misc/cache.js';
diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts
index d00bb89c76..b197d335d8 100644
--- a/packages/backend/src/core/UserSuspendService.ts
+++ b/packages/backend/src/core/UserSuspendService.ts
@@ -35,7 +35,7 @@ export class UserSuspendService {
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
- const content = this.apRendererService.addContext(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user));
+ const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
const queue: string[] = [];
@@ -65,7 +65,7 @@ export class UserSuspendService {
if (this.userEntityService.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信
- const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(`${this.config.url}/users/${user.id}`, user), user));
+ const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
const queue: string[] = [];
diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts
index 69df2d0c1b..3ee7990643 100644
--- a/packages/backend/src/core/WebfingerService.ts
+++ b/packages/backend/src/core/WebfingerService.ts
@@ -43,7 +43,8 @@ export class WebfingerService {
const m = query.match(/^([^@]+)@(.*)/);
if (m) {
const hostname = m[2];
- return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
+ const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true';
+ return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`;
}
throw new Error(`Invalid query (${query})`);
diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts
index 926115613b..57baade777 100644
--- a/packages/backend/src/core/WebhookService.ts
+++ b/packages/backend/src/core/WebhookService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/index.js';
import type { Webhook } from '@/models/entities/Webhook.js';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts
index 4b032be89a..2b404ebeca 100644
--- a/packages/backend/src/core/activitypub/ApDbResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts
@@ -8,7 +8,7 @@ import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { CacheService } from '@/core/CacheService.js';
import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js';
-import { RemoteUser, User } from '@/models/entities/User.js';
+import { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { getApId } from './type.js';
import { ApPersonService } from './models/ApPersonService.js';
import type { IObject } from './type.js';
@@ -101,7 +101,7 @@ export class ApDbResolverService {
* AP Person => Misskey User in DB
*/
@bindThis
- public async getUserFromApId(value: string | IObject): Promise<User | null> {
+ public async getUserFromApId(value: string | IObject): Promise<LocalUser | RemoteUser | null> {
const parsed = this.parseUri(value);
if (parsed.local) {
@@ -109,11 +109,11 @@ export class ApDbResolverService {
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
id: parsed.id,
- }).then(x => x ?? undefined)) ?? null;
+ }).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
} else {
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
uri: parsed.uri,
- }));
+ })) as RemoteUser | null;
}
}
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index 3fca0bb1fd..efef777fb0 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import { In, IsNull } from 'typeorm';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
@@ -13,13 +13,15 @@ import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
import { AppLockService } from '@/core/AppLockService.js';
import type Logger from '@/logger.js';
import { MetaService } from '@/core/MetaService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
import { IdService } from '@/core/IdService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { CacheService } from '@/core/CacheService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
-import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
+import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
@@ -76,6 +78,8 @@ export class ApInboxService {
private apNoteService: ApNoteService,
private apPersonService: ApPersonService,
private apQuestionService: ApQuestionService,
+ private accountMoveService: AccountMoveService,
+ private cacheService: CacheService,
private queueService: QueueService,
) {
this.logger = this.apLoggerService.logger;
@@ -140,7 +144,7 @@ export class ApInboxService {
} else if (isFlag(activity)) {
await this.flag(actor, activity);
} else if (isMove(activity)) {
- //await this.move(actor, activity);
+ await this.move(actor, activity);
} else {
this.logger.warn(`unrecognized activity type: ${activity.type}`);
}
@@ -158,6 +162,7 @@ export class ApInboxService {
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
}
+ // don't queue because the sender may attempt again when timeout
await this.userFollowingService.follow(actor, followee, activity.id);
return 'ok';
}
@@ -596,6 +601,7 @@ export class ApInboxService {
throw e;
});
+ // don't queue because the sender may attempt again when timeout
if (isFollow(object)) return await this.undoFollow(actor, object);
if (isBlock(object)) return await this.undoBlock(actor, object);
if (isLike(object)) return await this.undoLike(actor, object);
@@ -736,53 +742,7 @@ export class ApInboxService {
// fetch the new and old accounts
const targetUri = getApHrefNullable(activity.target);
if (!targetUri) return 'skip: invalid activity target';
- let new_acc = await this.apPersonService.resolvePerson(targetUri);
- let old_acc = await this.apPersonService.resolvePerson(actor.uri);
- // update them if they're remote
- if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
- if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
-
- // retrieve updated users
- new_acc = await this.apPersonService.resolvePerson(targetUri);
- old_acc = await this.apPersonService.resolvePerson(actor.uri);
-
- // check if alsoKnownAs of the new account is valid
- let isValidMove = true;
- if (old_acc.uri) {
- if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
- isValidMove = false;
- }
- } else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
- isValidMove = false;
- }
- if (!isValidMove) {
- return 'skip: accounts invalid';
- }
-
- // add target uri to movedToUri in order to indicate that the user has moved
- await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
-
- // follow the new account and unfollow the old one
- const followings = await this.followingsRepository.find({
- relations: {
- follower: true,
- },
- where: {
- followeeId: old_acc.id,
- followerHost: IsNull(), // follower is local
- },
- });
- for (const following of followings) {
- if (!following.follower) continue;
- try {
- await this.userFollowingService.follow(following.follower, new_acc);
- await this.userFollowingService.unfollow(following.follower, old_acc);
- } catch {
- /* empty */
- }
- }
-
- return 'ok';
+ return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do';
}
}
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 0b22aa9bcf..60e19bfca5 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -5,7 +5,7 @@ import { v4 as uuid } from 'uuid';
import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
+import type { PartialLocalUser, LocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import type { IMentionedRemoteUsers, Note } from '@/models/entities/Note.js';
import type { Blocking } from '@/models/entities/Blocking.js';
import type { Relay } from '@/models/entities/Relay.js';
@@ -66,7 +66,7 @@ export class ApRendererService {
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
return {
type: 'Accept',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
object,
};
}
@@ -75,7 +75,7 @@ export class ApRendererService {
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
return {
type: 'Add',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
target,
object,
};
@@ -83,7 +83,7 @@ export class ApRendererService {
@bindThis
public renderAnnounce(object: any, note: Note): IAnnounce {
- const attributedTo = `${this.config.url}/users/${note.userId}`;
+ const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
let to: string[] = [];
let cc: string[] = [];
@@ -103,7 +103,7 @@ export class ApRendererService {
return {
id: `${this.config.url}/notes/${note.id}/activity`,
- actor: `${this.config.url}/users/${note.userId}`,
+ actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Announce',
published: note.createdAt.toISOString(),
to,
@@ -126,7 +126,7 @@ export class ApRendererService {
return {
type: 'Block',
id: `${this.config.url}/blocks/${block.id}`,
- actor: `${this.config.url}/users/${block.blockerId}`,
+ actor: this.userEntityService.genLocalUserUri(block.blockerId),
object: block.blockee.uri,
};
}
@@ -135,7 +135,7 @@ export class ApRendererService {
public renderCreate(object: IObject, note: Note): ICreate {
const activity = {
id: `${this.config.url}/notes/${note.id}/activity`,
- actor: `${this.config.url}/users/${note.userId}`,
+ actor: this.userEntityService.genLocalUserUri(note.userId),
type: 'Create',
published: note.createdAt.toISOString(),
object,
@@ -151,7 +151,7 @@ export class ApRendererService {
public renderDelete(object: IObject | string, user: { id: User['id']; host: null }): IDelete {
return {
type: 'Delete',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
object,
published: new Date().toISOString(),
};
@@ -188,7 +188,7 @@ export class ApRendererService {
public renderFlag(user: LocalUser, object: IObject | string, content: string): IFlag {
return {
type: 'Flag',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
content,
object,
};
@@ -199,7 +199,7 @@ export class ApRendererService {
return {
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
type: 'Follow',
- actor: `${this.config.url}/users/${relayActor.id}`,
+ actor: this.userEntityService.genLocalUserUri(relayActor.id),
object: 'https://www.w3.org/ns/activitystreams#Public',
};
}
@@ -210,21 +210,21 @@ export class ApRendererService {
*/
@bindThis
public async renderFollowUser(id: User['id']) {
- const user = await this.usersRepository.findOneByOrFail({ id: id });
- return this.userEntityService.isLocalUser(user) ? `${this.config.url}/users/${user.id}` : user.uri;
+ const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
+ return this.userEntityService.getUserUri(user);
}
@bindThis
public renderFollow(
- follower: { id: User['id']; host: User['host']; uri: User['host'] },
- followee: { id: User['id']; host: User['host']; uri: User['host'] },
+ follower: PartialLocalUser | PartialRemoteUser,
+ followee: PartialLocalUser | PartialRemoteUser,
requestId?: string,
): IFollow {
return {
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow',
- actor: this.userEntityService.isLocalUser(follower) ? `${this.config.url}/users/${follower.id}` : follower.uri!,
- object: this.userEntityService.isLocalUser(followee) ? `${this.config.url}/users/${followee.id}` : followee.uri!,
+ actor: this.userEntityService.getUserUri(follower)!,
+ object: this.userEntityService.getUserUri(followee)!,
};
}
@@ -252,7 +252,7 @@ export class ApRendererService {
return {
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
type: 'Key',
- owner: `${this.config.url}/users/${user.id}`,
+ owner: this.userEntityService.genLocalUserUri(user.id),
publicKeyPem: createPublicKey(key.publicKey).export({
type: 'spki',
format: 'pem',
@@ -284,21 +284,21 @@ export class ApRendererService {
}
@bindThis
- public renderMention(mention: User): IApMention {
+ public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
return {
type: 'Mention',
- href: this.userEntityService.isRemoteUser(mention) ? mention.uri! : `${this.config.url}/users/${(mention as LocalUser).id}`,
+ href: this.userEntityService.getUserUri(mention)!,
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
};
}
@bindThis
public renderMove(
- src: { id: User['id']; host: User['host']; uri: User['host'] },
- dst: { id: User['id']; host: User['host']; uri: User['host'] },
+ src: PartialLocalUser | PartialRemoteUser,
+ dst: PartialLocalUser | PartialRemoteUser,
): IMove {
- const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
- const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
+ const actor = this.userEntityService.getUserUri(src)!;
+ const target = this.userEntityService.getUserUri(dst)!;
return {
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
actor,
@@ -351,7 +351,7 @@ export class ApRendererService {
}
}
- const attributedTo = `${this.config.url}/users/${note.userId}`;
+ const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
@@ -376,7 +376,7 @@ export class ApRendererService {
}) : [];
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
- const mentionTags = mentionedUsers.map(u => this.renderMention(u));
+ const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
const files = await getPromisedFiles(note.fileIds);
@@ -450,7 +450,7 @@ export class ApRendererService {
@bindThis
public async renderPerson(user: LocalUser) {
- const id = `${this.config.url}/users/${user.id}`;
+ const id = this.userEntityService.genLocalUserUri(user.id);
const isSystem = !!user.username.match(/\./);
const [avatar, banner, profile] = await Promise.all([
@@ -538,7 +538,7 @@ export class ApRendererService {
return {
type: 'Question',
id: `${this.config.url}/questions/${note.id}`,
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
content: note.text ?? '',
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
name: text,
@@ -555,7 +555,7 @@ export class ApRendererService {
public renderReject(object: any, user: { id: User['id'] }): IReject {
return {
type: 'Reject',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
object,
};
}
@@ -564,7 +564,7 @@ export class ApRendererService {
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
return {
type: 'Remove',
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
target,
object,
};
@@ -585,7 +585,7 @@ export class ApRendererService {
return {
type: 'Undo',
...(id ? { id } : {}),
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
object,
published: new Date().toISOString(),
};
@@ -595,7 +595,7 @@ export class ApRendererService {
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
return {
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Update',
to: ['https://www.w3.org/ns/activitystreams#Public'],
object,
@@ -607,14 +607,14 @@ export class ApRendererService {
public renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: RemoteUser): ICreate {
return {
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
- actor: `${this.config.url}/users/${user.id}`,
+ actor: this.userEntityService.genLocalUserUri(user.id),
type: 'Create',
to: [pollOwner.uri],
published: new Date().toISOString(),
object: {
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
type: 'Note',
- attributedTo: `${this.config.url}/users/${user.id}`,
+ attributedTo: this.userEntityService.genLocalUserUri(user.id),
to: [pollOwner.uri],
inReplyTo: note.uri,
name: poll.choices[vote.choice],
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index df7bb46405..d3e0345c9c 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { LocalUser } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -151,7 +151,7 @@ export class Resolver {
return Promise.all(
[parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })),
)
- .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower, followee, url)));
+ .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url)));
default:
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
}
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 3b671af127..0043907c21 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -12,6 +12,7 @@ import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
+import { checkHttps } from '@/misc/check-https.js';
@Injectable()
export class ApImageService {
@@ -48,8 +49,8 @@ export class ApImageService {
throw new Error('invalid image: url not privided');
}
- if (!image.url.startsWith('https://')) {
- throw new Error('invalid image: unexpected shcema of url: ' + image.url);
+ if (!checkHttps(image.url)) {
+ throw new Error('invalid image: unexpected schema of url: ' + image.url);
}
this.logger.info(`Creating the Image: ${image.url}`);
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 5ca5f6e843..87a9db405f 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -32,6 +32,7 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
+import { checkHttps } from '@/misc/check-https.js';
@Injectable()
export class ApNoteService {
@@ -71,7 +72,7 @@ export class ApNoteService {
}
@bindThis
- public validateNote(object: any, uri: string) {
+ public validateNote(object: IObject, uri: string) {
const expectHost = this.utilityService.extractDbHost(uri);
if (object == null) {
@@ -85,9 +86,10 @@ export class ApNoteService {
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
-
- if (object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
- return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.attributedTo)}`);
+
+ const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
+ if (object.attributedTo && actualHost !== expectHost) {
+ return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
return null;
@@ -129,13 +131,13 @@ export class ApNoteService {
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
- if (note.id && !note.id.startsWith('https://')) {
+ if (note.id && !checkHttps(note.id)) {
throw new Error('unexpected shcema of note.id: ' + note.id);
}
const url = getOneApHrefNullable(note.url);
- if (url && !url.startsWith('https://')) {
+ if (url && !checkHttps(url)) {
throw new Error('unexpected shcema of note url: ' + url);
}
@@ -148,7 +150,7 @@ export class ApNoteService {
if (actor.isSuspended) {
throw new Error('actor has been suspended');
}
-
+
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers;
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 21797cfcb7..eea1d1b848 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
-import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
+import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
-import type { RemoteUser } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
import { User } from '@/models/entities/User.js';
import { truncate } from '@/misc/truncate.js';
import type { CacheService } from '@/core/CacheService.js';
@@ -42,6 +42,8 @@ import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { ApImageService } from './ApImageService.js';
import type { IActor, IObject } from '../type.js';
+import type { AccountMoveService } from '@/core/AccountMoveService.js';
+import { checkHttps } from '@/misc/check-https.js';
const nameLength = 128;
const summaryLength = 2048;
@@ -66,6 +68,7 @@ export class ApPersonService implements OnModuleInit {
private usersChart: UsersChart;
private instanceChart: InstanceChart;
private apLoggerService: ApLoggerService;
+ private accountMoveService: AccountMoveService;
private logger: Logger;
constructor(
@@ -131,9 +134,16 @@ export class ApPersonService implements OnModuleInit {
this.usersChart = this.moduleRef.get('UsersChart');
this.instanceChart = this.moduleRef.get('InstanceChart');
this.apLoggerService = this.moduleRef.get('ApLoggerService');
+ this.accountMoveService = this.moduleRef.get('AccountMoveService');
this.logger = this.apLoggerService.logger;
}
+ private punyHost(url: string): string {
+ const urlObj = new URL(url);
+ const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
+ return host;
+ }
+
/**
* Validate and convert to actor object
* @param x Fetched object
@@ -141,7 +151,7 @@ export class ApPersonService implements OnModuleInit {
*/
@bindThis
private validateActor(x: IObject, uri: string): IActor {
- const expectHost = this.utilityService.toPuny(new URL(uri).hostname);
+ const expectHost = this.punyHost(uri);
if (x == null) {
throw new Error('invalid Actor: object is null');
@@ -182,7 +192,7 @@ export class ApPersonService implements OnModuleInit {
x.summary = truncate(x.summary, summaryLength);
}
- const idHost = this.utilityService.toPuny(new URL(x.id!).hostname);
+ const idHost = this.punyHost(x.id);
if (idHost !== expectHost) {
throw new Error('invalid Actor: id has different host');
}
@@ -192,7 +202,7 @@ export class ApPersonService implements OnModuleInit {
throw new Error('invalid Actor: publicKey.id is not a string');
}
- const publicKeyIdHost = this.utilityService.toPuny(new URL(x.publicKey.id).hostname);
+ const publicKeyIdHost = this.punyHost(x.publicKey.id);
if (publicKeyIdHost !== expectHost) {
throw new Error('invalid Actor: publicKey.id has different host');
}
@@ -202,27 +212,27 @@ export class ApPersonService implements OnModuleInit {
}
/**
- * Personをフェッチします。
+ * uriからUser(Person)をフェッチします。
*
- * Misskeyに対象のPersonが登録されていればそれを返します。
+ * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。
*/
@bindThis
- public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
+ public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
if (typeof uri !== 'string') throw new Error('uri is not string');
- const cached = this.cacheService.uriPersonCache.get(uri);
+ const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
- if (uri.startsWith(this.config.url + '/')) {
+ if (uri.startsWith(`${this.config.url}/`)) {
const id = uri.split('/').pop();
- const u = await this.usersRepository.findOneBy({ id });
+ const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
if (u) this.cacheService.uriPersonCache.set(uri, u);
return u;
}
//#region このサーバーに既に登録されていたらそれを返す
- const exist = await this.usersRepository.findOneBy({ uri });
+ const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
if (exist) {
this.cacheService.uriPersonCache.set(uri, exist);
@@ -237,7 +247,7 @@ export class ApPersonService implements OnModuleInit {
* Personを作成します。
*/
@bindThis
- public async createPerson(uri: string, resolver?: Resolver): Promise<User> {
+ public async createPerson(uri: string, resolver?: Resolver): Promise<RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
if (uri.startsWith(this.config.url)) {
@@ -252,7 +262,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Creating the Person: ${person.id}`);
- const host = this.utilityService.toPuny(new URL(object.id).hostname);
+ const host = this.punyHost(object.id);
const { fields } = this.analyzeAttachments(person.attachment ?? []);
@@ -264,8 +274,8 @@ export class ApPersonService implements OnModuleInit {
const url = getOneApHrefNullable(person.url);
- if (url && !url.startsWith('https://')) {
- throw new Error('unexpected shcema of person url: ' + url);
+ if (url && !checkHttps(url)) {
+ throw new Error('unexpected schema of person url: ' + url);
}
// Create user
@@ -282,6 +292,7 @@ export class ApPersonService implements OnModuleInit {
name: truncate(person.name, nameLength),
isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo,
+ movedAt: person.movedTo ? new Date() : null,
alsoKnownAs: person.alsoKnownAs,
isExplorable: !!person.discoverable,
username: person.preferredUsername,
@@ -404,23 +415,26 @@ export class ApPersonService implements OnModuleInit {
/**
* Personの情報を更新します。
* Misskeyに対象のPersonが登録されていなければ無視します。
+ * もしアカウントの移行が確認された場合、アカウント移行処理を行います。
+ *
* @param uri URI of Person
* @param resolver Resolver
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
+ * @param movePreventUris ここに指定されたURIがPersonのmovedToに指定されていたり10回より多く回っている場合これ以上アカウント移行を行わない(無限ループ防止)
*/
@bindThis
- public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
+ public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ
- if (uri.startsWith(this.config.url + '/')) {
+ if (uri.startsWith(`${this.config.url}/`)) {
return;
}
//#region このサーバーに既に登録されているか
- const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser;
+ const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
- if (exist == null) {
+ if (exist === null) {
return;
}
//#endregion
@@ -459,8 +473,8 @@ export class ApPersonService implements OnModuleInit {
const url = getOneApHrefNullable(person.url);
- if (url && !url.startsWith('https://')) {
- throw new Error('unexpected shcema of person url: ' + url);
+ if (url && !checkHttps(url)) {
+ throw new Error('unexpected schema of person url: ' + url);
}
const updates = {
@@ -478,7 +492,16 @@ export class ApPersonService implements OnModuleInit {
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: !!person.discoverable,
- } as Partial<User>;
+ } as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
+
+ const moving =
+ // 移行先がない→ある
+ (!exist.movedToUri && updates.movedToUri) ||
+ // 移行先がある→別のもの
+ (exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
+ // 移行先がある→ない、ない→ないは無視
+
+ if (moving) updates.movedAt = new Date();
if (avatar) {
updates.avatarId = avatar.id;
@@ -523,6 +546,31 @@ export class ApPersonService implements OnModuleInit {
});
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
+
+ const updated = { ...exist, ...updates };
+
+ this.cacheService.uriPersonCache.set(uri, updated);
+
+ // 移行処理を行う
+ if (updated.movedAt && (
+ // 初めて移行する場合はmovedAtがnullなので移行処理を許可
+ exist.movedAt == null ||
+ // 以前のmovingから14日以上経過した場合のみ移行処理を許可
+ // (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく)
+ exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime()
+ )) {
+ this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`);
+ return this.processRemoteMove(updated, movePreventUris)
+ .then(result => {
+ this.logger.info(`Processing Move Finished [${result}] @${updated.username}@${updated.host} (${uri})`);
+ return result;
+ })
+ .catch(e => {
+ this.logger.info(`Processing Move Failed @${updated.username}@${updated.host} (${uri})`, { stack: e });
+ });
+ }
+
+ return 'skip';
}
/**
@@ -532,7 +580,7 @@ export class ApPersonService implements OnModuleInit {
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
@bindThis
- public async resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
+ public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す
@@ -607,4 +655,53 @@ export class ApPersonService implements OnModuleInit {
}
});
}
+
+ /**
+ * リモート由来のアカウント移行処理を行います
+ * @param src 移行元アカウント(リモートかつupdatePerson後である必要がある、というかこれ自体がupdatePersonで呼ばれる前提)
+ * @param movePreventUris ここに列挙されたURIにsrc.movedToUriが含まれる場合、移行処理はしない(無限ループ防止)
+ */
+ @bindThis
+ private async processRemoteMove(src: RemoteUser, movePreventUris: string[] = []): Promise<string> {
+ if (!src.movedToUri) return 'skip: no movedToUri';
+ if (src.uri === src.movedToUri) return 'skip: movedTo itself (src)'; // ???
+ if (movePreventUris.length > 10) return 'skip: too many moves';
+
+ // まずサーバー内で検索して様子見
+ let dst = await this.fetchPerson(src.movedToUri);
+
+ if (dst && this.userEntityService.isLocalUser(dst)) {
+ // targetがローカルユーザーだった場合データベースから引っ張ってくる
+ dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as LocalUser;
+ } else if (dst) {
+ if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
+
+ // targetを見つけたことがあるならtargetをupdatePersonする
+ await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
+ dst = await this.fetchPerson(src.movedToUri) ?? dst;
+ } else {
+ if (src.movedToUri.startsWith(`${this.config.url}/`)) {
+ // ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
+ return 'failed: movedTo is local but not found';
+ }
+
+ // targetが知らない人だったらresolvePerson
+ // (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
+ dst = await this.resolvePerson(src.movedToUri);
+ }
+
+ if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
+ if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
+ if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
+ if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
+ return 'skip: dst.alsoKnownAs is empty';
+ }
+ if (!dst.alsoKnownAs?.includes(src.uri)) {
+ return 'skip: alsoKnownAs does not include from.uri';
+ }
+
+ await this.accountMoveService.postMoveProcess(src, dst);
+
+ return 'ok';
+ }
}
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index 987002606f..15ffd44861 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -74,6 +74,8 @@ export class ChannelEntityService {
userId: channel.userId,
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
pinnedNoteIds: channel.pinnedNoteIds,
+ color: channel.color,
+ isArchived: channel.isArchived,
usersCount: channel.usersCount,
notesCount: channel.notesCount,
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 26debd6adc..32269a4101 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -335,6 +335,7 @@ export class NoteEntityService implements OnModuleInit {
channel: channel ? {
id: channel.id,
name: channel.name,
+ color: channel.color,
} : undefined,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined,
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 0dc63d969f..d76b863957 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { AccessTokensRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
+import type { AccessTokensRepository, FollowRequestsRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js';
import type { Note } from '@/models/entities/Note.js';
@@ -35,6 +35,9 @@ export class NotificationEntityService implements OnModuleInit {
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
+ @Inject(DI.followRequestsRepository)
+ private followRequestsRepository: FollowRequestsRepository,
+
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
@@ -131,6 +134,15 @@ export class NotificationEntityService implements OnModuleInit {
});
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
+ // 既に解決されたフォローリクエストの通知を除外
+ const followRequestNotifications = validNotifications.filter(x => x.type === 'receiveFollowRequest');
+ if (followRequestNotifications.length > 0) {
+ const reqs = await this.followRequestsRepository.find({
+ where: { followerId: In(followRequestNotifications.map(x => x.notifierId!)) },
+ });
+ validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
+ }
+
return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
packedNotes,
packedUsers,
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
index e111a10b77..54818782dd 100644
--- a/packages/backend/src/core/entities/RoleEntityService.ts
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -59,6 +59,7 @@ export class RoleEntityService {
isPublic: role.isPublic,
isAdministrator: role.isAdministrator,
isModerator: role.isModerator,
+ isExplorable: role.isExplorable,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator,
displayOrder: role.displayOrder,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index e02f7535d4..a7f62c05f7 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
@@ -9,10 +9,9 @@ import type { Packed } from '@/misc/json-schema.js';
import type { Promiseable } from '@/misc/prelude/await-all.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
-import type { Instance } from '@/models/entities/Instance.js';
-import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
+import type { LocalUser, PartialLocalUser, PartialRemoteUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
+import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@@ -35,13 +34,13 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
const ajv = new Ajv();
function isLocalUser(user: User): user is LocalUser;
-function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
+function isLocalUser<T extends { host: User['host'] }>(user: T): user is (T & { host: null; });
function isLocalUser(user: User | { host: User['host'] }): boolean {
return user.host == null;
}
function isRemoteUser(user: User): user is RemoteUser;
-function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
+function isRemoteUser<T extends { host: User['host'] }>(user: T): user is (T & { host: string; });
function isRemoteUser(user: User | { host: User['host'] }): boolean {
return !isLocalUser(user);
}
@@ -113,6 +112,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
+
+ @Inject(DI.userMemosRepository)
+ private userMemosRepository: UserMemoRepository,
//private noteEntityService: NoteEntityService,
//private driveFileEntityService: DriveFileEntityService,
@@ -277,9 +279,20 @@ export class UserEntityService implements OnModuleInit {
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
}
+ @bindThis
+ public getUserUri(user: LocalUser | PartialLocalUser | RemoteUser | PartialRemoteUser): string {
+ return this.isRemoteUser(user)
+ ? user.uri : this.genLocalUserUri(user.id);
+ }
+
+ @bindThis
+ public genLocalUserUri(userId: string): string {
+ return `${this.config.url}/users/${userId}`;
+ }
+
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
src: User['id'] | User,
- me?: { id: User['id'] } | null | undefined,
+ me?: { id: User['id']; } | null | undefined,
options?: {
detail?: D,
includeSecrets?: boolean,
@@ -293,26 +306,9 @@ export class UserEntityService implements OnModuleInit {
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
- // migration
- if (user.avatarId != null && user.avatarUrl === null) {
- const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
- user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
- this.usersRepository.update(user.id, {
- avatarUrl: user.avatarUrl,
- avatarBlurhash: avatar.blurhash,
- });
- }
- if (user.bannerId != null && user.bannerUrl === null) {
- const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
- user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
- this.usersRepository.update(user.id, {
- bannerUrl: user.bannerUrl,
- bannerBlurhash: banner.blurhash,
- });
- }
-
const meId = me ? me.id : null;
const isMe = meId === user.id;
+ const iAmModerator = me ? await this.roleService.isModerator(me as User) : false;
const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin')
@@ -366,8 +362,11 @@ export class UserEntityService implements OnModuleInit {
...(opts.detail ? {
url: profile!.url,
uri: user.uri,
- movedToUri: user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null,
- alsoKnownAs: user.alsoKnownAs,
+ movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
+ alsoKnownAs: user.alsoKnownAs
+ ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
+ .then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[])
+ : null,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
@@ -409,6 +408,11 @@ export class UserEntityService implements OnModuleInit {
isAdministrator: role.isAdministrator,
displayOrder: role.displayOrder,
}))),
+ memo: meId == null ? null : await this.userMemosRepository.findOneBy({
+ userId: meId,
+ targetUserId: user.id,
+ }).then(row => row?.memo ?? null),
+ moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
...(opts.detail && isMe ? {
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index 1688bb2ba7..bb190cf60f 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -40,7 +40,7 @@ export class ServerStatsService implements OnApplicationShutdown {
const stats = {
cpu: roundCpu(cpu),
mem: {
- used: round(memStats.used - memStats.buffers - memStats.cached),
+ used: round(memStats.total - memStats.available),
active: round(memStats.active),
},
net: {
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index d4b1fb31b1..c06c7a7159 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -1,6 +1,7 @@
export const DI = {
config: Symbol('config'),
db: Symbol('db'),
+ meilisearch: Symbol('meilisearch'),
redis: Symbol('redis'),
redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'),
@@ -70,5 +71,6 @@ export const DI = {
roleAssignmentsRepository: Symbol('roleAssignmentsRepository'),
flashsRepository: Symbol('flashsRepository'),
flashLikesRepository: Symbol('flashLikesRepository'),
+ userMemosRepository: Symbol('userMemosRepository'),
//#endregion
};
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index f413246a1f..5610929648 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -1,4 +1,4 @@
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
export class RedisKVCache<T> {
@@ -38,7 +38,7 @@ export class RedisKVCache<T> {
await this.redisClient.set(
`kvcache:${this.name}:${key}`,
this.toRedisConverter(value),
- 'ex', Math.round(this.lifetime / 1000),
+ 'EX', Math.round(this.lifetime / 1000),
);
}
}
@@ -122,7 +122,7 @@ export class RedisSingleCache<T> {
await this.redisClient.set(
`singlecache:${this.name}`,
this.toRedisConverter(value),
- 'ex', Math.round(this.lifetime / 1000),
+ 'EX', Math.round(this.lifetime / 1000),
);
}
}
diff --git a/packages/backend/src/misc/check-https.ts b/packages/backend/src/misc/check-https.ts
new file mode 100644
index 0000000000..b33f019973
--- /dev/null
+++ b/packages/backend/src/misc/check-https.ts
@@ -0,0 +1,4 @@
+export function checkHttps(url: string) {
+ return url.startsWith('https://') ||
+ (url.startsWith('http://') && process.env.NODE_ENV !== 'production');
+}
diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts
index e8c66683cc..910bebfcfe 100644
--- a/packages/backend/src/misc/check-word-mute.ts
+++ b/packages/backend/src/misc/check-word-mute.ts
@@ -1,3 +1,4 @@
+import { AhoCorasick } from 'slacc';
import RE2 from 're2';
import type { Note } from '@/models/entities/Note.js';
import type { User } from '@/models/entities/User.js';
@@ -12,6 +13,8 @@ type UserLike = {
id: User['id'];
};
+const acCache = new Map<string, AhoCorasick>();
+
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
// 自分自身
if (me && (note.userId === me.id)) return false;
@@ -21,7 +24,22 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
if (text === '') return false;
- const matched = mutedWords.some(filter => {
+ const acable = mutedWords.filter(filter => Array.isArray(filter) && filter.length === 1).map(filter => filter[0]).sort();
+ const unacable = mutedWords.filter(filter => !Array.isArray(filter) || filter.length !== 1);
+ const acCacheKey = acable.join('\n');
+ const ac = acCache.get(acCacheKey) ?? AhoCorasick.withPatterns(acable);
+ acCache.delete(acCacheKey);
+ for (const obsoleteKeys of acCache.keys()) {
+ if (acCache.size > 1000) {
+ acCache.delete(obsoleteKeys);
+ }
+ }
+ acCache.set(acCacheKey, ac);
+ if (ac.isMatch(text)) {
+ return true;
+ }
+
+ const matched = unacable.some(filter => {
if (Array.isArray(filter)) {
return filter.every(keyword => text.includes(keyword));
} else {
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 7be7b81904..588c98b58d 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -388,6 +388,12 @@ const $roleAssignmentsRepository: Provider = {
inject: [DI.db],
};
+const $userMemosRepository: Provider = {
+ provide: DI.userMemosRepository,
+ useFactory: (db: DataSource) => db.getRepository(UserMemo),
+ inject: [DI.db],
+};
+
@Module({
imports: [
],
@@ -456,6 +462,7 @@ const $roleAssignmentsRepository: Provider = {
$roleAssignmentsRepository,
$flashsRepository,
$flashLikesRepository,
+ $userMemosRepository,
],
exports: [
$usersRepository,
@@ -522,6 +529,7 @@ const $roleAssignmentsRepository: Provider = {
$roleAssignmentsRepository,
$flashsRepository,
$flashLikesRepository,
+ $userMemosRepository,
],
})
export class RepositoryModule {}
diff --git a/packages/backend/src/models/entities/Channel.ts b/packages/backend/src/models/entities/Channel.ts
index 2d346fdf9d..d7c4583da3 100644
--- a/packages/backend/src/models/entities/Channel.ts
+++ b/packages/backend/src/models/entities/Channel.ts
@@ -64,6 +64,18 @@ export class Channel {
})
public pinnedNoteIds: string[];
+ @Column('varchar', {
+ length: 16,
+ default: '#86b300',
+ })
+ public color: string;
+
+ @Index()
+ @Column('boolean', {
+ default: false,
+ })
+ public isArchived: boolean;
+
@Index()
@Column('integer', {
default: 0,
diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts
index 2e4f90b57f..6d44e4edc7 100644
--- a/packages/backend/src/models/entities/Meta.ts
+++ b/packages/backend/src/models/entities/Meta.ts
@@ -405,4 +405,16 @@ export class Meta {
default: { },
})
public policies: Record<string, any>;
+
+ @Column('varchar', {
+ length: 280,
+ array: true,
+ default: '{}',
+ })
+ public serverRules: string[];
+
+ @Column('varchar', {
+ length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
+ })
+ public preservedUsernames: string[];
}
diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts
index eca9bcf270..61f40d59da 100644
--- a/packages/backend/src/models/entities/Role.ts
+++ b/packages/backend/src/models/entities/Role.ts
@@ -154,6 +154,11 @@ export class Role {
@Column('boolean', {
default: false,
})
+ public isExplorable: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
public canEditMembersByModerator: boolean;
// UIに表示する際の並び順用(大きいほど先頭)
diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts
index 04dfa21107..8e10f999b6 100644
--- a/packages/backend/src/models/entities/User.ts
+++ b/packages/backend/src/models/entities/User.ts
@@ -75,6 +75,12 @@ export class User {
})
public movedToUri: string | null;
+ @Column('timestamp with time zone', {
+ nullable: true,
+ comment: 'When the user moved to another account',
+ })
+ public movedAt: Date | null;
+
@Column('simple-array', {
nullable: true,
comment: 'URIs the user is known as too',
@@ -253,11 +259,23 @@ export type LocalUser = User & {
uri: null;
}
+export type PartialLocalUser = Partial<User> & {
+ id: User['id'];
+ host: null;
+ uri: null;
+}
+
export type RemoteUser = User & {
host: string;
uri: string;
}
+export type PartialRemoteUser = Partial<User> & {
+ id: User['id'];
+ host: string;
+ uri: string;
+}
+
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
export const passwordSchema = { type: 'string', minLength: 1 } as const;
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
diff --git a/packages/backend/src/models/entities/UserMemo.ts b/packages/backend/src/models/entities/UserMemo.ts
new file mode 100644
index 0000000000..7dc34b4346
--- /dev/null
+++ b/packages/backend/src/models/entities/UserMemo.ts
@@ -0,0 +1,42 @@
+import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+
+@Entity()
+@Index(['userId', 'targetUserId'], { unique: true })
+export class UserMemo {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of author.',
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The ID of target user.',
+ })
+ public targetUserId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public targetUser: User | null;
+
+ @Column('varchar', {
+ length: 2048,
+ comment: 'Memo.',
+ })
+ public memo: string;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 48d6e15f2a..b8ba28db9b 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -55,6 +55,7 @@ import { UserPending } from '@/models/entities/UserPending.js';
import { UserProfile } from '@/models/entities/UserProfile.js';
import { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
+import { UserMemo } from '@/models/entities/UserMemo.js';
import { Webhook } from '@/models/entities/Webhook.js';
import { Channel } from '@/models/entities/Channel.js';
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
@@ -129,6 +130,7 @@ export {
RoleAssignment,
Flash,
FlashLike,
+ UserMemo,
};
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
@@ -195,3 +197,4 @@ export type RolesRepository = Repository<Role>;
export type RoleAssignmentsRepository = Repository<RoleAssignment>;
export type FlashsRepository = Repository<Flash>;
export type FlashLikesRepository = Repository<FlashLike>;
+export type UserMemoRepository = Repository<UserMemo>;
diff --git a/packages/backend/src/models/json-schema/channel.ts b/packages/backend/src/models/json-schema/channel.ts
index 745b39a6ba..fd61a70c0e 100644
--- a/packages/backend/src/models/json-schema/channel.ts
+++ b/packages/backend/src/models/json-schema/channel.ts
@@ -30,6 +30,10 @@ export const packedChannelSchema = {
format: 'url',
nullable: true, optional: false,
},
+ isArchived: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
notesCount: {
type: 'number',
nullable: false, optional: false,
@@ -59,5 +63,9 @@ export const packedChannelSchema = {
format: 'id',
},
},
+ color: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 7d40979e3d..529c1303d1 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -80,9 +80,14 @@ export const packedUserDetailedNotMeOnlySchema = {
},
alsoKnownAs: {
type: 'array',
- format: 'uri',
nullable: true,
optional: false,
+ items: {
+ type: 'string',
+ format: 'id',
+ nullable: false,
+ optional: false,
+ },
},
createdAt: {
type: 'string',
@@ -143,6 +148,7 @@ export const packedUserDetailedNotMeOnlySchema = {
fields: {
type: 'array',
nullable: false, optional: false,
+ maxItems: 16,
items: {
type: 'object',
nullable: false, optional: false,
@@ -156,7 +162,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
},
},
- maxLength: 4,
},
},
followersCount: {
@@ -250,6 +255,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: true,
},
+ memo: {
+ type: 'string',
+ nullable: false, optional: true,
+ },
//#endregion
},
} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index bb21ed827e..f3d404e6c9 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -70,6 +70,7 @@ import { Role } from '@/models/entities/Role.js';
import { RoleAssignment } from '@/models/entities/RoleAssignment.js';
import { Flash } from '@/models/entities/Flash.js';
import { FlashLike } from '@/models/entities/FlashLike.js';
+import { UserMemo } from '@/models/entities/UserMemo.js';
import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js';
@@ -183,6 +184,7 @@ export const entities = [
RoleAssignment,
Flash,
FlashLike,
+ UserMemo,
...charts,
];
diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts
index 233a36dd04..df8ac3a301 100644
--- a/packages/backend/src/queue/DbQueueProcessorsService.ts
+++ b/packages/backend/src/queue/DbQueueProcessorsService.ts
@@ -9,11 +9,13 @@ import { ExportFollowingProcessorService } from './processors/ExportFollowingPro
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
+import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js';
import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js';
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
+import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
import type Bull from 'bull';
@@ -32,11 +34,13 @@ export class DbQueueProcessorsService {
private exportMutingProcessorService: ExportMutingProcessorService,
private exportBlockingProcessorService: ExportBlockingProcessorService,
private exportUserListsProcessorService: ExportUserListsProcessorService,
+ private exportAntennasProcessorService: ExportAntennasProcessorService,
private importFollowingProcessorService: ImportFollowingProcessorService,
private importMutingProcessorService: ImportMutingProcessorService,
private importBlockingProcessorService: ImportBlockingProcessorService,
private importUserListsProcessorService: ImportUserListsProcessorService,
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
+ private importAntennasProcessorService: ImportAntennasProcessorService,
private deleteAccountProcessorService: DeleteAccountProcessorService,
) {
}
@@ -51,6 +55,7 @@ export class DbQueueProcessorsService {
q.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done));
q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done));
q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done));
+ q.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done));
q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done));
q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job));
q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done));
@@ -58,6 +63,7 @@ export class DbQueueProcessorsService {
q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job));
q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done));
q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done));
+ q.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done));
q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job));
}
}
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 4db9b38547..3d4cc77321 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -24,11 +24,13 @@ import { ExportFollowingProcessorService } from './processors/ExportFollowingPro
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
+import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js';
import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js';
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
+import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
@@ -55,11 +57,13 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ExportMutingProcessorService,
ExportBlockingProcessorService,
ExportUserListsProcessorService,
+ ExportAntennasProcessorService,
ImportFollowingProcessorService,
ImportMutingProcessorService,
ImportBlockingProcessorService,
ImportUserListsProcessorService,
ImportCustomEmojisProcessorService,
+ ImportAntennasProcessorService,
DeleteAccountProcessorService,
DeleteFileProcessorService,
CleanRemoteFilesProcessorService,
diff --git a/packages/backend/src/queue/RelationshipQueueProcessorsService.ts b/packages/backend/src/queue/RelationshipQueueProcessorsService.ts
index af086fa4e7..736b4fa80d 100644
--- a/packages/backend/src/queue/RelationshipQueueProcessorsService.ts
+++ b/packages/backend/src/queue/RelationshipQueueProcessorsService.ts
@@ -17,7 +17,7 @@ export class RelationshipQueueProcessorsService {
@bindThis
public start(q: Bull.Queue): void {
- const maxJobs = (this.config.deliverJobConcurrency ?? 128) / 4; // conservative?
+ const maxJobs = this.config.relashionshipJobConcurrency ?? 16;
q.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job));
q.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job));
q.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job));
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 0e99b7bcd2..f293bd4d7e 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -79,10 +79,7 @@ export class DeliverProcessorService {
// Update stats
this.federatedInstanceService.fetch(host).then(i => {
if (i.isNotResponding) {
- this.instancesRepository.update(i.id, {
- isNotResponding: false,
- });
- this.federatedInstanceService.updateCachePartial(host, {
+ this.federatedInstanceService.update(i.id, {
isNotResponding: false,
});
}
@@ -101,10 +98,7 @@ export class DeliverProcessorService {
// Update stats
this.federatedInstanceService.fetch(host).then(i => {
if (!i.isNotResponding) {
- this.instancesRepository.update(i.id, {
- isNotResponding: true,
- });
- this.federatedInstanceService.updateCachePartial(host, {
+ this.federatedInstanceService.update(i.id, {
isNotResponding: true,
});
}
@@ -123,10 +117,7 @@ export class DeliverProcessorService {
// 相手が閉鎖していることを明示しているため、配送停止する
if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => {
- this.instancesRepository.update(i.id, {
- isSuspended: true,
- });
- this.federatedInstanceService.updateCachePartial(host, {
+ this.federatedInstanceService.update(i.id, {
isSuspended: true,
});
});
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
new file mode 100644
index 0000000000..894903e79b
--- /dev/null
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -0,0 +1,103 @@
+import fs from 'node:fs';
+import { Inject, Injectable } from '@nestjs/common';
+import { format as DateFormat } from 'date-fns';
+import { In } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, User } from '@/models/index.js';
+import type { Config } from '@/config.js';
+import Logger from '@/logger.js';
+import { DriveService } from '@/core/DriveService.js';
+import { bindThis } from '@/decorators.js';
+import { createTemp } from '@/misc/create-temp.js';
+import { UtilityService } from '@/core/UtilityService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type { DBExportAntennasData } from '../types.js';
+import type Bull from 'bull';
+
+@Injectable()
+export class ExportAntennasProcessorService {
+ private logger: Logger;
+
+ constructor (
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.antennasRepository)
+ private antennsRepository: AntennasRepository,
+
+ @Inject(DI.userListJoiningsRepository)
+ private userListJoiningsRepository: UserListJoiningsRepository,
+
+ private driveService: DriveService,
+ private utilityService: UtilityService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
+ }
+
+ @bindThis
+ public async process(job: Bull.Job<DBExportAntennasData>, done: () => void): Promise<void> {
+ const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
+ if (user == null) {
+ done();
+ return;
+ }
+ const [path, cleanup] = await createTemp();
+ const stream = fs.createWriteStream(path, { flags: 'a' });
+ const write = (input: string): Promise<void> => {
+ return new Promise((resolve, reject) => {
+ stream.write(input, err => {
+ if (err) {
+ this.logger.error(err);
+ reject();
+ } else {
+ resolve();
+ }
+ });
+ });
+ };
+ try {
+ const antennas = await this.antennsRepository.findBy({ userId: job.data.user.id });
+ write('[');
+ for (const [index, antenna] of antennas.entries()) {
+ let users: User[] | undefined;
+ if (antenna.userListId !== null) {
+ const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId });
+ users = await this.usersRepository.findBy({
+ id: In(joinings.map(j => j.userId)),
+ });
+ }
+ write(JSON.stringify({
+ name: antenna.name,
+ src: antenna.src,
+ keywords: antenna.keywords,
+ excludeKeywords: antenna.excludeKeywords,
+ users: antenna.users,
+ userListAccts: typeof users !== 'undefined' ? users.map((u) => {
+ return this.utilityService.getFullApAccount(u.username, u.host); // acct
+ }) : null,
+ caseSensitive: antenna.caseSensitive,
+ withReplies: antenna.withReplies,
+ withFile: antenna.withFile,
+ notify: antenna.notify,
+ }));
+ if (antennas.length - 1 !== index) {
+ write(', ');
+ }
+ }
+ write(']');
+ stream.end();
+
+ const fileName = 'antennas-' + 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();
+ done();
+ }
+ }
+}
+
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index a020006732..c7b54070d6 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -106,7 +106,7 @@ export class ExportBlockingProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index daefcdf2f5..f2f2383a88 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -121,7 +121,7 @@ export class ExportFavoritesProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'favorites-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index 59443de57f..fa9c1ac1ea 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -110,7 +110,7 @@ export class ExportFollowingProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index a2a718b892..b14bf5f5b1 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -110,7 +110,7 @@ export class ExportMutingProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index 1aa20d6f1d..e4f12ad101 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -117,7 +117,7 @@ export class ExportNotesProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index ce8ed2f5e8..54bde44044 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -86,7 +86,7 @@ export class ExportUserListsProcessorService {
this.logger.succ(`Exported to: ${path}`);
const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
- const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true });
+ const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
new file mode 100644
index 0000000000..d06131b8c8
--- /dev/null
+++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts
@@ -0,0 +1,96 @@
+import { Injectable, Inject } from '@nestjs/common';
+import Ajv from 'ajv';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import Logger from '@/logger.js';
+import type { AntennasRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import { DBAntennaImportJobData } from '../types.js';
+import type Bull from 'bull';
+
+const validate = new Ajv().compile({
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 100 },
+ src: { type: 'string', enum: ['home', 'all', 'users', 'list'] },
+ userListAccts: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ nullable: true,
+ },
+ keywords: { type: 'array', items: {
+ type: 'array', items: {
+ type: 'string',
+ },
+ } },
+ excludeKeywords: { type: 'array', items: {
+ type: 'array', items: {
+ type: 'string',
+ },
+ } },
+ users: { type: 'array', items: {
+ type: 'string',
+ } },
+ caseSensitive: { type: 'boolean' },
+ withReplies: { type: 'boolean' },
+ withFile: { type: 'boolean' },
+ notify: { type: 'boolean' },
+ },
+ required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+});
+
+@Injectable()
+export class ImportAntennasProcessorService {
+ private logger: Logger;
+
+ constructor (
+ @Inject(DI.antennasRepository)
+ private antennasRepository: AntennasRepository,
+
+ private queueLoggerService: QueueLoggerService,
+ private idService: IdService,
+ private globalEventService: GlobalEventService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('import-antennas');
+ }
+
+ @bindThis
+ public async process(job: Bull.Job<DBAntennaImportJobData>, done: () => void): Promise<void> {
+ const now = new Date();
+ try {
+ for (const antenna of job.data.antenna) {
+ if (antenna.keywords.length === 0 || antenna.keywords[0].every(x => x === '')) continue;
+ if (!validate(antenna)) {
+ this.logger.warn('Validation Failed');
+ continue;
+ }
+ const result = await this.antennasRepository.insert({
+ id: this.idService.genId(),
+ createdAt: now,
+ lastUsedAt: now,
+ userId: job.data.user.id,
+ name: antenna.name,
+ src: antenna.src === 'list' && antenna.userListAccts ? 'users' : antenna.src,
+ userListId: null,
+ keywords: antenna.keywords,
+ excludeKeywords: antenna.excludeKeywords,
+ users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean),
+ caseSensitive: antenna.caseSensitive,
+ withReplies: antenna.withReplies,
+ withFile: antenna.withFile,
+ notify: antenna.notify,
+ }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
+ this.logger.succ('Antenna created: ' + result.id);
+ this.globalEventService.publishInternalEvent('antennaCreated', result);
+ }
+ } catch (err: any) {
+ this.logger.error(err);
+ } finally {
+ done();
+ }
+ }
+}
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index ed7f38d013..ab8b1e9e22 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -84,9 +84,9 @@ export class InboxProcessorService {
// HTTP-Signature keyIdを元にDBから取得
let authUser: {
- user: RemoteUser;
- key: UserPublickey | null;
- } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
+ user: RemoteUser;
+ key: UserPublickey | null;
+ } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {
@@ -174,13 +174,10 @@ export class InboxProcessorService {
// Update stats
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
- this.instancesRepository.update(i.id, {
+ this.federatedInstanceService.update(i.id, {
latestRequestReceivedAt: new Date(),
isNotResponding: false,
});
- this.federatedInstanceService.updateCachePartial(host, {
- isNotResponding: false,
- });
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
index a5006dcf03..ff454df455 100644
--- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts
+++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts
@@ -10,6 +10,7 @@ import { QueueLoggerService } from '../QueueLoggerService.js';
import { RelationshipJobData } from '../types.js';
import type { UsersRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
+import { LocalUser, RemoteUser } from '@/models/entities/User.js';
@Injectable()
export class RelationshipProcessorService {
@@ -39,7 +40,7 @@ export class RelationshipProcessorService {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
- ]);
+ ]) as [LocalUser | RemoteUser, LocalUser | RemoteUser];
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
return 'ok';
}
diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts
index 23c973d449..776dd3aa12 100644
--- a/packages/backend/src/queue/types.ts
+++ b/packages/backend/src/queue/types.ts
@@ -1,3 +1,4 @@
+import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Note } from '@/models/entities/Note.js';
import type { User } from '@/models/entities/User.js';
@@ -33,12 +34,14 @@ export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
export type DbJobMap = {
deleteDriveFiles: DbJobDataWithUser;
exportCustomEmojis: DbJobDataWithUser;
+ exportAntennas: DBExportAntennasData;
exportNotes: DbJobDataWithUser;
exportFavorites: DbJobDataWithUser;
exportFollowing: DbExportFollowingData;
exportMuting: DbJobDataWithUser;
exportBlocking: DbJobDataWithUser;
exportUserLists: DbJobDataWithUser;
+ importAntennas: DBAntennaImportJobData;
importFollowing: DbUserImportJobData;
importFollowingToDb: DbUserImportToDbJobData;
importMuting: DbUserImportJobData;
@@ -59,6 +62,10 @@ export type DbExportFollowingData = {
excludeInactive: boolean;
};
+export type DBExportAntennasData = {
+ user: ThinUser
+}
+
export type DbUserDeleteJobData = {
user: ThinUser;
soft?: boolean;
@@ -69,6 +76,11 @@ export type DbUserImportJobData = {
fileId: DriveFile['id'];
};
+export type DBAntennaImportJobData = {
+ user: ThinUser,
+ antenna: Antenna
+}
+
export type DbUserImportToDbJobData = {
user: ThinUser;
target: string;
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index e13e9265ab..e675d9cf1b 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -11,7 +11,7 @@ import * as url from '@/misc/prelude/url.js';
import type { Config } from '@/config.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { QueueService } from '@/core/QueueService.js';
-import type { LocalUser, User } from '@/models/entities/User.js';
+import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import type { Following } from '@/models/entities/Following.js';
import { countIf } from '@/misc/prelude/array.js';
@@ -630,7 +630,7 @@ export class ActivityPubServerService {
id: request.params.followee,
host: Not(IsNull()),
}),
- ]);
+ ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
if (follower == null || followee == null) {
reply.code(404);
@@ -665,7 +665,7 @@ export class ActivityPubServerService {
id: followRequest.followeeId,
host: Not(IsNull()),
}),
- ]);
+ ]) as [LocalUser | RemoteUser | null, LocalUser | RemoteUser | null];
if (follower == null || followee == null) {
reply.code(404);
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 794fa76d9e..98329ddffa 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -297,7 +297,8 @@ export class FileServerService {
} else if ('badge' in request.query) {
const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, {
- fit: 'inside',
+ fit: 'contain',
+ position: 'centre',
withoutEnlargement: false,
})
.greyscale()
@@ -453,7 +454,8 @@ export class FileServerService {
fileRole: 'original',
file,
filename: file.name,
- mime: file.type,
+ // 古いファイルは修正前のmimeを持っているのでできるだけ修正してあげる
+ mime: this.fileInfoService.fixMime(file.type),
ext: null,
path,
};
diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts
index e722563036..9bf8deb221 100644
--- a/packages/backend/src/server/WellKnownServerService.ts
+++ b/packages/backend/src/server/WellKnownServerService.ts
@@ -8,6 +8,7 @@ import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js';
import type { User } from '@/models/entities/User.js';
import * as Acct from '@/misc/acct.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { FindOptionsWhere } from 'typeorm';
import { bindThis } from '@/decorators.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@@ -23,6 +24,7 @@ export class WellKnownServerService {
private usersRepository: UsersRepository,
private nodeinfoServerService: NodeinfoServerService,
+ private userEntityService: UserEntityService,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -130,7 +132,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
const self = {
rel: 'self',
type: 'application/activity+json',
- href: `${this.config.url}/users/${user.id}`,
+ href: this.userEntityService.genLocalUserUri(user.id),
};
const profilePage = {
rel: 'http://webfinger.net/rel/profile-page',
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index bf5cb20918..e3483c82c6 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -261,6 +261,17 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
+ if (ep.meta.prohibitMoved) {
+ if (user?.movedToUri) {
+ throw new ApiError({
+ message: 'You have moved your account.',
+ code: 'YOUR_ACCOUNT_MOVED',
+ id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
+ httpStatusCode: 403,
+ });
+ }
+ }
+
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 689f90287e..ee1aae5b6c 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
+import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
@@ -193,6 +194,7 @@ 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_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';
import * as ep___i_favorites from './endpoints/i/favorites.js';
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
@@ -201,6 +203,7 @@ import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
+import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
import * as ep___i_notifications from './endpoints/i/notifications.js';
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
import * as ep___i_pages from './endpoints/i/pages.js';
@@ -222,7 +225,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_move from './endpoints/i/move.js';
-import * as ep___i_knownAs from './endpoints/i/known-as.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@@ -330,6 +332,7 @@ import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
+import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
@@ -364,6 +367,7 @@ const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass:
const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default };
const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default };
const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
+const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
@@ -528,6 +532,7 @@ const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.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 };
const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
@@ -536,6 +541,7 @@ const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass:
const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default };
+const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default };
const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default };
const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
@@ -557,7 +563,6 @@ const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.defau
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
-const $i_knownAs: Provider = { provide: 'ep:i/known-as', useClass: ep___i_knownAs.default };
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
@@ -665,6 +670,7 @@ const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___use
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
+const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@@ -703,6 +709,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_emoji_removeAliasesBulk,
$admin_emoji_setAliasesBulk,
$admin_emoji_setCategoryBulk,
+ $admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
@@ -867,6 +874,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_exportNotes,
$i_exportFavorites,
$i_exportUserLists,
+ $i_exportAntennas,
$i_favorites,
$i_gallery_likes,
$i_gallery_posts,
@@ -875,6 +883,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_importFollowing,
$i_importMuting,
$i_importUserLists,
+ $i_importAntennas,
$i_notifications,
$i_pageLikes,
$i_pages,
@@ -896,7 +905,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_updateEmail,
$i_update,
$i_move,
- $i_knownAs,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,
@@ -1004,6 +1012,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_show,
$users_stats,
$users_achievements,
+ $users_updateMemo,
$fetchRss,
$retention,
],
@@ -1036,6 +1045,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_emoji_removeAliasesBulk,
$admin_emoji_setAliasesBulk,
$admin_emoji_setCategoryBulk,
+ $admin_emoji_setLicenseBulk,
$admin_emoji_update,
$admin_federation_deleteAllFiles,
$admin_federation_refreshRemoteInstanceMetadata,
@@ -1200,6 +1210,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_exportNotes,
$i_exportFavorites,
$i_exportUserLists,
+ $i_exportAntennas,
$i_favorites,
$i_gallery_likes,
$i_gallery_posts,
@@ -1208,6 +1219,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_importFollowing,
$i_importMuting,
$i_importUserLists,
+ $i_importAntennas,
$i_notifications,
$i_pageLikes,
$i_pages,
@@ -1229,7 +1241,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_updateEmail,
$i_update,
$i_move,
- $i_knownAs,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,
@@ -1335,6 +1346,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_show,
$users_stats,
$users_achievements,
+ $users_updateMemo,
$fetchRss,
$retention,
],
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 1f8915ecca..fe2db1d66a 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import Limiter from 'ratelimiter';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index fbabf47aff..b2bd7d82e7 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import rndstr from 'rndstr';
import bcrypt from 'bcryptjs';
+import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
@@ -15,7 +16,6 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
-import { IsNull } from 'typeorm';
@Injectable()
export class SignupApiService {
@@ -137,6 +137,11 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'USED_USERNAME');
}
+ const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+ if (isPreserved) {
+ throw new FastifyReplyError(400, 'DENIED_USERNAME');
+ }
+
const code = rndstr('a-z0-9', 16);
// Generate hash of password
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 769a4490d6..258e8de034 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import * as websocket from 'websocket';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index d0fe6a57c1..09bd7cbff4 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
+import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
@@ -193,6 +194,7 @@ 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_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';
import * as ep___i_favorites from './endpoints/i/favorites.js';
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
@@ -201,6 +203,7 @@ import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
+import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
import * as ep___i_notifications from './endpoints/i/notifications.js';
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
import * as ep___i_pages from './endpoints/i/pages.js';
@@ -222,7 +225,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_move from './endpoints/i/move.js';
-import * as ep___i_knownAs from './endpoints/i/known-as.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@@ -330,6 +332,7 @@ import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
+import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___retention from './endpoints/retention.js';
@@ -362,6 +365,7 @@ const eps = [
['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
+ ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
['admin/emoji/update', ep___admin_emoji_update],
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
@@ -526,6 +530,7 @@ const eps = [
['i/export-notes', ep___i_exportNotes],
['i/export-favorites', ep___i_exportFavorites],
['i/export-user-lists', ep___i_exportUserLists],
+ ['i/export-antennas', ep___i_exportAntennas],
['i/favorites', ep___i_favorites],
['i/gallery/likes', ep___i_gallery_likes],
['i/gallery/posts', ep___i_gallery_posts],
@@ -534,6 +539,7 @@ const eps = [
['i/import-following', ep___i_importFollowing],
['i/import-muting', ep___i_importMuting],
['i/import-user-lists', ep___i_importUserLists],
+ ['i/import-antennas', ep___i_importAntennas],
['i/notifications', ep___i_notifications],
['i/page-likes', ep___i_pageLikes],
['i/pages', ep___i_pages],
@@ -554,8 +560,7 @@ const eps = [
['i/unpin', ep___i_unpin],
['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update],
- //['i/move', ep___i_move],
- //['i/known-as', ep___i_knownAs],
+ ['i/move', ep___i_move],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],
@@ -663,6 +668,7 @@ const eps = [
['users/show', ep___users_show],
['users/stats', ep___users_stats],
['users/achievements', ep___users_achievements],
+ ['users/update-memo', ep___users_updateMemo],
['fetch-rss', ep___fetchRss],
['retention', ep___retention],
];
@@ -701,6 +707,12 @@ export interface IEndpointMeta {
readonly requireRolePolicy?: keyof RolePolicies;
/**
+ * 引っ越し済みのユーザーによるリクエストを禁止するか
+ * 省略した場合は false として解釈されます。
+ */
+ readonly prohibitMoved?: boolean;
+
+ /**
* エンドポイントのリミテーションに関するやつ
* 省略した場合はリミテーションは無いものとして解釈されます。
*/
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index bac8ae16e5..8a3541dffe 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -52,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const { account, secret } = await this.signupService.signup({
username: ps.username,
password: ps.password,
+ ignorePreservedUsernames: true,
});
const res = await this.userEntityService.pack(account, account, {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 814668294f..4aa4ad82b4 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -87,12 +87,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
//const emojis = await q.take(ps.limit).getMany();
emojis = await q.getMany();
+ const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
- emojis = emojis.filter(emoji =>
- emoji.name.includes(ps.query!) ||
- emoji.aliases.some(a => a.includes(ps.query!)) ||
- emoji.category?.includes(ps.query!));
-
+ if (queryarry) {
+ emojis = emojis.filter(emoji =>
+ queryarry.includes(`:${emoji.name}:`)
+ );
+ } else {
+ emojis = emojis.filter(emoji =>
+ emoji.name.includes(ps.query!) ||
+ emoji.aliases.some(a => a.includes(ps.query!)) ||
+ emoji.category?.includes(ps.query!));
+ }
emojis.splice(ps.limit + 1);
} else {
emojis = await q.take(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
new file mode 100644
index 0000000000..b90b9757be
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
@@ -0,0 +1,37 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canManageCustomEmojis',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ ids: { type: 'array', items: {
+ type: 'string', format: 'misskey:id',
+ } },
+ license: {
+ type: 'string',
+ nullable: true,
+ description: 'Use `null` to reset the license.',
+ },
+ },
+ required: ['ids'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ private customEmojiService: CustomEmojiService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
index 54ce095488..83f729953a 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts
@@ -39,9 +39,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const pairs = await Promise.all(followings.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }),
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
- ])));
+ ]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
- this.queueService.createUnfollowJob(pairs.map(p => ({ to: p[0], from: p[1], silent: true })));
+ this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index 0a529ecb08..4fd74e591d 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -3,6 +3,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { InstancesRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
+import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
export const meta = {
tags: ['admin'],
@@ -28,6 +29,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private instancesRepository: InstancesRepository,
private utilityService: UtilityService,
+ private federatedInstanceService: FederatedInstanceService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(ps.host) });
@@ -36,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('instance not found');
}
- this.instancesRepository.update({ host: this.utilityService.toPuny(ps.host) }, {
+ this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended,
});
});
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index fc318a621a..87a2d22ac2 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -118,6 +118,14 @@ export const meta = {
optional: false, nullable: false,
},
},
+ preservedUsernames: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
hcaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
@@ -311,6 +319,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
sensitiveWords: instance.sensitiveWords,
+ preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey,
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
index 1359894634..916172f54a 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
@@ -25,6 +25,7 @@ export const paramDef = {
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
+ isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
@@ -76,12 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator,
+ isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
-
+
this.globalEventService.publishInternalEvent('roleCreated', created);
return await this.roleEntityService.pack(created, me);
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 37b68c4c41..467f157a61 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -33,6 +33,7 @@ export const paramDef = {
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
+ isExplorable: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
displayOrder: { type: 'number' },
@@ -85,6 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isPublic: ps.isPublic,
isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator,
+ isExplorable: ps.isExplorable,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
displayOrder: ps.displayOrder,
diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts
index 9c576dffe9..4ef4fdc665 100644
--- a/packages/backend/src/server/api/endpoints/admin/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts
@@ -2,7 +2,7 @@ import * as os from 'node:os';
import si from 'systeminformation';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 9d19efbbcf..42229c8f23 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isSilenced: isSilenced,
isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate,
- moderationNote: profile.moderationNote,
+ moderationNote: profile.moderationNote ?? '',
signins,
policies: await this.roleService.getUserPolicies(user.id),
roles: await this.roleEntityService.packMany(roles, me),
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 11de29bf83..0e94f56cfd 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -94,6 +94,8 @@ export const paramDef = {
enableActiveEmailValidation: { type: 'boolean' },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
+ serverRules: { type: 'array', items: { type: 'string' } },
+ preservedUsernames: { type: 'array', items: { type: 'string' } },
},
required: [],
} as const;
@@ -387,6 +389,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
+ if (ps.serverRules !== undefined) {
+ set.serverRules = ps.serverRules;
+ }
+
+ if (ps.preservedUsernames !== undefined) {
+ set.preservedUsernames = ps.preservedUsernames;
+ }
+
await this.metaService.update(set);
this.moderationLogService.insertModerationLog(me, 'updateMeta');
});
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index b7ce3363a9..5754a9f12a 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 88623ce26a..dca0f443b7 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennasRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 3f85442131..5f980bdbeb 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -11,6 +11,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
errors: {
@@ -71,7 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
-
+
private antennaEntityService: AntennaEntityService,
private globalEventService: GlobalEventService,
) {
diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts
index dff8a9d10d..69e2f2504c 100644
--- a/packages/backend/src/server/api/endpoints/channels/create.ts
+++ b/packages/backend/src/server/api/endpoints/channels/create.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:channels',
limit: {
@@ -41,6 +43,7 @@ export const paramDef = {
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
+ color: { type: 'string', minLength: 1, maxLength: 16 },
},
required: ['name'],
} as const;
@@ -78,6 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name,
description: ps.description ?? null,
bannerId: banner ? banner.id : null,
+ ...(ps.color !== undefined ? { color: ps.color } : {}),
} as Channel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
return await this.channelEntityService.pack(channel, me);
diff --git a/packages/backend/src/server/api/endpoints/channels/favorite.ts b/packages/backend/src/server/api/endpoints/channels/favorite.ts
index f52b45ccf3..c8544273a1 100644
--- a/packages/backend/src/server/api/endpoints/channels/favorite.ts
+++ b/packages/backend/src/server/api/endpoints/channels/favorite.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:channels',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts
index d25faae38d..1a8d1164c7 100644
--- a/packages/backend/src/server/api/endpoints/channels/featured.ts
+++ b/packages/backend/src/server/api/endpoints/channels/featured.ts
@@ -38,6 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const query = this.channelsRepository.createQueryBuilder('channel')
.where('channel.lastNotedAt IS NOT NULL')
+ .andWhere('channel.isArchived = FALSE')
.orderBy('channel.lastNotedAt', 'DESC');
const channels = await query.take(10).getMany();
diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts
index 8ab59991c7..f3ca66cfd2 100644
--- a/packages/backend/src/server/api/endpoints/channels/follow.ts
+++ b/packages/backend/src/server/api/endpoints/channels/follow.ts
@@ -11,6 +11,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:channels',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts
index 59df0616be..8fae972cb1 100644
--- a/packages/backend/src/server/api/endpoints/channels/owned.ts
+++ b/packages/backend/src/server/api/endpoints/channels/owned.ts
@@ -44,7 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
+ const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
+ .andWhere('channel.isArchived = FALSE')
.andWhere({ userId: me.id });
const channels = await query
diff --git a/packages/backend/src/server/api/endpoints/channels/search.ts b/packages/backend/src/server/api/endpoints/channels/search.ts
index a954ba224c..a3b40b0bbd 100644
--- a/packages/backend/src/server/api/endpoints/channels/search.ts
+++ b/packages/backend/src/server/api/endpoints/channels/search.ts
@@ -46,15 +46,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId);
+ const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
+ .andWhere('channel.isArchived = FALSE');
- if (ps.type === 'nameAndDescription') {
- query.andWhere(new Brackets(qb => { qb
- .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
- .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
- }));
- } else {
- query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
+ if (ps.query !== '') {
+ if (ps.type === 'nameAndDescription') {
+ query.andWhere(new Brackets(qb => { qb
+ .where('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
+ .orWhere('channel.description ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
+ }));
+ } else {
+ query.andWhere('channel.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
+ }
}
const channels = await query
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 2491d14235..c881074bab 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository, Note, NotesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
diff --git a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts
index 0c3f6c4855..67fb1ea03e 100644
--- a/packages/backend/src/server/api/endpoints/channels/unfavorite.ts
+++ b/packages/backend/src/server/api/endpoints/channels/unfavorite.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:channels',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
index 855ba47f8c..f46ff9f286 100644
--- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts
+++ b/packages/backend/src/server/api/endpoints/channels/unfollow.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:channels',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index 084b3f919e..30d7f8b244 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -47,12 +47,14 @@ export const paramDef = {
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
+ isArchived: { type: 'boolean', nullable: true },
pinnedNoteIds: {
type: 'array',
items: {
type: 'string', format: 'misskey:id',
},
},
+ color: { type: 'string', minLength: 1, maxLength: 16 },
},
required: ['channelId'],
} as const;
@@ -104,6 +106,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...(ps.name !== undefined ? { name: ps.name } : {}),
...(ps.description !== undefined ? { description: ps.description } : {}),
...(ps.pinnedNoteIds !== undefined ? { pinnedNoteIds: ps.pinnedNoteIds } : {}),
+ ...(ps.color !== undefined ? { color: ps.color } : {}),
+ ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}),
...(banner ? { bannerId: banner.id } : {}),
});
diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts
index b9d8dce47a..c3561e2a71 100644
--- a/packages/backend/src/server/api/endpoints/clips/add-note.ts
+++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts
index a770dc986d..5395a5c373 100644
--- a/packages/backend/src/server/api/endpoints/clips/create.ts
+++ b/packages/backend/src/server/api/endpoints/clips/create.ts
@@ -12,6 +12,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
res: {
@@ -57,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ApiError(meta.errors.tooManyClips);
}
-
+
const clip = await this.clipsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts
index 6addf743a2..f08caaf8d7 100644
--- a/packages/backend/src/server/api/endpoints/clips/favorite.ts
+++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:clip-favorite',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts
index 5d88870ed2..50c5d758bd 100644
--- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts
+++ b/packages/backend/src/server/api/endpoints/clips/remove-note.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts
index 244843d50f..3da252a226 100644
--- a/packages/backend/src/server/api/endpoints/clips/unfavorite.ts
+++ b/packages/backend/src/server/api/endpoints/clips/unfavorite.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:clip-favorite',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts
index a103c3f7d3..70f1959353 100644
--- a/packages/backend/src/server/api/endpoints/clips/update.ts
+++ b/packages/backend/src/server/api/endpoints/clips/update.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index b3bdef41d3..a1c1f9325e 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -15,6 +15,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
limit: {
duration: ms('1hour'),
max: 120,
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 3141e0fc01..3ecbba22b5 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -40,8 +40,13 @@ export const meta = {
code: 'NO_SUCH_FOLDER',
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
},
+
+ restrictedByRole: {
+ message: 'This feature is restricted by your role.',
+ code: 'RESTRICTED_BY_ROLE',
+ id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
+ },
},
-
res: {
type: 'object',
optional: false, nullable: false,
@@ -77,7 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
-
+ const alwaysMarkNsfw = (await this.roleService.getUserPolicies(me.id)).alwaysMarkNsfw;
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
@@ -93,6 +98,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.comment !== undefined) file.comment = ps.comment;
+ if (ps.isSensitive !== undefined && ps.isSensitive !== file.isSensitive && alwaysMarkNsfw && !ps.isSensitive) {
+ throw new ApiError(meta.errors.restrictedByRole);
+ }
+
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
if (ps.folderId !== undefined) {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
index cfef793831..c835587c4a 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -19,6 +19,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:drive',
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
index f21d9d5c33..3172bdbfda 100644
--- a/packages/backend/src/server/api/endpoints/flash/create.ts
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -11,6 +11,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:flash',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts
index 5581b8ec60..23de2f3970 100644
--- a/packages/backend/src/server/api/endpoints/flash/like.ts
+++ b/packages/backend/src/server/api/endpoints/flash/like.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:flash-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/flash/unlike.ts b/packages/backend/src/server/api/endpoints/flash/unlike.ts
index b994f5d347..696512b06c 100644
--- a/packages/backend/src/server/api/endpoints/flash/unlike.ts
+++ b/packages/backend/src/server/api/endpoints/flash/unlike.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:flash-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/flash/update.ts b/packages/backend/src/server/api/endpoints/flash/update.ts
index cd4e413a40..78dfd4a06a 100644
--- a/packages/backend/src/server/api/endpoints/flash/update.ts
+++ b/packages/backend/src/server/api/endpoints/flash/update.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:flash',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts
index 411c39110a..4ad16de911 100644
--- a/packages/backend/src/server/api/endpoints/following/create.ts
+++ b/packages/backend/src/server/api/endpoints/following/create.ts
@@ -19,6 +19,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:following',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
index cb8b6a2e3e..ca6bfa7e0f 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:gallery',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
index 519e56ed6a..6ac5fa8606 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:gallery-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
index cfbedcc4d9..513089217d 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:gallery-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
index f14d644a3a..a2a10d8400 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts
@@ -11,6 +11,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:gallery',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index c3f2ea9ea7..dd3549020e 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -44,7 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.usersRepository.createQueryBuilder('user')
- .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) });
+ .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) })
+ .andWhere('user.isSuspended = FALSE');
const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index 102dae4fb7..4eef496385 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -4,6 +4,7 @@ import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService
export const meta = {
requireCredential: true,
+ prohibitMoved: true,
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/i/export-antennas.ts b/packages/backend/src/server/api/endpoints/i/export-antennas.ts
new file mode 100644
index 0000000000..4182c1b247
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/export-antennas.ts
@@ -0,0 +1,30 @@
+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('1hour'),
+ 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.createExportAntennasJob(me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
new file mode 100644
index 0000000000..efb5ce4223
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -0,0 +1,84 @@
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueueService } from '@/core/QueueService.js';
+import type { AntennasRepository, DriveFilesRepository, UsersRepository, Antenna as _Antenna } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
+import { DownloadService } from '@/core/DownloadService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ secure: true,
+ requireCredential: true,
+ prohibitMoved: true,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 1,
+ },
+ errors: {
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: '3b71d086-c3fa-431c-b01d-ded65a777172',
+ },
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: 'e842c379-8ac7-4cf7-b07a-4d4de7e4671c',
+ },
+ emptyFile: {
+ message: 'That file is empty.',
+ code: 'EMPTY_FILE',
+ id: '7f60115d-8d93-4b0f-bd0e-3815dcbb389f',
+ },
+ tooManyAntennas: {
+ message: 'You cannot create antenna any more.',
+ code: 'TOO_MANY_ANTENNAS',
+ id: '600917d4-a4cb-4cc5-8ba8-7ac8ea3c7779',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ fileId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['fileId'],
+} as const;
+
+@Injectable() // eslint-disable-next-line import/no-default-export
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor (
+ @Inject(DI.driveFilesRepository)
+ private driveFilesRepository: DriveFilesRepository,
+
+ @Inject(DI.antennasRepository)
+ private antennasRepository: AntennasRepository,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ private roleService: RoleService,
+ private queueService: QueueService,
+ private downloadService: DownloadService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const users = await this.usersRepository.findOneBy({ id: me.id });
+ if (users === null) throw new ApiError(meta.errors.noSuchUser);
+ const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
+ if (file === null) throw new ApiError(meta.errors.noSuchFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+ const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url));
+ const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
+ if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
+ throw new ApiError(meta.errors.tooManyAntennas);
+ }
+ this.queueService.createImportAntennasJob(me, antennas);
+ });
+ }
+}
+
+export type Antenna = (_Antenna & { userListAccts: string[] | null })[];
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 8c1c158ab1..811971591a 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
@@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ prohibitMoved: true,
limit: {
duration: ms('1hour'),
@@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private queueService: QueueService,
+ private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+ const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
+ me,
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ true
+ );
+ if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
+
this.queueService.createImportBlockingJob(me, file.id);
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index 383bdc02b5..8af278c883 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
@@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ prohibitMoved: true,
limit: {
duration: ms('1hour'),
max: 1,
@@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private queueService: QueueService,
+ private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+ const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
+ me,
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ true
+ );
+ if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
+
this.queueService.createImportFollowingJob(me, file.id);
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 345ad916cb..eb0f9ba474 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
@@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ prohibitMoved: true,
limit: {
duration: ms('1hour'),
@@ -58,15 +60,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private queueService: QueueService,
+ private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+ const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
+ me,
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ true
+ );
+ if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
+
this.queueService.createImportMutingJob(me, file.id);
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index 875af7ec23..4568e93901 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
import type { DriveFilesRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
@@ -9,6 +10,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ prohibitMoved: true,
limit: {
duration: ms('1hour'),
max: 1,
@@ -57,15 +59,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private queueService: QueueService,
+ private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
//if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
- if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+ const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
+ me,
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ true
+ );
+ if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
+
this.queueService.createImportUserListsJob(me, file.id);
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts
deleted file mode 100644
index 964704d82b..0000000000
--- a/packages/backend/src/server/api/endpoints/i/known-as.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import ms from 'ms';
-
-import { User } from '@/models/entities/User.js';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ApiError } from '@/server/api/error.js';
-
-import { AccountMoveService } from '@/core/AccountMoveService.js';
-import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
-
-export const meta = {
- tags: ['users'],
-
- secure: true,
- requireCredential: true,
-
- limit: {
- duration: ms('1day'),
- max: 30,
- },
-
- errors: {
- noSuchUser: {
- message: 'No such user.',
- code: 'NO_SUCH_USER',
- id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
- },
- notRemote: {
- message: 'User is not remote. You can only migrate from other instances.',
- code: 'NOT_REMOTE',
- id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
- },
- uriNull: {
- message: 'User ActivityPup URI is null.',
- code: 'URI_NULL',
- id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
- },
- },
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- alsoKnownAs: { type: 'string' },
- },
- required: ['alsoKnownAs'],
-} as const;
-
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- private userEntityService: UserEntityService,
- private remoteUserResolveService: RemoteUserResolveService,
- private apiLoggerService: ApiLoggerService,
- private accountMoveService: AccountMoveService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- // Check parameter
- if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
-
- let unfiltered = ps.alsoKnownAs;
- const updates = {} as Partial<User>;
-
- if (!unfiltered) {
- updates.alsoKnownAs = null;
- } else {
- // Parse user's input into the old account
- if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
- if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
- if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
-
- const userAddress = unfiltered.split('@');
- // Retrieve the old account
- const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
- this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
- throw new ApiError(meta.errors.noSuchUser);
- });
-
- const toUrl: string | null = knownAs.uri;
- if (!toUrl) throw new ApiError(meta.errors.uriNull);
- // Only allow moving from a remote account
- if (this.userEntityService.isLocalUser(knownAs)) throw new ApiError(meta.errors.notRemote);
-
- updates.alsoKnownAs = updates.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
- }
-
- return await this.accountMoveService.createAlias(me, updates);
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts
index ac76e1f620..261dd527c0 100644
--- a/packages/backend/src/server/api/endpoints/i/move.ts
+++ b/packages/backend/src/server/api/endpoints/i/move.ts
@@ -7,40 +7,35 @@ import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
+import { LocalUser, RemoteUser } from '@/models/entities/User.js';
+
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+
+import * as Acct from '@/misc/acct.js';
export const meta = {
tags: ['users'],
secure: true,
requireCredential: true,
+ prohibitMoved: true,
limit: {
duration: ms('1day'),
max: 5,
},
errors: {
- noSuchMoveTarget: {
- message: 'No such move target.',
- code: 'NO_SUCH_MOVE_TARGET',
- id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
- },
- remoteAccountForbids: {
+ destinationAccountForbids: {
message:
- 'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
- code: 'REMOTE_ACCOUNT_FORBIDS',
+ 'Destination account doesn\'t have proper \'Known As\' alias, or has already moved.',
+ code: 'DESTINATION_ACCOUNT_FORBIDS',
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
},
- notRemote: {
- message: 'User is not remote. You can only migrate to other instances.',
- code: 'NOT_REMOTE',
- id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
- },
rootForbidden: {
message: 'The root can\'t migrate.',
code: 'NOT_ROOT_FORBIDDEN',
@@ -84,57 +79,52 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.config)
private config: Config,
- private userEntityService: UserEntityService,
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService,
private getterService: GetterService,
private apPersonService: ApPersonService,
+ private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
// check parameter
- if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
+ if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
// abort if user is the root
if (me.isRoot) throw new ApiError(meta.errors.rootForbidden);
// abort if user has already moved
if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
- let unfiltered = ps.moveToAccount;
- if (!unfiltered) throw new ApiError(meta.errors.noSuchMoveTarget);
-
// parse user's input into the destination account
- if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
- if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
- if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
-
- const userAddress = unfiltered.split('@');
+ const { username, host } = Acct.parse(ps.moveToAccount);
// retrieve the destination account
- let moveTo = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
+ let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
- throw new ApiError(meta.errors.noSuchMoveTarget);
+ throw new ApiError(meta.errors.noSuchUser);
});
- const remoteMoveTo = await this.getterService.getRemoteUser(moveTo.id);
- if (!remoteMoveTo.uri) throw new ApiError(meta.errors.uriNull);
+ const destination = await this.getterService.getUser(moveTo.id) as LocalUser | RemoteUser;
+ const newUri = this.userEntityService.getUserUri(destination);
// update local db
- await this.apPersonService.updatePerson(remoteMoveTo.uri);
+ await this.apPersonService.updatePerson(newUri);
// retrieve updated user
- moveTo = await this.apPersonService.resolvePerson(remoteMoveTo.uri);
- // only allow moving to a remote account
- if (this.userEntityService.isLocalUser(moveTo)) throw new ApiError(meta.errors.notRemote);
+ moveTo = await this.apPersonService.resolvePerson(newUri);
- let allowed = false;
-
- const fromUrl = `${this.config.url}/users/${me.id}`;
// make sure that the user has indicated the old account as an alias
- moveTo.alsoKnownAs?.forEach((elem) => {
- if (fromUrl.includes(elem)) allowed = true;
- });
+ const fromUrl = this.userEntityService.genLocalUserUri(me.id);
+ let allowed = false;
+ if (moveTo.alsoKnownAs) {
+ for (const knownAs of moveTo.alsoKnownAs) {
+ if (knownAs.includes(fromUrl)) {
+ allowed = true;
+ break;
+ }
+ }
+ }
// abort if unintended
- if (!(allowed && moveTo.uri && fromUrl)) throw new ApiError(meta.errors.remoteAccountForbids);
+ if (!allowed || moveTo.movedToUri) throw new ApiError(meta.errors.destinationAccountForbids);
- return await this.accountMoveService.moveToRemote(me, moveTo);
+ return await this.accountMoveService.moveFromLocal(me, moveTo);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index ba0487f223..e141be764a 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -1,5 +1,5 @@
import { Brackets, In } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js';
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts
index d4af00027e..2293500945 100644
--- a/packages/backend/src/server/api/endpoints/i/pin.ts
+++ b/packages/backend/src/server/api/endpoints/i/pin.ts
@@ -8,6 +8,7 @@ export const meta = {
tags: ['account', 'notes'],
requireCredential: true,
+ prohibitMoved: true,
kind: 'write:account',
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index be1c72b207..6c66300bb7 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
+import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/entities/User.js';
@@ -19,7 +20,10 @@ import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
+import { AccountMoveService } from '@/core/AccountMoveService.js';
+import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -71,6 +75,30 @@ export const meta = {
code: 'TOO_MANY_MUTED_WORDS',
id: '010665b1-a211-42d2-bc64-8f6609d79785',
},
+
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
+ },
+
+ uriNull: {
+ message: 'User ActivityPup URI is null.',
+ code: 'URI_NULL',
+ id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
+ },
+
+ forbiddenToSetYourself: {
+ message: 'You can\'t set yourself as your own alias.',
+ code: 'FORBIDDEN_TO_SET_YOURSELF',
+ id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
+ },
+
+ restrictedByRole: {
+ message: 'This feature is restricted by your role.',
+ code: 'RESTRICTED_BY_ROLE',
+ id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
+ }
},
res: {
@@ -129,6 +157,12 @@ export const paramDef = {
emailNotificationTypes: { type: 'array', items: {
type: 'string',
} },
+ alsoKnownAs: {
+ type: 'array',
+ maxItems: 10,
+ uniqueItems: true,
+ items: { type: 'string' },
+ },
},
} as const;
@@ -153,6 +187,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
private accountUpdateService: AccountUpdateService,
+ private accountMoveService: AccountMoveService,
+ private remoteUserResolveService: RemoteUserResolveService,
+ private apiLoggerService: ApiLoggerService,
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
@@ -208,7 +245,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
- if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
+ if (typeof ps.alwaysMarkNsfw === 'boolean') {
+ if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
+ profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
+ }
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
@@ -221,6 +261,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
updates.avatarBlurhash = avatar.blurhash;
+ } else if (ps.avatarId === null) {
+ updates.avatarId = null;
+ updates.avatarUrl = null;
+ updates.avatarBlurhash = null;
}
if (ps.bannerId) {
@@ -232,6 +276,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
updates.bannerBlurhash = banner.blurhash;
+ } else if (ps.bannerId === null) {
+ updates.bannerId = null;
+ updates.bannerUrl = null;
+ updates.bannerBlurhash = null;
}
if (ps.pinnedPageId) {
@@ -252,6 +300,38 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
}
+ if (ps.alsoKnownAs) {
+ if (_user.movedToUri) {
+ throw new ApiError({
+ message: 'You have moved your account.',
+ code: 'YOUR_ACCOUNT_MOVED',
+ id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
+ httpStatusCode: 403,
+ });
+ }
+
+ // Parse user's input into the old account
+ const newAlsoKnownAs = new Set<string>();
+ for (const line of ps.alsoKnownAs) {
+ if (!line) throw new ApiError(meta.errors.noSuchUser);
+ const { username, host } = Acct.parse(line);
+
+ // Retrieve the old account
+ const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
+ this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
+ throw new ApiError(meta.errors.noSuchUser);
+ });
+ if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
+
+ const toUrl = this.userEntityService.getUserUri(knownAs);
+ if (!toUrl) throw new ApiError(meta.errors.uriNull);
+
+ newAlsoKnownAs.add(toUrl);
+ }
+
+ updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
+ }
+
//#region emojis/tags
let emojis = [] as string[];
@@ -279,6 +359,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
//#endregion
if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
+ if (Object.keys(updates).includes('alsoKnownAs')) {
+ this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
+ }
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 37974ce2a3..584ea07c3b 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -201,10 +201,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
- elasticsearch: {
- type: 'boolean',
- optional: false, nullable: false,
- },
hcaptcha: {
type: 'boolean',
optional: false, nullable: false,
@@ -310,6 +306,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
translatorAvailable: instance.deeplAuthKey != null,
+ serverRules: instance.serverRules,
+
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
@@ -329,7 +327,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
response.features = {
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
- elasticsearch: this.config.elasticsearch ? true : false,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts
index 6e24e1024d..ee358d5c6c 100644
--- a/packages/backend/src/server/api/endpoints/mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/mute/create.ts
@@ -11,6 +11,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
+ prohibitMoved: true,
kind: 'write:mutes',
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 69fafcb9c7..3f7f2cdece 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -18,6 +18,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
limit: {
duration: ms('1hour'),
max: 300,
@@ -260,7 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let channel: Channel | null = null;
if (ps.channelId != null) {
- channel = await this.channelsRepository.findOneBy({ id: ps.channelId });
+ channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
index 0ce80a1a63..611ea19560 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -12,6 +12,7 @@ export const meta = {
tags: ['notes', 'favorites'],
requireCredential: true,
+ prohibitMoved: true,
kind: 'write:favorites',
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
index 2a44dc537e..3a33b037f8 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts
@@ -17,6 +17,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:votes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index 04e374d1ae..97cb026779 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:reactions',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index fb5abd917f..f6385400c3 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -1,11 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { QueryService } from '@/core/QueryService.js';
+import { SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
-import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@@ -43,8 +42,7 @@ export const paramDef = {
offset: { type: 'integer', default: 0 },
host: {
type: 'string',
- nullable: true,
- description: 'The local host is represented with `null`.',
+ description: 'The local host is represented with `.`.',
},
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
@@ -61,11 +59,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.config)
private config: Config,
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
-
private noteEntityService: NoteEntityService,
- private queryService: QueryService,
+ private searchService: SearchService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -74,27 +69,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.unavailable);
}
- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
-
- if (ps.userId) {
- query.andWhere('note.userId = :userId', { userId: ps.userId });
- } else if (ps.channelId) {
- query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
- }
-
- query
- .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
- .innerJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('renote.user', 'renoteUser');
-
- this.queryService.generateVisibilityQuery(query, me);
- if (me) this.queryService.generateMutedUserQuery(query, me);
- if (me) this.queryService.generateBlockedUserQuery(query, me);
-
- const notes = await query.take(ps.limit).getMany();
+ const notes = await this.searchService.searchNote(ps.query, me, {
+ userId: ps.userId,
+ channelId: ps.channelId,
+ host: ps.host,
+ }, {
+ untilId: ps.untilId,
+ sinceId: ps.sinceId,
+ limit: ps.limit,
+ });
return await this.noteEntityService.packMany(notes, me);
});
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index 4015bf1f29..e08ab399f8 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:pages',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts
index d27990f7e1..543c126d9c 100644
--- a/packages/backend/src/server/api/endpoints/pages/like.ts
+++ b/packages/backend/src/server/api/endpoints/pages/like.ts
@@ -10,6 +10,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:page-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts
index e397e2a23b..f0c0198460 100644
--- a/packages/backend/src/server/api/endpoints/pages/unlike.ts
+++ b/packages/backend/src/server/api/endpoints/pages/unlike.ts
@@ -9,6 +9,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:page-likes',
errors: {
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index 35b402ec56..751274067e 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -11,6 +11,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:pages',
limit: {
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
index b285269617..beb5850d78 100644
--- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
@@ -13,6 +13,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
+ prohibitMoved: true,
kind: 'write:mutes',
diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts
index 655dd7cd83..4ced6d3ff1 100644
--- a/packages/backend/src/server/api/endpoints/reset-db.ts
+++ b/packages/backend/src/server/api/endpoints/reset-db.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { resetDb } from '@/misc/reset-db.js';
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index b45d4af1fe..6202c740f1 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
+import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, RolesRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
@@ -65,12 +65,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({
id: ps.roleId,
+ isPublic: true,
});
if (role == null) {
throw new ApiError(meta.errors.noSuchRole);
}
-
+ if (!role.isExplorable) {
+ return [];
+ }
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const noteIdsRes = await this.redisClient.xrevrange(
`roleTimeline:${role.id}`,
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
index c80b6efdcd..6293c5cb50 100644
--- a/packages/backend/src/server/api/endpoints/username/available.ts
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -4,6 +4,7 @@ import type { UsedUsernamesRepository, UsersRepository } from '@/models/index.js
import { Endpoint } from '@/server/api/endpoint-base.js';
import { localUsernameSchema } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
+import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['users'],
@@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
+
+ private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
- // Get exist
const exist = await this.usersRepository.countBy({
host: IsNull(),
usernameLower: ps.username.toLowerCase(),
@@ -49,8 +51,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
+ const meta = await this.metaService.fetch();
+ const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
+
return {
- available: exist === 0 && exist2 === 0,
+ available: exist === 0 && exist2 === 0 && !isPreserved,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index 8becb68a34..28cd9f6ce5 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -50,8 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.usersRepository.createQueryBuilder('user');
- query.where('user.isExplorable = TRUE');
+ const query = this.usersRepository.createQueryBuilder('user')
+ .where('user.isExplorable = TRUE')
+ .andWhere('user.isSuspended = FALSE');
switch (ps.state) {
case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts
index a840c1a04e..7510889526 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts
@@ -13,6 +13,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
description: 'Create a new list of users.',
@@ -58,7 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists);
}
-
+
const userList = await this.userListsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
index d2dd5731ee..d50b70efc2 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts
@@ -12,6 +12,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
description: 'Remove a user from a list.',
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
index 1c1fdc23f1..925037e484 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/push.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -12,6 +12,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
kind: 'write:account',
description: 'Add a user to an existing list.',
diff --git a/packages/backend/src/server/api/endpoints/users/update-memo.ts b/packages/backend/src/server/api/endpoints/users/update-memo.ts
new file mode 100644
index 0000000000..ca7756ef75
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/update-memo.ts
@@ -0,0 +1,85 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { IdService } from '@/core/IdService.js';
+import type { UserMemoRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { GetterService } from '@/server/api/GetterService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['account'],
+
+ requireCredential: true,
+
+ kind: 'write:account',
+
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '6fef56f3-e765-4957-88e5-c6f65329b8a5',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ memo: {
+ type: 'string',
+ nullable: true,
+ description: 'A personal memo for the target user. If null or empty, delete the memo.',
+ },
+ },
+ required: ['userId', 'memo'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userMemosRepository)
+ private userMemosRepository: UserMemoRepository,
+ private getterService: GetterService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ // Get target
+ const target = await this.getterService.getUser(ps.userId).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ // 引数がnullか空文字であれば、パーソナルメモを削除する
+ if (ps.memo === '' || ps.memo == null) {
+ await this.userMemosRepository.delete({
+ userId: me.id,
+ targetUserId: target.id,
+ });
+ return;
+ }
+
+ // 以前に作成されたパーソナルメモがあるかどうか確認
+ const previousMemo = await this.userMemosRepository.findOneBy({
+ userId: me.id,
+ targetUserId: target.id,
+ });
+
+ if (!previousMemo) {
+ await this.userMemosRepository.insert({
+ id: this.idService.genId(),
+ userId: me.id,
+ targetUserId: target.id,
+ memo: ps.memo,
+ });
+ } else {
+ await this.userMemosRepository.update(previousMemo.id, {
+ userId: me.id,
+ targetUserId: target.id,
+ memo: ps.memo,
+ });
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index b3e193cd34..e61e92c623 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -70,10 +70,10 @@ export class UrlPreviewService {
await summaly(url, {
followRedirects: false,
lang: lang ?? 'ja-JP',
- agent: {
+ agent: this.config.proxy ? {
http: this.httpRequestService.httpAgent,
https: this.httpRequestService.httpsAgent,
- },
+ } : undefined,
});
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index d772ac3184..cb5d05a403 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -36,7 +36,7 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
//- https://github.com/misskey-dev/misskey/issues/9842
- link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.12.0')
+ link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists