mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 15:34:13 +00:00 
			
		
		
		
	View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/669 Closes #723 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Julia <julia@insertdomain.name>
This commit is contained in:
		
						commit
						1520bc1715
					
				
					 24 changed files with 244 additions and 68 deletions
				
			
		| 
						 | 
				
			
			@ -167,8 +167,18 @@ id: 'aidx'
 | 
			
		|||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 | 
			
		||||
# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1)
 | 
			
		||||
maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1)
 | 
			
		||||
#maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteNoteLength: 100000
 | 
			
		||||
# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1)
 | 
			
		||||
#maxCwLength: 500
 | 
			
		||||
# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteCwLength: 5000
 | 
			
		||||
# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1)
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
# Proxy for HTTP/HTTPS
 | 
			
		||||
#proxy: http://127.0.0.1:3128
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -179,6 +179,19 @@ id: 'aidx'
 | 
			
		|||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 | 
			
		||||
# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1)
 | 
			
		||||
#maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteNoteLength: 100000
 | 
			
		||||
# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1)
 | 
			
		||||
#maxCwLength: 500
 | 
			
		||||
# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteCwLength: 5000
 | 
			
		||||
# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1)
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
# Proxy for HTTP/HTTPS
 | 
			
		||||
#proxy: http://127.0.0.1:3128
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -250,8 +250,18 @@ id: 'aidx'
 | 
			
		|||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 | 
			
		||||
# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1)
 | 
			
		||||
maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1)
 | 
			
		||||
#maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteNoteLength: 100000
 | 
			
		||||
# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1)
 | 
			
		||||
#maxCwLength: 500
 | 
			
		||||
# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteCwLength: 5000
 | 
			
		||||
# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1)
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
# Proxy for HTTP/HTTPS
 | 
			
		||||
#proxy: http://127.0.0.1:3128
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -261,8 +261,18 @@ id: 'aidx'
 | 
			
		|||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
 | 
			
		||||
#outgoingAddressFamily: ipv4
 | 
			
		||||
 | 
			
		||||
# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1)
 | 
			
		||||
maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1)
 | 
			
		||||
#maxNoteLength: 3000
 | 
			
		||||
# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteNoteLength: 100000
 | 
			
		||||
# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1)
 | 
			
		||||
#maxCwLength: 500
 | 
			
		||||
# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1)
 | 
			
		||||
#maxRemoteCwLength: 5000
 | 
			
		||||
# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1)
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
# Proxy for HTTP/HTTPS
 | 
			
		||||
