Merge tag '2023.12.0-beta.6' into merge-upstream

This commit is contained in:
riku6460
2023-12-21 11:56:34 +09:00
251 changed files with 2328 additions and 1176 deletions

View File

@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ffVisibility1702718871541 {
constructor() {
this.name = 'ffVisibility1702718871541';
}
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`);
await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`);
await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`);
await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`UPDATE "user_profile" SET ffVisibility = "user_profile"."followingVisibility"`);
await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`);
}
}

View File

@@ -65,7 +65,7 @@
"@bull-board/api": "5.10.2",
"@bull-board/fastify": "5.10.2",
"@bull-board/ui": "5.10.2",
"@discordapp/twemoji": "14.1.2",
"@discordapp/twemoji": "15.0.2",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.2.0",
"@fastify/cors": "8.4.2",
@@ -83,6 +83,7 @@
"@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63",
"@swc/core": "1.3.100",
"@twemoji/parser": "15.0.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "6.0.1",
@@ -121,8 +122,8 @@
"jsonld": "8.3.2",
"jsrsasign": "10.9.0",
"meilisearch": "0.36.0",
"mfm-js": "0.23.3",
"microformats-parser": "1.5.2",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
@@ -166,7 +167,6 @@
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
"typescript": "5.3.3",
"ulid": "2.3.0",
@@ -195,7 +195,7 @@
"@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.10.4",
"@types/node": "20.10.5",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4",

View File

@@ -77,6 +77,17 @@ export class FeaturedService {
return Array.from(ranking.keys());
}
@bindThis
private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
await redisPipeline.exec();
}
@bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
@@ -126,4 +137,9 @@ export class FeaturedService {
public getHashtagsRanking(threshold: number): Promise<string[]> {
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold);
}
@bindThis
public removeHashtagsFromRanking(hashtag: string): Promise<void> {
return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag);
}
}

View File

