diff --git a/locales/en-US.yml b/locales/en-US.yml index 804b9281b..8b1aae481 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -762,8 +762,7 @@ no: "No" driveFilesCount: "Number of Drive files" driveUsage: "Drive space usage" noCrawle: "Reject crawler indexing" -noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages, - etc." +noCrawleDescription: "Ask external search engines to not index your content." lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your posts will be visible to anyone, even if you require followers to be manually approved." alwaysMarkSensitive: "Mark as NSFW by default" @@ -1139,6 +1138,8 @@ confirm: "Confirm" importZip: "Import ZIP" exportZip: "Export ZIP" emojiPackCreator: "Emoji pack creator" +indexable: "Indexable" +indexableDescription: "Allow built-in search to show your public posts" languageForTranslation: "Post translation language" detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages" diff --git a/packages/backend/native-utils/migration/src/lib.rs b/packages/backend/native-utils/migration/src/lib.rs index 57ca203f6..f8be136f4 100644 --- a/packages/backend/native-utils/migration/src/lib.rs +++ b/packages/backend/native-utils/migration/src/lib.rs @@ -6,6 +6,7 @@ mod m20230531_180824_drop_reversi; mod m20230627_185451_index_note_url; mod m20230709_000510_move_antenna_to_cache; mod m20230806_170616_fix_antenna_stream_ids; +mod m20230904_013244_is_indexable; pub struct Migrator; @@ -17,6 +18,7 @@ impl MigratorTrait for Migrator { Box::new(m20230627_185451_index_note_url::Migration), Box::new(m20230709_000510_move_antenna_to_cache::Migration), Box::new(m20230806_170616_fix_antenna_stream_ids::Migration), + Box::new(m20230904_013244_is_indexable::Migration), ] } } diff --git a/packages/backend/native-utils/migration/src/m20230904_013244_is_indexable.rs b/packages/backend/native-utils/migration/src/m20230904_013244_is_indexable.rs new file mode 100644 index 000000000..84898fd95 --- /dev/null +++ b/packages/backend/native-utils/migration/src/m20230904_013244_is_indexable.rs @@ -0,0 +1,74 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .add_column( + ColumnDef::new(User::IsIndexable) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await?; + manager + .alter_table( + Table::alter() + .table(UserProfile::Table) + .add_column( + ColumnDef::new(UserProfile::IsIndexable) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .drop_column(User::IsIndexable) + .to_owned(), + ) + .await?; + manager + .alter_table( + Table::alter() + .table(UserProfile::Table) + .drop_column(UserProfile::IsIndexable) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +/// Learn more at https://docs.rs/sea-query#iden +#[derive(Iden)] +enum User { + Table, + #[iden = "isIndexable"] + IsIndexable, +} + +#[derive(Iden)] +enum UserProfile { + Table, + #[iden = "isIndexable"] + IsIndexable, +} diff --git a/packages/backend/native-utils/src/model/entity/user.rs b/packages/backend/native-utils/src/model/entity/user.rs index e76ae08c7..7988bec3a 100644 --- a/packages/backend/native-utils/src/model/entity/user.rs +++ b/packages/backend/native-utils/src/model/entity/user.rs @@ -71,6 +71,8 @@ pub struct Model { pub also_known_as: Option, #[sea_orm(column_name = "speakAsCat")] pub speak_as_cat: bool, + #[sea_orm(column_name = "isIndexable")] + pub is_indexable: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend/native-utils/src/model/entity/user_profile.rs b/packages/backend/native-utils/src/model/entity/user_profile.rs index 4c2f903d4..5fd09dea3 100644 --- a/packages/backend/native-utils/src/model/entity/user_profile.rs +++ b/packages/backend/native-utils/src/model/entity/user_profile.rs @@ -75,6 +75,8 @@ pub struct Model { pub moderation_note: String, #[sea_orm(column_name = "preventAiLearning")] pub prevent_ai_learning: bool, + #[sea_orm(column_name = "isIndexable")] + pub is_indexable: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index 686fab343..0b8863867 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -167,6 +167,12 @@ export class UserProfile { }) 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/entities/user.ts b/packages/backend/src/models/entities/user.ts index 6bbd939b5..2d5e5dca3 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -265,6 +265,13 @@ export class User { }) public driveCapacityOverrideMb: number | 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/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index e7c4b6f00..86bef413b 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -454,6 +454,7 @@ export const UserRepository = db.getRepository(User).extend({ isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, isLocked: user.isLocked, + isIndexable: user.isIndexable, isCat: user.isCat || falsy, speakAsCat: user.speakAsCat || falsy, instance: user.host diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 4c840d0ba..d625308f0 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -66,6 +66,11 @@ export const packedUserLiteSchema = { nullable: false, optional: true, }, + isIndexable: { + type: "boolean", + nullable: false, + optional: true, + }, speakAsCat: { type: "boolean", nullable: false, diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index dab2b9a6e..564de3661 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -205,10 +205,10 @@ export async function createPerson( if (typeof person.followers === "string") { try { - let data = await fetch(person.followers, { + const data = await fetch(person.followers, { headers: { Accept: "application/json" }, }); - let json_data = JSON.parse(await data.text()); + const json_data = JSON.parse(await data.text()); followersCount = json_data.totalItems; } catch { @@ -220,10 +220,10 @@ export async function createPerson( if (typeof person.following === "string") { try { - let data = await fetch(person.following, { + const data = await fetch(person.following, { headers: { Accept: "application/json" }, }); - let json_data = JSON.parse(await data.text()); + const json_data = JSON.parse(await data.text()); followingCount = json_data.totalItems; } catch (e) { @@ -235,10 +235,10 @@ export async function createPerson( if (typeof person.outbox === "string") { try { - let data = await fetch(person.outbox, { + const data = await fetch(person.outbox, { headers: { Accept: "application/json" }, }); - let json_data = JSON.parse(await data.text()); + const json_data = JSON.parse(await data.text()); notesCount = json_data.totalItems; } catch (e) { @@ -302,6 +302,7 @@ export async function createPerson( tags, isBot, isCat: (person as any).isCat === true, + isIndexable: person.indexable, }), )) as IRemoteUser; @@ -547,6 +548,7 @@ export async function updatePerson( tags, isBot: getApType(object) !== "Person", isCat: (person as any).isCat === true, + isIndexable: person.indexable, isLocked: !!person.manuallyApprovesFollowers, movedToUri: person.movedTo || null, alsoKnownAs: person.alsoKnownAs || null, diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts index 7b98cf2d7..bb5dcdfc1 100644 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ b/packages/backend/src/remote/activitypub/renderer/index.ts @@ -30,6 +30,7 @@ export const renderActivity = (x: any): IActivity | null => { 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/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts index 1122a3a27..d91c0a911 100644 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ b/packages/backend/src/remote/activitypub/renderer/person.ts @@ -81,6 +81,7 @@ export async function renderPerson(user: ILocalUser) { discoverable: !!user.isExplorable, publicKey: renderKey(user, keypair, "#main-key"), isCat: user.isCat, + indexable: user.isIndexable, attachment: attachment.length ? attachment : undefined, } as any; diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index b0bdb0a8b..ecaf6d687 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -190,8 +190,9 @@ export interface IActor extends IObject { movedTo?: string; alsoKnownAs?: string[]; discoverable?: boolean; + indexable?: boolean; inbox: string; - sharedInbox?: string; // backward compatibility.. ig + sharedInbox?: string; // Backwards compatibility publicKey?: { id: string; publicKeyPem: string; diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 7a2bf2365..3c3a0913d 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -60,6 +60,7 @@ export default define(meta, paramDef, async (ps, me) => { emailVerified: profile.emailVerified, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, + isIndexable: profile.isIndexable, preventAiLearning: profile.preventAiLearning, alwaysMarkNsfw: profile.alwaysMarkNsfw, autoSensitive: profile.autoSensitive, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 6d3bde2b8..0037839b5 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -120,6 +120,7 @@ export const paramDef = { isBot: { type: "boolean" }, isCat: { type: "boolean" }, speakAsCat: { type: "boolean" }, + isIndexable: { type: "boolean" }, injectFeaturedNote: { type: "boolean" }, receiveAnnouncementEmail: { type: "boolean" }, alwaysMarkNsfw: { type: "boolean" }, @@ -206,6 +207,10 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (typeof ps.preventAiLearning === "boolean") profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; + if (typeof ps.isIndexable === "boolean") { + updates.isIndexable = ps.isIndexable; + profileUpdates.isIndexable = ps.isIndexable; + } if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; if (typeof ps.injectFeaturedNote === "boolean") profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index a80a08a4c..8daf44b48 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -608,7 +608,7 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchNote); } - if (publishing) { + if (publishing && user.isIndexable) { index(note, true); // Publish update event for the updated note details diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 689bd6616..18b524ecb 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -165,6 +165,7 @@ export default async ( createdAt: User["createdAt"]; isBot: User["isBot"]; inbox?: User["inbox"]; + isIndexable?: User["isIndexable"]; }, data: Option, silent = false, @@ -652,7 +653,9 @@ export default async ( } // Register to search database - await index(note, false); + if (user.isIndexable) { + await index(note, false); + } }); async function renderNoteOrRenoteActivity(data: Option, note: Note) { diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 721ffc2f5..1090dc1cb 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -151,6 +151,7 @@ describe("ユーザー", () => { carefulBot: user.carefulBot, autoAcceptFollowed: user.autoAcceptFollowed, noCrawle: user.noCrawle, + isIndexable: user.isIndexable, preventAiLearning: user.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, @@ -529,6 +530,8 @@ describe("ユーザー", () => { { parameters: (): object => ({ autoAcceptFollowed: false }) }, { parameters: (): object => ({ noCrawle: true }) }, { parameters: (): object => ({ noCrawle: false }) }, + { parameters: (): object => ({ isIndexable: true }) }, + { parameters: (): object => ({ isIndexable: false }) }, { parameters: (): object => ({ preventAiLearning: false }) }, { parameters: (): object => ({ preventAiLearning: true }) }, { parameters: (): object => ({ isBot: true }) }, diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index 2957a117f..acadf4d5d 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -52,6 +52,14 @@ i18n.ts.hideOnlineStatusDescription }} + + {{ i18n.ts.indexable }} + +