#proxy: http://127.0.0.1:3128
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class SoftLimitDriveComment1728348353115 {
 | 
			
		||||
    name = 'SoftLimitDriveComment1728348353115'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
			await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE text`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
			await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE varchar(100000)`);
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +73,11 @@ type Source = {
 | 
			
		|||
 | 
			
		||||
	maxFileSize?: number;
 | 
			
		||||
	maxNoteLength?: number;
 | 
			
		||||
	maxCwLength?: number;
 | 
			
		||||
	maxRemoteCwLength?: number;
 | 
			
		||||
	maxRemoteNoteLength?: number;
 | 
			
		||||
	maxAltTextLength?: number;
 | 
			
		||||
	maxRemoteAltTextLength?: number;
 | 
			
		||||
 | 
			
		||||
	clusterLimit?: number;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +154,11 @@ export type Config = {
 | 
			
		|||
	allowedPrivateNetworks: string[] | undefined;
 | 
			
		||||
	maxFileSize: number;
 | 
			
		||||
	maxNoteLength: number;
 | 
			
		||||
	maxRemoteNoteLength: number;
 | 
			
		||||
	maxCwLength: number;
 | 
			
		||||
	maxRemoteCwLength: number;
 | 
			
		||||
	maxAltTextLength: number;
 | 
			
		||||
	maxRemoteAltTextLength: number;
 | 
			
		||||
	clusterLimit: number | undefined;
 | 
			
		||||
	id: string;
 | 
			
		||||
	outgoingAddress: string | undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -301,6 +311,11 @@ export function loadConfig(): Config {
 | 
			
		|||
		allowedPrivateNetworks: config.allowedPrivateNetworks,
 | 
			
		||||
		maxFileSize: config.maxFileSize ?? 262144000,
 | 
			
		||||
		maxNoteLength: config.maxNoteLength ?? 3000,
 | 
			
		||||
		maxRemoteNoteLength: config.maxRemoteNoteLength ?? 100000,
 | 
			
		||||
		maxCwLength: config.maxCwLength ?? 500,
 | 
			
		||||
		maxRemoteCwLength: config.maxRemoteCwLength ?? 5000,
 | 
			
		||||
		maxAltTextLength: config.maxAltTextLength ?? 20000,
 | 
			
		||||
		maxRemoteAltTextLength: config.maxRemoteAltTextLength ?? 100000,
 | 
			
		||||
		clusterLimit: config.clusterLimit,
 | 
			
		||||
		outgoingAddress: config.outgoingAddress,
 | 
			
		||||
		outgoingAddressFamily: config.outgoingAddressFamily,
 | 
			
		||||
| 
						 | 
				
			
			@ -475,7 +490,7 @@ function applyEnvOverrides(config: Source) {
 | 
			
		|||
	_apply_top(['sentryForBackend', 'enableNodeProfiling']);
 | 
			
		||||
	_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
 | 
			
		||||
	_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
 | 
			
		||||
	_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
 | 
			
		||||
	_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]);
 | 
			
		||||
	_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
 | 
			
		||||
	_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,30 +3,11 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
 | 
			
		||||
 | 
			
		||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
 | 
			
		||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
 | 
			
		||||
 | 
			
		||||
export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
 | 
			
		||||
 | 
			
		||||
//#region hard limits
 | 
			
		||||
// If you change DB_* values, you must also change the DB schema.
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Maximum note text length that can be stored in DB.
 | 
			
		||||
 * Content Warnings are included in this limit.
 | 
			
		||||
 * Surrogate pairs count as one
 | 
			
		||||
 */
 | 
			
		||||
export const DB_MAX_NOTE_TEXT_LENGTH = 100000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Maximum image description length that can be stored in DB.
 | 
			
		||||
 * Surrogate pairs count as one
 | 
			
		||||
 */
 | 
			
		||||
export const DB_MAX_IMAGE_COMMENT_LENGTH = 100000;
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
// ブラウザで直接表示することを許可するファイルの種類のリスト
 | 
			
		||||
// ここに含まれないものは application/octet-stream としてレスポンスされる
 | 
			
		||||
// SVGはXSSを生むので許可しない
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
 | 
			
		|||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { SearchService } from '@/core/SearchService.js';
 | 
			
		||||
import { FeaturedService } from '@/core/FeaturedService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -335,9 +334,13 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
			data.localOnly = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const maxTextLength = user.host == null
 | 
			
		||||
			? this.config.maxNoteLength
 | 
			
		||||
			: this.config.maxRemoteNoteLength;
 | 
			
		||||
 | 
			
		||||
		if (data.text) {
 | 
			
		||||
			if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
 | 
			
		||||
				data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
 | 
			
		||||
			if (data.text.length > maxTextLength) {
 | 
			
		||||
				data.text = data.text.slice(0, maxTextLength);
 | 
			
		||||
			}
 | 
			
		||||
			data.text = data.text.trim();
 | 
			
		||||
			if (data.text === '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -347,9 +350,13 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
			data.text = null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const maxCwLength = user.host == null
 | 
			
		||||
			? this.config.maxCwLength
 | 
			
		||||
			: this.config.maxRemoteCwLength;
 | 
			
		||||
 | 
			
		||||
		if (data.cw) {
 | 
			
		||||
			if (data.cw.length > DB_MAX_NOTE_TEXT_LENGTH) {
 | 
			
		||||
				data.cw = data.cw.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
 | 
			
		||||
			if (data.cw.length > maxCwLength) {
 | 
			
		||||
				data.cw = data.cw.slice(0, maxCwLength);
 | 
			
		||||
			}
 | 
			
		||||
			data.cw = data.cw.trim();
 | 
			
		||||
			if (data.cw === '') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
 | 
			
		|||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { SearchService } from '@/core/SearchService.js';
 | 
			
		||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -365,9 +364,13 @@ export class NoteEditService implements OnApplicationShutdown {
 | 
			
		|||
			data.localOnly = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const maxTextLength = user.host == null
 | 
			
		||||
			? this.config.maxNoteLength
 | 
			
		||||
			: this.config.maxRemoteNoteLength;
 | 
			
		||||
 | 
			
		||||
		if (data.text) {
 | 
			
		||||
			if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
 | 
			
		||||
				data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
 | 
			
		||||
			if (data.text.length > maxTextLength) {
 | 
			
		||||
				data.text = data.text.slice(0, maxTextLength);
 | 
			
		||||
			}
 | 
			
		||||
			data.text = data.text.trim();
 | 
			
		||||
			if (data.text === '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -377,9 +380,13 @@ export class NoteEditService implements OnApplicationShutdown {
 | 
			
		|||
			data.text = null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const maxCwLength = user.host == null
 | 
			
		||||
			? this.config.maxCwLength
 | 
			
		||||
			: this.config.maxRemoteCwLength;
 | 
			
		||||
 | 
			
		||||
		if (data.cw) {
 | 
			
		||||
			if (data.cw.length > DB_MAX_NOTE_TEXT_LENGTH) {
 | 
			
		||||
				data.cw = data.cw.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
 | 
			
		||||
			if (data.cw.length > maxCwLength) {
 | 
			
		||||
				data.cw = data.cw.slice(0, maxCwLength);
 | 
			
		||||
			}
 | 
			
		||||
			data.cw = data.cw.trim();
 | 
			
		||||
			if (data.cw === '') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,12 @@ import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
 | 
			
		|||
import type { MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import { truncate } from '@/misc/truncate.js';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { checkHttps } from '@/misc/check-https.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { ApResolverService } from '../ApResolverService.js';
 | 
			
		||||
import { ApLoggerService } from '../ApLoggerService.js';
 | 
			
		||||
import { isDocument, type IObject } from '../type.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +29,8 @@ export class ApImageService {
 | 
			
		|||
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		private apResolverService: ApResolverService,
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +85,7 @@ export class ApImageService {
 | 
			
		|||
			uri: image.url,
 | 
			
		||||
			sensitive: !!(image.sensitive),
 | 
			
		||||
			isLink: !shouldBeCached,
 | 
			
		||||
			comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
 | 
			
		||||
			comment: truncate(image.name ?? undefined, this.config.maxRemoteAltTextLength),
 | 
			
		||||
		});
 | 
			
		||||
		if (!file.isLink || file.url === image.url) return file;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,6 +110,11 @@ export class MetaEntityService {
 | 
			
		|||
			backgroundImageUrl: instance.backgroundImageUrl,
 | 
			
		||||
			logoImageUrl: instance.logoImageUrl,
 | 
			
		||||
			maxNoteTextLength: this.config.maxNoteLength,
 | 
			
		||||
			maxRemoteNoteTextLength: this.config.maxRemoteNoteLength,
 | 
			
		||||
			maxCwLength: this.config.maxCwLength,
 | 
			
		||||
			maxRemoteCwLength: this.config.maxRemoteCwLength,
 | 
			
		||||
			maxAltTextLength: this.config.maxAltTextLength,
 | 
			
		||||
			maxRemoteAltTextLength: this.config.maxRemoteAltTextLength,
 | 
			
		||||
			defaultLightTheme,
 | 
			
		||||
			defaultDarkTheme,
 | 
			
		||||
			defaultLike: instance.defaultLike,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiDriveFolder } from './DriveFolder.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -61,8 +60,7 @@ export class MiDriveFile {
 | 
			
		|||
	})
 | 
			
		||||
	public size: number;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: DB_MAX_IMAGE_COMMENT_LENGTH,
 | 
			
		||||
	@Column('text', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		comment: 'The comment of the DriveFile.',
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -168,6 +168,26 @@ export const packedMetaLiteSchema = {
 | 
			
		|||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxRemoteNoteTextLength: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxCwLength: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxRemoteCwLength: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxAltTextLength: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		maxRemoteAltTextLength: {
 | 
			
		||||
			type: 'number',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		ads: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,6 +122,11 @@ export class NodeinfoServerService {
 | 
			
		|||
					enableMcaptcha: meta.enableMcaptcha,
 | 
			
		||||
					enableTurnstile: meta.enableTurnstile,
 | 
			
		||||
					maxNoteTextLength: this.config.maxNoteLength,
 | 
			
		||||
					maxRemoteNoteTextLength: this.config.maxRemoteNoteLength,
 | 
			
		||||
					maxCwLength: this.config.maxCwLength,
 | 
			
		||||
					maxRemoteCwLength: this.config.maxRemoteCwLength,
 | 
			
		||||
					maxAltTextLength: this.config.maxAltTextLength,
 | 
			
		||||
					maxRemoteAltTextLength: this.config.maxRemoteAltTextLength,
 | 
			
		||||
					enableEmail: meta.enableEmail,
 | 
			
		||||
					enableServiceWorker: meta.enableServiceWorker,
 | 
			
		||||
					proxyAccountName: proxyAccount ? proxyAccount.username : null,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,11 @@
 | 
			
		|||
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
import { MiMeta } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +56,12 @@ export const meta = {
 | 
			
		|||
			code: 'NO_FREE_SPACE',
 | 
			
		||||
			id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		commentTooLong: {
 | 
			
		||||
			message: 'Cannot upload the file because the comment exceeds the instance limit.',
 | 
			
		||||
			code: 'COMMENT_TOO_LONG',
 | 
			
		||||
			id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +70,7 @@ export const paramDef = {
 | 
			
		|||
	properties: {
 | 
			
		||||
		folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
 | 
			
		||||
		name: { type: 'string', nullable: true, default: null },
 | 
			
		||||
		comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null },
 | 
			
		||||
		comment: { type: 'string', nullable: true, default: null },
 | 
			
		||||
		isSensitive: { type: 'boolean', default: false },
 | 
			
		||||
		force: { type: 'boolean', default: false },
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +83,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
		@Inject(DI.meta)
 | 
			
		||||
		private serverSettings: MiMeta,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
	) {
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +103,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.commentTooLong);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				// Create file
 | 
			
		||||
				const driveFile = await this.driveService.addFile({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,8 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		|||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['drive'],
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +51,12 @@ export const meta = {
 | 
			
		|||
			code: 'RESTRICTED_BY_ROLE',
 | 
			
		||||
			id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		commentTooLong: {
 | 
			
		||||
			message: 'Cannot upload the file because the comment exceeds the instance limit.',
 | 
			
		||||
			code: 'COMMENT_TOO_LONG',
 | 
			
		||||
			id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +72,7 @@ export const paramDef = {
 | 
			
		|||
		folderId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		name: { type: 'string' },
 | 
			
		||||
		isSensitive: { type: 'boolean' },
 | 
			
		||||
		comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH },
 | 
			
		||||
		comment: { type: 'string', nullable: true },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['fileId'],
 | 
			
		||||
} as const;
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +82,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
	constructor(
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +98,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				throw new ApiError(meta.errors.accessDenied);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.commentTooLong);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let packedFile;
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,12 +4,14 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['drive'],
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +28,14 @@ export const meta = {
 | 
			
		|||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:drive',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		commentTooLong: {
 | 
			
		||||
			message: 'Cannot upload the file because the comment exceeds the instance limit.',
 | 
			
		||||
			code: 'COMMENT_TOO_LONG',
 | 
			
		||||
			id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +44,7 @@ export const paramDef = {
 | 
			
		|||
		url: { type: 'string' },
 | 
			
		||||
		folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
 | 
			
		||||
		isSensitive: { type: 'boolean', default: false },
 | 
			
		||||
		comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null },
 | 
			
		||||
		comment: { type: 'string', nullable: true, default: null },
 | 
			
		||||
		marker: { type: 'string', nullable: true, default: null },
 | 
			
		||||
		force: { type: 'boolean', default: false },
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			@ -44,11 +54,18 @@ export const paramDef = {
 | 
			
		|||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
 | 
			
		||||
			if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.commentTooLong);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
 | 
			
		||||
				this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
 | 
			
		||||
					this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,15 +5,12 @@
 | 
			
		|||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import { readFile } from 'node:fs/promises';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { describe, test, expect } from '@jest/globals';
 | 
			
		||||
import { loadConfig } from '@/config.js';
 | 
			
		||||
import { getValidator } from '../../../../../test/prelude/get-api-validator.js';
 | 
			
		||||
import { paramDef } from './create.js';
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
const config = loadConfig();
 | 
			
		||||
 | 
			
		||||
const VALID = true;
 | 
			
		||||
const INVALID = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +18,12 @@ const INVALID = false;
 | 
			
		|||
describe('api:notes/create', () => {
 | 
			
		||||
	describe('validation', () => {
 | 
			
		||||
		const v = getValidator(paramDef);
 | 
			
		||||
		const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8');
 | 
			
		||||
		const tooLong = (limit: number) => {
 | 
			
		||||
			const arr: string[] = [''];
 | 
			
		||||
			arr.length = limit + 1;
 | 
			
		||||
			arr.fill('a');
 | 
			
		||||
			return arr.join('');
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		test('reject empty', () => {
 | 
			
		||||
			const valid = v({ });
 | 
			
		||||
| 
						 | 
				
			
			@ -71,8 +73,8 @@ describe('api:notes/create', () => {
 | 
			
		|||
					.toBe(INVALID);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			test('over 500 characters cw', async () => {
 | 
			
		||||
				expect(v({ text: 'Body', cw: await tooLong }))
 | 
			
		||||
			test('over max characters cw', async () => {
 | 
			
		||||
				expect(v({ text: '', cw: tooLong(config.maxNoteLength) }))
 | 
			
		||||
					.toBe(INVALID);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -220,7 +222,7 @@ describe('api:notes/create', () => {
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			test('reject poll with too long choice', async () => {
 | 
			
		||||
				expect(v({ poll: { choices: [await tooLong, '2'] } }))
 | 
			
		||||
				expect(v({ poll: { choices: [tooLong(config.maxNoteLength), '2'] } }))
 | 
			
		||||
					.toBe(INVALID);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,6 +90,12 @@ export const meta = {
 | 
			
		|||
			id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		maxCwLength: {
 | 
			
		||||
			message: 'You tried posting a content warning which is too long.',
 | 
			
		||||
			code: 'MAX_CW_LENGTH',
 | 
			
		||||
			id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
 | 
			
		||||
			message: 'You cannot reply to a specified visibility note with extended visibility.',
 | 
			
		||||
			code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
 | 
			
		||||
| 
						 | 
				
			
			@ -147,7 +153,7 @@ export const paramDef = {
 | 
			
		|||
		visibleUserIds: { type: 'array', uniqueItems: true, items: {
 | 
			
		||||
			type: 'string', format: 'misskey:id',
 | 
			
		||||
		} },
 | 
			
		||||
		cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
 | 
			
		||||
		cw: { type: 'string', nullable: true, minLength: 1 },
 | 
			
		||||
		localOnly: { type: 'boolean', default: false },
 | 
			
		||||
		reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
 | 
			
		||||
		noExtractMentions: { type: 'boolean', default: false },
 | 
			
		||||
| 
						 | 
				
			
			@ -250,10 +256,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
		private noteCreateService: NoteCreateService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const contentLength = (ps.text?.length ?? 0) + (ps.cw?.length ?? 0);
 | 
			
		||||
			if (contentLength > this.config.maxNoteLength) {
 | 
			
		||||
			if (ps.text && ps.text.length > this.config.maxNoteLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.maxLength);
 | 
			
		||||
			}
 | 
			
		||||
			if (ps.cw && ps.cw.length > this.config.maxCwLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.maxCwLength);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let visibleUsers: MiUser[] = [];
 | 
			
		||||
			if (ps.visibleUserIds) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,6 +86,12 @@ export const meta = {
 | 
			
		|||
			id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		maxCwLength: {
 | 
			
		||||
			message: 'You tried posting a content warning which is too long.',
 | 
			
		||||
			code: 'MAX_CW_LENGTH',
 | 
			
		||||
			id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
 | 
			
		||||
			message: 'You cannot reply to a specified visibility note with extended visibility.',
 | 
			
		||||
			code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +203,7 @@ export const paramDef = {
 | 
			
		|||
				format: 'misskey:id',
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 },
 | 
			
		||||
		cw: { type: 'string', nullable: true, minLength: 1 },
 | 
			
		||||
		localOnly: { type: 'boolean', default: false },
 | 
			
		||||
		reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
 | 
			
		||||
		noExtractMentions: { type: 'boolean', default: false },
 | 
			
		||||
| 
						 | 
				
			
			@ -297,10 +303,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
		private noteEditService: NoteEditService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const contentLength = (ps.text?.length ?? 0) + (ps.cw?.length ?? 0);
 | 
			
		||||
			if (contentLength > this.config.maxNoteLength) {
 | 
			
		||||
			if (ps.text && ps.text.length > this.config.maxNoteLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.maxLength);
 | 
			
		||||
			}
 | 
			
		||||
			if (ps.cw && ps.cw.length > this.config.maxCwLength) {
 | 
			
		||||
				throw new ApiError(meta.errors.maxCwLength);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let visibleUsers: MiUser[] = [];
 | 
			
		||||
			if (ps.visibleUserIds) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ export async function getInstance(
 | 
			
		|||
			},
 | 
			
		||||
			polls: {
 | 
			
		||||
				max_options: 10,
 | 
			
		||||
				max_characters_per_option: 50,
 | 
			
		||||
				max_characters_per_option: 150,
 | 
			
		||||
				min_expiration: 50,
 | 
			
		||||
				max_expiration: 2629746,
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,12 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
// Important: this must match the value of maxNoteLength in .config/ci.yml!
 | 
			
		||||
const MAX_NOTE_TEXT_LENGTH = 3000;
 | 
			
		||||
 | 
			
		||||
describe('Note', () => {
 | 
			
		||||
	let Notes: Repository<MiNote>;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,7 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
 | 
			
		||||
	<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 | 
			
		||||
	<div v-show="useCw" :class="$style.cwFrame">
 | 
			
		||||
		<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 | 
			
		||||
		<div v-if="maxCwLength - cwLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: cwLength > maxCwLength }]">{{ maxCwLength - cwLength }}</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
 | 
			
		||||
		<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
 | 
			
		||||
		<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -247,13 +250,16 @@ const submitText = computed((): string => {
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
const textLength = computed((): number => {
 | 
			
		||||
	return (text.value + imeText.value).length + (cw.value?.length ?? 0);
 | 
			
		||||
	return (text.value + imeText.value).length;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const maxTextLength = computed((): number => {
 | 
			
		||||
	return instance ? instance.maxNoteTextLength : 1000;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const cwLength = computed(() => cw.value?.length ?? 0);
 | 
			
		||||
const maxCwLength = computed(() => instance.maxCwLength);
 | 
			
		||||
 | 
			
		||||
const canPost = computed((): boolean => {
 | 
			
		||||
	return !props.mock && !posting.value && !posted.value &&
 | 
			
		||||
		(
 | 
			
		||||
| 
						 | 
				
			
			@ -264,6 +270,7 @@ const canPost = computed((): boolean => {
 | 
			
		|||
			quoteId.value != null
 | 
			
		||||
		) &&
 | 
			
		||||
		(textLength.value <= maxTextLength.value) &&
 | 
			
		||||
		(cwLength.value <= maxCwLength.value) &&
 | 
			
		||||
		(!poll.value || poll.value.choices.length >= 2);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1316,10 +1323,13 @@ defineExpose({
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cw {
 | 
			
		||||
.cwFrame {
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
	padding-bottom: 8px;
 | 
			
		||||
	border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.hashtags {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5153,6 +5153,11 @@ export type components = {
 | 
			
		|||
      iconUrl: string | null;
 | 
			
		||||
      sidebarLogoUrl: string | null;
 | 
			
		||||
      maxNoteTextLength: number;
 | 
			
		||||
      maxRemoteNoteTextLength: number;
 | 
			
		||||
      maxCwLength: number;
 | 
			
		||||
      maxRemoteCwLength: number;
 | 
			
		||||
      maxAltTextLength: number;
 | 
			
		||||
      maxRemoteAltTextLength: number;
 | 
			
		||||
      ads: {
 | 
			
		||||
          /**
 | 
			
		||||
           * Format: id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue