Merge branch 'develop' into fix/use-pagination-in-note

This commit is contained in:
naskya 2024-04-22 05:58:55 +09:00
commit 78b6228b3c
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
28 changed files with 208 additions and 18 deletions

View file

@ -4,6 +4,7 @@ Breaking changes are indicated by the :warning: icon.
## Unreleased ## 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: - Added `filter` optional parameter to `notes/renotes` endpoint to filter the types of renotes. It can take the following values:
- `all` (default) - `all` (default)
- `renote` - `renote`

View file

@ -1,6 +1,7 @@
BEGIN; BEGIN;
DELETE FROM "migrations" WHERE name IN ( DELETE FROM "migrations" WHERE name IN (
'AddDriveFileUsage1713451569342',
'ConvertCwVarcharToText1713225866247', 'ConvertCwVarcharToText1713225866247',
'FixChatFileConstraint1712855579316', 'FixChatFileConstraint1712855579316',
'DropTimeZone1712425488543', 'DropTimeZone1712425488543',
@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN (
'RemoveNativeUtilsMigration1705877093218' '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"; DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512); 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); CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);

View file

@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
invite: "Invite" invite: "Invite"
driveCapacityPerLocalAccount: "Drive capacity per local user" driveCapacityPerLocalAccount: "Drive capacity per local user"
driveCapacityPerRemoteAccount: "Drive capacity per remote user" driveCapacityPerRemoteAccount: "Drive capacity per remote user"
antennaLimit: "The maximum number of antennas that each user can create"
inMb: "In megabytes" inMb: "In megabytes"
iconUrl: "Icon URL" iconUrl: "Icon URL"
bannerUrl: "Banner image URL" bannerUrl: "Banner image URL"

View file

@ -340,6 +340,7 @@ invite: "邀请"
driveCapacityPerLocalAccount: "每个本地用户的网盘容量" driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量" driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
inMb: "以兆字节 (MegaByte) 为单位" inMb: "以兆字节 (MegaByte) 为单位"
antennaLimit: "每个用户最多可以创建的天线数量"
iconUrl: "图标 URL" iconUrl: "图标 URL"
bannerUrl: "横幅图 URL" bannerUrl: "横幅图 URL"
backgroundImageUrl: "背景图 URL" backgroundImageUrl: "背景图 URL"

View file

@ -26,7 +26,9 @@
"debug": "pnpm run build:debug && pnpm run start", "debug": "pnpm run build:debug && pnpm run start",
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp", "build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
"mocha": "pnpm --filter backend run mocha", "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": "pnpm run format:ts; pnpm run format:rs",
"format:ts": "pnpm -r --parallel run format", "format:ts": "pnpm -r --parallel run format",
"format:rs": "cargo fmt --all --", "format:rs": "cargo fmt --all --",

View file

@ -348,6 +348,7 @@ export interface DriveFile {
webpublicType: string | null webpublicType: string | null
requestHeaders: Json | null requestHeaders: Json | null
requestIp: string | null requestIp: string | null
usageHint: DriveFileUsageHintEnum | null
} }
export interface DriveFolder { export interface DriveFolder {
id: string id: string
@ -491,6 +492,7 @@ export interface Meta {
recaptchaSecretKey: string | null recaptchaSecretKey: string | null
localDriveCapacityMb: number localDriveCapacityMb: number
remoteDriveCapacityMb: number remoteDriveCapacityMb: number
antennaLimit: number
summalyProxy: string | null summalyProxy: string | null
enableEmail: boolean enableEmail: boolean
email: string | null email: string | null
@ -780,6 +782,10 @@ export enum AntennaSrcEnum {
List = 'list', List = 'list',
Users = 'users' Users = 'users'
} }
export enum DriveFileUsageHintEnum {
UserAvatar = 'userAvatar',
UserBanner = 'userBanner'
}
export enum MutedNoteReasonEnum { export enum MutedNoteReasonEnum {
Manual = 'manual', Manual = 'manual',
Other = 'other', Other = 'other',

View file

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) 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.readEnvironmentConfig = readEnvironmentConfig
module.exports.readServerConfig = readServerConfig module.exports.readServerConfig = readServerConfig
@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction
module.exports.countReactions = countReactions module.exports.countReactions = countReactions
module.exports.toDbReaction = toDbReaction module.exports.toDbReaction = toDbReaction
module.exports.AntennaSrcEnum = AntennaSrcEnum module.exports.AntennaSrcEnum = AntennaSrcEnum
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
module.exports.NoteVisibilityEnum = NoteVisibilityEnum module.exports.NoteVisibilityEnum = NoteVisibilityEnum
module.exports.NotificationTypeEnum = NotificationTypeEnum module.exports.NotificationTypeEnum = NotificationTypeEnum

View file

@ -1,5 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
use super::sea_orm_active_enums::DriveFileUsageHintEnum;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@ -52,6 +53,8 @@ pub struct Model {
pub request_headers: Option<Json>, pub request_headers: Option<Json>,
#[sea_orm(column_name = "requestIp")] #[sea_orm(column_name = "requestIp")]
pub request_ip: Option<String>, pub request_ip: Option<String>,
#[sea_orm(column_name = "usageHint")]
pub usage_hint: Option<DriveFileUsageHintEnum>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -173,6 +173,8 @@ pub struct Model {
pub more_urls: Json, pub more_urls: Json,
#[sea_orm(column_name = "markLocalFilesNsfwByDefault")] #[sea_orm(column_name = "markLocalFilesNsfwByDefault")]
pub mark_local_files_nsfw_by_default: bool, pub mark_local_files_nsfw_by_default: bool,
#[sea_orm(column_name = "antennaLimit")]
pub antenna_limit: i32,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -23,6 +23,20 @@ pub enum AntennaSrcEnum {
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[cfg_attr(not(feature = "napi"), derive(Clone))] #[cfg_attr(not(feature = "napi"), derive(Clone))]
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))] #[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( #[sea_orm(
rs_type = "String", rs_type = "String",
db_type = "Enum", db_type = "Enum",

View file

@ -0,0 +1,19 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class antennaLimit1712937600000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
`ALTER TABLE "meta" DROP COLUMN "antennaLimit"`,
undefined,
);
}
}

View file

@ -0,0 +1,17 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class AddDriveFileUsage1713451569342 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`);
await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`);
}
}