@@ -11,6 +11,7 @@ import { MiMeta } from '@/models/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
) {
//this.onMessage = this.onMessage.bind(this);
@@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown {
@bindThis
public async update(data: Partial<MiMeta>): Promise<MiMeta> {
let before: MiMeta | undefined;
const updated = await this.db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
@@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown {
},
});
const meta = metas[0];
before = metas[0];
if (meta) {
await transactionalEntityManager.update(MiMeta, meta.id, data);
if (before) {
await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
@@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown {
}
});
if (data.hiddenTags) {
process.nextTick(() => {
const hiddenTags = new Set<string>(data.hiddenTags);
if (before) {
for (const previousHiddenTag of before.hiddenTags) {
hiddenTags.delete(previousHiddenTag);
}
}
for (const hiddenTag of hiddenTags) {
this.featuredService.removeHashtagsFromRanking(hiddenTag);
}
});
}
this.globalEventService.publishInternalEvent('metaUpdated', updated);
return updated;

View File

@@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Check blocking
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
if (data.renote && this.isQuote(data)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@@ -623,7 +623,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// If it is renote
if (data.renote) {
const type = data.text ? 'quote' : 'renote';
const type = this.isQuote(data) ? 'quote' : 'renote';
// Notify
if (data.renote.userHost === null) {
@@ -730,6 +730,11 @@ export class NoteCreateService implements OnApplicationShutdown {
return false;
}
@bindThis
private isQuote(note: Option): boolean {
return !!note.text || !!note.cw || !!note.files || !!note.poll;
}
@bindThis
private incRenoteCount(renote: MiNote) {
this.notesRepository.createQueryBuilder().update()
@@ -796,7 +801,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
const content = data.renote && this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);

View File

@@ -6,7 +6,14 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { In } from 'typeorm';
import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
import { ModuleRef } from '@nestjs/core';
import type {
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
RolesRepository,
UsersRepository,
} from '@/models/_.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MiUser } from '@/models/User.js';
@@ -17,12 +24,13 @@ import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = {
gtlAvailable: boolean;
@@ -85,17 +93,23 @@ export const DEFAULT_POLICIES: RolePolicies = {
};
@Injectable()
export class RoleService implements OnApplicationShutdown {
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
constructor(
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
private moduleRef: ModuleRef,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -121,6 +135,10 @@ export class RoleService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage);
}
async onModuleInit() {
this.notificationService = this.moduleRef.get(NotificationService.name);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
@@ -427,6 +445,12 @@ export class RoleService implements OnApplicationShutdown {
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
} else if (existing.expiresAt !== expiresAt) {
await this.roleAssignmentsRepository.update(existing.id, {
expiresAt: expiresAt,

View File

@@ -10,15 +10,15 @@ import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js';
import type { MiUserListMembership } from '@/models/UserListMembership.js';
import { IdService } from '@/core/IdService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class UserListService implements OnApplicationShutdown {

View File

@@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
@@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're
export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private customEmojiService: CustomEmojiService;
private roleEntityService: RoleEntityService;
constructor(
private moduleRef: ModuleRef,
@@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
//private customEmojiService: CustomEmojiService,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.roleEntityService = this.moduleRef.get('RoleEntityService');
}
@bindThis
@@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit {
detail: false,
})
) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({
id: notification.id,
@@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
@@ -217,6 +220,8 @@ export class NotificationEntityService implements OnModuleInit {
});
}
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
@@ -227,6 +232,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),

View File

@@ -334,13 +334,13 @@ export class UserEntityService implements OnModuleInit {
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
(profile.followersVisibility === 'public') || isMe ? user.followersCount :
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
@@ -417,7 +417,8 @@ export class UserEntityService implements OnModuleInit {
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions,
ffVisibility: profile!.ffVisibility,
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled

File diff suppressed because one or more lines are too long

View File

@@ -3,11 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { notificationTypes } from '@/types.js';
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import { MiFollowRequest } from './FollowRequest.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
export type MiNotification = {
type: 'note';
@@ -68,6 +67,11 @@ export type MiNotification = {
id: string;
createdAt: string;
notifierId: MiUser['id'];
} | {
type: 'roleAssigned';
id: string;
createdAt: string;
roleId: MiRole['id'];
} | {
type: 'achievementEarned';
id: string;

View File

@@ -4,7 +4,7 @@
*/
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js';
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
@@ -94,10 +94,16 @@ export class MiUserProfile {
public publicReactions: boolean;
@Column('enum', {
enum: ffVisibility,
enum: followingVisibilities,
default: 'public',
})
public ffVisibility: typeof ffVisibility[number];
public followingVisibility: typeof followingVisibilities[number];
@Column('enum', {
enum: followersVisibilities,
default: 'public',
})
public followersVisibility: typeof followersVisibilities[number];
@Column('varchar', {
length: 128, nullable: true,

View File

@@ -315,7 +315,12 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
ffVisibility: {
followingVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
followersVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
@@ -541,9 +546,7 @@ export const packedMeDetailedOnlySchema = {
mention: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
},
},
emailNotificationTypes: {

View File

@@ -159,8 +159,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -198,8 +198,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -222,8 +222,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -246,8 +246,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -270,8 +270,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -299,8 +299,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion
@@ -322,8 +322,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.objectStorageQueueWorker
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
//#endregion

View File

@@ -195,11 +195,11 @@ export class ActivityPubServerService {
//#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followersVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followersVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
@@ -287,11 +287,11 @@ export class ActivityPubServerService {
//#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followingVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followingVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;

View File

@@ -13,6 +13,8 @@ import { AbuseUserReportEntityService } from '@/core/entities/AbuseUserReportEnt
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -15,6 +15,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
res: {
type: 'object',
optional: false, nullable: false,

View File

@@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -13,6 +13,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -10,6 +10,8 @@ import { AnnouncementService } from '@/core/AnnouncementService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -10,6 +10,8 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
} as const;

View File

@@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
errors: {

View File

@@ -15,6 +15,8 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',

View File

@@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',

View File

@@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,
} as const;

View File

@@ -10,6 +10,8 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -14,6 +14,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -14,6 +14,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -16,6 +16,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -8,7 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
export const meta = {
secure: true,
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -15,6 +15,8 @@ import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -15,6 +15,8 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -10,6 +10,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
} as const;

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',

View File

@@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -14,6 +14,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin',
tags: ['admin'],
} as const;

View File

@@ -12,6 +12,8 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin',
tags: ['admin'],
res: {

View File

@@ -12,6 +12,8 @@ import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -16,6 +16,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -12,6 +12,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js';
export const meta = {
tags: ['meta'],
kind: 'read:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -13,6 +13,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -12,6 +12,8 @@ import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -11,6 +11,8 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -10,6 +10,8 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -12,6 +12,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -10,6 +10,8 @@ import { RelayService } from '@/core/RelayService.js';
export const meta = {
tags: ['admin'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -10,6 +10,8 @@ import { RelayService } from '@/core/RelayService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -14,6 +14,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -15,6 +15,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -11,6 +11,8 @@ import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -12,6 +12,8 @@ import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'read:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -13,6 +13,8 @@ import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,

View File

@@ -11,6 +11,8 @@ import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,
} as const;

View File

@@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin', 'role'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,

View File

@@ -16,6 +16,8 @@ import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin', 'role', 'users'],
kind: 'read:admin',
requireCredential: false,
requireAdmin: true,

View File

@@ -10,6 +10,8 @@ import { EmailService } from '@/core/EmailService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -17,6 +17,8 @@ export const meta = {
tags: ['admin', 'meta'],
kind: 'read:admin',
res: {
type: 'object',
optional: false, nullable: false,

View File

@@ -16,6 +16,8 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin',
res: {
type: 'array',
optional: false, nullable: false,

View File

@@ -17,6 +17,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin',
res: {
type: 'object',
nullable: false, optional: false,

View File

@@ -17,6 +17,8 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin',
res: {
type: 'array',
nullable: false, optional: false,

View File

@@ -19,6 +19,8 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -13,6 +13,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireAdmin: true,
} as const;

View File

@@ -12,6 +12,8 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
kind: 'write:admin',
requireCredential: true,
requireModerator: true,
} as const;

View File

@@ -171,7 +171,8 @@ export const paramDef = {
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
autoSensitive: { type: 'boolean' },
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
mutedWords: { type: 'array', items: {
oneOf: [
@@ -240,7 +241,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
if (ps.mutedWords !== undefined) {
const length = ps.mutedWords.length;
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {

View File

@@ -93,11 +93,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followersVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followersVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {

View File

@@ -101,11 +101,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followingVisibility === 'private') {
if (me == null || (me.id !== user.id)) {
throw new ApiError(meta.errors.forbidden);
}
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followingVisibility === 'followers') {
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {

View File

@@ -60,7 +60,7 @@ export class FeedService {
title: `${author.name} (@${user.username}@${this.config.host})`,
updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined,
generator: 'Misskey',
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link,
image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
feedLinks: {

View File

@@ -14,18 +14,34 @@
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* roleAssigned - ロールが付与された
* achievementEarned - 実績を獲得
* app - アプリ通知
* test - テスト通知(サーバー側)
*/
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'achievementEarned',
'app',
'test'] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const ffVisibility = ['public', 'followers', 'private'] as const;
export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const;
export const moderationLogTypes = [
'updateServerSettings',

View File

@@ -26,9 +26,10 @@ describe('FF visibility', () => {
await app.close();
});
test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', {
ffVisibility: 'public',
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
@@ -44,9 +45,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
});
test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
ffVisibility: 'followers',
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
@@ -62,9 +142,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
});
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => {
await api('/i/update', {
ffVisibility: 'followers',
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
@@ -78,9 +237,82 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400);
});
test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れ', async () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
});
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
});
test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => {
await api('/i/update', {
ffVisibility: 'followers',
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
@@ -100,9 +332,106 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワー見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'public',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
});
test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
await api('/following/create', {
userId: alice.id,
}, bob);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => {
await api('/i/update', {
ffVisibility: 'private',
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
@@ -118,9 +447,88 @@ describe('FF visibility', () => {
assert.strictEqual(Array.isArray(followersRes.body), true);
});
test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, alice);
assert.strictEqual(followingRes.status, 200);
assert.strictEqual(Array.isArray(followingRes.body), true);
}
});
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, alice);
assert.strictEqual(followersRes.status, 200);
assert.strictEqual(Array.isArray(followersRes.body), true);
}
});
test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => {
await api('/i/update', {
ffVisibility: 'private',
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
@@ -134,36 +542,129 @@ describe('FF visibility', () => {
assert.strictEqual(followersRes.status, 400);
});
test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'public',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'followers',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await api('/users/following', {
userId: alice.id,
}, bob);
assert.strictEqual(followingRes.status, 400);
}
});
test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => {
{
await api('/i/update', {
followingVisibility: 'public',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'followers',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
{
await api('/i/update', {
followingVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followersRes = await api('/users/followers', {
userId: alice.id,
}, bob);
assert.strictEqual(followersRes.status, 400);
}
});
describe('AP', () => {
test('ffVisibility が public 以外ならばAPからは取得できない', async () => {
test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => {
{
await api('/i/update', {
ffVisibility: 'public',
followingVisibility: 'public',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 200);
}
{
await api('/i/update', {
followingVisibility: 'followers',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
}
{
await api('/i/update', {
followingVisibility: 'private',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
}
});
test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => {
{
await api('/i/update', {
followersVisibility: 'public',
}, alice);
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followersRes.status, 200);
}
{
await api('/i/update', {
ffVisibility: 'followers',
followersVisibility: 'followers',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
{
await api('/i/update', {
ffVisibility: 'private',
followersVisibility: 'private',
}, alice);
const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json');
const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json');
assert.strictEqual(followingRes.status, 403);
assert.strictEqual(followersRes.status, 403);
}
});

View File

@@ -113,7 +113,8 @@ describe('ユーザー', () => {
pinnedPageId: user.pinnedPageId,
pinnedPage: user.pinnedPage,
publicReactions: user.publicReactions,
ffVisibility: user.ffVisibility,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
twoFactorEnabled: user.twoFactorEnabled,
usePasswordLessLogin: user.usePasswordLessLogin,
securityKeys: user.securityKeys,
@@ -387,7 +388,8 @@ describe('ユーザー', () => {
assert.strictEqual(response.pinnedPageId, null);
assert.strictEqual(response.pinnedPage, null);
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.ffVisibility, 'public');
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
assert.strictEqual(response.twoFactorEnabled, false);
assert.strictEqual(response.usePasswordLessLogin, false);
assert.strictEqual(response.securityKeys, false);
@@ -496,9 +498,12 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ alwaysMarkNsfw: false }) },
{ parameters: (): object => ({ autoSensitive: true }) },
{ parameters: (): object => ({ autoSensitive: false }) },
{ parameters: (): object => ({ ffVisibility: 'private' }) },
{ parameters: (): object => ({ ffVisibility: 'followers' }) },
{ parameters: (): object => ({ ffVisibility: 'public' }) },
{ parameters: (): object => ({ followingVisibility: 'private' }) },
{ parameters: (): object => ({ followingVisibility: 'followers' }) },
{ parameters: (): object => ({ followingVisibility: 'public' }) },
{ parameters: (): object => ({ followersVisibility: 'private' }) },
{ parameters: (): object => ({ followersVisibility: 'followers' }) },
{ parameters: (): object => ({ followersVisibility: 'public' }) },
{ parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) },
{ parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) },
{ parameters: (): object => ({ mutedWords: [] }) },

View File

@@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
@@ -32,6 +33,7 @@ describe('RoleService', () => {
let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository;
let metaService: jest.Mocked<MetaService>;
let notificationService: jest.Mocked<NotificationService>;
let clock: lolex.InstalledClock;
function createUser(data: Partial<MiUser> = {}) {
@@ -76,6 +78,8 @@ describe('RoleService', () => {
.useMocker((token) => {
if (token === MetaService) {
return { fetch: jest.fn() };
} else if (token === NotificationService) {
return { createNotification: jest.fn() };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
@@ -93,6 +97,7 @@ describe('RoleService', () => {
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
});
afterEach(async () => {
@@ -273,4 +278,53 @@ describe('RoleService', () => {
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
});
});
describe('assign', () => {
test('公開ロールの場合は通知される', async () => {
const user = await createUser();
const role = await createRole({
isPublic: true,
});
await roleService.assign(user.id, role.id);
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).toHaveBeenCalled();
expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id);
expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned');
expect(notificationService.createNotification.mock.lastCall![2]).toBe({
roleId: role.id,
});
});
test('非公開ロールの場合は通知されない', async () => {
const user = await createUser();
const role = await createRole({
isPublic: false,
});
await roleService.assign(user.id, role.id);
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).not.toHaveBeenCalled();
});
});
});