fix: フォロー・フォロワーの公開状況によってはフォロー関連のチャートを表示できないように

This commit is contained in:
kakkokari-gtyih 2024-10-24 20:25:42 +09:00
parent 15ae1605ec
commit 8bacd4aa27
4 changed files with 74 additions and 15 deletions

View file

@ -3,19 +3,29 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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 { Endpoint } from '@/server/api/endpoint-base.js';
import { getJsonSchema } from '@/core/chart/core.js'; import { getJsonSchema } from '@/core/chart/core.js';
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
import { schema } from '@/core/chart/charts/entities/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 = { export const meta = {
tags: ['charts', 'users', 'following'], tags: ['charts', 'users', 'following'],
res: getJsonSchema(schema), res: getJsonSchema(schema),
allowGet: true, errors: {
cacheSec: 60 * 60, ffIsMarkedAsPrivate: {
message: 'This user\'s followings and/or followers is marked as private.',
code: 'FF_IS_MARKED_AS_PRIVATE',
id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -32,10 +42,51 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private roleService: RoleService,
private userEntityService: UserEntityService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
) { ) {
super(meta, paramDef, async (ps, me) => { 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);
};
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
if (profile.followingVisibility === 'public' && profile.followersVisibility === 'public') {
done();
}
const iAmModerator = await this.roleService.isModerator(me);
if (iAmModerator) {
done();
}
if (
(profile.followingVisibility === 'private' || profile.followersVisibility === 'private') &&
(me != null && profile.userId === me.id)
) {
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) {
done();
}
}
throw new ApiError(meta.errors.ffIsMarkedAsPrivate);
}); });
} }
} }

View file

@ -53,7 +53,7 @@ export type ChartSrc =
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, ref, shallowRef, watch } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import * as Misskey from 'misskey-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 { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js'; import { chartVLine } from '@/scripts/chart-vline.js';
@ -758,8 +758,10 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
}; };
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
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 { return null;
});
return raw != null ? {
series: [{ series: [{
name: 'Local', name: 'Local',
type: 'area', type: 'area',
@ -769,12 +771,14 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(raw.remote.followings.total), data: format(raw.remote.followings.total),
}], }],
}; } : raw;
}; };
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => { const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
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 { return null;
});
return raw != null ? {
series: [{ series: [{
name: 'Local', name: 'Local',
type: 'area', type: 'area',
@ -784,7 +788,7 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
type: 'area', type: 'area',
data: format(raw.remote.followers.total), data: format(raw.remote.followers.total),
}], }],
}; } : raw;
}; };
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => { const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {

View file

@ -32,7 +32,7 @@ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.User;
}>(); }>();
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>();
const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null;
@ -88,7 +88,7 @@ async function renderChart() {
}, extra); }, extra);
} }
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value!, {
type: 'bar', type: 'bar',
data: { data: {
datasets: [ datasets: [

View file

@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><i class="ti ti-pencil"></i> Notes</template> <template #header><i class="ti ti-pencil"></i> Notes</template>
<XNotes :user="user"/> <XNotes :user="user"/>
</MkFoldableSection> </MkFoldableSection>
<MkFoldableSection class="item"> <MkFoldableSection
v-if="isFollowersVisibleForMe(user) && isFollowingVisibleForMe(user)"
class="item"
>
<template #header><i class="ti ti-users"></i> Following</template> <template #header><i class="ti ti-users"></i> Following</template>
<XFollowing :user="user"/> <XFollowing :user="user"/>
</MkFoldableSection> </MkFoldableSection>
@ -33,9 +36,10 @@ import XNotes from './activity.notes.vue';
import XFollowing from './activity.following.vue'; import XFollowing from './activity.following.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkHeatmap from '@/components/MkHeatmap.vue'; import MkHeatmap from '@/components/MkHeatmap.vue';
import { isFollowersVisibleForMe, isFollowingVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.UserDetailed;
}>(); }>();
</script> </script>