Merge branch 'develop' into redis

This commit is contained in:
naskya 2024-04-22 06:23:17 +09:00
commit 1347c6ff04
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
16 changed files with 145 additions and 51 deletions

View file

@ -2,6 +2,10 @@
Breaking changes are indicated by the :warning: icon. 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).
## v20240413 ## v20240413
- :warning: Removed `patrons` endpoint. - :warning: Removed `patrons` endpoint.

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

@ -557,6 +557,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

View file

@ -174,6 +174,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

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

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

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

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

@ -2,7 +2,16 @@ import { Feed } from "feed";
import { In, IsNull } from "typeorm"; import { In, IsNull } from "typeorm";
import { config } from "@/config.js"; import { config } from "@/config.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js";
import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js"; import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
import getNoteHtml from "@/remote/activitypub/misc/get-note-html.js";
/**
* If there is this part in the note, it will cause CDATA to be terminated early.
*/
function escapeCDATA(str: string) {
return str.replaceAll("]]>", "]]]]><![CDATA[>");
}
export default async function ( export default async function (
user: User, user: User,
@ -15,7 +24,7 @@ export default async function (
const author = { const author = {
link: `${config.url}/@${user.username}`, link: `${config.url}/@${user.username}`,
email: `${user.username}@${config.host}`, email: `${user.username}@${config.host}`,
name: user.name || user.username, name: escapeCDATA(user.name || user.username),
}; };
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
@ -44,11 +53,13 @@ export default async function (
title: `${author.name} (@${user.username}@${config.host})`, title: `${author.name} (@${user.username}@${config.host})`,
updated: notes[0].createdAt, updated: notes[0].createdAt,
generator: "Firefish", generator: "Firefish",
description: `${user.notesCount} Notes, ${ description: escapeCDATA(
profile.ffVisibility === "public" ? user.followingCount : "?" `${user.notesCount} Notes, ${
} Following, ${ profile.ffVisibility === "public" ? user.followingCount : "?"
profile.ffVisibility === "public" ? user.followersCount : "?" } Following, ${
} Followers${profile.description ? ` · ${profile.description}` : ""}`, profile.ffVisibility === "public" ? user.followersCount : "?"
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
),
link: author.link, link: author.link,
image: await Users.getAvatarUrl(user), image: await Users.getAvatarUrl(user),
feedLinks: { feedLinks: {
@ -88,19 +99,23 @@ export default async function (
} }
feed.addItem({ feed.addItem({
title: title title: escapeCDATA(
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") title
.substring(0, 100), .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
.substring(0, 100),
),
link: `${config.url}/notes/${note.id}`, link: `${config.url}/notes/${note.id}`,
date: note.createdAt, date: note.createdAt,
description: note.cw description: note.cw
? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") ? escapeCDATA(note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""))
: undefined, : undefined,
content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""), content: escapeCDATA(
contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
),
}); });
} }
async function noteToString(note, isTheNote = false) { async function noteToString(note: Note, isTheNote = false) {
const author = isTheNote const author = isTheNote
? null ? null
: await Users.findOneBy({ id: note.userId }); : await Users.findOneBy({ id: note.userId });
@ -135,7 +150,10 @@ export default async function (
}">${file.name}</a>`; }">${file.name}</a>`;
} }
} }
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
outstr += `${note.cw ? note.cw + "<br>" : ""}${
getNoteHtml(note) || ""
}${fileEle}`;
if (isTheNote) { if (isTheNote) {
outstr += ` <span class="${ outstr += ` <span class="${
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note" note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"

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

@ -4,30 +4,35 @@
><MkPageHeader :display-back-button="true" ><MkPageHeader :display-back-button="true"
/></template> /></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<MkLoading v-if="!loaded" /> <MkLoading v-if="note == null" />
<MkPagination <div v-else>
v-else <MkRemoteCaution
ref="pagingComponent" v-if="note.user.host != null"
v-slot="{ items }" :href="note.url ?? note.uri!"
:pagination="pagination" />
> <MkPagination
<div ref="tlEl" class="giivymft noGap"> ref="pagingComponent"
<XList v-slot="{ items }"
v-slot="{ item }" :pagination="pagination"
:items="convertNoteEditsToNotes(items)" >
class="notes" <div ref="tlEl" class="giivymft noGap">
:no-gap="true" <XList
> v-slot="{ item }"
<XNote :items="convertNoteEditsToNotes(items)"
:key="item.id" class="notes"
class="qtqtichx" :no-gap="true"
:note="item" >
:hide-footer="true" <XNote
:detailed-view="true" :key="item.id"
/> class="qtqtichx"
</XList> :note="item"
</div> :hide-footer="true"
</MkPagination> :detailed-view="true"
/>
</XList>
</div>
</MkPagination>
</div>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
const pagingComponent = ref<MkPaginationType< const pagingComponent = ref<MkPaginationType<
typeof pagination.endpoint typeof pagination.endpoint
@ -69,8 +75,7 @@ definePageMetadata(
})), })),
); );
const note = ref<entities.Note>({} as entities.Note); const note = ref<entities.Note | null>(null);
const loaded = ref(false);
onMounted(() => { onMounted(() => {
api("notes/show", { api("notes/show", {
@ -83,20 +88,19 @@ onMounted(() => {
res.replyId = null; res.replyId = null;
note.value = res; note.value = res;
loaded.value = true;
}); });
}); });
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) { function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
const now: entities.NoteEdit = { const now: entities.NoteEdit = {
id: "EditionNow", id: "EditionNow",
noteId: note.value.id, noteId: note.value!.id,
updatedAt: note.value.createdAt, updatedAt: note.value!.createdAt,
text: note.value.text, text: note.value!.text,
cw: note.value.cw, cw: note.value!.cw,
files: note.value.files, files: note.value!.files,
fileIds: note.value.fileIds, fileIds: note.value!.fileIds,
emojis: note.value.emojis, emojis: note.value!.emojis,
}; };
return [now] return [now]
@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
_shouldInsertAd_: false, _shouldInsertAd_: false,
files: noteEdit.files, files: noteEdit.files,
fileIds: noteEdit.fileIds, fileIds: noteEdit.fileIds,
emojis: note.value.emojis.concat(noteEdit.emojis), emojis: note.value!.emojis.concat(noteEdit.emojis),
}); });
}); });
} }

View file

@ -356,6 +356,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;