Merge branch Sharkey:develop into feat/better-beta-emoji-panel
This commit is contained in:
commit
d5ce4810a3
24 changed files with 235 additions and 72 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
|
|
|
|||
30
locales/index.d.ts
vendored
30
locales/index.d.ts
vendored
|
|
@ -13285,6 +13285,36 @@ export interface Locale extends ILocale {
|
|||
* Signup Reason
|
||||
*/
|
||||
"signupReason": string;
|
||||
"clearCachedFilesOptions": {
|
||||
/**
|
||||
* Delete all cached remote files
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* Only delete files older than:
|
||||
*/
|
||||
"olderThan": string;
|
||||
/**
|
||||
* now
|
||||
*/
|
||||
"now": string;
|
||||
/**
|
||||
* one week
|
||||
*/
|
||||
"oneWeek": string;
|
||||
/**
|
||||
* one month
|
||||
*/
|
||||
"oneMonth": string;
|
||||
/**
|
||||
* one year
|
||||
*/
|
||||
"oneYear": string;
|
||||
/**
|
||||
* Don't delete files used as avatars&c
|
||||
*/
|
||||
"keepFilesInUse": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
|||
|
|
@ -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)`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RegistryUniqueConstraints1750591589187 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DELETE FROM "registry_item" WHERE "id" IN (
|
||||
SELECT t."id" FROM (
|
||||
SELECT *, ROW_NUMBER() OVER (PARTITION BY "userId","key","scope","domain" ORDER BY "updatedAt" DESC) rn
|
||||
FROM "registry_item"
|
||||
) t WHERE t.rn>1)`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9c48d580287308f8c1f674946" ON "registry_item" ("userId", "key", "scope", "domain") NULLS NOT DISTINCT`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d9c48d580287308f8c1f674946"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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']]);
|
||||
|
|
|
|||
|
|
@ -684,8 +684,11 @@ export class QueueService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public createCleanRemoteFilesJob() {
|
||||
return this.objectStorageQueue.add('cleanRemoteFiles', {}, {
|
||||
public createCleanRemoteFilesJob(olderThanSeconds: number = 0, keepFilesInUse: boolean = false) {
|
||||
return this.objectStorageQueue.add('cleanRemoteFiles', {
|
||||
keepFilesInUse,
|
||||
olderThanSeconds,
|
||||
}, {
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
count: 30,
|
||||
|
|
|
|||
|
|
@ -27,25 +27,9 @@ export class RegistryApiService {
|
|||
public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) {
|
||||
// TODO: 作成できるキーの数を制限する
|
||||
|
||||
const query = this.registryItemsRepository.createQueryBuilder('item');
|
||||
if (domain) {
|
||||
query.where('item.domain = :domain', { domain: domain });
|
||||
} else {
|
||||
query.where('item.domain IS NULL');
|
||||
}
|
||||
query.andWhere('item.userId = :userId', { userId: userId });
|
||||
query.andWhere('item.key = :key', { key: key });
|
||||
query.andWhere('item.scope = :scope', { scope: scope });
|
||||
|
||||
const existingItem = await query.getOne();
|
||||
|
||||
if (existingItem) {
|
||||
await this.registryItemsRepository.update(existingItem.id, {
|
||||
updatedAt: new Date(),
|
||||
value: value,
|
||||
});
|
||||
} else {
|
||||
await this.registryItemsRepository.insert({
|
||||
await this.registryItemsRepository.createQueryBuilder('item')
|
||||
.insert()
|
||||
.values({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
userId: userId,
|
||||
|
|
@ -53,8 +37,13 @@ export class RegistryApiService {
|
|||
scope: scope,
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
})
|
||||
.orUpdate(
|
||||
['updatedAt', 'value'],
|
||||
['userId', 'key', 'scope', 'domain'],
|
||||
{ upsertType: 'on-conflict-do-update' }
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (domain == null) {
|
||||
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
|
||||
|
|
|
|||
|
|
@ -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 }, {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
|
||||
@Entity('registry_item')
|
||||
@Index(['userId', 'key', 'scope', 'domain'], { unique: true })
|
||||
export class MiRegistryItem {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull, MoreThan, Not } from 'typeorm';
|
||||
import { IsNull, MoreThan, Not, Brackets } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { CleanRemoteFilesJobData } from '../types.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class CleanRemoteFilesProcessorService {
|
||||
|
|
@ -23,35 +26,54 @@ export class CleanRemoteFilesProcessorService {
|
|||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-files');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
|
||||
public async process(job: Bull.Job<CleanRemoteFilesJobData>): Promise<void> {
|
||||
this.logger.info('Deleting cached remote files...');
|
||||
|
||||
const olderThanTimestamp = Date.now() - (job.data.olderThanSeconds ?? 0) * 1000;
|
||||
const olderThanDate = new Date(olderThanTimestamp);
|
||||
const keepFilesInUse = job.data.keepFilesInUse ?? false;
|
||||
let deletedCount = 0;
|
||||
let cursor: MiDriveFile['id'] | null = null;
|
||||
let errorCount = 0;
|
||||
|
||||
const total = await this.driveFilesRepository.countBy({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
});
|
||||
const filesQuery = this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userHost IS NOT NULL') // remote files
|
||||
.andWhere('file.isLink = FALSE') // cached
|
||||
.andWhere('file.id <= :id', { id: this.idService.gen(olderThanTimestamp) }) // and old
|
||||
.orderBy('file.id', 'ASC');
|
||||
|
||||
if (keepFilesInUse) {
|
||||
filesQuery
|
||||
// are they used as avatar&&c?
|
||||
.leftJoinAndSelect(
|
||||
MiUser, 'fileuser',
|
||||
'fileuser."avatarId"="file"."id" OR fileuser."bannerId"="file"."id" OR fileuser."backgroundId"="file"."id"'
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('fileuser.id IS NULL') // not used
|
||||
.orWhere( // or attached to a user
|
||||
new Brackets((qb) => {
|
||||
qb.where('fileuser.lastFetchedAt IS NOT NULL') // weird? maybe this only applies to local users
|
||||
.andWhere('fileuser.lastFetchedAt < :old', { old: olderThanDate }); // old user
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const total = await filesQuery.clone().getCount();
|
||||
|
||||
while (true) {
|
||||
const files = await this.driveFilesRepository.find({
|
||||
where: {
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 256,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
const thisBatchQuery = filesQuery.clone();
|
||||
if (cursor) thisBatchQuery.andWhere('file.id > :cursor', { cursor });
|
||||
const files = await thisBatchQuery.take(256).getMany();
|
||||
|
||||
if (files.length === 0) {
|
||||
job.updateProgress(100);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ export type RelationshipJobData = {
|
|||
withReplies?: boolean;
|
||||
};
|
||||
|
||||
export type CleanRemoteFilesJobData = {
|
||||
keepFilesInUse: boolean;
|
||||
olderThanSeconds: number;
|
||||
};
|
||||
|
||||
export type DbJobData<T extends keyof DbJobMap> = DbJobMap[T];
|
||||
|
||||
export type DbJobMap = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export const meta = {
|
|||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
properties: {
|
||||
olderThanSeconds: { type: 'number' },
|
||||
keepFilesInUse: { type: 'boolean' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
|
|
@ -30,7 +33,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.moderationLogService.log(me, 'clearRemoteFiles', {});
|
||||
await this.queueService.createCleanRemoteFilesJob();
|
||||
await this.queueService.createCleanRemoteFilesJob(
|
||||
ps.olderThanSeconds ?? 0,
|
||||
ps.keepFilesInUse ?? false,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)');
|
||||
|
|
|
|||
|
|
@ -58,14 +58,40 @@ const pagination = {
|
|||
})),
|
||||
};
|
||||
|
||||
function clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.clearCachedFilesConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
async function clear() {
|
||||
const { canceled, result } = await os.form(i18n.ts.clearCachedFilesOptions.title, {
|
||||
olderThanEnum: {
|
||||
label: i18n.ts.clearCachedFilesOptions.olderThan,
|
||||
type: 'enum',
|
||||
default: 'now',
|
||||
required: true,
|
||||
enum: [
|
||||
{ label: i18n.ts.clearCachedFilesOptions.now, value: 'now' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneWeek, value: 'oneWeek' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneMonth, value: 'oneMonth' },
|
||||
{ label: i18n.ts.clearCachedFilesOptions.oneYear, value: 'oneYear' },
|
||||
],
|
||||
},
|
||||
keepFilesInUse: {
|
||||
label: i18n.ts.clearCachedFilesOptions.keepFilesInUse,
|
||||
description: i18n.ts.clearCachedFilesOptions.keepFilesInUseDescription,
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
os.apiWithDialog('admin/drive/clean-remote-files', {});
|
||||
if (canceled) return;
|
||||
|
||||
const timesMap = {
|
||||
now: 0,
|
||||
oneWeek: 7 * 86400,
|
||||
oneMonth: 30 * 86400,
|
||||
oneYear: 365 * 86400,
|
||||
};
|
||||
|
||||
await os.apiWithDialog('admin/drive/clean-remote-files', {
|
||||
olderThanSeconds: timesMap[result.olderThanEnum] ?? 0,
|
||||
keepFilesInUse: result.keepFilesInUse,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['description', 'bio']">
|
||||
<MkTextarea v-model="profile.description" :max="500" tall manualSave mfmAutocomplete :mfmPreview="true">
|
||||
<MkTextarea v-model="profile.description" :max="instance.maxBioLength" tall manualSave mfmAutocomplete :mfmPreview="true">
|
||||
<template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
|
||||
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
|
||||
</MkTextarea>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5668,6 +5668,8 @@ export type components = {
|
|||
maxRemoteCwLength: number;
|
||||
maxAltTextLength: number;
|
||||
maxRemoteAltTextLength: number;
|
||||
maxBioLength: number;
|
||||
maxRemoteBioLength: number;
|
||||
ads: {
|
||||
/**
|
||||
* Format: id
|
||||
|
|
|
|||
|
|
@ -636,3 +636,13 @@ rawInfoDescription: "Extended user data in its raw form. These fields are privat
|
|||
rawApDescription: "ActivityPub user data in its raw form. These fields are public and accessible to other instances."
|
||||
|
||||
signupReason: "Signup Reason"
|
||||
|
||||
clearCachedFilesOptions:
|
||||
title: "Delete all cached remote files"
|
||||
olderThan: "Only delete files older than:"
|
||||
now: "now"
|
||||
oneWeek: "one week"
|
||||
oneMonth: "one month"
|
||||
oneYear: "one year"
|
||||
keepFilesInUse: "Don't delete files used as avatars&c"
|
||||
keepFilesInUseDescription: "this option requires more complicated database queries, you may need to increase the value of db.extra.statement_timeout in the configuration file"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue