summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/migration/1665091090561-add-renote-muting.js16
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/QueryService.ts26
-rw-r--r--packages/backend/src/core/entities/RenoteMutingEntityService.ts47
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts13
-rw-r--r--packages/backend/src/di-symbols.ts1
-rw-r--r--packages/backend/src/misc/schema.ts2
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/entities/RenoteMuting.ts42
-rw-r--r--packages/backend/src/models/index.ts3
-rw-r--r--packages/backend/src/models/schema/renote-muting.ts26
-rw-r--r--packages/backend/src/models/schema/user.ts4
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts12
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/notes/timeline.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/create.ts99
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/delete.ts87
-rw-r--r--packages/backend/src/server/api/endpoints/renote-mute/list.ts57
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts8
-rw-r--r--packages/backend/src/server/api/stream/channel.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts2
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts2
-rw-r--r--packages/backend/src/server/api/stream/index.ts18
-rw-r--r--packages/backend/test/e2e/block.ts6
-rw-r--r--packages/backend/test/e2e/endpoints.ts6
-rw-r--r--packages/backend/test/e2e/mute.ts12
-rw-r--r--packages/backend/test/e2e/renote-mute.ts85
-rw-r--r--packages/backend/test/utils.ts4
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue64
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts12
41 files changed, 680 insertions, 26 deletions
diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js
new file mode 100644
index 0000000000..d2ed2bd2e9
--- /dev/null
+++ b/packages/backend/migration/1665091090561-add-renote-muting.js
@@ -0,0 +1,16 @@
+
+export class addRenoteMuting1665091090561 {
+ constructor() {
+ this.name = 'addRenoteMuting1665091090561';
+ }
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`);
+ await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
+ await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
+ await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
+ }
+
+ async down(queryRunner) {
+ }
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 491d8ab113..1fd2d15004 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -82,6 +82,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js';
+import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
import { NoteEntityService } from './entities/NoteEntityService.js';
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
@@ -203,6 +204,7 @@ const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useEx
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
+const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
@@ -325,6 +327,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService,
ModerationLogEntityService,
MutingEntityService,
+ RenoteMutingEntityService,
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
@@ -442,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService,
$ModerationLogEntityService,
$MutingEntityService,
+ $RenoteMutingEntityService,
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
@@ -559,6 +563,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
InstanceEntityService,
ModerationLogEntityService,
MutingEntityService,
+ RenoteMutingEntityService,
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
@@ -675,6 +680,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$InstanceEntityService,
$ModerationLogEntityService,
$MutingEntityService,
+ $RenoteMutingEntityService,
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index c334d749e6..0cee2076bf 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
-import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js';
+import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm';
@@ -29,6 +29,9 @@ export class QueryService {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
+
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
) {
}
@@ -269,5 +272,24 @@ export class QueryService {
q.setParameters({ meId: me.id });
}
}
-}
+ @bindThis
+ public generateMutedUserRenotesQueryForNotes(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
+ const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
+ .select('renote_muting.muteeId')
+ .where('renote_muting.muterId = :muterId', { muterId: me.id });
+
+ q.andWhere(new Brackets(qb => {
+ qb
+ .where(new Brackets(qb => {
+ qb.where('note.renoteId IS NOT NULL');
+ qb.andWhere('note.text IS NULL');
+ qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
+ }))
+ .orWhere('note.renoteId IS NULL')
+ .orWhere('note.text IS NOT NULL');
+ }));
+
+ q.setParameters(mutingQuery.getParameters());
+ }
+}
diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
new file mode 100644
index 0000000000..66ee7305a1
--- /dev/null
+++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
@@ -0,0 +1,47 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { RenoteMutingsRepository } from '@/models/index.js';
+import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { Packed } from '@/misc/schema.js';
+import type { } from '@/models/entities/Blocking.js';
+import type { User } from '@/models/entities/User.js';
+import type { RenoteMuting } from '@/models/entities/RenoteMuting.js';
+import { bindThis } from '@/decorators.js';
+import { UserEntityService } from './UserEntityService.js';
+
+@Injectable()
+export class RenoteMutingEntityService {
+ constructor(
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
+ private userEntityService: UserEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: RenoteMuting['id'] | RenoteMuting,
+ me?: { id: User['id'] } | null | undefined,
+ ): Promise<Packed<'RenoteMuting'>> {
+ const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
+
+ return await awaitAll({
+ id: muting.id,
+ createdAt: muting.createdAt.toISOString(),
+ muteeId: muting.muteeId,
+ mutee: this.userEntityService.pack(muting.muteeId, me, {
+ detail: true,
+ }),
+ });
+ }
+
+ @bindThis
+ public packMany(
+ mutings: any[],
+ me: { id: User['id'] },
+ ) {
+ return Promise.all(mutings.map(x => this.pack(x, me)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 3635643218..e7aa885f3c 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, 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, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
+import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -78,6 +78,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@@ -195,6 +198,13 @@ export class UserEntityService implements OnModuleInit {
},
take: 1,
}).then(n => n > 0),
+ isRenoteMuted: this.renoteMutingsRepository.count({
+ where: {
+ muterId: me,
+ muteeId: target,
+ },
+ take: 1,
+ }).then(n => n > 0),
});
}
@@ -493,6 +503,7 @@ export class UserEntityService implements OnModuleInit {
isBlocking: relation.isBlocking,
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
+ isRenoteMuted: relation.isRenoteMuted,
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 05603093be..187f930ace 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -36,6 +36,7 @@ export const DI = {
notificationsRepository: Symbol('notificationsRepository'),
metasRepository: Symbol('metasRepository'),
mutingsRepository: Symbol('mutingsRepository'),
+ renoteMutingsRepository: Symbol('renoteMutingsRepository'),
blockingsRepository: Symbol('blockingsRepository'),
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
hashtagsRepository: Symbol('hashtagsRepository'),
diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts
index 6a0802f8a4..0681cdb67e 100644
--- a/packages/backend/src/misc/schema.ts
+++ b/packages/backend/src/misc/schema.ts
@@ -15,6 +15,7 @@ import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
import { packedFollowingSchema } from '@/models/schema/following.js';
import { packedMutingSchema } from '@/models/schema/muting.js';
+import { packedRenoteMutingSchema } from '@/models/schema/renote-muting.js';
import { packedBlockingSchema } from '@/models/schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
@@ -48,6 +49,7 @@ export const refs = {
DriveFolder: packedDriveFolderSchema,
Following: packedFollowingSchema,
Muting: packedMutingSchema,
+ RenoteMuting: packedRenoteMutingSchema,
Blocking: packedBlockingSchema,
Hashtag: packedHashtagSchema,
Page: packedPageSchema,
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index 311f875ba5..d29b07b020 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, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, 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, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -190,6 +190,12 @@ const $mutingsRepository: Provider = {
inject: [DI.db],
};
+const $renoteMutingsRepository: Provider = {
+ provide: DI.renoteMutingsRepository,
+ useFactory: (db: DataSource) => db.getRepository(RenoteMuting),
+ inject: [DI.db],
+};
+
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
useFactory: (db: DataSource) => db.getRepository(Blocking),
@@ -423,6 +429,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository,
$metasRepository,
$mutingsRepository,
+ $renoteMutingsRepository,
$blockingsRepository,
$swSubscriptionsRepository,
$hashtagsRepository,
@@ -489,6 +496,7 @@ const $roleAssignmentsRepository: Provider = {
$notificationsRepository,
$metasRepository,
$mutingsRepository,
+ $renoteMutingsRepository,
$blockingsRepository,
$swSubscriptionsRepository,
$hashtagsRepository,
diff --git a/packages/backend/src/models/entities/RenoteMuting.ts b/packages/backend/src/models/entities/RenoteMuting.ts
new file mode 100644
index 0000000000..2f803a5fa8
--- /dev/null
+++ b/packages/backend/src/models/entities/RenoteMuting.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { User } from './User.js';
+
+@Entity()
+@Index(['muterId', 'muteeId'], { unique: true })
+export class RenoteMuting {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Index()
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Muting.',
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The mutee user ID.',
+ })
+ public muteeId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public mutee: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The muter user ID.',
+ })
+ public muterId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public muter: User | null;
+}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 25ed9b89d8..4acb958b04 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -26,6 +26,7 @@ import { Meta } from '@/models/entities/Meta.js';
import { ModerationLog } from '@/models/entities/ModerationLog.js';
import { MutedNote } from '@/models/entities/MutedNote.js';
import { Muting } from '@/models/entities/Muting.js';
+import { RenoteMuting } from '@/models/entities/RenoteMuting.js';
import { Note } from '@/models/entities/Note.js';
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
@@ -93,6 +94,7 @@ export {
ModerationLog,
MutedNote,
Muting,
+ RenoteMuting,
Note,
NoteFavorite,
NoteReaction,
@@ -159,6 +161,7 @@ export type MetasRepository = Repository<Meta>;
export type ModerationLogsRepository = Repository<ModerationLog>;
export type MutedNotesRepository = Repository<MutedNote>;
export type MutingsRepository = Repository<Muting>;
+export type RenoteMutingsRepository = Repository<RenoteMuting>;
export type NotesRepository = Repository<Note>;
export type NoteFavoritesRepository = Repository<NoteFavorite>;
export type NoteReactionsRepository = Repository<NoteReaction>;
diff --git a/packages/backend/src/models/schema/renote-muting.ts b/packages/backend/src/models/schema/renote-muting.ts
new file mode 100644
index 0000000000..69ed8510da
--- /dev/null
+++ b/packages/backend/src/models/schema/renote-muting.ts
@@ -0,0 +1,26 @@
+export const packedRenoteMutingSchema = {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ example: 'xxxxxxxxxx',
+ },
+ createdAt: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'date-time',
+ },
+ muteeId: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ mutee: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserDetailed',
+ },
+ },
+} as const;
diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts
index c390018b46..8c61ee1f5f 100644
--- a/packages/backend/src/models/schema/user.ts
+++ b/packages/backend/src/models/schema/user.ts
@@ -234,6 +234,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: true,
},
+ isRenoteMuted: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
//#endregion
},
} as const;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index c2ee14b0f4..741985f3a1 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -34,6 +34,7 @@ import { Meta } from '@/models/entities/Meta.js';
import { ModerationLog } from '@/models/entities/ModerationLog.js';
import { MutedNote } from '@/models/entities/MutedNote.js';
import { Muting } from '@/models/entities/Muting.js';
+import { RenoteMuting } from '@/models/entities/RenoteMuting.js';
import { Note } from '@/models/entities/Note.js';
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
@@ -139,6 +140,7 @@ export const entities = [
Following,
FollowRequest,
Muting,
+ RenoteMuting,
Blocking,
Note,
NoteFavorite,
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index d3e2219bd5..2724649590 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -224,6 +224,9 @@ import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js';
import * as ep___mute_list from './endpoints/mute/list.js';
+import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
+import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
+import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
import * as ep___my_apps from './endpoints/my/apps.js';
import * as ep___notes from './endpoints/notes.js';
import * as ep___notes_children from './endpoints/notes/children.js';
@@ -545,6 +548,9 @@ const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: e
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default };
+const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default };
+const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default };
+const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default };
const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default };
const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default };
@@ -870,6 +876,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$mute_create,
$mute_delete,
$mute_list,
+ $renoteMute_create,
+ $renoteMute_delete,
+ $renoteMute_list,
$my_apps,
$notes,
$notes_children,
@@ -1189,6 +1198,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$mute_create,
$mute_delete,
$mute_list,
+ $renoteMute_create,
+ $renoteMute_delete,
+ $renoteMute_list,
$my_apps,
$notes,
$notes_children,
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index 487eef2d50..13526f277d 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -3,17 +3,17 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import * as websocket from 'websocket';
import { DI } from '@/di-symbols.js';
-import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository } from '@/models/index.js';
+import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { NotificationService } from '@/core/NotificationService.js';
+import { bindThis } from '@/decorators.js';
import { AuthenticateService } from './AuthenticateService.js';
import MainStreamConnection from './stream/index.js';
import { ChannelsService } from './stream/ChannelsService.js';
import type { ParsedUrlQuery } from 'querystring';
import type * as http from 'node:http';
-import { bindThis } from '@/decorators.js';
@Injectable()
export class StreamingApiServerService {
@@ -33,6 +33,9 @@ export class StreamingApiServerService {
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@@ -84,6 +87,7 @@ export class StreamingApiServerService {
const main = new MainStreamConnection(
this.followingsRepository,
this.mutingsRepository,
+ this.renoteMutingsRepository,
this.blockingsRepository,
this.channelFollowingsRepository,
this.userProfilesRepository,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4f521148e0..1f01865e0a 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -224,6 +224,9 @@ import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js';
import * as ep___mute_list from './endpoints/mute/list.js';
+import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
+import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
+import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
import * as ep___my_apps from './endpoints/my/apps.js';
import * as ep___notes from './endpoints/notes.js';
import * as ep___notes_children from './endpoints/notes/children.js';
@@ -543,6 +546,9 @@ const eps = [
['mute/create', ep___mute_create],
['mute/delete', ep___mute_delete],
['mute/list', ep___mute_list],
+ ['renote-mute/create', ep___renoteMute_create],
+ ['renote-mute/delete', ep___renoteMute_delete],
+ ['renote-mute/list', ep___renoteMute_list],
['my/apps', ep___my_apps],
['notes', ep___notes],
['notes/children', ep___notes_children],
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 5d0cdc3fca..9118d33936 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -89,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
if (ps.withFiles) {
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2819abb125..3802ae540c 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -107,6 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index 18ed6d4e21..3810016952 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -95,6 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
+ if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index e6de087c4a..5ce436ee1c 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => {
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
new file mode 100644
index 0000000000..051a005b67
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts
@@ -0,0 +1,99 @@
+import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { IdService } from '@/core/IdService.js';
+import type { RenoteMutingsRepository } from '@/models/index.js';
+import type { RenoteMuting } from '@/models/entities/RenoteMuting.js';
+import { GlobalEventService } from '@/core/GlobalEventService.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:mutes',
+
+ limit: {
+ duration: ms('1hour'),
+ max: 20,
+ },
+
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '5e0a5dff-1e94-4202-87ae-4d9c89eb2271',
+ },
+
+ muteeIsYourself: {
+ message: 'Mutee is yourself.',
+ code: 'MUTEE_IS_YOURSELF',
+ id: '37285718-52f7-4aef-b7de-c38b8e8a8420',
+ },
+
+ alreadyMuting: {
+ message: 'You are already muting that user.',
+ code: 'ALREADY_MUTING',
+ id: 'ccfecbe4-1f1c-4fc2-8a3d-c3ffee61cb7b',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
+ private globalEventService: GlobalEventService,
+ private getterService: GetterService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const muter = me;
+
+ // 自分自身
+ if (me.id === ps.userId) {
+ throw new ApiError(meta.errors.muteeIsYourself);
+ }
+
+ // Get mutee
+ const mutee = await getterService.getUser(ps.userId).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ // Check if already muting
+ const exist = await this.renoteMutingsRepository.findOneBy({
+ muterId: muter.id,
+ muteeId: mutee.id,
+ });
+
+ if (exist != null) {
+ throw new ApiError(meta.errors.alreadyMuting);
+ }
+
+ // Create mute
+ await this.renoteMutingsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: new Date(),
+ muterId: muter.id,
+ muteeId: mutee.id,
+ } as RenoteMuting);
+
+ // publishUserEvent(user.id, 'mute', mutee);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
new file mode 100644
index 0000000000..51a895fb7e
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts
@@ -0,0 +1,87 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RenoteMutingsRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.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:mutes',
+
+ errors: {
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '9b6728cf-638c-4aa1-bedb-e07d8101474d',
+ },
+
+ muteeIsYourself: {
+ message: 'Mutee is yourself.',
+ code: 'MUTEE_IS_YOURSELF',
+ id: '619b1314-0850-4597-a242-e245f3da42af',
+ },
+
+ notMuting: {
+ message: 'You are not muting that user.',
+ code: 'NOT_MUTING',
+ id: '2e4ef874-8bf0-4b4b-b069-4598f6d05817',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
+ private globalEventService: GlobalEventService,
+ private getterService: GetterService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const muter = me;
+
+ // Check if the mutee is yourself
+ if (me.id === ps.userId) {
+ throw new ApiError(meta.errors.muteeIsYourself);
+ }
+
+ // Get mutee
+ const mutee = await this.getterService.getUser(ps.userId).catch(err => {
+ if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
+ throw err;
+ });
+
+ // Check not muting
+ const exist = await this.renoteMutingsRepository.findOneBy({
+ muterId: muter.id,
+ muteeId: mutee.id,
+ });
+
+ if (exist == null) {
+ throw new ApiError(meta.errors.notMuting);
+ }
+
+ // Delete mute
+ await this.renoteMutingsRepository.delete({
+ id: exist.id,
+ });
+
+ // publishUserEvent(user.id, 'unmute', mutee);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts
new file mode 100644
index 0000000000..b2d7addb64
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/renote-mute/list.ts
@@ -0,0 +1,57 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RenoteMutingsRepository } from '@/models/index.js';
+import { QueryService } from '@/core/QueryService.js';
+import { RenoteMutingEntityService } from '@/core/entities/RenoteMutingEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ tags: ['account'],
+
+ requireCredential: true,
+
+ kind: 'read:mutes',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'RenoteMuting',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ },
+ required: [],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.renoteMutingsRepository)
+ private renoteMutingsRepository: RenoteMutingsRepository,
+
+ private renoteMutingEntityService: RenoteMutingEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.renoteMutingsRepository.createQueryBuilder('muting'), ps.sinceId, ps.untilId)
+ .andWhere('muting.muterId = :meId', { meId: me.id });
+
+ const mutings = await query
+ .take(ps.limit)
+ .getMany();
+
+ return await this.renoteMutingEntityService.packMany(mutings, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index ac9104bf92..3267c18846 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -50,6 +50,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isRenoteMuted: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
},
{
@@ -91,6 +95,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isRenoteMuted: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
},
},
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 3e67880b45..32935325aa 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -27,6 +27,10 @@ export default abstract class Channel {
return this.connection.muting;
}
+ protected get renoteMuting() {
+ return this.connection.renoteMuting;
+ }
+
protected get blocking() {
return this.connection.blocking;
}
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 18604d94f0..e2a42fbfe9 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -39,6 +39,8 @@ class AntennaChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
this.connection.cacheNote(note);
this.send('note', note);
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index f5ef1d1102..1234738ce8 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -51,6 +51,8 @@ class ChannelChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
this.connection.cacheNote(note);
this.send('note', note);
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index b8c0076ed9..ab439a1717 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -68,6 +68,8 @@ class GlobalTimelineChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 00f8d8ecd2..63a2dd5b3a 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -49,6 +49,8 @@ class HashtagChannel extends Channel {
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
this.connection.cacheNote(note);
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 04a9f29686..678fbe12d6 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -75,6 +75,8 @@ class HomeTimelineChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index ab52aabb30..e33a280499 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -86,6 +86,8 @@ class HybridTimelineChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index d8532c477b..341c4e32ce 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -65,6 +65,8 @@ class LocalTimelineChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 7254d0a6d4..e7899245b6 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -93,6 +93,8 @@ class UserListChannel extends Channel {
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.blocking)) return;
+ if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
+
this.send('note', note);
}
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
index d3056aca57..0a4fd8393a 100644
--- a/packages/backend/src/server/api/stream/index.ts
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -1,6 +1,6 @@
import type { User } from '@/models/entities/User.js';
import type { Channel as ChannelModel } from '@/models/entities/Channel.js';
-import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js';
+import type { FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js';
import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import type { Packed } from '@/misc/schema.js';
@@ -22,6 +22,7 @@ export default class Connection {
public userProfile?: UserProfile | null;
public following: Set<User['id']> = new Set();
public muting: Set<User['id']> = new Set();
+ public renoteMuting: Set<User['id']> = new Set();
public blocking: Set<User['id']> = new Set(); // "被"blocking
public followingChannels: Set<ChannelModel['id']> = new Set();
public token?: AccessToken;
@@ -34,6 +35,7 @@ export default class Connection {
constructor(
private followingsRepository: FollowingsRepository,
private mutingsRepository: MutingsRepository,
+ private renoteMutingsRepository: RenoteMutingsRepository,
private blockingsRepository: BlockingsRepository,
private channelFollowingsRepository: ChannelFollowingsRepository,
private userProfilesRepository: UserProfilesRepository,
@@ -66,6 +68,7 @@ export default class Connection {
if (this.user) {
this.updateFollowing();
this.updateMuting();
+ this.updateRenoteMuting();
this.updateBlocking();
this.updateFollowingChannels();
this.updateUserProfile();
@@ -93,6 +96,7 @@ export default class Connection {
this.muting.delete(data.body.id);
break;
+ // TODO: renote mute events
// TODO: block events
case 'followChannel':
@@ -343,6 +347,18 @@ export default class Connection {
}
@bindThis
+ private async updateRenoteMuting() {
+ const renoteMutings = await this.renoteMutingsRepository.find({
+ where: {
+ muterId: this.user!.id,
+ },
+ select: ['muteeId'],
+ });
+
+ this.renoteMuting = new Set<string>(renoteMutings.map(x => x.muteeId));
+ }
+
+ @bindThis
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
const blockings = await this.blockingsRepository.find({
where: {
diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts
index 4e9030f85d..5fee2b93a0 100644
--- a/packages/backend/test/e2e/block.ts
+++ b/packages/backend/test/e2e/block.ts
@@ -70,9 +70,9 @@ describe('Block', () => {
// TODO: ユーザーリストから除外されるテスト
test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
- const aliceNote = await post(alice);
- const bobNote = await post(bob);
- const carolNote = await post(carol);
+ const aliceNote = await post(alice, { text: 'hi' });
+ const bobNote = await post(bob, { text: 'hi' });
+ const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, bob);
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 42bdc5f24d..c130093732 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -206,7 +206,7 @@ describe('Endpoints', () => {
describe('notes/reactions/create', () => {
test('リアクションできる', async () => {
- const bobPost = await post(bob);
+ const bobPost = await post(bob, { text: 'hi' });
const res = await api('/notes/reactions/create', {
noteId: bobPost.id,
@@ -224,7 +224,7 @@ describe('Endpoints', () => {
});
test('自分の投稿にもリアクションできる', async () => {
- const myPost = await post(alice);
+ const myPost = await post(alice, { text: 'hi' });
const res = await api('/notes/reactions/create', {
noteId: myPost.id,
@@ -235,7 +235,7 @@ describe('Endpoints', () => {
});
test('二重にリアクションすると上書きされる', async () => {
- const bobPost = await post(bob);
+ const bobPost = await post(bob, { text: 'hi' });
await api('/notes/reactions/create', {
noteId: bobPost.id,
diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts
index 6654a290be..5811d6baf4 100644
--- a/packages/backend/test/e2e/mute.ts
+++ b/packages/backend/test/e2e/mute.ts
@@ -76,9 +76,9 @@ describe('Mute', () => {
describe('Timeline', () => {
test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
- const aliceNote = await post(alice);
- const bobNote = await post(bob);
- const carolNote = await post(carol);
+ const aliceNote = await post(alice, { text: 'hi' });
+ const bobNote = await post(bob, { text: 'hi' });
+ const carolNote = await post(carol, { text: 'hi' });
const res = await api('/notes/local-timeline', {}, alice);
@@ -90,8 +90,8 @@ describe('Mute', () => {
});
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
- const aliceNote = await post(alice);
- const carolNote = await post(carol);
+ const aliceNote = await post(alice, { text: 'hi' });
+ const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, {
renoteId: carolNote.id,
});
@@ -108,7 +108,7 @@ describe('Mute', () => {
describe('Notification', () => {
test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
- const aliceNote = await post(alice);
+ const aliceNote = await post(alice, { text: 'hi' });
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts
new file mode 100644
index 0000000000..32c8ebe2c5
--- /dev/null
+++ b/packages/backend/test/e2e/renote-mute.ts
@@ -0,0 +1,85 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { signup, api, post, react, startServer, waitFire } from '../utils.js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+describe('Renote Mute', () => {
+ let p: INestApplicationContext;
+
+ // alice mutes carol
+ let alice: any;
+ let bob: any;
+ let carol: any;
+
+ beforeAll(async () => {
+ p = await startServer();
+ alice = await signup({ username: 'alice' });
+ bob = await signup({ username: 'bob' });
+ carol = await signup({ username: 'carol' });
+ }, 1000 * 60 * 2);
+
+ afterAll(async () => {
+ await p.close();
+ });
+
+ test('ミュート作成', async () => {
+ const res = await api('/renote-mute/create', {
+ userId: carol.id,
+ }, alice);
+
+ assert.strictEqual(res.status, 204);
+ });
+
+ test('タイムラインにリノートミュートしているユーザーのリノートが含まれない', async () => {
+ const bobNote = await post(bob, { text: 'hi' });
+ const carolRenote = await post(carol, { renoteId: bobNote.id });
+ const carolNote = await post(carol, { text: 'hi' });
+
+ const res = await api('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+ });
+
+ test('タイムラインにリノートミュートしているユーザーの引用が含まれる', async () => {
+ const bobNote = await post(bob, { text: 'hi' });
+ const carolRenote = await post(carol, { renoteId: bobNote.id, text: 'kore' });
+ const carolNote = await post(carol, { text: 'hi' });
+
+ const res = await api('/notes/local-timeline', {}, alice);
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(Array.isArray(res.body), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true);
+ assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
+ });
+
+ test('ストリームにリノートミュートしているユーザーのリノートが流れない', async () => {
+ const bobNote = await post(bob, { text: 'hi' });
+
+ const fired = await waitFire(
+ alice, 'localTimeline',
+ () => api('notes/create', { renoteId: bobNote.id }, carol),
+ msg => msg.type === 'note' && msg.body.userId === carol.id,
+ );
+
+ assert.strictEqual(fired, false);
+ });
+
+ test('ストリームにリノートミュートしているユーザーの引用が流れる', async () => {
+ const bobNote = await post(bob, { text: 'hi' });
+
+ const fired = await waitFire(
+ alice, 'localTimeline',
+ () => api('notes/create', { renoteId: bobNote.id, text: 'kore' }, carol),
+ msg => msg.type === 'note' && msg.body.userId === carol.id,
+ );
+
+ assert.strictEqual(fired, true);
+ });
+});
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 8203e49359..37e5ae10d6 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -57,9 +57,7 @@ export const signup = async (params?: any): Promise<any> => {
};
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
- const q = Object.assign({
- text: 'test',
- }, params);
+ const q = params;
const res = await api('notes/create', q, user);
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 577337868f..3d0463f708 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -1,10 +1,40 @@
<template>
<div class="_gaps_m">
- <MkTab v-model="tab" style="margin-bottom: var(--margin);">
+ <MkTab v-model="tab">
+ <option value="renoteMute">{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</option>
<option value="mute">{{ i18n.ts.mutedUsers }}</option>
<option value="block">{{ i18n.ts.blockedUsers }}</option>
</MkTab>
- <div v-if="tab === 'mute'">
+
+ <div v-if="tab === 'renoteMute'">
+ <MkPagination :pagination="renoteMutingPagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ i18n.ts.noUsers }}</div>
+ </div>
+ </template>
+
+ <template #default="{ items }">
+ <div class="_gaps_s">
+ <div v-for="item in items" :key="item.mutee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedRenoteMuteItems.includes(item.id) }]">
+ <div :class="$style.userItemMain">
+ <MkA :class="$style.userItemMainBody" :to="`/user-info/${item.mutee.id}`">
+ <MkUserCardMini :user="item.mutee"/>
+ </MkA>
+ <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
+ <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub">
+ <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
+ </div>
+ </div>
+ </div>
+ </template>
+ </MkPagination>
+ </div>
+
+ <div v-else-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination">
<template #empty>
<div class="_fullinfo">
@@ -33,7 +63,8 @@
</template>
</MkPagination>
</div>
- <div v-if="tab === 'block'">
+
+ <div v-else-if="tab === 'block'">
<MkPagination :pagination="blockingPagination">
<template #empty>
<div class="_fullinfo">
@@ -77,7 +108,12 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os';
-let tab = $ref('mute');
+let tab = $ref('renoteMute');
+
+const renoteMutingPagination = {
+ endpoint: 'renote-mute/list' as const,
+ limit: 10,
+};
const mutingPagination = {
endpoint: 'mute/list' as const,
@@ -89,9 +125,21 @@ const blockingPagination = {
limit: 10,
};
+let expandedRenoteMuteItems = $ref([]);
let expandedMuteItems = $ref([]);
let expandedBlockItems = $ref([]);
+async function unrenoteMute(user, ev) {
+ os.popupMenu([{
+ text: i18n.ts.renoteUnmute,
+ icon: 'ti ti-x',
+ action: async () => {
+ await os.apiWithDialog('renote-mute/delete', { userId: user.id });
+ //role.users = role.users.filter(u => u.id !== user.id);
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
async function unmute(user, ev) {
os.popupMenu([{
text: i18n.ts.unmute,
@@ -114,6 +162,14 @@ async function unblock(user, ev) {
}], ev.currentTarget ?? ev.target);
}
+async function toggleRenoteMuteItem(item) {
+ if (expandedRenoteMuteItems.includes(item.id)) {
+ expandedRenoteMuteItems = expandedRenoteMuteItems.filter(x => x !== item.id);
+ } else {
+ expandedRenoteMuteItems.push(item.id);
+ }
+}
+
async function toggleMuteItem(item) {
if (expandedMuteItems.includes(item.id)) {
expandedMuteItems = expandedMuteItems.filter(x => x !== item.id);
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index f5aa551bdb..d7eb331183 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -53,6 +53,14 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
}
}
+ async function toggleRenoteMute() {
+ os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', {
+ userId: user.id,
+ }).then(() => {
+ user.isRenoteMuted = !user.isRenoteMuted;
+ });
+ }
+
async function toggleBlock() {
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
@@ -180,6 +188,10 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
action: toggleMute,
}, {
+ icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off',
+ text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute,
+ action: toggleRenoteMute,
+ }, {
icon: 'ti ti-ban',
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
action: toggleBlock,