feat: Per-user renote mute (#10249)
* feat: per-user renote muting From FoundKey/c414f24a2c https://akkoma.dev/FoundKeyGang/FoundKey * Update ja-JP.yml * Delete renote-muting.ts * rename * fix ids * lint * fix * Update CHANGELOG.md * リノートをミュートしたユーザー一覧を見れるように * 🎨 * add test * fix test --------- Co-authored-by: Hélène <pleroma-dev@helene.moe>
This commit is contained in:
parent
8bf6911d4b
commit
4c2f7c64cc
43 changed files with 683 additions and 26 deletions
|
@ -13,6 +13,7 @@ You should also include the user name that made the change.
|
||||||
## 13.x.x (unreleased)
|
## 13.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
- ユーザーごとにRenoteをミュートできるように
|
||||||
- enhance(client): DM作成時にメンションも含むように
|
- enhance(client): DM作成時にメンションも含むように
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
|
@ -122,6 +122,8 @@ unmarkAsSensitive: "閲覧注意を解除する"
|
||||||
enterFileName: "ファイル名を入力"
|
enterFileName: "ファイル名を入力"
|
||||||
mute: "ミュート"
|
mute: "ミュート"
|
||||||
unmute: "ミュート解除"
|
unmute: "ミュート解除"
|
||||||
|
renoteMute: "リノートをミュート"
|
||||||
|
renoteUnmute: "リノートのミュートを解除"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
unblock: "ブロック解除"
|
unblock: "ブロック解除"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
|
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,6 +82,7 @@ import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
||||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
||||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
||||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||||
|
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||||
import { NoteEntityService } from './entities/NoteEntityService.js';
|
import { NoteEntityService } from './entities/NoteEntityService.js';
|
||||||
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
|
||||||
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.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 $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
||||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
||||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||||
|
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||||
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
|
||||||
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
|
||||||
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
|
||||||
|
@ -325,6 +327,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
|
@ -442,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
|
@ -559,6 +563,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
|
RenoteMutingEntityService,
|
||||||
NoteEntityService,
|
NoteEntityService,
|
||||||
NoteFavoriteEntityService,
|
NoteFavoriteEntityService,
|
||||||
NoteReactionEntityService,
|
NoteReactionEntityService,
|
||||||
|
@ -675,6 +680,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
|
$RenoteMutingEntityService,
|
||||||
$NoteEntityService,
|
$NoteEntityService,
|
||||||
$NoteFavoriteEntityService,
|
$NoteFavoriteEntityService,
|
||||||
$NoteReactionEntityService,
|
$NoteReactionEntityService,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Brackets, ObjectLiteral } from 'typeorm';
|
import { Brackets, ObjectLiteral } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { User } from '@/models/entities/User.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 { bindThis } from '@/decorators.js';
|
||||||
import type { SelectQueryBuilder } from 'typeorm';
|
import type { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
@ -29,6 +29,9 @@ export class QueryService {
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.renoteMutingsRepository)
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,5 +272,24 @@ export class QueryService {
|
||||||
q.setParameters({ meId: me.id });
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } 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 { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -78,6 +78,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.renoteMutingsRepository)
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
|
@ -195,6 +198,13 @@ export class UserEntityService implements OnModuleInit {
|
||||||
},
|
},
|
||||||
take: 1,
|
take: 1,
|
||||||
}).then(n => n > 0),
|
}).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,
|
isBlocking: relation.isBlocking,
|
||||||
isBlocked: relation.isBlocked,
|
isBlocked: relation.isBlocked,
|
||||||
isMuted: relation.isMuted,
|
isMuted: relation.isMuted,
|
||||||
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const DI = {
|
||||||
notificationsRepository: Symbol('notificationsRepository'),
|
notificationsRepository: Symbol('notificationsRepository'),
|
||||||
metasRepository: Symbol('metasRepository'),
|
metasRepository: Symbol('metasRepository'),
|
||||||
mutingsRepository: Symbol('mutingsRepository'),
|
mutingsRepository: Symbol('mutingsRepository'),
|
||||||
|
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
|
||||||
blockingsRepository: Symbol('blockingsRepository'),
|
blockingsRepository: Symbol('blockingsRepository'),
|
||||||
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
swSubscriptionsRepository: Symbol('swSubscriptionsRepository'),
|
||||||
hashtagsRepository: Symbol('hashtagsRepository'),
|
hashtagsRepository: Symbol('hashtagsRepository'),
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
|
||||||
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
|
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
|
||||||
import { packedFollowingSchema } from '@/models/schema/following.js';
|
import { packedFollowingSchema } from '@/models/schema/following.js';
|
||||||
import { packedMutingSchema } from '@/models/schema/muting.js';
|
import { packedMutingSchema } from '@/models/schema/muting.js';
|
||||||
|
import { packedRenoteMutingSchema } from '@/models/schema/renote-muting.js';
|
||||||
import { packedBlockingSchema } from '@/models/schema/blocking.js';
|
import { packedBlockingSchema } from '@/models/schema/blocking.js';
|
||||||
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
|
||||||
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
|
import { packedHashtagSchema } from '@/models/schema/hashtag.js';
|
||||||
|
@ -48,6 +49,7 @@ export const refs = {
|
||||||
DriveFolder: packedDriveFolderSchema,
|
DriveFolder: packedDriveFolderSchema,
|
||||||
Following: packedFollowingSchema,
|
Following: packedFollowingSchema,
|
||||||
Muting: packedMutingSchema,
|
Muting: packedMutingSchema,
|
||||||
|
RenoteMuting: packedRenoteMutingSchema,
|
||||||
Blocking: packedBlockingSchema,
|
Blocking: packedBlockingSchema,
|
||||||
Hashtag: packedHashtagSchema,
|
Hashtag: packedHashtagSchema,
|
||||||
Page: packedPageSchema,
|
Page: packedPageSchema,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -190,6 +190,12 @@ const $mutingsRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $renoteMutingsRepository: Provider = {
|
||||||
|
provide: DI.renoteMutingsRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(RenoteMuting),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $blockingsRepository: Provider = {
|
const $blockingsRepository: Provider = {
|
||||||
provide: DI.blockingsRepository,
|
provide: DI.blockingsRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(Blocking),
|
useFactory: (db: DataSource) => db.getRepository(Blocking),
|
||||||
|
@ -423,6 +429,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$notificationsRepository,
|
$notificationsRepository,
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
|
$renoteMutingsRepository,
|
||||||
$blockingsRepository,
|
$blockingsRepository,
|
||||||
$swSubscriptionsRepository,
|
$swSubscriptionsRepository,
|
||||||
$hashtagsRepository,
|
$hashtagsRepository,
|
||||||
|
@ -489,6 +496,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$notificationsRepository,
|
$notificationsRepository,
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
|
$renoteMutingsRepository,
|
||||||
$blockingsRepository,
|
$blockingsRepository,
|
||||||
$swSubscriptionsRepository,
|
$swSubscriptionsRepository,
|
||||||
$hashtagsRepository,
|
$hashtagsRepository,
|
||||||
|
|
42
packages/backend/src/models/entities/RenoteMuting.ts
Normal file
42
packages/backend/src/models/entities/RenoteMuting.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import { Meta } from '@/models/entities/Meta.js';
|
||||||
import { ModerationLog } from '@/models/entities/ModerationLog.js';
|
import { ModerationLog } from '@/models/entities/ModerationLog.js';
|
||||||
import { MutedNote } from '@/models/entities/MutedNote.js';
|
import { MutedNote } from '@/models/entities/MutedNote.js';
|
||||||
import { Muting } from '@/models/entities/Muting.js';
|
import { Muting } from '@/models/entities/Muting.js';
|
||||||
|
import { RenoteMuting } from '@/models/entities/RenoteMuting.js';
|
||||||
import { Note } from '@/models/entities/Note.js';
|
import { Note } from '@/models/entities/Note.js';
|
||||||
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
|
@ -93,6 +94,7 @@ export {
|
||||||
ModerationLog,
|
ModerationLog,
|
||||||
MutedNote,
|
MutedNote,
|
||||||
Muting,
|
Muting,
|
||||||
|
RenoteMuting,
|
||||||
Note,
|
Note,
|
||||||
NoteFavorite,
|
NoteFavorite,
|
||||||
NoteReaction,
|
NoteReaction,
|
||||||
|
@ -159,6 +161,7 @@ export type MetasRepository = Repository<Meta>;
|
||||||
export type ModerationLogsRepository = Repository<ModerationLog>;
|
export type ModerationLogsRepository = Repository<ModerationLog>;
|
||||||
export type MutedNotesRepository = Repository<MutedNote>;
|
export type MutedNotesRepository = Repository<MutedNote>;
|
||||||
export type MutingsRepository = Repository<Muting>;
|
export type MutingsRepository = Repository<Muting>;
|
||||||
|
export type RenoteMutingsRepository = Repository<RenoteMuting>;
|
||||||
export type NotesRepository = Repository<Note>;
|
export type NotesRepository = Repository<Note>;
|
||||||
export type NoteFavoritesRepository = Repository<NoteFavorite>;
|
export type NoteFavoritesRepository = Repository<NoteFavorite>;
|
||||||
export type NoteReactionsRepository = Repository<NoteReaction>;
|
export type NoteReactionsRepository = Repository<NoteReaction>;
|
||||||
|
|
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
26
packages/backend/src/models/schema/renote-muting.ts
Normal file
|
@ -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;
|
|
@ -234,6 +234,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: true,
|
||||||
|
},
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { Meta } from '@/models/entities/Meta.js';
|
||||||
import { ModerationLog } from '@/models/entities/ModerationLog.js';
|
import { ModerationLog } from '@/models/entities/ModerationLog.js';
|
||||||
import { MutedNote } from '@/models/entities/MutedNote.js';
|
import { MutedNote } from '@/models/entities/MutedNote.js';
|
||||||
import { Muting } from '@/models/entities/Muting.js';
|
import { Muting } from '@/models/entities/Muting.js';
|
||||||
|
import { RenoteMuting } from '@/models/entities/RenoteMuting.js';
|
||||||
import { Note } from '@/models/entities/Note.js';
|
import { Note } from '@/models/entities/Note.js';
|
||||||
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
|
@ -139,6 +140,7 @@ export const entities = [
|
||||||
Following,
|
Following,
|
||||||
FollowRequest,
|
FollowRequest,
|
||||||
Muting,
|
Muting,
|
||||||
|
RenoteMuting,
|
||||||
Blocking,
|
Blocking,
|
||||||
Note,
|
Note,
|
||||||
NoteFavorite,
|
NoteFavorite,
|
||||||
|
|
|
@ -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_create from './endpoints/mute/create.js';
|
||||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||||
import * as ep___mute_list from './endpoints/mute/list.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___my_apps from './endpoints/my/apps.js';
|
||||||
import * as ep___notes from './endpoints/notes.js';
|
import * as ep___notes from './endpoints/notes.js';
|
||||||
import * as ep___notes_children from './endpoints/notes/children.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_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_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 $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 $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default };
|
||||||
const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
|
const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
|
||||||
const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.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_create,
|
||||||
$mute_delete,
|
$mute_delete,
|
||||||
$mute_list,
|
$mute_list,
|
||||||
|
$renoteMute_create,
|
||||||
|
$renoteMute_delete,
|
||||||
|
$renoteMute_list,
|
||||||
$my_apps,
|
$my_apps,
|
||||||
$notes,
|
$notes,
|
||||||
$notes_children,
|
$notes_children,
|
||||||
|
@ -1189,6 +1198,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$mute_create,
|
$mute_create,
|
||||||
$mute_delete,
|
$mute_delete,
|
||||||
$mute_list,
|
$mute_list,
|
||||||
|
$renoteMute_create,
|
||||||
|
$renoteMute_delete,
|
||||||
|
$renoteMute_list,
|
||||||
$my_apps,
|
$my_apps,
|
||||||
$notes,
|
$notes,
|
||||||
$notes_children,
|
$notes_children,
|
||||||
|
|
|
@ -3,17 +3,17 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import * as websocket from 'websocket';
|
import * as websocket from 'websocket';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 type { Config } from '@/config.js';
|
||||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { AuthenticateService } from './AuthenticateService.js';
|
import { AuthenticateService } from './AuthenticateService.js';
|
||||||
import MainStreamConnection from './stream/index.js';
|
import MainStreamConnection from './stream/index.js';
|
||||||
import { ChannelsService } from './stream/ChannelsService.js';
|
import { ChannelsService } from './stream/ChannelsService.js';
|
||||||
import type { ParsedUrlQuery } from 'querystring';
|
import type { ParsedUrlQuery } from 'querystring';
|
||||||
import type * as http from 'node:http';
|
import type * as http from 'node:http';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StreamingApiServerService {
|
export class StreamingApiServerService {
|
||||||
|
@ -33,6 +33,9 @@ export class StreamingApiServerService {
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.renoteMutingsRepository)
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@ -84,6 +87,7 @@ export class StreamingApiServerService {
|
||||||
const main = new MainStreamConnection(
|
const main = new MainStreamConnection(
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
this.mutingsRepository,
|
this.mutingsRepository,
|
||||||
|
this.renoteMutingsRepository,
|
||||||
this.blockingsRepository,
|
this.blockingsRepository,
|
||||||
this.channelFollowingsRepository,
|
this.channelFollowingsRepository,
|
||||||
this.userProfilesRepository,
|
this.userProfilesRepository,
|
||||||
|
|
|
@ -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_create from './endpoints/mute/create.js';
|
||||||
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
import * as ep___mute_delete from './endpoints/mute/delete.js';
|
||||||
import * as ep___mute_list from './endpoints/mute/list.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___my_apps from './endpoints/my/apps.js';
|
||||||
import * as ep___notes from './endpoints/notes.js';
|
import * as ep___notes from './endpoints/notes.js';
|
||||||
import * as ep___notes_children from './endpoints/notes/children.js';
|
import * as ep___notes_children from './endpoints/notes/children.js';
|
||||||
|
@ -543,6 +546,9 @@ const eps = [
|
||||||
['mute/create', ep___mute_create],
|
['mute/create', ep___mute_create],
|
||||||
['mute/delete', ep___mute_delete],
|
['mute/delete', ep___mute_delete],
|
||||||
['mute/list', ep___mute_list],
|
['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],
|
['my/apps', ep___my_apps],
|
||||||
['notes', ep___notes],
|
['notes', ep___notes],
|
||||||
['notes/children', ep___notes_children],
|
['notes/children', ep___notes_children],
|
||||||
|
|
|
@ -89,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
this.queryService.generateMutedNoteQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
|
|
|
@ -107,6 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
this.queryService.generateMutedNoteQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
|
|
@ -95,6 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||||
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
this.queryService.generateMutedUserQuery(query, me);
|
this.queryService.generateMutedUserQuery(query, me);
|
||||||
this.queryService.generateMutedNoteQuery(query, me);
|
this.queryService.generateMutedNoteQuery(query, me);
|
||||||
this.queryService.generateBlockedUserQuery(query, me);
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -91,6 +95,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isRenoteMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,10 @@ export default abstract class Channel {
|
||||||
return this.connection.muting;
|
return this.connection.muting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get renoteMuting() {
|
||||||
|
return this.connection.renoteMuting;
|
||||||
|
}
|
||||||
|
|
||||||
protected get blocking() {
|
protected get blocking() {
|
||||||
return this.connection.blocking;
|
return this.connection.blocking;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ class AntennaChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
|
|
@ -51,6 +51,8 @@ class ChannelChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
|
|
@ -68,6 +68,8 @@ class GlobalTimelineChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
|
|
@ -49,6 +49,8 @@ class HashtagChannel extends Channel {
|
||||||
if (isUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,8 @@ class HomeTimelineChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
|
|
@ -86,6 +86,8 @@ class HybridTimelineChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
|
|
@ -65,6 +65,8 @@ class LocalTimelineChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
|
|
@ -93,6 +93,8 @@ class UserListChannel extends Channel {
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Channel as ChannelModel } from '@/models/entities/Channel.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 { AccessToken } from '@/models/entities/AccessToken.js';
|
||||||
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
||||||
import type { Packed } from '@/misc/schema.js';
|
import type { Packed } from '@/misc/schema.js';
|
||||||
|
@ -22,6 +22,7 @@ export default class Connection {
|
||||||
public userProfile?: UserProfile | null;
|
public userProfile?: UserProfile | null;
|
||||||
public following: Set<User['id']> = new Set();
|
public following: Set<User['id']> = new Set();
|
||||||
public muting: 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 blocking: Set<User['id']> = new Set(); // "被"blocking
|
||||||
public followingChannels: Set<ChannelModel['id']> = new Set();
|
public followingChannels: Set<ChannelModel['id']> = new Set();
|
||||||
public token?: AccessToken;
|
public token?: AccessToken;
|
||||||
|
@ -34,6 +35,7 @@ export default class Connection {
|
||||||
constructor(
|
constructor(
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
@ -66,6 +68,7 @@ export default class Connection {
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
this.updateFollowing();
|
this.updateFollowing();
|
||||||
this.updateMuting();
|
this.updateMuting();
|
||||||
|
this.updateRenoteMuting();
|
||||||
this.updateBlocking();
|
this.updateBlocking();
|
||||||
this.updateFollowingChannels();
|
this.updateFollowingChannels();
|
||||||
this.updateUserProfile();
|
this.updateUserProfile();
|
||||||
|
@ -93,6 +96,7 @@ export default class Connection {
|
||||||
this.muting.delete(data.body.id);
|
this.muting.delete(data.body.id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// TODO: renote mute events
|
||||||
// TODO: block events
|
// TODO: block events
|
||||||
|
|
||||||
case 'followChannel':
|
case 'followChannel':
|
||||||
|
@ -342,6 +346,18 @@ export default class Connection {
|
||||||
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
this.muting = new Set<string>(mutings.map(x => x.muteeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@bindThis
|
||||||
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
private async updateBlocking() { // ここでいうBlockingは被Blockingの意
|
||||||
const blockings = await this.blockingsRepository.find({
|
const blockings = await this.blockingsRepository.find({
|
||||||
|
|
|
@ -70,9 +70,9 @@ describe('Block', () => {
|
||||||
// TODO: ユーザーリストから除外されるテスト
|
// TODO: ユーザーリストから除外されるテスト
|
||||||
|
|
||||||
test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
|
test('タイムライン(LTL)にブロックされているユーザーの投稿が含まれない', async () => {
|
||||||
const aliceNote = await post(alice);
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
const bobNote = await post(bob);
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
const carolNote = await post(carol);
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
const res = await api('/notes/local-timeline', {}, bob);
|
const res = await api('/notes/local-timeline', {}, bob);
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ describe('Endpoints', () => {
|
||||||
|
|
||||||
describe('notes/reactions/create', () => {
|
describe('notes/reactions/create', () => {
|
||||||
test('リアクションできる', async () => {
|
test('リアクションできる', async () => {
|
||||||
const bobPost = await post(bob);
|
const bobPost = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
const res = await api('/notes/reactions/create', {
|
const res = await api('/notes/reactions/create', {
|
||||||
noteId: bobPost.id,
|
noteId: bobPost.id,
|
||||||
|
@ -224,7 +224,7 @@ describe('Endpoints', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('自分の投稿にもリアクションできる', async () => {
|
test('自分の投稿にもリアクションできる', async () => {
|
||||||
const myPost = await post(alice);
|
const myPost = await post(alice, { text: 'hi' });
|
||||||
|
|
||||||
const res = await api('/notes/reactions/create', {
|
const res = await api('/notes/reactions/create', {
|
||||||
noteId: myPost.id,
|
noteId: myPost.id,
|
||||||
|
@ -235,7 +235,7 @@ describe('Endpoints', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('二重にリアクションすると上書きされる', async () => {
|
test('二重にリアクションすると上書きされる', async () => {
|
||||||
const bobPost = await post(bob);
|
const bobPost = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
await api('/notes/reactions/create', {
|
await api('/notes/reactions/create', {
|
||||||
noteId: bobPost.id,
|
noteId: bobPost.id,
|
||||||
|
|
|
@ -76,9 +76,9 @@ describe('Mute', () => {
|
||||||
|
|
||||||
describe('Timeline', () => {
|
describe('Timeline', () => {
|
||||||
test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
|
test('タイムラインにミュートしているユーザーの投稿が含まれない', async () => {
|
||||||
const aliceNote = await post(alice);
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
const bobNote = await post(bob);
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
const carolNote = await post(carol);
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
|
|
||||||
const res = await api('/notes/local-timeline', {}, alice);
|
const res = await api('/notes/local-timeline', {}, alice);
|
||||||
|
|
||||||
|
@ -90,8 +90,8 @@ describe('Mute', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
test('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async () => {
|
||||||
const aliceNote = await post(alice);
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
const carolNote = await post(carol);
|
const carolNote = await post(carol, { text: 'hi' });
|
||||||
const bobNote = await post(bob, {
|
const bobNote = await post(bob, {
|
||||||
renoteId: carolNote.id,
|
renoteId: carolNote.id,
|
||||||
});
|
});
|
||||||
|
@ -108,7 +108,7 @@ describe('Mute', () => {
|
||||||
|
|
||||||
describe('Notification', () => {
|
describe('Notification', () => {
|
||||||
test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
|
test('通知にミュートしているユーザーの通知が含まれない(リアクション)', async () => {
|
||||||
const aliceNote = await post(alice);
|
const aliceNote = await post(alice, { text: 'hi' });
|
||||||
await react(bob, aliceNote, 'like');
|
await react(bob, aliceNote, 'like');
|
||||||
await react(carol, aliceNote, 'like');
|
await react(carol, aliceNote, 'like');
|
||||||
|
|
||||||
|
|
85
packages/backend/test/e2e/renote-mute.ts
Normal file
85
packages/backend/test/e2e/renote-mute.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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> => {
|
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
|
||||||
const q = Object.assign({
|
const q = params;
|
||||||
text: 'test',
|
|
||||||
}, params);
|
|
||||||
|
|
||||||
const res = await api('notes/create', q, user);
|
const res = await api('notes/create', q, user);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<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="mute">{{ i18n.ts.mutedUsers }}</option>
|
||||||
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
<option value="block">{{ i18n.ts.blockedUsers }}</option>
|
||||||
</MkTab>
|
</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">
|
<MkPagination :pagination="mutingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -33,7 +63,8 @@
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tab === 'block'">
|
|
||||||
|
<div v-else-if="tab === 'block'">
|
||||||
<MkPagination :pagination="blockingPagination">
|
<MkPagination :pagination="blockingPagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
|
@ -77,7 +108,12 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import * as os from '@/os';
|
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 = {
|
const mutingPagination = {
|
||||||
endpoint: 'mute/list' as const,
|
endpoint: 'mute/list' as const,
|
||||||
|
@ -89,9 +125,21 @@ const blockingPagination = {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let expandedRenoteMuteItems = $ref([]);
|
||||||
let expandedMuteItems = $ref([]);
|
let expandedMuteItems = $ref([]);
|
||||||
let expandedBlockItems = $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) {
|
async function unmute(user, ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.unmute,
|
text: i18n.ts.unmute,
|
||||||
|
@ -114,6 +162,14 @@ async function unblock(user, ev) {
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], 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) {
|
async function toggleMuteItem(item) {
|
||||||
if (expandedMuteItems.includes(item.id)) {
|
if (expandedMuteItems.includes(item.id)) {
|
||||||
expandedMuteItems = expandedMuteItems.filter(x => x !== item.id);
|
expandedMuteItems = expandedMuteItems.filter(x => x !== item.id);
|
||||||
|
|
|
@ -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() {
|
async function toggleBlock() {
|
||||||
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return;
|
||||||
|
|
||||||
|
@ -179,6 +187,10 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||||
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off',
|
||||||
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
|
||||||
action: toggleMute,
|
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',
|
icon: 'ti ti-ban',
|
||||||
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
||||||
|
|
Loading…
Reference in a new issue