Compare commits
18 Commits
0f77cb7ebf
...
b45c302b5c
Author | SHA1 | Date |
---|---|---|
鴇峰 朔華 | b45c302b5c | |
鴇峰 朔華 | 6236c936f1 | |
鴇峰 朔華 | fd9b7edeff | |
鴇峰 朔華 | ac95b12f0b | |
鴇峰 朔華 | a96ae92f47 | |
鴇峰 朔華 | 292809a324 | |
鴇峰 朔華 | 34da11f371 | |
鴇峰 朔華 | da94dbee00 | |
鴇峰 朔華 | 0301e86aff | |
鴇峰 朔華 | 26652949cb | |
鴇峰 朔華 | 24792e09b5 | |
鴇峰 朔華 | 3ea69b6203 | |
鴇峰 朔華 | 202fceed22 | |
鴇峰 朔華 | 9ef2dbbd30 | |
鴇峰 朔華 | 1b0ac28825 | |
鴇峰 朔華 | 51a2a7d81c | |
鴇峰 朔華 | 37627bb0e6 | |
鴇峰 朔華 | 3dd5af3003 |
|
@ -606,6 +606,14 @@ export interface Locale extends ILocale {
|
|||
* ブロック解除
|
||||
*/
|
||||
"unblock": string;
|
||||
/**
|
||||
* リアクションをブロック
|
||||
*/
|
||||
"blockReactionUser": string;
|
||||
/**
|
||||
* リアクションのブロックを解除
|
||||
*/
|
||||
"unblockReactionUser": string;
|
||||
/**
|
||||
* 凍結
|
||||
*/
|
||||
|
@ -622,6 +630,14 @@ export interface Locale extends ILocale {
|
|||
* ブロック解除しますか?
|
||||
*/
|
||||
"unblockConfirm": string;
|
||||
/**
|
||||
* リアクションをブロックしますか?
|
||||
*/
|
||||
"blockReactionUserConfirm": string;
|
||||
/**
|
||||
* リアクションのブロックを解除しますか?
|
||||
*/
|
||||
"unblockReactionUserConfirm": string;
|
||||
/**
|
||||
* 凍結しますか?
|
||||
*/
|
||||
|
@ -994,6 +1010,10 @@ export interface Locale extends ILocale {
|
|||
* ブロックしたユーザー
|
||||
*/
|
||||
"blockedUsers": string;
|
||||
/**
|
||||
* リアクションをブロックしたユーザー
|
||||
*/
|
||||
"reactionBlockedUsers": string;
|
||||
/**
|
||||
* ユーザーはいません
|
||||
*/
|
||||
|
|
|
@ -147,10 +147,14 @@ renoteMute: "リノートをミュート"
|
|||
renoteUnmute: "リノートのミュートを解除"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
blockReactionUser: "リアクションをブロック"
|
||||
unblockReactionUser: "リアクションのブロックを解除"
|
||||
suspend: "凍結"
|
||||
unsuspend: "解凍"
|
||||
blockConfirm: "ブロックしますか?"
|
||||
unblockConfirm: "ブロック解除しますか?"
|
||||
blockReactionUserConfirm: "リアクションをブロックしますか?"
|
||||
unblockReactionUserConfirm: "リアクションのブロックを解除しますか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
|
@ -244,6 +248,7 @@ federationAllowedHostsDescription: "連合を許可するサーバーのホス
|
|||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
reactionBlockedUsers: "リアクションをブロックしたユーザー"
|
||||
noUsers: "ユーザーはいません"
|
||||
editProfile: "プロフィールを編集"
|
||||
noteDeleteConfirm: "このノートを削除しますか?"
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AddBlockingReactionUser1731932268436 {
|
||||
name = 'AddBlockingReactionUser1731932268436'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "blocking" ADD "blockType" character varying NOT NULL DEFAULT 'user'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_cd38e7ea08163899a2d1f4427d" ON "blocking" ("blockType") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DELETE FROM blocking WHERE "blockType" = 'reaction'`); // blockingテーブルのblockTypeがreactionの行を削除
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_cd38e7ea08163899a2d1f4427d"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`);
|
||||
await queryRunner.query(`ALTER TABLE "blocking" DROP COLUMN "blockType"`);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
|||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -24,6 +25,8 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||
public userReactionBlockingCache: RedisKVCache<Set<string>>; // NOTE: リアクションBlockキャッシュ
|
||||
public userReactionBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」リアクションBlockキャッシュ
|
||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||
|
||||
|
@ -80,7 +83,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.User }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
@ -88,7 +91,23 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.User }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.userReactionBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userReactionBlocking', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.Reaction }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.userReactionBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userReactionBlocked', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.Reaction }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
|
|
@ -223,6 +223,8 @@ export interface InternalEventTypes {
|
|||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingReactionCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingReactionDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
policiesUpdated: MiRole['policies'];
|
||||
roleCreated: MiRole;
|
||||
roleDeleted: MiRole;
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { MiUser } from '@/models/User.js';
|
|||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
|
@ -72,7 +73,8 @@ export class QueryService {
|
|||
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
|
@ -97,7 +99,8 @@ export class QueryService {
|
|||
public generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockeeId')
|
||||
.where('blocking.blockerId = :blockerId', { blockerId: me.id });
|
||||
.where('blocking.blockerId = :blockerId', { blockerId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
const blockedQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
|
|
|
@ -107,7 +107,8 @@ export class ReactionService {
|
|||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
const reactionBlocked = await this.userBlockingService.checkReactionBlocked(note.userId, user.id);
|
||||
if (blocked || reactionBlocked) {
|
||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,16 @@ import { ModuleRef } from '@nestjs/core';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import type {
|
||||
BlockingsRepository,
|
||||
FollowRequestsRepository,
|
||||
UserListMembershipsRepository,
|
||||
UserListsRepository,
|
||||
} from '@/models/_.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
|
@ -67,14 +73,27 @@ export class UserBlockingService implements OnModuleInit {
|
|||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
const blocking = {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
}).then(blocking => {
|
||||
if (blocking) {
|
||||
return blocking;
|
||||
}
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
blocker,
|
||||
blockerId: blocker.id,
|
||||
blockee,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
} as MiBlocking;
|
||||
});
|
||||
|
||||
if (blocking.blockType === MiBlockingType.Reaction) {
|
||||
await this.reactionUnblock(blocker, blockee);
|
||||
}
|
||||
blocking.blockType = MiBlockingType.User;
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
|
@ -160,6 +179,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
});
|
||||
|
||||
if (blocking == null) {
|
||||
|
@ -169,8 +189,9 @@ export class UserBlockingService implements OnModuleInit {
|
|||
|
||||
// Since we already have the blocker and blockee, we do not need to fetch
|
||||
// them in the query above and can just manually insert them here.
|
||||
blocking.blocker = blocker;
|
||||
blocking.blockee = blockee;
|
||||
// But we don't need to do this because we are not using them in this function.
|
||||
// blocking.blocker = blocker;
|
||||
// blocking.blockee = blockee;
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
|
@ -193,4 +214,73 @@ export class UserBlockingService implements OnModuleInit {
|
|||
public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
|
||||
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async reactionBlock(blocker: MiUser, blockee: MiUser, silent = false) {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
}).then(blocking => {
|
||||
if (blocking) {
|
||||
return blocking;
|
||||
}
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
blocker,
|
||||
blockerId: blocker.id,
|
||||
blockee,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
} as MiBlocking;
|
||||
});
|
||||
|
||||
if (blocking.blockType === MiBlockingType.User) {
|
||||
await this.unblock(blocker, blockee);
|
||||
}
|
||||
blocking.blockType = MiBlockingType.Reaction;
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userReactionBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userReactionBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingReactionCreated', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async reactionUnblock(blocker: MiUser, blockee: MiUser) {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
});
|
||||
|
||||
if (blocking == null) {
|
||||
this.logger.warn('Unblock requested, but the target was not blocked.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we already have the blocker and blockee, we do not need to fetch
|
||||
// them in the query above and can just manually insert them here.
|
||||
blocking.blocker = blocker;
|
||||
blocking.blockee = blockee;
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
this.cacheService.userReactionBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userReactionBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingReactionDeleted', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkReactionBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
|
||||
return (await this.cacheService.userReactionBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import type {
|
|||
UserSecurityKeysRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
|
@ -76,6 +77,8 @@ export type UserRelation = {
|
|||
hasPendingFollowRequestToYou: boolean
|
||||
isBlocking: boolean
|
||||
isBlocked: boolean
|
||||
isReactionBlocking: boolean
|
||||
isReactionBlocked: boolean
|
||||
isMuted: boolean
|
||||
isRenoteMuted: boolean
|
||||
}
|
||||
|
@ -169,6 +172,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isReactionBlocking,
|
||||
isReactionBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
] = await Promise.all([
|
||||
|
@ -198,12 +203,28 @@ export class UserEntityService implements OnModuleInit {
|
|||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
}),
|
||||
this.mutingsRepository.exists({
|
||||
|
@ -229,6 +250,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isReactionBlocking,
|
||||
isReactionBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
};
|
||||
|
@ -243,6 +266,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
followeesRequests,
|
||||
blockers,
|
||||
blockees,
|
||||
reactionBlockers,
|
||||
reactionBlockees,
|
||||
muters,
|
||||
renoteMuters,
|
||||
] = await Promise.all([
|
||||
|
@ -266,11 +291,25 @@ export class UserEntityService implements OnModuleInit {
|
|||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockeeId')
|
||||
.where('b.blockerId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.User })
|
||||
.getRawMany<{ b_blockeeId: string }>()
|
||||
.then(it => it.map(it => it.b_blockeeId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockerId')
|
||||
.where('b.blockeeId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.User })
|
||||
.getRawMany<{ b_blockerId: string }>()
|
||||
.then(it => it.map(it => it.b_blockerId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockeeId')
|
||||
.where('b.blockerId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.Reaction })
|
||||
.getRawMany<{ b_blockeeId: string }>()
|
||||
.then(it => it.map(it => it.b_blockeeId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockerId')
|
||||
.where('b.blockeeId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.Reaction })
|
||||
.getRawMany<{ b_blockerId: string }>()
|
||||
.then(it => it.map(it => it.b_blockerId)),
|
||||
this.mutingsRepository.createQueryBuilder('m')
|
||||
|
@ -300,6 +339,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou: followeesRequests.includes(target),
|
||||
isBlocking: blockers.includes(target),
|
||||
isBlocked: blockees.includes(target),
|
||||
isReactionBlocking: reactionBlockers.includes(target),
|
||||
isReactionBlocked: reactionBlockees.includes(target),
|
||||
isMuted: muters.includes(target),
|
||||
isRenoteMuted: renoteMuters.includes(target),
|
||||
},
|
||||
|
@ -638,6 +679,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
|
||||
isBlocking: relation.isBlocking,
|
||||
isBlocked: relation.isBlocked,
|
||||
isReactionBlocking: relation.isReactionBlocking,
|
||||
isReactionBlocked: relation.isReactionBlocked,
|
||||
isMuted: relation.isMuted,
|
||||
isRenoteMuted: relation.isRenoteMuted,
|
||||
notify: relation.following?.notify ?? 'none',
|
||||
|
|
|
@ -7,6 +7,11 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export enum MiBlockingType {
|
||||
User = 'user',
|
||||
Reaction = 'reaction',
|
||||
}
|
||||
|
||||
@Entity('blocking')
|
||||
@Index(['blockerId', 'blockeeId'], { unique: true })
|
||||
export class MiBlocking {
|
||||
|
@ -38,4 +43,11 @@ export class MiBlocking {
|
|||
})
|
||||
@JoinColumn()
|
||||
public blocker: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
comment: 'Block type.',
|
||||
default: MiBlockingType.User,
|
||||
})
|
||||
public blockType: MiBlockingType;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { MiAntenna } from '@/models/Antenna.js';
|
|||
import { MiApp } from '@/models/App.js';
|
||||
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
||||
import { MiAuthSession } from '@/models/AuthSession.js';
|
||||
import { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiBlocking, MiBlockingType } from '@/models/Blocking.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiClip } from '@/models/Clip.js';
|
||||
|
@ -136,6 +136,7 @@ export {
|
|||
MiAvatarDecoration,
|
||||
MiAuthSession,
|
||||
MiBlocking,
|
||||
MiBlockingType,
|
||||
MiChannelFollowing,
|
||||
MiChannelFavorite,
|
||||
MiClip,
|
||||
|
|
|
@ -27,5 +27,10 @@ export const packedBlockingSchema = {
|
|||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
blockType: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['user', 'reaction'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -416,6 +416,14 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isReactionBlocking: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isReactionBlocked: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isMuted: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
|
|
|
@ -231,6 +231,9 @@ import * as ep___i_favorites from './endpoints/i/favorites.js';
|
|||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||
import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js';
|
||||
import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js';
|
||||
import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js';
|
||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
||||
|
@ -502,6 +505,9 @@ const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', us
|
|||
const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default };
|
||||
const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default };
|
||||
const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default };
|
||||
const $blocking_reaction_user_create: Provider = { provide: 'ep:blocking-reaction-user/create', useClass: ep___blocking_reaction_user_create.default };
|
||||
const $blocking_reaction_user_delete: Provider = { provide: 'ep:blocking-reaction-user/delete', useClass: ep___blocking_reaction_user_delete.default };
|
||||
const $blocking_reaction_user_list: Provider = { provide: 'ep:blocking-reaction-user/list', useClass: ep___blocking_reaction_user_list.default };
|
||||
const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default };
|
||||
const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default };
|
||||
const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default };
|
||||
|
@ -894,6 +900,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$blocking_create,
|
||||
$blocking_delete,
|
||||
$blocking_list,
|
||||
$blocking_reaction_user_create,
|
||||
$blocking_reaction_user_delete,
|
||||
$blocking_reaction_user_list,
|
||||
$channels_create,
|
||||
$channels_featured,
|
||||
$channels_follow,
|
||||
|
|
|
@ -120,6 +120,9 @@ import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'
|
|||
import * as ep___blocking_create from './endpoints/blocking/create.js';
|
||||
import * as ep___blocking_delete from './endpoints/blocking/delete.js';
|
||||
import * as ep___blocking_list from './endpoints/blocking/list.js';
|
||||
import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js';
|
||||
import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js';
|
||||
import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js';
|
||||
import * as ep___channels_create from './endpoints/channels/create.js';
|
||||
import * as ep___channels_featured from './endpoints/channels/featured.js';
|
||||
import * as ep___channels_follow from './endpoints/channels/follow.js';
|
||||
|
@ -506,6 +509,9 @@ const eps = [
|
|||
['blocking/create', ep___blocking_create],
|
||||
['blocking/delete', ep___blocking_delete],
|
||||
['blocking/list', ep___blocking_list],
|
||||
['blocking-reaction-user/create', ep___blocking_reaction_user_create],
|
||||
['blocking-reaction-user/delete', ep___blocking_reaction_user_delete],
|
||||
['blocking-reaction-user/list', ep___blocking_reaction_user_list],
|
||||
['channels/create', ep___channels_create],
|
||||
['channels/featured', ep___channels_featured],
|
||||
['channels/follow', ep___channels_follow],
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 20,
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:blocks',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
|
||||
},
|
||||
|
||||
blockeeIsYourself: {
|
||||
message: 'Blockee is yourself.',
|
||||
code: 'BLOCKEE_IS_YOURSELF',
|
||||
id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6',
|
||||
},
|
||||
|
||||
alreadyBlocking: {
|
||||
message: 'You are already blocking that user.',
|
||||
code: 'ALREADY_BLOCKING',
|
||||
id: '787fed64-acb9-464a-82eb-afbd745b9614',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private getterService: GetterService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
// 自分自身
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||
}
|
||||
|
||||
// Get blockee
|
||||
const blockee = 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 if already blocking
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
|
||||
await this.userBlockingService.reactionBlock(blocker, blockee);
|
||||
|
||||
return await this.userEntityService.pack(blockee.id, blocker, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 100,
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:blocks',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '8621d8bf-c358-4303-a066-5ea78610eb3f',
|
||||
},
|
||||
|
||||
blockeeIsYourself: {
|
||||
message: 'Blockee is yourself.',
|
||||
code: 'BLOCKEE_IS_YOURSELF',
|
||||
id: '06f6fac6-524b-473c-a354-e97a40ae6eac',
|
||||
},
|
||||
|
||||
notBlocking: {
|
||||
message: 'You are not blocking that user.',
|
||||
code: 'NOT_BLOCKING',
|
||||
id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private getterService: GetterService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
// Check if the blockee is yourself
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||
}
|
||||
|
||||
// Get blockee
|
||||
const blockee = 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 blocking
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
|
||||
// Delete blocking
|
||||
await this.userBlockingService.reactionUnblock(blocker, blockee);
|
||||
|
||||
return await this.userEntityService.pack(blockee.id, blocker, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BlockingsRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:blocks',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Blocking',
|
||||
},
|
||||
},
|
||||
} 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;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private blockingEntityService: BlockingEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.Reaction });
|
||||
|
||||
const blockings = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.blockingEntityService.packMany(blockings, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { BlockingsRepository } from '@/models/_.js';
|
|||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -49,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id });
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
const blockings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -12,7 +12,7 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
|
|||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import {
|
||||
BlockingsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository, MiBlockingType,
|
||||
MiUserProfile, MutingsRepository, RenoteMutingsRepository,
|
||||
UserMemoRepository,
|
||||
UserProfilesRepository,
|
||||
|
@ -115,6 +115,16 @@ describe('UserEntityService', () => {
|
|||
id: genAidx(Date.now()),
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
});
|
||||
}
|
||||
|
||||
async function blockReaction(blocker: MiUser, blockee: MiUser) {
|
||||
await blockingRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -260,6 +270,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -275,6 +287,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -290,6 +304,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -305,6 +321,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(true);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -320,6 +338,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(true);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -339,6 +359,41 @@ describe('UserEntityService', () => {
|
|||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
|
||||
// meがリアクションをブロックしてる人たち
|
||||
const reactionBlockingYou = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of reactionBlockingYou) {
|
||||
await blockReaction(me, who);
|
||||
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
|
||||
expect(actual.isFollowing).toBe(false);
|
||||
expect(actual.isFollowed).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(true);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
// meのリアクションをブロックしてる人たち
|
||||
const reactionBlockingMe = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of reactionBlockingMe) {
|
||||
await blockReaction(who, me);
|
||||
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
|
||||
expect(actual.isFollowing).toBe(false);
|
||||
expect(actual.isFollowed).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(true);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
// meがミュートしてる人たち
|
||||
const muters = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of muters) {
|
||||
|
@ -350,6 +405,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(true);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -365,6 +422,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(true);
|
||||
}
|
||||
|
|
|
@ -122,6 +122,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ban"></i></template>
|
||||
<template #label>{{ i18n.ts.reactionBlockedUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="blockingReactionUserPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noUsers }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
|
||||
<MkUserCardMini :user="item.blockee"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
<button class="_button" :class="$style.remove" @click="unblockReactionUser(item.blockee, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -157,6 +190,11 @@ const blockingPagination = {
|
|||
limit: 10,
|
||||
};
|
||||
|
||||
const blockingReactionUserPagination = {
|
||||
endpoint: 'blocking-reaction-user/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const expandedRenoteMuteItems = ref([]);
|
||||
const expandedMuteItems = ref([]);
|
||||
const expandedBlockItems = ref([]);
|
||||
|
@ -194,6 +232,16 @@ async function unblock(user, ev) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function unblockReactionUser(user, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unblock,
|
||||
icon: 'ti ti-x',
|
||||
action: async () => {
|
||||
await os.apiWithDialog('blocking-reaction-user/delete', { userId: user.id });
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function toggleRenoteMuteItem(item) {
|
||||
if (expandedRenoteMuteItems.value.includes(item.id)) {
|
||||
expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
|
||||
|
|
|
@ -84,6 +84,16 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
});
|
||||
}
|
||||
|
||||
async function toggleReactionBlock() {
|
||||
if (!await getConfirmed(user.isReactionBlocking ? i18n.ts.unblockReactionUserConfirm : i18n.ts.blockReactionUserConfirm)) return;
|
||||
|
||||
os.apiWithDialog(user.isReactionBlocking ? 'blocking-reaction-user/delete' : 'blocking-reaction-user/create', {
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
user.isReactionBlocking = !user.isReactionBlocking;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleNotify() {
|
||||
os.apiWithDialog('following/update', {
|
||||
userId: user.id,
|
||||
|
@ -373,6 +383,10 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
icon: 'ti ti-ban',
|
||||
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
||||
action: toggleBlock,
|
||||
}, {
|
||||
icon: 'ti ti-ban',
|
||||
text: user.isReactionBlocking ? i18n.ts.unblockReactionUser : i18n.ts.blockReactionUser,
|
||||
action: toggleReactionBlock,
|
||||
});
|
||||
|
||||
if (user.isFollowed) {
|
||||
|
|
|
@ -551,6 +551,24 @@ type BlockingListRequest = operations['blocking___list']['requestBody']['content
|
|||
// @public (undocumented)
|
||||
type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserCreateRequest = operations['blocking-reaction-user___create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserListResponse = operations['blocking-reaction-user___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1381,6 +1399,12 @@ declare namespace entities {
|
|||
BlockingDeleteResponse,
|
||||
BlockingListRequest,
|
||||
BlockingListResponse,
|
||||
BlockingReactionUserCreateRequest,
|
||||
BlockingReactionUserCreateResponse,
|
||||
BlockingReactionUserDeleteRequest,
|
||||
BlockingReactionUserDeleteResponse,
|
||||
BlockingReactionUserListRequest,
|
||||
BlockingReactionUserListResponse,
|
||||
ChannelsCreateRequest,
|
||||
ChannelsCreateResponse,
|
||||
ChannelsFeaturedResponse,
|
||||
|
|
|
@ -1204,6 +1204,39 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/create', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/delete', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/list', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -156,6 +156,12 @@ import type {
|
|||
BlockingDeleteResponse,
|
||||
BlockingListRequest,
|
||||
BlockingListResponse,
|
||||
BlockingReactionUserCreateRequest,
|
||||
BlockingReactionUserCreateResponse,
|
||||
BlockingReactionUserDeleteRequest,
|
||||
BlockingReactionUserDeleteResponse,
|
||||
BlockingReactionUserListRequest,
|
||||
BlockingReactionUserListResponse,
|
||||
ChannelsCreateRequest,
|
||||
ChannelsCreateResponse,
|
||||
ChannelsFeaturedResponse,
|
||||
|
@ -690,6 +696,9 @@ export type Endpoints = {
|
|||
'blocking/create': { req: BlockingCreateRequest; res: BlockingCreateResponse };
|
||||
'blocking/delete': { req: BlockingDeleteRequest; res: BlockingDeleteResponse };
|
||||
'blocking/list': { req: BlockingListRequest; res: BlockingListResponse };
|
||||
'blocking-reaction-user/create': { req: BlockingReactionUserCreateRequest; res: BlockingReactionUserCreateResponse };
|
||||
'blocking-reaction-user/delete': { req: BlockingReactionUserDeleteRequest; res: BlockingReactionUserDeleteResponse };
|
||||
'blocking-reaction-user/list': { req: BlockingReactionUserListRequest; res: BlockingReactionUserListResponse };
|
||||
'channels/create': { req: ChannelsCreateRequest; res: ChannelsCreateResponse };
|
||||
'channels/featured': { req: EmptyRequest; res: ChannelsFeaturedResponse };
|
||||
'channels/follow': { req: ChannelsFollowRequest; res: EmptyResponse };
|
||||
|
|
|
@ -159,6 +159,12 @@ export type BlockingDeleteRequest = operations['blocking___delete']['requestBody
|
|||
export type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json'];
|
||||
export type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json'];
|
||||
export type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserCreateRequest = operations['blocking-reaction-user___create']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserListResponse = operations['blocking-reaction-user___list']['responses']['200']['content']['application/json'];
|
||||
export type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json'];
|
||||
export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json'];
|
||||
export type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -997,6 +997,33 @@ export type paths = {
|
|||
*/
|
||||
post: operations['blocking___list'];
|
||||
};
|
||||
'/blocking-reaction-user/create': {
|
||||
/**
|
||||
* blocking-reaction-user/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___create'];
|
||||
};
|
||||
'/blocking-reaction-user/delete': {
|
||||
/**
|
||||
* blocking-reaction-user/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___delete'];
|
||||
};
|
||||
'/blocking-reaction-user/list': {
|
||||
/**
|
||||
* blocking-reaction-user/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___list'];
|
||||
};
|
||||
'/channels/create': {
|
||||
/**
|
||||
* channels/create
|
||||
|
@ -3825,6 +3852,8 @@ export type components = {
|
|||
hasPendingFollowRequestToYou?: boolean;
|
||||
isBlocking?: boolean;
|
||||
isBlocked?: boolean;
|
||||
isReactionBlocking?: boolean;
|
||||
isReactionBlocked?: boolean;
|
||||
isMuted?: boolean;
|
||||
isRenoteMuted?: boolean;
|
||||
/** @enum {string} */
|
||||
|
@ -4486,6 +4515,8 @@ export type components = {
|
|||
/** Format: id */
|
||||
blockeeId: string;
|
||||
blockee: components['schemas']['UserDetailedNotMe'];
|
||||
/** @enum {string} */
|
||||
blockType: 'user' | 'reaction';
|
||||
};
|
||||
Hashtag: {
|
||||
/** @example misskey */
|
||||
|
@ -11705,6 +11736,184 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___create': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UserDetailedNotMe'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___delete': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UserDetailedNotMe'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___list': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @default 30 */
|
||||
limit?: number;
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Blocking'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* channels/create
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in New Issue