diff --git a/locales/en-US.yml b/locales/en-US.yml index 176a8d0882..d5bce02770 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2951,3 +2951,7 @@ _dice: rollDice: "Roll!" diceCount: "Number of dice" diceFaces: "Number of dice's faces" + +_isIndexable: + title: "Indexable" + description: "This is kmyblue compatible feature. If you want to prevent your account from being indexed by search engines, please disable this option." diff --git a/locales/index.d.ts b/locales/index.d.ts index 162cd182aa..58e117b15b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11628,6 +11628,16 @@ export interface Locale extends ILocale { */ "diceFaces": string; }; + "_isIndexable": { + /** + * 公開ノートをインデックス化 + */ + "title": string; + /** + * kmy互換機能。公開ノートをインデックス化するかどうかを設定します。 + */ + "description": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5dab451598..90c8eb8005 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3104,3 +3104,7 @@ _dice: rollDice: "サイコロを振る" diceCount: "サイコロの数" diceFaces: "サイコロの面数" + +_isIndexable: + title: "公開ノートをインデックス化" + description: "kmy互換機能。公開ノートをインデックス化するかどうかを設定します。" diff --git a/packages/backend/migration/1721299883211-AddIsIndexable.js b/packages/backend/migration/1721299883211-AddIsIndexable.js new file mode 100644 index 0000000000..c16566fb2f --- /dev/null +++ b/packages/backend/migration/1721299883211-AddIsIndexable.js @@ -0,0 +1,13 @@ +export class AddIsIndexable1721299883211 { + name = 'AddIsIndexable1721299883211' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "isIndexable" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "isIndexable" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isIndexable"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "isIndexable"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index bb575d9a5c..4754575411 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -239,6 +239,7 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; isCat: MiUser['isCat']; + isIndexable: MiUser['isIndexable']; }, data: Option, silent = false): Promise { // チャンネル外にリプライしたら対象のスコープに合わせる // (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで) @@ -535,6 +536,7 @@ export class NoteCreateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; + isIndexable: MiUser['isIndexable']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { const meta = await this.metaService.fetch(); @@ -764,7 +766,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // Register to search database - this.index(note); + if (user.isIndexable) this.index(note); } @bindThis diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index fc51a845e5..497803ca13 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -78,6 +78,7 @@ export class NoteUpdateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; + isIndexable: MiUser['isIndexable']; }, data: Option, note: MiNote, silent = false): Promise { if (data.updatedAt == null) data.updatedAt = new Date(); @@ -213,6 +214,7 @@ export class NoteUpdateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; + isIndexable: MiUser['isIndexable']; }, silent: boolean) { if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a21cafca08..ef2ce28eec 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -526,6 +526,7 @@ export class ApRendererService { discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, + isIndexable: user.isIndexable, attachment: attachment.length ? attachment : undefined, }; diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 4700672f5c..d597197dbe 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -543,6 +543,7 @@ const extension_context_definition = { Emoji: 'toot:Emoji', featured: 'toot:featured', discoverable: 'toot:discoverable', + indexable: 'toot:indexable', // schema schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index ea0654e9ad..141d6e7d4f 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -421,6 +421,7 @@ export class ApPersonService implements OnModuleInit { tags, isBot, isCat: (person as any).isCat === true, + isIndexable: person.isIndexable ?? true, emojis, })) as MiRemoteUser; @@ -634,6 +635,7 @@ export class ApPersonService implements OnModuleInit { tags, isBot: getApType(object) === 'Service' || getApType(object) === 'Application', isCat: (person as any).isCat === true, + isIndexable: person.isIndexable ?? true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 2000e11e3a..82949b58c6 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -187,6 +187,7 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; + isIndexable?: boolean; } export const isCollection = (object: IObject): object is ICollection => diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 33eaee5127..03d7a7dc5b 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -531,6 +531,7 @@ export class UserEntityService implements OnModuleInit { }))) : [], isBot: user.isBot, isCat: user.isCat, + isIndexable: user.isIndexable, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { name: instance.name, softwareName: instance.softwareName, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index a7f270b44f..6123175808 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -256,6 +256,13 @@ export class MiUser { }) public token: string | null; + @Index() + @Column('boolean', { + default: true, + comment: 'Whether the User is indexable', + }) + public isIndexable: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index dddd434c03..f082dada00 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -171,6 +171,12 @@ export class MiUserProfile { }) public noCrawle: boolean; + @Column('boolean', { + default: true, + comment: 'Whether User is indexable.', + }) + public isIndexable: boolean; + @Column('boolean', { default: true, }) diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index b8efacd1a8..2bc54d4628 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -489,6 +489,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + isIndexable: { + type: 'boolean', + nullable: false, optional: false, + }, isDeleted: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index bd7e5ad673..9e1c39c3c1 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -173,6 +173,7 @@ export const paramDef = { autoAcceptFollowed: { type: 'boolean' }, noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, + isIndexable: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, @@ -314,6 +315,7 @@ export default class extends Endpoint { // eslint- if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; + if (typeof ps.isIndexable === 'boolean') profileUpdates.isIndexable = ps.isIndexable; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index e4f84d7628..286243decd 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -4103,6 +4103,7 @@ export type components = { noCrawle: boolean; preventAiLearning: boolean; isExplorable: boolean; + isIndexable: boolean; isDeleted: boolean; /** @enum {string} */ twoFactorBackupCodesStock: 'full' | 'partial' | 'none'; @@ -20457,6 +20458,7 @@ export type operations = { autoAcceptFollowed?: boolean; noCrawle?: boolean; preventAiLearning?: boolean; + isIndexable?: boolean; isBot?: boolean; isCat?: boolean; injectFeaturedNote?: boolean; diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index d418be624e..c5899f4ecb 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -43,6 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.makeExplorable }} + + {{ i18n.ts._isIndexable.title }} + +
@@ -89,6 +93,7 @@ const isLocked = ref($i.isLocked); const autoAcceptFollowed = ref($i.autoAcceptFollowed); const noCrawle = ref($i.noCrawle); const preventAiLearning = ref($i.preventAiLearning); +const isIndexable = ref($i.isIndexable); const isExplorable = ref($i.isExplorable); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); @@ -106,6 +111,7 @@ function save() { autoAcceptFollowed: !!autoAcceptFollowed.value, noCrawle: !!noCrawle.value, preventAiLearning: !!preventAiLearning.value, + isIndexable: !!isIndexable.value, isExplorable: !!isExplorable.value, hideOnlineStatus: !!hideOnlineStatus.value, publicReactions: !!publicReactions.value,