diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..67efc83246 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -3,19 +3,29 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Injectable, Inject } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { UserProfilesRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { RoleService } from '@/core/RoleService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { getJsonSchema } from '@/core/chart/core.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { schema } from '@/core/chart/charts/entities/per-user-following.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['charts', 'users', 'following'], res: getJsonSchema(schema), - allowGet: true, - cacheSec: 60 * 60, + errors: { + ffIsMarkedAsPrivate: { + message: 'This user\'s followings and/or followers is marked as private.', + code: 'FF_IS_MARKED_AS_PRIVATE', + id: '52e90f27-3dfd-441e-a1f2-ca6ac7068040', + }, + }, } as const; export const paramDef = { @@ -32,10 +42,48 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, + private userEntityService: UserEntityService, private perUserFollowingChart: PerUserFollowingChart, ) { super(meta, paramDef, async (ps, me) => { - return await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + const done = async () => { + return await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); + }; + + if (me != null && me.id === ps.userId) { + return await done(); + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + + if (profile.followingVisibility === 'public' && profile.followersVisibility === 'public') { + return await done(); + } + + const iAmModerator = await this.roleService.isModerator(me); + + if (iAmModerator) { + return await done(); + } + + if ( + me != null && ( + (profile.followingVisibility === 'followers' && profile.followersVisibility === 'followers') || + (profile.followingVisibility === 'followers' && profile.followersVisibility === 'public') || + (profile.followingVisibility === 'public' && profile.followersVisibility === 'followers') + ) + ) { + const relations = await this.userEntityService.getRelation(me.id, ps.userId); + if (relations.following) { + return await done(); + } + } + + throw new ApiError(meta.errors.ffIsMarkedAsPrivate); }); } } diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index d05f4921f6..2302e6c16a 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -53,7 +53,7 @@ export type ChartSrc = import { onMounted, ref, shallowRef, watch } from 'vue'; import { Chart } from 'chart.js'; import * as Misskey from 'misskey-js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { misskeyApiGet, misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -758,7 +758,10 @@ const fetchPerUserPvChart = async (): Promise => { }; const fetchPerUserFollowingChart = async (): Promise => { - const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); + const raw = await misskeyApi('charts/user/following', { userId: props.args?.user?.id!, limit: props.limit, span: props.span }).catch(() => { + return null; + }); + if (raw == null) return null; return { series: [{ name: 'Local', @@ -773,7 +776,10 @@ const fetchPerUserFollowingChart = async (): Promise => { }; const fetchPerUserFollowersChart = async (): Promise => { - const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span }); + const raw = await misskeyApi('charts/user/following', { userId: props.args?.user?.id!, limit: props.limit, span: props.span }).catch(() => { + return null; + }); + if (raw == null) return null; return { series: [{ name: 'Local', diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index aa2c791c76..15963f0aaa 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -32,7 +32,7 @@ const props = defineProps<{ user: Misskey.entities.User; }>(); -const chartEl = shallowRef(null); +const chartEl = shallowRef(); const legendEl = shallowRef>(); const now = new Date(); let chartInstance: Chart = null; @@ -88,7 +88,7 @@ async function renderChart() { }, extra); } - chartInstance = new Chart(chartEl.value, { + chartInstance = new Chart(chartEl.value!, { type: 'bar', data: { datasets: [ diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index 994bd52705..f555d72132 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -33,9 +36,10 @@ import XNotes from './activity.notes.vue'; import XFollowing from './activity.following.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkHeatmap from '@/components/MkHeatmap.vue'; +import { isFollowersVisibleForMe, isFollowingVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; const props = defineProps<{ - user: Misskey.entities.User; + user: Misskey.entities.UserDetailed; }>(); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index a5333d4f93..8ce154b086 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1227,13 +1227,6 @@ export type paths = { post: operations['charts___user___drive']; }; '/charts/user/following': { - /** - * charts/user/following - * @description No description provided. - * - * **Credential required**: *No* - */ - get: operations['charts___user___following']; /** * charts/user/following * @description No description provided.