From df77f339eca115620db744a28107302015dca26f Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:02:20 +0200 Subject: [PATCH 1/6] feat(backend): Add Config Option For Bio Length --- .config/docker_example.yml | 4 ++++ .config/example.yml | 4 ++++ .../1750541176036000-userDescriptionText.js | 15 +++++++++++++++ packages/backend/src/config.ts | 8 +++++++- .../core/activitypub/models/ApPersonService.ts | 15 ++++++--------- .../src/core/entities/MetaEntityService.ts | 2 ++ packages/backend/src/models/User.ts | 2 +- packages/backend/src/models/UserProfile.ts | 4 ++-- packages/backend/src/models/json-schema/meta.ts | 8 ++++++++ .../backend/src/server/NodeinfoServerService.ts | 2 ++ .../backend/src/server/api/endpoints/i/update.ts | 14 +++++++++++++- packages/backend/test/e2e/users.ts | 8 +++++++- 12 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 packages/backend/migration/1750541176036000-userDescriptionText.js diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 3aaa56e333..9b57eec88b 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -341,6 +341,10 @@ id: 'aidx' #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) #maxRemoteAltTextLength: 100000 +# Amount of characters that can be used when writing user bios. Longer descriptions will be rejected. (minimum: 1) +#maxBioLength: 1500 +# Amount of characters that will be saved for remote user bios. Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteBioLength: 15000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index 8cac42c050..8afaaa124d 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -344,6 +344,10 @@ id: 'aidx' #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) #maxRemoteAltTextLength: 100000 +# Amount of characters that can be used when writing user bios. Longer descriptions will be truncated to this length. (minimum: 1) +#maxBioLength: 1500 +# Amount of characters that will be saved for remote user bios. Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteBioLength: 15000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/packages/backend/migration/1750541176036000-userDescriptionText.js b/packages/backend/migration/1750541176036000-userDescriptionText.js new file mode 100644 index 0000000000..5189e90637 --- /dev/null +++ b/packages/backend/migration/1750541176036000-userDescriptionText.js @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: Lillychan and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserDescriptionText1750541176036000 { + name = 'UserDescriptionText1750541176036000' + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE TEXT USING NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c2e7efd456..21396a1f7f 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -96,6 +96,8 @@ type Source = { maxRemoteNoteLength?: number; maxAltTextLength?: number; maxRemoteAltTextLength?: number; + maxBioLength?: number; + maxRemoteBioLength?: number; clusterLimit?: number; @@ -261,6 +263,8 @@ export type Config = { maxRemoteCwLength: number; maxAltTextLength: number; maxRemoteAltTextLength: number; + maxBioLength: number; + maxRemoteBioLength: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -461,6 +465,8 @@ export function loadConfig(): Config { maxRemoteCwLength: config.maxRemoteCwLength ?? 5000, maxAltTextLength: config.maxAltTextLength ?? 20000, maxRemoteAltTextLength: config.maxRemoteAltTextLength ?? 100000, + maxBioLength: config.maxBioLength ?? 1500, + maxRemoteBioLength: config.maxRemoteBioLength ?? 15000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, @@ -658,7 +664,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForFrontend', 'browserTracingIntegration', 'routeLabel']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaDirectory', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'maxBioLength', 'maxRemoteBioLength', 'pidFile', 'filePermissionBits']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword', 'disallowExternalApRedirect']]); _apply_top(['logging', 'sql', ['disableQueryTruncation', 'enableQueryParamLogging']]); diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 29f7459219..1ca3a007c3 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -31,7 +31,6 @@ import type UsersChart from '@/core/chart/charts/users.js'; import type InstanceChart from '@/core/chart/charts/instance.js'; import type { HashtagService } from '@/core/HashtagService.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; -import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -45,6 +44,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { verifyFieldLinks } from '@/misc/verify-field-link.js'; import { isRetryableError } from '@/misc/is-retryable-error.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -55,10 +55,8 @@ import type { ApLoggerService } from '../ApLoggerService.js'; import type { ApImageService } from './ApImageService.js'; import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; const nameLength = 128; -const summaryLength = 2048; type Field = Record<'name' | 'value', string>; @@ -220,7 +218,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { if (!(typeof x.summary === 'string' && x.summary.length > 0)) { throw new UnrecoverableError(`invalid Actor ${uri}: wrong summary`); } - x.summary = truncate(x.summary, summaryLength); + x.summary = truncate(x.summary, this.config.maxRemoteBioLength); } const idHost = this.utilityService.punyHostPSLDomain(x.id); @@ -458,9 +456,9 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { let _description: string | null = null; if (person._misskey_summary) { - _description = truncate(person._misskey_summary, summaryLength); + _description = truncate(person._misskey_summary, this.config.maxRemoteBioLength); } else if (person.summary) { - _description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag); + _description = this.apMfmService.htmlToMfm(truncate(person.summary, this.config.maxRemoteBioLength), person.tag); } await transactionalEntityManager.save(new MiUserProfile({ @@ -575,7 +573,6 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { if (exist === null) return; //#endregion - // eslint-disable-next-line no-param-reassign if (resolver == null) resolver = this.apResolverService.createResolver(); const object = hint ?? await resolver.resolve(uri); @@ -717,9 +714,9 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { let _description: string | null = null; if (person._misskey_summary) { - _description = truncate(person._misskey_summary, summaryLength); + _description = truncate(person._misskey_summary, this.config.maxRemoteBioLength); } else if (person.summary) { - _description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag); + _description = this.apMfmService.htmlToMfm(truncate(person.summary, this.config.maxRemoteBioLength), person.tag); } await this.userProfilesRepository.update({ userId: exist.id }, { diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 294187feba..3e16266d7d 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -117,6 +117,8 @@ export class MetaEntityService { maxRemoteCwLength: this.config.maxRemoteCwLength, maxAltTextLength: this.config.maxAltTextLength, maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, + maxBioLength: this.config.maxBioLength, + maxRemoteBioLength: this.config.maxRemoteBioLength, defaultLightTheme, defaultDarkTheme, defaultLike: instance.defaultLike, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index f40bb41a22..c282fa327f 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -433,7 +433,7 @@ export type MiPartialRemoteUser = Partial & { export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const; export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; -export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; +export const descriptionSchema = { type: 'string', minLength: 1 } as const; export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const listenbrainzSchema = { type: 'string', minLength: 1, maxLength: 128 } as const; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 6ee72e6ddd..7b35cd5961 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -43,8 +43,8 @@ export class MiUserProfile { }) public listenbrainz: string | null; - @Column('varchar', { - length: 2048, nullable: true, + @Column('text', { + nullable: true, comment: 'The description (bio) of the User.', }) public description: string | null; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 8cc1686ac6..35e470c459 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -206,6 +206,14 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + maxBioLength: { + type: 'number', + optional: false, nullable: false, + }, + maxRemoteBioLength: { + type: 'number', + optional: false, nullable: false, + }, ads: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 55e8827696..99441cbe01 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -128,6 +128,8 @@ export class NodeinfoServerService { maxRemoteCwLength: this.config.maxRemoteCwLength, maxAltTextLength: this.config.maxAltTextLength, maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, + maxBioLength: this.config.maxBioLength, + maxRemoteBioLength: this.config.maxRemoteBioLength, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount.username, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 5767880531..9e76572300 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -141,6 +141,13 @@ export const meta = { code: 'MAX_CW_LENGTH', id: '7004c478-bda3-4b4f-acb2-4316398c9d52', }, + + maxBioLength: { + message: 'You tried setting a bio which is too long.', + code: 'MAX_BIO_LENGTH', + id: 'f3bb3543-8bd1-4e6d-9375-55efaf2b4102', + httpStatusCode: 422, + }, }, res: { @@ -329,7 +336,12 @@ export default class extends Endpoint { // eslint- updates.name = trimmedName === '' ? null : trimmedName; } } - if (ps.description !== undefined) profileUpdates.description = ps.description; + if (ps.description !== undefined) { + if (ps.description && ps.description.length > this.config.maxBioLength) { + throw new ApiError(meta.errors.maxBioLength); + } + profileUpdates.description = ps.description; + }; if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 4b3ec856f1..c3c9b1a850 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; -import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; +import { api, failedApiCall, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; @@ -920,6 +920,12 @@ describe('ユーザー', () => { //#endregion + test('user with to long bio', async () => { + await failedApiCall({ endpoint: 'i/update', user: alice, parameters: { + description: 'x'.repeat(10000), + } }, { status: 422, code: 'MAX_BIO_LENGTH', id: 'f3bb3543-8bd1-4e6d-9375-55efaf2b4102' }); + }); + test.todo('を管理人として確認することができる(admin/show-user)'); test.todo('を管理人として確認することができる(admin/show-users)'); test.todo('をサーバー向けに取得することができる(federation/users)'); From 1cd478ab365ffb62c465a0114da5f701daf263a7 Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:13:21 +0200 Subject: [PATCH 2/6] feat(frontend): Respect New Bio Length Setting --- packages/frontend/src/pages/settings/profile.vue | 10 +++------- packages/misskey-js/src/autogen/types.ts | 2 ++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 21bc74326a..f44c8d8514 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -195,6 +195,7 @@ import { langmap } from '@/utility/langmap.js'; import { definePage } from '@/page.js'; import { claimAchievement } from '@/utility/achievements.js'; import { store } from '@/store.js'; +import { instance } from '@/instance.js'; import MkInfo from '@/components/MkInfo.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -270,18 +271,13 @@ function save() { } os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing name: profile.name || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing description: profile.description || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing followedMessage: profile.followedMessage || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line id-denylist location: profile.location || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing birthday: profile.birthday || null, listenbrainz: profile.listenbrainz || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing lang: profile.lang || null, isBot: !!profile.isBot, isCat: !!profile.isCat, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 97356e0c6e..37856f7c46 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5659,6 +5659,8 @@ export type components = { maxRemoteCwLength: number; maxAltTextLength: number; maxRemoteAltTextLength: number; + maxBioLength: number; + maxRemoteBioLength: number; ads: { /** * Format: id From 593443a01c5fa70ece83ade0b258eb1b44e40ce2 Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:17:41 +0200 Subject: [PATCH 3/6] docs: Fix Settings Docs --- .config/example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/example.yml b/.config/example.yml index 8afaaa124d..46ace4abc2 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -344,7 +344,7 @@ id: 'aidx' #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) #maxRemoteAltTextLength: 100000 -# Amount of characters that can be used when writing user bios. Longer descriptions will be truncated to this length. (minimum: 1) +# Amount of characters that can be used when writing user bios. Longer descriptions will be rejected. (minimum: 1) #maxBioLength: 1500 # Amount of characters that will be saved for remote user bios. Longer descriptions will be truncated to this length. (minimum: 1) #maxRemoteBioLength: 15000 From 2ac5f2f412c2bf92b9223a219ecb0b0d953f018e Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:31:55 +0200 Subject: [PATCH 4/6] refactor: Remove Migration Name --- .../backend/migration/1750541176036000-userDescriptionText.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/migration/1750541176036000-userDescriptionText.js b/packages/backend/migration/1750541176036000-userDescriptionText.js index 5189e90637..78d6d9088b 100644 --- a/packages/backend/migration/1750541176036000-userDescriptionText.js +++ b/packages/backend/migration/1750541176036000-userDescriptionText.js @@ -4,7 +4,6 @@ */ export class UserDescriptionText1750541176036000 { - name = 'UserDescriptionText1750541176036000' async up(queryRunner) { await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE TEXT USING NULL`); } From 2329618418b9a31283e4f3ad583007bdc27e830c Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:39:47 +0200 Subject: [PATCH 5/6] refactor: Remove Migration Name --- .../migration/1750541176036-userDescriptionText.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/backend/migration/1750541176036-userDescriptionText.js diff --git a/packages/backend/migration/1750541176036-userDescriptionText.js b/packages/backend/migration/1750541176036-userDescriptionText.js new file mode 100644 index 0000000000..56161dc969 --- /dev/null +++ b/packages/backend/migration/1750541176036-userDescriptionText.js @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: Lillychan and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserDescriptionText1750541176036 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE TEXT USING NULL`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`); + } +} From 4d077732acaeade6d7d596b12131f7414b4ce7ac Mon Sep 17 00:00:00 2001 From: Lilly Schramm Date: Sun, 22 Jun 2025 01:39:53 +0200 Subject: [PATCH 6/6] refactor: Remove Migration Name --- .../1750541176036000-userDescriptionText.js | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 packages/backend/migration/1750541176036000-userDescriptionText.js diff --git a/packages/backend/migration/1750541176036000-userDescriptionText.js b/packages/backend/migration/1750541176036000-userDescriptionText.js deleted file mode 100644 index 78d6d9088b..0000000000 --- a/packages/backend/migration/1750541176036000-userDescriptionText.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: Lillychan and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class UserDescriptionText1750541176036000 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE TEXT USING NULL`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`); - } -}