From 7a7a0517797541be1614aad04ea9892af52638df Mon Sep 17 00:00:00 2001 From: penginn-net <121443048+penginn-net@users.noreply.github.com> Date: Mon, 21 Oct 2024 08:13:45 +0900 Subject: [PATCH 01/47] WIP --- locales/index.d.ts | 26 +++ locales/ja-JP.yml | 7 + .../migration/1729457336777-AddSearchable.js | 20 +++ .../backend/src/core/NoteCreateService.ts | 3 + .../src/core/activitypub/ApAudienceService.ts | 18 +- .../src/core/activitypub/ApRendererService.ts | 16 +- .../src/core/activitypub/misc/contexts.ts | 6 + .../core/activitypub/models/ApNoteService.ts | 4 + .../activitypub/models/ApPersonService.ts | 15 +- packages/backend/src/core/activitypub/type.ts | 1 + .../src/core/entities/NoteEntityService.ts | 1 + .../src/core/entities/UserEntityService.ts | 2 + packages/backend/src/models/Note.ts | 15 +- packages/backend/src/models/User.ts | 14 ++ .../src/server/ActivityPubServerService.ts | 2 +- .../src/server/api/endpoints/notes/create.ts | 3 + packages/backend/src/types.ts | 1 + .../cherrypick-js/etc/cherrypick-js.api.md | 3 + packages/cherrypick-js/src/autogen/types.ts | 5 + packages/cherrypick-js/src/consts.ts | 2 + packages/cherrypick-js/src/index.ts | 1 + .../src/components/CPSearchbilityPicker.vue | 162 ++++++++++++++++++ .../frontend/src/components/MkPostForm.vue | 24 +++ packages/frontend/src/store.ts | 12 ++ 24 files changed, 357 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1729457336777-AddSearchable.js create mode 100644 packages/frontend/src/components/CPSearchbilityPicker.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 128a90b568..6ad6b8118f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12024,6 +12024,32 @@ export interface Locale extends ILocale { */ "confirm": string; }; + "_searchbility": { + /** + * 検索可能範囲 + */ + "tooltip": string; + /** + * が検索できます + */ + "canSearch": string; + /** + * すべてのユーザー + */ + "public": string; + /** + * フォロワーと反応した人 + */ + "followersAndReacted": string; + /** + * 反応した人 + */ + "reactedOnly": string; + /** + * あなただけ + */ + "private": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b3a5a5d4a9..c258c60c22 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3214,3 +3214,10 @@ _renoteConfirm: title: "このノートはリノートしたばかりです" caption: "リノートしますか?" confirm: 'リノートする' +_searchbility: + tooltip: "検索可能範囲" + canSearch: "が検索できます" + public: "すべてのユーザー" + followersAndReacted: "フォロワーと反応した人" + reactedOnly: "反応した人" + private: "あなただけ" diff --git a/packages/backend/migration/1729457336777-AddSearchable.js b/packages/backend/migration/1729457336777-AddSearchable.js new file mode 100644 index 0000000000..b8485e57ef --- /dev/null +++ b/packages/backend/migration/1729457336777-AddSearchable.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project, yojo-art team + * SPDX-License-Identifier: AGPL-3.0-only + */ +export class AddSearchable1729457336777 { + name = 'AddSearchable1729457336777'; + + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "user" ADD "searchableBy" "public"."user_searchableby_enum"'); + await queryRunner.query('CREATE TYPE "public"."note_searchableby_enum" AS ENUM(\'public\', \'followersAndReacted\', \'reactedOnly\', \'private\')'); + await queryRunner.query('ALTER TABLE "note" ADD "searchableBy" "public"."note_searchableby_enum"'); + await queryRunner.query('CREATE INDEX "IDX_3932b42da4cf440203d2013649" ON "user" ("searchableBy") '); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "note" DROP COLUMN "searchableBy"'); + await queryRunner.query('DROP TYPE "public"."note_searchableby_enum"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "searchableBy"'); + } +}; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 4feac3d667..6ff8557e42 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -64,6 +64,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { MiNoteSchedule } from '@/models/_.js'; +import { searchableTypes } from '../types.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -146,6 +147,7 @@ type Option = { disableRightClick?: boolean | null; cw?: string | null; visibility?: string | null; + searchableBy: string | null, visibleUsers?: MinimumUser[] | null; channel?: MiChannel | null; apMentions?: MinimumUser[] | null; @@ -423,6 +425,7 @@ export class NoteCreateService implements OnApplicationShutdown { reactionAcceptance: data.reactionAcceptance, disableRightClick: data.disableRightClick!, visibility: data.visibility as any, + searchableBy: data.searchableBy as any, visibleUserIds: data.visibility === 'specified' ? data.visibleUsers ? data.visibleUsers.map(u => u.id) diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 5a5a76f7d6..b8441e86b0 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import { concat, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; +import { searchableTypes } from '@/types.js'; import { getApIds } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { ApObject } from './type.js'; @@ -72,7 +73,22 @@ export class ApAudienceService { visibleUsers: mentionedUsers, }; } - + public async parseSearchableBy (actor: MiRemoteUser, searchableBy?: string[]): Promise { + if (!searchableBy) { + return null; + } + console.log(searchableBy); + if (searchableBy.includes('https://www.w3.org/ns/activitystreams#Public')) { + return searchableTypes[0]; + } else if (actor.followersUri && searchableBy.includes(actor.followersUri)) { + return searchableTypes[1]; + } else if (searchableBy.includes(actor.uri)) { + return searchableTypes[2]; + } else if (searchableBy.includes('as:Limited') || searchableBy.includes('kmyblue:Limited')) { + return searchableTypes[3]; + } + return null; + } @bindThis private groupingAudience(ids: string[], actor: MiRemoteUser): GroupedAudience { const groups: GroupedAudience = { diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index defeedd6c2..3383d94d12 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -28,6 +28,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { IdService } from '@/core/IdService.js'; +import { searchableTypes } from '@/types.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; @@ -378,7 +379,18 @@ export class ApRendererService { } else { to = mentions; } - + let searchableBy: string[]| undefined = []; + if (note.searchableBy === null) { + searchableBy = undefined; + } else if (note.searchableBy === searchableTypes[0]) { + searchableBy = ['https://www.w3.org/ns/activitystreams#Public']; + } else if (note.searchableBy === searchableTypes[1]) { + searchableBy = [`${this.config.url}/users/${note.userId}/followers`]; + } else if (note.searchableBy === searchableTypes[2]) { + searchableBy = [`${this.config.url}/users/${note.userId}`]; + } else { // if (note.searchableBy === searchableTypes[3]) + searchableBy = ['as:Limited', 'kmyblue:Limited']; + } const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({ id: In(note.mentions), }) : []; @@ -462,6 +474,7 @@ export class ApRendererService { updated: note.updatedAt?.toISOString() ?? undefined, to, cc, + ...(searchableBy ? { searchableBy: searchableBy } : {}), inReplyTo, attachment: files.map(x => this.renderDocument(x)), sensitive: note.cw != null || files.some(file => file.isSensitive), @@ -540,6 +553,7 @@ export class ApRendererService { manuallyApprovesFollowers: user.isLocked, discoverable: user.isExplorable, indexable: user.isIndexable, + searchableBy: [`${this.config.url}/users/${user.id}`], publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, 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 d3c14869b8..215bc96653 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -544,6 +544,12 @@ const extension_context_definition = { featured: 'toot:featured', discoverable: 'toot:discoverable', indexable: 'toot:indexable', + // Fefibird + fedibird: 'http://fedibird.com/ns#', + searchableBy: { + '@id': 'fedibird:searchableBy', + '@type': '@id', + }, // schema schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index a300c2ef22..148f63a74d 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -208,6 +208,9 @@ export class ApNoteService { } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); + const searchableBy = await this.apAudienceService.parseSearchableBy(actor, note.searchableBy); + this.logger.info(JSON.stringify(note.searchableBy)); + this.logger.info(actor.uri); let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; @@ -341,6 +344,7 @@ export class ApNoteService { disableRightClick: false, visibility, visibleUsers, + searchableBy: searchableBy, apMentions, apHashtags, apEmojis, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 92524cbed7..7fa8e6fbfa 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -40,6 +40,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { searchableTypes } from '@/types.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -409,6 +410,7 @@ export class ApPersonService implements OnModuleInit { alsoKnownAs: person.alsoKnownAs, isExplorable: person.discoverable, isIndexable: person.indexable ?? true, + searchableBy: this.getSearchableType(tags), username: person.preferredUsername, usernameLower: person.preferredUsername?.toLowerCase(), host, @@ -649,10 +651,10 @@ export class ApPersonService implements OnModuleInit { } } const role_policy = await this.roleService.getUserPolicies(exist.id); - + this.logger.info(JSON.stringify(tags)); const updates = { lastFetchedAt: new Date(), - isIndexable: person.indexable ?? true, + searchableBy: this.getSearchableType(tags), inbox: person.inbox, sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox, outbox: typeof person.outbox === 'string' ? person.outbox : null, @@ -1001,4 +1003,13 @@ export class ApPersonService implements OnModuleInit { return false; } + + @bindThis + private getSearchableType(tags: string[]): 'public' | 'followersAndReacted' | 'reactedOnly' | 'private' | null { + if (tags.includes('searchable_by_all_users')) return searchableTypes[0]; + if (tags.includes('searchable_by_followers_only')) return searchableTypes[1]; + if (tags.includes('searchable_by_reacted_users_only')) return searchableTypes[2]; + if (tags.includes('searchable_by_nobody')) return searchableTypes[3]; + return null; + } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index bd26c15a92..ff80eebd48 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -140,6 +140,7 @@ export interface IPost extends IObject { _misskey_content?: string; quoteUrl?: string; _misskey_talk?: boolean; + searchableBy?: string[]; } export interface IQuestion extends IObject { diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 1119411880..719b7aa399 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -347,6 +347,7 @@ export class NoteEntityService implements OnModuleInit { text: text, cw: note.cw, visibility: note.visibility, + searchableBy: note.searchableBy, localOnly: note.localOnly, reactionAcceptance: note.reactionAcceptance, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 3c189da2c7..c20a650c8c 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -529,6 +529,8 @@ export class UserEntityService implements OnModuleInit { }))) : [], isBot: user.isBot, isCat: user.isCat, + isIndexable: user.isIndexable, + searchableBy: user.searchableBy, 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/Note.ts b/packages/backend/src/models/Note.ts index e9037bf494..3b19ac1f6f 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -4,7 +4,7 @@ */ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { noteVisibilities } from '@/types.js'; +import { noteVisibilities, searchableTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiChannel } from './Channel.js'; @@ -146,6 +146,19 @@ export class MiNote { @Column('enum', { enum: noteVisibilities }) public visibility: typeof noteVisibilities[number]; + /** + * public ... だれでも + * followers ... フォロワーのみ + * reacted ... 返信かリアクションしたユーザーのみ + * null ... ユーザーのsearchableByを見る + */ + @Column('enum', + { + enum: searchableTypes, + nullable: true, + }) + public searchableBy: typeof searchableTypes[number] | null; + @Index({ unique: true }) @Column('varchar', { length: 512, nullable: true, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index f8bee91dbb..7249ec9da2 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -4,6 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { searchableTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiDriveFile } from './DriveFile.js'; @@ -201,6 +202,19 @@ export class MiUser { }) public isIndexable: boolean; + /** + * public ... だれでも + * followers ... フォロワーのみ + * reacted ... 返信かリアクションしたユーザーのみ + * null ... isIndexableを見る + */ + @Column('enum', + { + enum: searchableTypes, + nullable: true, + }) + public searchableBy: typeof searchableTypes[number] | null; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 365207b34d..7e5417f291 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -109,7 +109,7 @@ export class ActivityPubServerService { reply.code(401); return; } - + console.info(JSON.stringify(request)); if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) { // Host not specified or not match. diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 662aa4580a..6c67b09fbc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -20,6 +20,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { searchableTypes } from '@/types.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -144,6 +145,7 @@ export const paramDef = { } }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + searchableBy: { type: 'string', nullable: true, enum: searchableTypes, default: 'public' }, disableRightClick: { type: 'boolean', default: false }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, @@ -388,6 +390,7 @@ export default class extends Endpoint { // eslint- localOnly: false, reactionAcceptance: ps.reactionAcceptance, disableRightClick: ps.disableRightClick, + searchableBy: ps.searchableBy, visibility: ps.visibility, visibleUsers, channel, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 7fbd139bee..d1a5b8a8bf 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -53,6 +53,7 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const followingVisibilities = ['public', 'followers', 'private'] as const; export const followersVisibilities = ['public', 'followers', 'private'] as const; +export const searchableTypes = ['public', 'followersAndReacted', 'reactedOnly', 'private'] as const; export const moderationLogTypes = [ 'updateServerSettings', diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index e1b18a298f..d73ff6635d 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -2678,6 +2678,9 @@ type NotesCreateResponse = operations['notes___create']['responses']['200']['con // @public (undocumented) type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json']; +// @public (undocumented) +export const noteSearchbility: readonly ["public", "followersAndReacted", "reactedOnly", "private"]; + // @public (undocumented) type NotesEventsSearchRequest = operations['notes___events___search']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 8d9c0ed574..936c5646bf 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -22051,6 +22051,11 @@ export type operations = { * @enum {string|null} */ reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + /** + * @default public + * @enum {string|null} + */ + searchableBy?: 'public' | 'followersAndReacted' | 'reactedOnly' | 'private'; /** @default false */ disableRightClick?: boolean; /** @default false */ diff --git a/packages/cherrypick-js/src/consts.ts b/packages/cherrypick-js/src/consts.ts index a6a113c8e3..7372cbbe5f 100644 --- a/packages/cherrypick-js/src/consts.ts +++ b/packages/cherrypick-js/src/consts.ts @@ -26,6 +26,8 @@ export const followingVisibilities = ['public', 'followers', 'private'] as const export const followersVisibilities = ['public', 'followers', 'private'] as const; +export const noteSearchbility = ['public', 'followersAndReacted', 'reactedOnly', 'private'] as const; + export const permissions = [ 'read:account', 'write:account', diff --git a/packages/cherrypick-js/src/index.ts b/packages/cherrypick-js/src/index.ts index ace9738e6a..a50947f73f 100644 --- a/packages/cherrypick-js/src/index.ts +++ b/packages/cherrypick-js/src/index.ts @@ -23,6 +23,7 @@ export const followingVisibilities = consts.followingVisibilities; export const followersVisibilities = consts.followersVisibilities; export const moderationLogTypes = consts.moderationLogTypes; export const reversiUpdateKeys = consts.reversiUpdateKeys; +export const noteSearchbility = consts.noteSearchbility; // api extractor not supported yet //export * as api from './api.js'; diff --git a/packages/frontend/src/components/CPSearchbilityPicker.vue b/packages/frontend/src/components/CPSearchbilityPicker.vue new file mode 100644 index 0000000000..4c2f677abf --- /dev/null +++ b/packages/frontend/src/components/CPSearchbilityPicker.vue @@ -0,0 +1,162 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 07c3b2665e..4952d3bf20 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -29,6 +29,13 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._visibility[visibility] }} + - + +