Merge branch 'develop' into redis
This commit is contained in:
commit
1347c6ff04
16 changed files with 145 additions and 51 deletions
|
@ -2,6 +2,10 @@
|
|||
|
||||
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
|
||||
|
||||
- :warning: Removed `patrons` endpoint.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -340,6 +340,7 @@ invite: "邀请"
|
|||
driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
inMb: "以兆字节 (MegaByte) 为单位"
|
||||
antennaLimit: "每个用户最多可以创建的天线数量"
|
||||
iconUrl: "图标 URL"
|
||||
bannerUrl: "横幅图 URL"
|
||||
backgroundImageUrl: "背景图 URL"
|
||||
|
|
|
@ -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 --",
|
||||
|
|
1
packages/backend-rs/index.d.ts
vendored
1
packages/backend-rs/index.d.ts
vendored
|
@ -557,6 +557,7 @@ export interface Meta {
|
|||
recaptchaSecretKey: string | null
|
||||
localDriveCapacityMb: number
|
||||
remoteDriveCapacityMb: number
|
||||
antennaLimit: number
|
||||
summalyProxy: string | null
|
||||
enableEmail: boolean
|
||||
email: string | null
|
||||
|
|
|
@ -174,6 +174,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)]
|
||||
|
|
19
packages/backend/src/migration/1712937600000-antennaLimit.ts
Normal file
19
packages/backend/src/migration/1712937600000-antennaLimit.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,7 +2,16 @@ import { Feed } from "feed";
|
|||
import { In, IsNull } from "typeorm";
|
||||
import { config } from "@/config.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 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 (
|
||||
user: User,
|
||||
|
@ -15,7 +24,7 @@ export default async function (
|
|||
const author = {
|
||||
link: `${config.url}/@${user.username}`,
|
||||
email: `${user.username}@${config.host}`,
|
||||
name: user.name || user.username,
|
||||
name: escapeCDATA(user.name || user.username),
|
||||
};
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
@ -44,11 +53,13 @@ export default async function (
|
|||
title: `${author.name} (@${user.username}@${config.host})`,
|
||||
updated: notes[0].createdAt,
|
||||
generator: "Firefish",
|
||||
description: `${user.notesCount} Notes, ${
|
||||
profile.ffVisibility === "public" ? user.followingCount : "?"
|
||||
} Following, ${
|
||||
profile.ffVisibility === "public" ? user.followersCount : "?"
|
||||
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
|
||||
description: escapeCDATA(
|
||||
`${user.notesCount} Notes, ${
|
||||
profile.ffVisibility === "public" ? user.followingCount : "?"
|
||||
} Following, ${
|
||||
profile.ffVisibility === "public" ? user.followersCount : "?"
|
||||
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
|
||||
),
|
||||
link: author.link,
|
||||
image: await Users.getAvatarUrl(user),
|
||||
feedLinks: {
|
||||
|
@ -88,19 +99,23 @@ export default async function (
|
|||
}
|
||||
|
||||
feed.addItem({
|
||||
title: title
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||
.substring(0, 100),
|
||||
title: escapeCDATA(
|
||||
title
|
||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||
.substring(0, 100),
|
||||
),
|
||||
link: `${config.url}/notes/${note.id}`,
|
||||
date: note.createdAt,
|
||||
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,
|
||||
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
|
||||
? null
|
||||
: await Users.findOneBy({ id: note.userId });
|
||||
|
@ -135,7 +150,10 @@ export default async function (
|
|||
}">${file.name}</a>`;
|
||||
}
|
||||
}
|
||||
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
|
||||
|
||||
outstr += `${note.cw ? note.cw + "<br>" : ""}${
|
||||
getNoteHtml(note) || ""
|
||||
}${fileEle}`;
|
||||
if (isTheNote) {
|
||||
outstr += ` <span class="${
|
||||
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
|
||||
|
|
|
@ -350,6 +350,19 @@
|
|||
</FormSplit>
|
||||
</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>
|
||||
<template #label>ServiceWorker</template>
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -4,30 +4,35 @@
|
|||
><MkPageHeader :display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkLoading v-if="!loaded" />
|
||||
<MkPagination
|
||||
v-else
|
||||
ref="pagingComponent"
|
||||
v-slot="{ items }"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<div ref="tlEl" class="giivymft noGap">
|
||||
<XList
|
||||
v-slot="{ item }"
|
||||
:items="convertNoteEditsToNotes(items)"
|
||||
class="notes"
|
||||
:no-gap="true"
|
||||
>
|
||||
<XNote
|
||||
:key="item.id"
|
||||
class="qtqtichx"
|
||||
:note="item"
|
||||
:hide-footer="true"
|
||||
:detailed-view="true"
|
||||
/>
|
||||
</XList>
|
||||
</div>
|
||||
</MkPagination>
|
||||
<MkLoading v-if="note == null" />
|
||||
<div v-else>
|
||||
<MkRemoteCaution
|
||||
v-if="note.user.host != null"
|
||||
:href="note.url ?? note.uri!"
|
||||
/>
|
||||
<MkPagination
|
||||
ref="pagingComponent"
|
||||
v-slot="{ items }"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<div ref="tlEl" class="giivymft noGap">
|
||||
<XList
|
||||
v-slot="{ item }"
|
||||
:items="convertNoteEditsToNotes(items)"
|
||||
class="notes"
|
||||
:no-gap="true"
|
||||
>
|
||||
<XNote
|
||||
:key="item.id"
|
||||
class="qtqtichx"
|
||||
:note="item"
|
||||
:hide-footer="true"
|
||||
:detailed-view="true"
|
||||
/>
|
||||
</XList>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
|
|||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import icon from "@/scripts/icon";
|
||||
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
|
||||
|
||||
const pagingComponent = ref<MkPaginationType<
|
||||
typeof pagination.endpoint
|
||||
|
@ -69,8 +75,7 @@ definePageMetadata(
|
|||
})),
|
||||
);
|
||||
|
||||
const note = ref<entities.Note>({} as entities.Note);
|
||||
const loaded = ref(false);
|
||||
const note = ref<entities.Note | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
api("notes/show", {
|
||||
|
@ -83,20 +88,19 @@ onMounted(() => {
|
|||
res.replyId = null;
|
||||
|
||||
note.value = res;
|
||||
loaded.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
|
||||
const now: entities.NoteEdit = {
|
||||
id: "EditionNow",
|
||||
noteId: note.value.id,
|
||||
updatedAt: note.value.createdAt,
|
||||
text: note.value.text,
|
||||
cw: note.value.cw,
|
||||
files: note.value.files,
|
||||
fileIds: note.value.fileIds,
|
||||
emojis: note.value.emojis,
|
||||
noteId: note.value!.id,
|
||||
updatedAt: note.value!.createdAt,
|
||||
text: note.value!.text,
|
||||
cw: note.value!.cw,
|
||||
files: note.value!.files,
|
||||
fileIds: note.value!.fileIds,
|
||||
emojis: note.value!.emojis,
|
||||
};
|
||||
|
||||
return [now]
|
||||
|
@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
|
|||
_shouldInsertAd_: false,
|
||||
files: noteEdit.files,
|
||||
fileIds: noteEdit.fileIds,
|
||||
emojis: note.value.emojis.concat(noteEdit.emojis),
|
||||
emojis: note.value!.emojis.concat(noteEdit.emojis),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -356,6 +356,7 @@ export type LiteInstanceMetadata = {
|
|||
disableGlobalTimeline: boolean;
|
||||
driveCapacityPerLocalUserMb: number;
|
||||
driveCapacityPerRemoteUserMb: number;
|
||||
antennaLimit: number;
|
||||
enableHcaptcha: boolean;
|
||||
hcaptchaSiteKey: string | null;
|
||||
enableRecaptcha: boolean;
|
||||
|
|
Loading…
Reference in a new issue