diff --git a/docs/api-change.md b/docs/api-change.md index eec5f0d81c..f55ceab58f 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -4,6 +4,7 @@ Breaking changes are indicated by the :warning: icon. ## Unreleased +- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional). - Added `filter` optional parameter to `notes/renotes` endpoint to filter the types of renotes. It can take the following values: - `all` (default) - `renote` diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 77dea27573..44222f818f 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'AddDriveFileUsage1713451569342', 'ConvertCwVarcharToText1713225866247', 'FixChatFileConstraint1712855579316', 'DropTimeZone1712425488543', @@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); ---convert-cw-varchar-to-text +-- AddDriveFileUsage +ALTER TABLE "drive_file" DROP COLUMN "usageHint"; +DROP TYPE "drive_file_usage_hint_enum"; + +-- convert-cw-varchar-to-text DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512); CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2); diff --git a/locales/en-US.yml b/locales/en-US.yml index d552e1e3a2..f44c3d1842 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration" invite: "Invite" driveCapacityPerLocalAccount: "Drive capacity per local user" driveCapacityPerRemoteAccount: "Drive capacity per remote user" +antennaLimit: "The maximum number of antennas that each user can create" inMb: "In megabytes" iconUrl: "Icon URL" bannerUrl: "Banner image URL" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c496d05381..c8579a4c1a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -340,6 +340,7 @@ invite: "邀请" driveCapacityPerLocalAccount: "每个本地用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" inMb: "以兆字节 (MegaByte) 为单位" +antennaLimit: "每个用户最多可以创建的天线数量" iconUrl: "图标 URL" bannerUrl: "横幅图 URL" backgroundImageUrl: "背景图 URL" diff --git a/package.json b/package.json index a62fae09f0..93594a5698 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,9 @@ "debug": "pnpm run build:debug && pnpm run start", "build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp", "mocha": "pnpm --filter backend run mocha", - "test": "pnpm run mocha", + "test": "pnpm run test:ts && pnpm run test:rs", + "test:ts": "pnpm run mocha", + "test:rs": "cargo test", "format": "pnpm run format:ts; pnpm run format:rs", "format:ts": "pnpm -r --parallel run format", "format:rs": "cargo fmt --all --", diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 1cf961bd30..04b26c3e5a 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -348,6 +348,7 @@ export interface DriveFile { webpublicType: string | null requestHeaders: Json | null requestIp: string | null + usageHint: DriveFileUsageHintEnum | null } export interface DriveFolder { id: string @@ -491,6 +492,7 @@ export interface Meta { recaptchaSecretKey: string | null localDriveCapacityMb: number remoteDriveCapacityMb: number + antennaLimit: number summalyProxy: string | null enableEmail: boolean email: string | null @@ -780,6 +782,10 @@ export enum AntennaSrcEnum { List = 'list', Users = 'users' } +export enum DriveFileUsageHintEnum { + UserAvatar = 'userAvatar', + UserBanner = 'userBanner' +} export enum MutedNoteReasonEnum { Manual = 'manual', Other = 'other', diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 1ea7bb5bed..6d64f6ec75 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding +const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding module.exports.readEnvironmentConfig = readEnvironmentConfig module.exports.readServerConfig = readServerConfig @@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction module.exports.countReactions = countReactions module.exports.toDbReaction = toDbReaction module.exports.AntennaSrcEnum = AntennaSrcEnum +module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum module.exports.NotificationTypeEnum = NotificationTypeEnum diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index e3e4622a62..a6926e7af2 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -1,5 +1,6 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 +use super::sea_orm_active_enums::DriveFileUsageHintEnum; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -52,6 +53,8 @@ pub struct Model { pub request_headers: Option, #[sea_orm(column_name = "requestIp")] pub request_ip: Option, + #[sea_orm(column_name = "usageHint")] + pub usage_hint: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/model/entity/meta.rs b/packages/backend-rs/src/model/entity/meta.rs index b9a89914bd..fcba9e0be9 100644 --- a/packages/backend-rs/src/model/entity/meta.rs +++ b/packages/backend-rs/src/model/entity/meta.rs @@ -173,6 +173,8 @@ pub struct Model { pub more_urls: Json, #[sea_orm(column_name = "markLocalFilesNsfwByDefault")] pub mark_local_files_nsfw_by_default: bool, + #[sea_orm(column_name = "antennaLimit")] + pub antenna_limit: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs index 38820e1bd8..36281f4dc5 100644 --- a/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs +++ b/packages/backend-rs/src/model/entity/sea_orm_active_enums.rs @@ -23,6 +23,20 @@ pub enum AntennaSrcEnum { #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[cfg_attr(not(feature = "napi"), derive(Clone))] #[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "drive_file_usage_hint_enum" +)] +pub enum DriveFileUsageHintEnum { + #[sea_orm(string_value = "userAvatar")] + UserAvatar, + #[sea_orm(string_value = "userBanner")] + UserBanner, +} +#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[cfg_attr(not(feature = "napi"), derive(Clone))] +#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[sea_orm( rs_type = "String", db_type = "Enum", diff --git a/packages/backend/src/migration/1712937600000-antennaLimit.ts b/packages/backend/src/migration/1712937600000-antennaLimit.ts new file mode 100644 index 0000000000..cd8f9ff658 --- /dev/null +++ b/packages/backend/src/migration/1712937600000-antennaLimit.ts @@ -0,0 +1,19 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class antennaLimit1712937600000 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "meta" ADD "antennaLimit" integer NOT NULL DEFAULT 5`, + undefined, + ); + await queryRunner.query( + `COMMENT ON COLUMN "meta"."antennaLimit" IS 'Antenna Limit'`, + ); + } + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "meta" DROP COLUMN "antennaLimit"`, + undefined, + ); + } +} diff --git a/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts new file mode 100644 index 0000000000..3bdb1aafc8 --- /dev/null +++ b/packages/backend/src/migration/1713451569342-AddDriveFileUsage.ts @@ -0,0 +1,17 @@ +import type { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDriveFileUsage1713451569342 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`, + ); + await queryRunner.query( + `ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`); + await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`); + } +} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 3c49e89fd5..81f564115f 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { NoteFile } from "./note-file.js"; +export type DriveFileUsageHint = "userAvatar" | "userBanner" | null; + @Entity() @Index(["userId", "folderId", "id"]) export class DriveFile { @@ -177,6 +179,14 @@ export class DriveFile { }) public isSensitive: boolean; + // Hint for what this file is used for + @Column({ + type: "enum", + enum: ["userAvatar", "userBanner"], + nullable: true, + }) + public usageHint: DriveFileUsageHint; + /** * 外部の(信頼されていない)URLへの直リンクか否か */ diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index cdb8e14c3f..5e267a8e24 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -276,6 +276,12 @@ export class Meta { }) public remoteDriveCapacityMb: number; + @Column("integer", { + default: 5, + comment: "Antenna Limit", + }) + public antennaLimit: number; + @Column("varchar", { length: 128, nullable: true, diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 2321f20d4c..18b139caff 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), @@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ md5: file.md5, size: file.size, isSensitive: file.isSensitive, + usageHint: file.usageHint, blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false), diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts index 30db9e7d48..929dbb472e 100644 --- a/packages/backend/src/models/schema/drive-file.ts +++ b/packages/backend/src/models/schema/drive-file.ts @@ -44,6 +44,12 @@ export const packedDriveFileSchema = { optional: false, nullable: false, }, + usageHint: { + type: "string", + optional: false, + nullable: true, + enum: ["userAvatar", "userBanner"], + }, blurhash: { type: "string", optional: false, diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts index e2072a963a..a6ac698feb 100644 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ b/packages/backend/src/remote/activitypub/models/image.ts @@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js"; import Resolver from "../resolver.js"; import { fetchMeta } from "backend-rs"; import { apLogger } from "../logger.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { truncate } from "@/misc/truncate.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; @@ -16,6 +19,7 @@ const logger = apLogger; export async function createImage( actor: CacheableRemoteUser, value: any, + usage: DriveFileUsageHint, ): Promise { // Skip if author is frozen. if (actor.isSuspended) { @@ -43,6 +47,7 @@ export async function createImage( sensitive: image.sensitive, isLink: !instance.cacheRemoteFiles, comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), + usageHint: usage, }); if (file.isLink) { @@ -73,9 +78,10 @@ export async function createImage( export async function resolveImage( actor: CacheableRemoteUser, value: any, + usage: DriveFileUsageHint, ): Promise { // TODO // Fetch from remote server and register - return await createImage(actor, value); + return await createImage(actor, value, usage); } diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index ad59930457..b2fd67288c 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -213,7 +213,8 @@ export async function createNote( ? ( await Promise.all( note.attachment.map( - (x) => limit(() => resolveImage(actor, x)) as Promise, + (x) => + limit(() => resolveImage(actor, x, null)) as Promise, ), ) ).filter((image) => image != null) @@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { fileList.map( (x) => limit(async () => { - const file = await resolveImage(actor, x); + const file = await resolveImage(actor, x, null); const update: Partial = {}; const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH); diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index e91280125f..4baa2c021b 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -10,6 +10,7 @@ import { Followings, UserProfiles, UserPublickeys, + DriveFiles, } from "@/models/index.js"; import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js"; @@ -362,10 +363,14 @@ export async function createPerson( //#region Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user!, img).catch(() => null), + : resolveImage( + user, + img, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, + ).catch(() => null), ), ); @@ -438,10 +443,14 @@ export async function updatePerson( // Fetch avatar and header image const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => + [person.icon, person.image].map((img, index) => img == null ? Promise.resolve(null) - : resolveImage(user, img).catch(() => null), + : resolveImage( + user, + img, + index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null, + ).catch(() => null), ), ); @@ -561,10 +570,14 @@ export async function updatePerson( } as Partial; if (avatar) { + if (user?.avatarId) + await DriveFiles.update(user.avatarId, { usageHint: null }); updates.avatarId = avatar.id; } if (banner) { + if (user?.bannerId) + await DriveFiles.update(user.bannerId, { usageHint: null }); updates.bannerId = banner.id; } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index c7731c6c81..ab04800944 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -24,6 +24,11 @@ export const meta = { optional: false, nullable: false, }, + antennaLimit: { + type: "number", + optional: false, + nullable: false, + }, cacheRemoteFiles: { type: "boolean", optional: false, @@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => { enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, + antennaLimit: instance.antennaLimit, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 604ef3a0fc..e5234ea720 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -94,6 +94,7 @@ export const paramDef = { defaultDarkTheme: { type: "string", nullable: true }, localDriveCapacityMb: { type: "integer" }, remoteDriveCapacityMb: { type: "integer" }, + antennaLimit: { type: "integer" }, cacheRemoteFiles: { type: "boolean" }, markLocalFilesNsfwByDefault: { type: "boolean" }, emailRequiredForSignup: { type: "boolean" }, @@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => { set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; } + if (ps.antennaLimit !== undefined) { + set.antennaLimit = ps.antennaLimit; + } + if (ps.cacheRemoteFiles !== undefined) { set.cacheRemoteFiles = ps.cacheRemoteFiles; } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 792301d4de..aa5dcee044 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -1,5 +1,5 @@ import define from "@/server/api/define.js"; -import { genId } from "backend-rs"; +import { fetchMeta, genId } from "backend-rs"; import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js"; import { ApiError } from "@/server/api/error.js"; import { publishInternalEvent } from "@/services/stream.js"; @@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => { let userList; let userGroupJoining; + const instance = await fetchMeta(true); + const antennas = await Antennas.findBy({ userId: user.id, }); - if (antennas.length > 5 && !user.isAdmin) { + if (antennas.length >= instance.antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 4389688a12..4f65c59a9e 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { verifyLink } from "@/services/fetch-rel-me.js"; import { ApiError } from "@/server/api/error.js"; import define from "@/server/api/define.js"; +import { DriveFile } from "@/models/entities/drive-file"; export const meta = { tags: ["account"], @@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; + let avatar: DriveFile | null = null; if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); + avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => { throw new ApiError(meta.errors.avatarNotAnImage); } + let banner: DriveFile | null = null; if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); + banner = await DriveFiles.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => { updateUsertags(user, tags); //#endregion + // Update old/new avatar usage hints + if (avatar) { + if (user.avatarId) + await DriveFiles.update(user.avatarId, { usageHint: null }); + await DriveFiles.update(avatar.id, { usageHint: "userAvatar" }); + } + + // Update old/new banner usage hints + if (banner) { + if (user.bannerId) + await DriveFiles.update(user.bannerId, { usageHint: null }); + await DriveFiles.update(banner.id, { usageHint: "userBanner" }); + } + if (Object.keys(updates).length > 0) await Users.update(user.id, updates); if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 31677ee2ef..ec8f701976 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -126,6 +126,11 @@ export const meta = { optional: false, nullable: false, }, + antennaLimit: { + type: "number", + optional: false, + nullable: false, + }, cacheRemoteFiles: { type: "boolean", optional: false, @@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => { enableGuestTimeline: instance.enableGuestTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, + antennaLimit: instance.antennaLimit, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 24ad9f8f02..d180bbabf3 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -16,6 +16,7 @@ import { UserProfiles, } from "@/models/index.js"; import { DriveFile } from "@/models/entities/drive-file.js"; +import type { DriveFileUsageHint } from "@/models/entities/drive-file.js"; import type { IRemoteUser, User } from "@/models/entities/user.js"; import { genId } from "backend-rs"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; @@ -65,6 +66,7 @@ function urlPathJoin( * @param type Content-Type for original * @param hash Hash for original * @param size Size for original + * @param usage Optional usage hint for file (f.e. "userAvatar") */ async function save( file: DriveFile, @@ -73,6 +75,7 @@ async function save( type: string, hash: string, size: number, + usage: DriveFileUsageHint = null, ): Promise { // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); @@ -161,6 +164,7 @@ async function save( file.md5 = hash; file.size = size; file.storedInternal = false; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -204,6 +208,7 @@ async function save( file.type = type; file.md5 = hash; file.size = size; + file.usageHint = usage ?? null; return await DriveFiles.insert(file).then((x) => DriveFiles.findOneByOrFail(x.identifiers[0]), @@ -450,6 +455,9 @@ type AddFileArgs = { requestIp?: string | null; requestHeaders?: Record | null; + + /** Whether this file has a known use case, like user avatar or instance icon */ + usageHint?: DriveFileUsageHint; }; /** @@ -469,6 +477,7 @@ export async function addFile({ sensitive = null, requestIp = null, requestHeaders = null, + usageHint = null, }: AddFileArgs): Promise { const info = await getFileInfo(path); logger.info(`${JSON.stringify(info)}`); @@ -581,6 +590,7 @@ export async function addFile({ file.isLink = isLink; file.requestIp = requestIp; file.requestHeaders = requestHeaders; + file.usageHint = usageHint; file.isSensitive = user ? Users.isLocalUser(user) && (instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw) @@ -639,6 +649,7 @@ export async function addFile({ info.type.mime, info.md5, info.size, + usageHint, ); } diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 551d3757ca..e7b084bda1 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js"; import { createTemp } from "@/misc/create-temp.js"; import { downloadUrl, isPrivateIp } from "@/misc/download-url.js"; import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; +import type { + DriveFile, + DriveFileUsageHint, +} from "@/models/entities/drive-file.js"; import { DriveFiles } from "@/models/index.js"; import { driveLogger } from "./logger.js"; import { addFile } from "./add-file.js"; @@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader"); type Args = { url: string; - user: { id: User["id"]; host: User["host"] } | null; + user: { + id: User["id"]; + host: User["host"]; + driveCapacityOverrideMb: User["driveCapacityOverrideMb"]; + } | null; folderId?: DriveFolder["id"] | null; uri?: string | null; sensitive?: boolean; @@ -22,6 +29,7 @@ type Args = { comment?: string | null; requestIp?: string | null; requestHeaders?: Record | null; + usageHint?: DriveFileUsageHint; }; export async function uploadFromUrl({ @@ -35,6 +43,7 @@ export async function uploadFromUrl({ comment = null, requestIp = null, requestHeaders = null, + usageHint = null, }: Args): Promise { const parsedUrl = new URL(url); if ( @@ -75,9 +84,10 @@ export async function uploadFromUrl({ sensitive, requestIp, requestHeaders, + usageHint, }); logger.succ(`Got: ${driveFile.id}`); - return driveFile!; + return driveFile; } catch (e) { logger.error(`Failed to create drive file:\n${inspect(e)}`); throw e; diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index f69c4fb23b..1c2188daac 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -350,6 +350,19 @@ + + + + + + + @@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false); const markLocalFilesNsfwByDefault = ref(false); const localDriveCapacityMb = ref(0); const remoteDriveCapacityMb = ref(0); +const antennaLimit = ref(0); const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); const enableServiceWorker = ref(false); @@ -579,6 +593,7 @@ async function init() { markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault; localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb; remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb; + antennaLimit.value = meta.antennaLimit; enableRegistration.value = !meta.disableRegistration; emailRequiredForSignup.value = meta.emailRequiredForSignup; enableServiceWorker.value = meta.enableServiceWorker; @@ -631,6 +646,7 @@ function save() { markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value, localDriveCapacityMb: localDriveCapacityMb.value, remoteDriveCapacityMb: remoteDriveCapacityMb.value, + antennaLimit: antennaLimit.value, disableRegistration: !enableRegistration.value, emailRequiredForSignup: emailRequiredForSignup.value, enableServiceWorker: enableServiceWorker.value, diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 02362194eb..9ab3c2fff6 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -358,6 +358,7 @@ export type LiteInstanceMetadata = { disableGlobalTimeline: boolean; driveCapacityPerLocalUserMb: number; driveCapacityPerRemoteUserMb: number; + antennaLimit: number; enableHcaptcha: boolean; hcaptchaSiteKey: string | null; enableRecaptcha: boolean;