View file

@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js";
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
import { NoteFile } from "./note-file.js"; import { NoteFile } from "./note-file.js";
export type DriveFileUsageHint = "userAvatar" | "userBanner" | null;
@Entity() @Entity()
@Index(["userId", "folderId", "id"]) @Index(["userId", "folderId", "id"])
export class DriveFile { export class DriveFile {
@ -177,6 +179,14 @@ export class DriveFile {
}) })
public isSensitive: boolean; public isSensitive: boolean;
// Hint for what this file is used for
@Column({
type: "enum",
enum: ["userAvatar", "userBanner"],
nullable: true,
})
public usageHint: DriveFileUsageHint;
/** /**
* ()URLへの直リンクか否か * ()URLへの直リンクか否か
*/ */

View file

@ -276,6 +276,12 @@ export class Meta {
}) })
public remoteDriveCapacityMb: number; public remoteDriveCapacityMb: number;
@Column("integer", {
default: 5,
comment: "Antenna Limit",
})
public antennaLimit: number;
@Column("varchar", { @Column("varchar", {
length: 128, length: 128,
nullable: true, nullable: true,

View file

@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
md5: file.md5, md5: file.md5,
size: file.size, size: file.size,
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
usageHint: file.usageHint,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file, false),
@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
md5: file.md5, md5: file.md5,
size: file.size, size: file.size,
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
usageHint: file.usageHint,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file), properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false), url: opts.self ? file.url : this.getPublicUrl(file, false),

View file

@ -44,6 +44,12 @@ export const packedDriveFileSchema = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
usageHint: {
type: "string",
optional: false,
nullable: true,
enum: ["userAvatar", "userBanner"],
},
blurhash: { blurhash: {
type: "string", type: "string",
optional: false, optional: false,

View file

@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
import Resolver from "../resolver.js"; import Resolver from "../resolver.js";
import { fetchMeta } from "backend-rs"; import { fetchMeta } from "backend-rs";
import { apLogger } from "../logger.js"; 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 { DriveFiles } from "@/models/index.js";
import { truncate } from "@/misc/truncate.js"; import { truncate } from "@/misc/truncate.js";
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
@ -16,6 +19,7 @@ const logger = apLogger;
export async function createImage( export async function createImage(
actor: CacheableRemoteUser, actor: CacheableRemoteUser,
value: any, value: any,
usage: DriveFileUsageHint,
): Promise<DriveFile> { ): Promise<DriveFile> {
// Skip if author is frozen. // Skip if author is frozen.
if (actor.isSuspended) { if (actor.isSuspended) {
@ -43,6 +47,7 @@ export async function createImage(
sensitive: image.sensitive, sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles, isLink: !instance.cacheRemoteFiles,
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
usageHint: usage,
}); });
if (file.isLink) { if (file.isLink) {
@ -73,9 +78,10 @@ export async function createImage(
export async function resolveImage( export async function resolveImage(
actor: CacheableRemoteUser, actor: CacheableRemoteUser,
value: any, value: any,
usage: DriveFileUsageHint,
): Promise<DriveFile> { ): Promise<DriveFile> {
// TODO // TODO
// Fetch from remote server and register // Fetch from remote server and register
return await createImage(actor, value); return await createImage(actor, value, usage);
} }

View file

@ -213,7 +213,8 @@ export async function createNote(
? ( ? (
await Promise.all( await Promise.all(
note.attachment.map( note.attachment.map(
(x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>, (x) =>
limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
), ),
) )
).filter((image) => image != null) ).filter((image) => image != null)
@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
fileList.map( fileList.map(
(x) => (x) =>
limit(async () => { limit(async () => {
const file = await resolveImage(actor, x); const file = await resolveImage(actor, x, null);
const update: Partial<DriveFile> = {}; const update: Partial<DriveFile> = {};
const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH); const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);

View file

@ -10,6 +10,7 @@ import {
Followings, Followings,
UserProfiles, UserProfiles,
UserPublickeys, UserPublickeys,
DriveFiles,
} from "@/models/index.js"; } from "@/models/index.js";
import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js"; import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
import { User } 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 //#region Fetch avatar and header image
const [avatar, banner] = await Promise.all( const [avatar, banner] = await Promise.all(
[person.icon, person.image].map((img) => [person.icon, person.image].map((img, index) =>
img == null img == null
? Promise.resolve(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 // Fetch avatar and header image
const [avatar, banner] = await Promise.all( const [avatar, banner] = await Promise.all(
[person.icon, person.image].map((img) => [person.icon, person.image].map((img, index) =>
img == null img == null
? Promise.resolve(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<User>; } as Partial<User>;
if (avatar) { if (avatar) {
if (user?.avatarId)
await DriveFiles.update(user.avatarId, { usageHint: null });
updates.avatarId = avatar.id; updates.avatarId = avatar.id;
} }
if (banner) { if (banner) {
if (user?.bannerId)
await DriveFiles.update(user.bannerId, { usageHint: null });
updates.bannerId = banner.id; updates.bannerId = banner.id;
} }

View file

@ -24,6 +24,11 @@ export const meta = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: { cacheRemoteFiles: {
type: "boolean", type: "boolean",
optional: false, optional: false,
@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => {
enableGuestTimeline: instance.enableGuestTimeline, enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,

View file

@ -94,6 +94,7 @@ export const paramDef = {
defaultDarkTheme: { type: "string", nullable: true }, defaultDarkTheme: { type: "string", nullable: true },
localDriveCapacityMb: { type: "integer" }, localDriveCapacityMb: { type: "integer" },
remoteDriveCapacityMb: { type: "integer" }, remoteDriveCapacityMb: { type: "integer" },
antennaLimit: { type: "integer" },
cacheRemoteFiles: { type: "boolean" }, cacheRemoteFiles: { type: "boolean" },
markLocalFilesNsfwByDefault: { type: "boolean" }, markLocalFilesNsfwByDefault: { type: "boolean" },
emailRequiredForSignup: { type: "boolean" }, emailRequiredForSignup: { type: "boolean" },
@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
} }
if (ps.antennaLimit !== undefined) {
set.antennaLimit = ps.antennaLimit;
}
if (ps.cacheRemoteFiles !== undefined) { if (ps.cacheRemoteFiles !== undefined) {
set.cacheRemoteFiles = ps.cacheRemoteFiles; set.cacheRemoteFiles = ps.cacheRemoteFiles;
} }

View file

@ -1,5 +1,5 @@
import define from "@/server/api/define.js"; 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 { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
import { publishInternalEvent } from "@/services/stream.js"; import { publishInternalEvent } from "@/services/stream.js";
@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => {
let userList; let userList;
let userGroupJoining; let userGroupJoining;
const instance = await fetchMeta(true);
const antennas = await Antennas.findBy({ const antennas = await Antennas.findBy({
userId: user.id, userId: user.id,
}); });
if (antennas.length > 5 && !user.isAdmin) { if (antennas.length >= instance.antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }

View file

@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import { verifyLink } from "@/services/fetch-rel-me.js"; import { verifyLink } from "@/services/fetch-rel-me.js";
import { ApiError } from "@/server/api/error.js"; import { ApiError } from "@/server/api/error.js";
import define from "@/server/api/define.js"; import define from "@/server/api/define.js";
import { DriveFile } from "@/models/entities/drive-file";
export const meta = { export const meta = {
tags: ["account"], tags: ["account"],
@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (ps.emailNotificationTypes !== undefined) if (ps.emailNotificationTypes !== undefined)
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
let avatar: DriveFile | null = null;
if (ps.avatarId) { 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) if (avatar == null || avatar.userId !== user.id)
throw new ApiError(meta.errors.noSuchAvatar); 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); throw new ApiError(meta.errors.avatarNotAnImage);
} }
let banner: DriveFile | null = null;
if (ps.bannerId) { 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) if (banner == null || banner.userId !== user.id)
throw new ApiError(meta.errors.noSuchBanner); throw new ApiError(meta.errors.noSuchBanner);
@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => {
updateUsertags(user, tags); updateUsertags(user, tags);
//#endregion //#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(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profileUpdates).length > 0) if (Object.keys(profileUpdates).length > 0)
await UserProfiles.update(user.id, profileUpdates); await UserProfiles.update(user.id, profileUpdates);

View file

@ -126,6 +126,11 @@ export const meta = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: { cacheRemoteFiles: {
type: "boolean", type: "boolean",
optional: false, optional: false,
@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => {
enableGuestTimeline: instance.enableGuestTimeline, enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey, hcaptchaSiteKey: instance.hcaptchaSiteKey,

View file

@ -16,6 +16,7 @@ import {
UserProfiles, UserProfiles,
} from "@/models/index.js"; } from "@/models/index.js";
import { DriveFile } from "@/models/entities/drive-file.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 type { IRemoteUser, User } from "@/models/entities/user.js";
import { genId } from "backend-rs"; import { genId } from "backend-rs";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@ -65,6 +66,7 @@ function urlPathJoin(
* @param type Content-Type for original * @param type Content-Type for original
* @param hash Hash for original * @param hash Hash for original
* @param size Size for original * @param size Size for original
* @param usage Optional usage hint for file (f.e. "userAvatar")
*/ */
async function save( async function save(
file: DriveFile, file: DriveFile,
@ -73,6 +75,7 @@ async function save(
type: string, type: string,
hash: string, hash: string,
size: number, size: number,
usage: DriveFileUsageHint = null,
): Promise<DriveFile> { ): Promise<DriveFile> {
// thunbnail, webpublic を必要なら生成 // thunbnail, webpublic を必要なら生成
const alts = await generateAlts(path, type, !file.uri); const alts = await generateAlts(path, type, !file.uri);
@ -161,6 +164,7 @@ async function save(
file.md5 = hash; file.md5 = hash;
file.size = size; file.size = size;
file.storedInternal = false; file.storedInternal = false;
file.usageHint = usage ?? null;
return await DriveFiles.insert(file).then((x) => return await DriveFiles.insert(file).then((x) =>
DriveFiles.findOneByOrFail(x.identifiers[0]), DriveFiles.findOneByOrFail(x.identifiers[0]),
@ -204,6 +208,7 @@ async function save(
file.type = type; file.type = type;
file.md5 = hash; file.md5 = hash;
file.size = size; file.size = size;
file.usageHint = usage ?? null;
return await DriveFiles.insert(file).then((x) => return await DriveFiles.insert(file).then((x) =>
DriveFiles.findOneByOrFail(x.identifiers[0]), DriveFiles.findOneByOrFail(x.identifiers[0]),
@ -450,6 +455,9 @@ type AddFileArgs = {
requestIp?: string | null; requestIp?: string | null;
requestHeaders?: Record<string, string> | null; requestHeaders?: Record<string, string> | 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, sensitive = null,
requestIp = null, requestIp = null,
requestHeaders = null, requestHeaders = null,
usageHint = null,
}: AddFileArgs): Promise<DriveFile> { }: AddFileArgs): Promise<DriveFile> {
const info = await getFileInfo(path); const info = await getFileInfo(path);
logger.info(`${JSON.stringify(info)}`); logger.info(`${JSON.stringify(info)}`);
@ -581,6 +590,7 @@ export async function addFile({
file.isLink = isLink; file.isLink = isLink;
file.requestIp = requestIp; file.requestIp = requestIp;
file.requestHeaders = requestHeaders; file.requestHeaders = requestHeaders;
file.usageHint = usageHint;
file.isSensitive = user file.isSensitive = user
? Users.isLocalUser(user) && ? Users.isLocalUser(user) &&
(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw) (instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
@ -639,6 +649,7 @@ export async function addFile({
info.type.mime, info.type.mime,
info.md5, info.md5,
info.size, info.size,
usageHint,
); );
} }

View file

@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js";
import { createTemp } from "@/misc/create-temp.js"; import { createTemp } from "@/misc/create-temp.js";
import { downloadUrl, isPrivateIp } from "@/misc/download-url.js"; import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
import type { DriveFolder } from "@/models/entities/drive-folder.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 { DriveFiles } from "@/models/index.js";
import { driveLogger } from "./logger.js"; import { driveLogger } from "./logger.js";
import { addFile } from "./add-file.js"; import { addFile } from "./add-file.js";
@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader");
type Args = { type Args = {
url: string; 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; folderId?: DriveFolder["id"] | null;
uri?: string | null; uri?: string | null;
sensitive?: boolean; sensitive?: boolean;
@ -22,6 +29,7 @@ type Args = {
comment?: string | null; comment?: string | null;
requestIp?: string | null; requestIp?: string | null;
requestHeaders?: Record<string, string> | null; requestHeaders?: Record<string, string> | null;
usageHint?: DriveFileUsageHint;
}; };
export async function uploadFromUrl({ export async function uploadFromUrl({
@ -35,6 +43,7 @@ export async function uploadFromUrl({
comment = null, comment = null,
requestIp = null, requestIp = null,
requestHeaders = null, requestHeaders = null,
usageHint = null,
}: Args): Promise<DriveFile> { }: Args): Promise<DriveFile> {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if ( if (
@ -75,9 +84,10 @@ export async function uploadFromUrl({
sensitive, sensitive,
requestIp, requestIp,
requestHeaders, requestHeaders,
usageHint,
}); });
logger.succ(`Got: ${driveFile.id}`); logger.succ(`Got: ${driveFile.id}`);
return driveFile!; return driveFile;
} catch (e) { } catch (e) {
logger.error(`Failed to create drive file:\n${inspect(e)}`); logger.error(`Failed to create drive file:\n${inspect(e)}`);
throw e; throw e;

View file

@ -350,6 +350,19 @@
</FormSplit> </FormSplit>
</FormSection> </FormSection>
<FormSection>
<template #label>{{ i18n.ts.antennas }}</template>
<FormInput
v-model="antennaLimit"
type="number"
class="_formBlock"
>
<template #label>{{
i18n.ts.antennaLimit
}}</template>
</FormInput>
</FormSection>
<FormSection> <FormSection>
<template #label>ServiceWorker</template> <template #label>ServiceWorker</template>
@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false);
const markLocalFilesNsfwByDefault = ref(false); const markLocalFilesNsfwByDefault = ref(false);
const localDriveCapacityMb = ref(0); const localDriveCapacityMb = ref(0);
const remoteDriveCapacityMb = ref(0); const remoteDriveCapacityMb = ref(0);
const antennaLimit = ref(0);
const enableRegistration = ref(false); const enableRegistration = ref(false);
const emailRequiredForSignup = ref(false); const emailRequiredForSignup = ref(false);
const enableServiceWorker = ref(false); const enableServiceWorker = ref(false);
@ -579,6 +593,7 @@ async function init() {
markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault; markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb; localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb; remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
antennaLimit.value = meta.antennaLimit;
enableRegistration.value = !meta.disableRegistration; enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup; emailRequiredForSignup.value = meta.emailRequiredForSignup;
enableServiceWorker.value = meta.enableServiceWorker; enableServiceWorker.value = meta.enableServiceWorker;
@ -631,6 +646,7 @@ function save() {
markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value, markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
localDriveCapacityMb: localDriveCapacityMb.value, localDriveCapacityMb: localDriveCapacityMb.value,
remoteDriveCapacityMb: remoteDriveCapacityMb.value, remoteDriveCapacityMb: remoteDriveCapacityMb.value,
antennaLimit: antennaLimit.value,
disableRegistration: !enableRegistration.value, disableRegistration: !enableRegistration.value,
emailRequiredForSignup: emailRequiredForSignup.value, emailRequiredForSignup: emailRequiredForSignup.value,
enableServiceWorker: enableServiceWorker.value, enableServiceWorker: enableServiceWorker.value,

View file

@ -358,6 +358,7 @@ export type LiteInstanceMetadata = {
disableGlobalTimeline: boolean; disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number; driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number; driveCapacityPerRemoteUserMb: number;
antennaLimit: number;
enableHcaptcha: boolean; enableHcaptcha: boolean;
hcaptchaSiteKey: string | null; hcaptchaSiteKey: string | null;
enableRecaptcha: boolean; enableRecaptcha: boolean;