Compare commits

...

9 Commits

Author SHA1 Message Date
Linca bfdb4474aa Merge branch 'fix/feed' into 'develop'
fix: notes in rss feed do not display HTML

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10744
2024-04-21 20:59:27 +00:00
naskya d1817d9a22 Merge branch 'feat/antenna_limit' into 'develop'
feat: antenna limit

Co-authored-by: 老周部落 <laozhoubuluo@gmail.com>

Closes #10894

See merge request firefish/firefish!10740
2024-04-21 20:58:09 +00:00
naskya c9de5f6095
docs: update api-changes.md 2024-04-22 05:56:46 +09:00
naskya c4658801aa
chore: regenerate entities 2024-04-22 05:54:32 +09:00
naskya a107d8c1ec
fix (backend): update import 2024-04-22 05:52:56 +09:00
naskya 4c91e8e37f
Merge branch 'develop' into feat/antenna_limit 2024-04-22 05:51:22 +09:00
Lhcfl 241c824ab5 fix: use better `]]>` replacer 2024-04-14 16:44:12 +08:00
Lhcfl 54d9916fec fix: rss feed no HTML 2024-04-14 16:34:33 +08:00
老周部落 5eff4da27b
feat: antenna limit 2024-04-13 10:02:32 +08:00
14 changed files with 103 additions and 15 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

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

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

@ -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/index.js"; import config from "@/config/index.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

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