From c21d2556043e8833426c5523b753593b0fa196ae Mon Sep 17 00:00:00 2001 From: Mar0xy Date: Wed, 18 Oct 2023 07:29:16 +0200 Subject: [PATCH] add: Bot Trending Toggle, Hide Bot in Timeline client option --- .../migration/1697603945000-BotTrending.js | 16 ++++++++++++++++ packages/backend/src/core/NoteCreateService.ts | 6 +++++- packages/backend/src/models/Meta.ts | 5 +++++ .../src/server/api/endpoints/admin/meta.ts | 5 +++++ .../server/api/endpoints/admin/update-meta.ts | 5 +++++ .../api/endpoints/notes/global-timeline.ts | 3 +++ .../api/endpoints/notes/hybrid-timeline.ts | 3 +++ .../server/api/endpoints/notes/local-timeline.ts | 3 +++ .../server/api/endpoints/notes/search-by-tag.ts | 6 ++++++ .../src/server/api/endpoints/notes/timeline.ts | 3 +++ .../api/stream/channels/global-timeline.ts | 3 +++ .../api/stream/channels/hybrid-timeline.ts | 3 +++ .../server/api/stream/channels/local-timeline.ts | 3 +++ packages/frontend/src/components/MkTimeline.vue | 8 ++++++++ .../frontend/src/pages/admin/other-settings.vue | 10 ++++++++++ packages/frontend/src/pages/settings/general.vue | 2 ++ packages/frontend/src/pages/timeline.vue | 2 ++ packages/frontend/src/store.ts | 4 ++++ packages/misskey-js/src/entities.ts | 1 + 19 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 packages/backend/migration/1697603945000-BotTrending.js diff --git a/packages/backend/migration/1697603945000-BotTrending.js b/packages/backend/migration/1697603945000-BotTrending.js new file mode 100644 index 000000000..73f1f598e --- /dev/null +++ b/packages/backend/migration/1697603945000-BotTrending.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class BotTrending1697603945000 { + name = 'BotTrending1697603945000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableBotTrending" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableBotTrending"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index da7e6b133..cc3dff850 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -511,7 +511,11 @@ export class NoteCreateService implements OnApplicationShutdown { // ハッシュタグ更新 if (data.visibility === 'public' || data.visibility === 'home') { - this.hashtagService.updateHashtags(user, tags); + if (user.isBot && meta.enableBotTrending) { + this.hashtagService.updateHashtags(user, tags); + } else if (!user.isBot) { + this.hashtagService.updateHashtags(user, tags); + } } // Increment notes count (user) diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index e4abe42de..96e44b225 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -247,6 +247,11 @@ export class MiMeta { }) public enableSensitiveMediaDetectionForVideos: boolean; + @Column('boolean', { + default: false, + }) + public enableBotTrending: boolean; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 88725ddbb..1d37c1012 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -178,6 +178,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableBotTrending: { + type: 'boolean', + optional: false, nullable: false, + }, proxyAccountId: { type: 'string', optional: false, nullable: true, @@ -391,6 +395,7 @@ export default class extends Endpoint { // eslint- sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + enableBotTrending: instance.enableBotTrending, proxyAccountId: instance.proxyAccountId, summalyProxy: instance.summalyProxy, email: instance.email, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 40cc50fe7..96456101b 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -72,6 +72,7 @@ export const paramDef = { sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, + enableBotTrending: { type: 'boolean' }, proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, @@ -301,6 +302,10 @@ export default class extends Endpoint { // eslint- set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; } + if (ps.enableBotTrending !== undefined) { + set.enableBotTrending = ps.enableBotTrending; + } + if (ps.proxyAccountId !== undefined) { set.proxyAccountId = ps.proxyAccountId; } diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index be7557c21..4a8505fe5 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -40,6 +40,7 @@ export const paramDef = { type: 'object', properties: { withFiles: { type: 'boolean', default: false }, + withBots: { type: 'boolean', default: true }, withRenotes: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -87,6 +88,8 @@ export default class extends Endpoint { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } + + if (!ps.withBots) query.andWhere('user.isBot = FALSE'); //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 378529e30..cb3a5ae73 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -56,6 +56,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withBots: { type: 'boolean', default: true }, }, required: [], } as const; @@ -134,6 +135,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + if (!ps.withBots) query.andWhere('user.isBot = FALSE'); + let timeline = await query.getMany(); timeline = timeline.filter(note => { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index f69e60ab5..0f7a2d31f 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -46,6 +46,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withBots: { type: 'boolean', default: true }, excludeNsfw: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -119,6 +120,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + if (!ps.withBots) query.andWhere('user.isBot = FALSE'); + let timeline = await query.getMany(); timeline = timeline.filter(note => { diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index b00f5207d..bc33d6948 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -12,6 +12,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['notes', 'hashtags'], @@ -71,6 +72,7 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, + private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -80,6 +82,10 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); + const meta = await this.metaService.fetch(true); + + if (!meta.enableBotTrending) query.andWhere('user.isBot = FALSE'); + this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 8f13b3a4b..8c068adf9 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -46,6 +46,7 @@ export const paramDef = { includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withBots: { type: 'boolean', default: true }, }, required: [], } as const; @@ -97,6 +98,8 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel'); + if (!ps.withBots) query.andWhere('user.isBot = FALSE'); + let timeline = await query.getMany(); timeline = timeline.filter(note => { diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index c499d1787..58e53fdd8 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -20,6 +20,7 @@ class GlobalTimelineChannel extends Channel { public static requireCredential = false; private withRenotes: boolean; private withFiles: boolean; + private withBots: boolean; constructor( private metaService: MetaService, @@ -40,6 +41,7 @@ class GlobalTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; + this.withBots = params.withBots ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -48,6 +50,7 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (!this.withBots && note.user.isBot) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 83f0bccd9..fd9079849 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -20,6 +20,7 @@ class HybridTimelineChannel extends Channel { public static requireCredential = true; private withRenotes: boolean; private withReplies: boolean; + private withBots: boolean; private withFiles: boolean; constructor( @@ -41,6 +42,7 @@ class HybridTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; + this.withBots = params.withBots ?? true; this.withFiles = params.withFiles ?? false; // Subscribe events @@ -50,6 +52,7 @@ class HybridTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (!this.withBots && note.user.isBot) return; // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index a21104113..1e15ed0fe 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -19,6 +19,7 @@ class LocalTimelineChannel extends Channel { public static requireCredential = false; private withRenotes: boolean; private withReplies: boolean; + private withBots: boolean; private withFiles: boolean; constructor( @@ -40,6 +41,7 @@ class LocalTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; + this.withBots = params.withBots ?? true; this.withFiles = params.withFiles ?? false; // Subscribe events @@ -49,6 +51,7 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (!this.withBots && note.user.isBot) return; if (note.user.host !== null) return; if (note.visibility !== 'public') return; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index cdd72febd..fa0438419 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -25,11 +25,13 @@ const props = withDefaults(defineProps<{ sound?: boolean; withRenotes?: boolean; withReplies?: boolean; + withBots?: boolean; onlyFiles?: boolean; }>(), { withRenotes: true, withReplies: false, onlyFiles: false, + withBots: true, }); const emit = defineEmits<{ @@ -93,11 +95,13 @@ if (props.src === 'antenna') { query = { withRenotes: props.withRenotes, withReplies: props.withReplies, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('localTimeline', { withRenotes: props.withRenotes, withReplies: props.withReplies, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -106,11 +110,13 @@ if (props.src === 'antenna') { query = { withRenotes: props.withRenotes, withReplies: props.withReplies, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('hybridTimeline', { withRenotes: props.withRenotes, withReplies: props.withReplies, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); @@ -118,10 +124,12 @@ if (props.src === 'antenna') { endpoint = 'notes/global-timeline'; query = { withRenotes: props.withRenotes, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }; connection = stream.useChannel('globalTimeline', { withRenotes: props.withRenotes, + withBots: props.withBots, withFiles: props.onlyFiles ? true : undefined, }); connection.on('note', prepend); diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index ee24bf0fd..7d889e83f 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -23,6 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ + + + +
+
@@ -61,6 +68,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; let enableServerMachineStats: boolean = $ref(false); let enableAchievements: boolean = $ref(false); +let enableBotTrending: boolean = $ref(false); let enableIdenticonGeneration: boolean = $ref(false); let enableChartsForRemoteUser: boolean = $ref(false); let enableChartsForFederatedInstances: boolean = $ref(false); @@ -69,6 +77,7 @@ async function init() { const meta = await os.api('admin/meta'); enableServerMachineStats = meta.enableServerMachineStats; enableAchievements = meta.enableAchievements; + enableBotTrending = meta.enableBotTrending; enableIdenticonGeneration = meta.enableIdenticonGeneration; enableChartsForRemoteUser = meta.enableChartsForRemoteUser; enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances; @@ -78,6 +87,7 @@ function save() { os.apiWithDialog('admin/update-meta', { enableServerMachineStats, enableAchievements, + enableBotTrending, enableIdenticonGeneration, enableChartsForRemoteUser, enableChartsForFederatedInstances, diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 981216daa..97b846080 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -152,6 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.enableInfiniteScroll }} {{ i18n.ts.keepScreenOn }} {{ i18n.ts.clickToOpen }} + Show bots in timeline
@@ -227,6 +228,7 @@ const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showC const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen')); +const showBots = computed(defaultStore.makeGetterSetter('tlWithBots')); const collapseFiles = computed(defaultStore.makeGetterSetter('collapseFiles')); const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index c375b3839..6d1e2dbd5 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withRenotes="withRenotes" :withReplies="withReplies" :onlyFiles="onlyFiles" + :withBots="withBots" :sound="true" @queue="queueUpdated" /> @@ -63,6 +64,7 @@ let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); const withRenotes = $ref(true); const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false); +const withBots = $ref($i ? defaultStore.state.tlWithBots : true); const onlyFiles = $ref(false); watch($$(src), () => queue = 0); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index ceb5b9ca5..004f52ec5 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -373,6 +373,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + tlWithBots: { + where: 'device', + default: true, + }, })); // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 5145b6a93..ce194f853 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -405,6 +405,7 @@ export type AdminInstanceMetadata = DetailedInstanceMetadata & { app192IconUrl: string | null; app512IconUrl: string | null; manifestJsonOverride: string; + enableBotTrending: boolean; }; export type ServerInfo = {