diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bcf30b603..aaf2234cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ - Fix: フォロワー限定投稿へのリプライをホーム投稿に出来る問題を修正 - Fix: ファイルをアップロードするAPIにて、処理終了後に一時ファイルが削除されないことがある問題を修正 - Fix: 初期設定で作成したアカウント以外でアカウント作成APIが使用できない問題を修正 +- Fix: フォロー中のチャンネルを再度フォローした際にALREADY_FOLLOWINGエラーを返すように ## 2026.6.0 diff --git a/packages/backend/src/core/ChannelFollowingService.ts b/packages/backend/src/core/ChannelFollowingService.ts index 7dae8d10f3..54198fdc06 100644 --- a/packages/backend/src/core/ChannelFollowingService.ts +++ b/packages/backend/src/core/ChannelFollowingService.ts @@ -13,6 +13,8 @@ import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; import { bindThis } from '@/decorators.js'; import type { MiLocalUser } from '@/models/User.js'; import { RedisKVCache } from '@/misc/cache.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @Injectable() export class ChannelFollowingService implements OnModuleInit { @@ -96,11 +98,18 @@ export class ChannelFollowingService implements OnModuleInit { requestUser: MiLocalUser, targetChannel: MiChannel, ): Promise { - await this.channelFollowingsRepository.insert({ - id: this.idService.gen(), - followerId: requestUser.id, - followeeId: targetChannel.id, - }); + try { + await this.channelFollowingsRepository.insert({ + id: this.idService.gen(), + followerId: requestUser.id, + followeeId: targetChannel.id, + }); + } catch (e) { + if (isDuplicateKeyValueError(e)) { + throw new IdentifiableError('6e335e39-0203-4418-a936-b3f2dc987845', 'already following'); + } + throw e; + } this.globalEventService.publishInternalEvent('followChannel', { userId: requestUser.id, diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts index 1812820ba2..b46552281f 100644 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ b/packages/backend/src/server/api/endpoints/channels/follow.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { ChannelsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -25,6 +26,11 @@ export const meta = { code: 'NO_SUCH_CHANNEL', id: 'c0031718-d573-4e85-928e-10039f1fbb68', }, + alreadyFollowing: { + message: 'You are already following that channel.', + code: 'ALREADY_FOLLOWING', + id: '7db31665-651e-40c1-8e6e-28e9ad829a2d', + }, }, } as const; @@ -52,7 +58,14 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchChannel); } - await this.channelFollowingService.follow(me, channel); + try { + await this.channelFollowingService.follow(me, channel); + } catch (e) { + if (e instanceof IdentifiableError) { + if (e.id === '6e335e39-0203-4418-a936-b3f2dc987845') throw new ApiError(meta.errors.alreadyFollowing); + } + throw e; + } }); } } diff --git a/packages/backend/test/e2e/channel.ts b/packages/backend/test/e2e/channel.ts new file mode 100644 index 0000000000..c2fd98cbf4 --- /dev/null +++ b/packages/backend/test/e2e/channel.ts @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; +import { api, castAsError, signup } from '../utils.js'; +import type * as misskey from 'misskey-js'; + +describe('Channel', () => { + let alice: misskey.entities.SignupResponse; + beforeAll(async () => { + alice = await signup({ username: 'alice' }); + }); + + describe('Follow', () => { + let channel: misskey.entities.ChannelsCreateResponse; + + beforeAll(async () => { + const res = await api('channels/create', { name: 'follow-test-channel' }, alice); + channel = res.body; + }); + + test('フォローしているチャンネルを再度フォローするとALREADY_FOLLOWINGエラーになる', async () => { + const res1 = await api('channels/follow', { channelId: channel.id }, alice); + assert.strictEqual(res1.status, 204); + + const res2 = await api('channels/follow', { channelId: channel.id }, alice); + assert.strictEqual(res2.status, 400); + assert.strictEqual(castAsError(res2.body as any).error.code, 'ALREADY_FOLLOWING'); + }); + }); +});