feat(backend): Add Config Option For Bio Length

This commit is contained in:
Lilly Schramm 2025-06-22 01:02:20 +02:00
parent a4c0ef824c
commit df77f339ec
12 changed files with 71 additions and 15 deletions

View file

@ -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

View file

@ -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

View file

@ -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)`);
}
}

View file

@ -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']]);

View file

@ -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 }, {

View file

@ -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,

View file

@ -433,7 +433,7 @@ export type MiPartialRemoteUser = Partial<MiUser> & {
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;

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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<typeof meta, typeof paramDef> { // 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;

View file

@ -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)');