Compare commits

...

23 Commits

Author SHA1 Message Date
naskya 07444ae7c1
Merge branch 'develop' into redis 2024-04-22 06:42:53 +09:00
naskya 455ecdf743 Merge branch 'docs/add_dependencies' into 'develop'
docs: add minimum dependencies

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

See merge request firefish/firefish!10703
2024-04-21 21:40:34 +00:00
naskya d98c564ead
docs: move the dependencies section to the top 2024-04-22 06:39:20 +09:00
naskya 56aac15a6b
docs (minor): paraphrase a bit 2024-04-22 06:32:07 +09:00
naskya 280dddf464 Merge branch 'fix/download-url-agent' into 'develop'
fix: download-url should use proxy bypass hosts

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

See merge request firefish/firefish!10739
2024-04-21 21:27:55 +00:00
naskya 1347c6ff04
Merge branch 'develop' into redis 2024-04-22 06:23:17 +09:00
naskya b3cc01c440 Merge branch 'feat/show-MkRemoteCaution-in-history' into 'develop'
feat: show MkRemoteCaution in note history page

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

See merge request firefish/firefish!10743
2024-04-21 21:23:02 +00:00
naskya ebaefb9697
chore (minor, client): remove redundant attribute 2024-04-22 06:02:08 +09:00
naskya d9982a0b6a
Merge branch 'develop' into feat/show-MkRemoteCaution-in-history 2024-04-22 06:01:19 +09:00
naskya 0cb2e94d99 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 21:00:18 +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
naskya 0c9dc92f07
fix (backend-rs): use hostname for Redis key prefix and host for Redis streams
I suspect this is a bug, but I keep this behavior for compatibility for now
2024-04-22 05:16:14 +09:00
naskya ce672f4edd
dev: add cargo test to pnpm scripts
mocha test has been unmaintained for a long time and is very broken :(
2024-04-21 22:36:05 +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
Lhcfl f0a50bc288 feat: show MkRemoteCaution in note history page 2024-04-14 13:59:27 +08:00
老周部落 5eff4da27b
feat: antenna limit 2024-04-13 10:02:32 +08:00
老周部落 f44a2937d4
fix: download-url should use proxy bypass hosts 2024-04-13 09:00:08 +08:00
老周部落 de4da3c1fd
docs: add minimum dependencies 2024-03-19 22:51:05 +08:00
21 changed files with 197 additions and 58 deletions

View File

@ -42,6 +42,8 @@ cargo --version
### PostgreSQL and PGroonga
Firefish requires PostgreSQL v12 or later. We recommend that you install v12.x for the same reason as Node.js.
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
```sh

View File

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

View File

@ -1,9 +1,36 @@
# Install Firefish
This document shows an example procedure for installing Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
Firefish depends on the following software.
## Runtime dependencies
- At least [NodeJS](https://nodejs.org/en/) v18.17.0 (v20/v21 recommended)
- At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) with [PGroonga](https://pgroonga.github.io/) extension
- At least [Redis](https://redis.io/) v7
- Web Proxy (one of the following)
- Caddy (recommended)
- Nginx (recommended)
- Apache
- [FFmpeg](https://ffmpeg.org/) for video transcoding (**optional**)
- Caching server (**optional**, one of the following)
- [DragonflyDB](https://www.dragonflydb.io/)
- [KeyDB](https://keydb.dev/)
- Another [Redis](https://redis.io/) server
## Build dependencies
- At least [Rust](https://www.rust-lang.org/) v1.74
- C/C++ compiler & build tools
- `build-essential` on Debian/Ubuntu Linux
- `base-devel` on Arch Linux
- [Python 3](https://www.python.org/)
This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
If you do not prepare your environment as document, be sure to meet the minimum dependencies given at the bottom of the page.
Make sure that you can use the `sudo` command before proceeding.
## 1. Install dependencies

View File

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

View File

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

View File

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

View File

@ -557,6 +557,7 @@ export interface Meta {
recaptchaSecretKey: string | null
localDriveCapacityMb: number
remoteDriveCapacityMb: number
antennaLimit: number
summalyProxy: string | null
enableEmail: boolean
email: string | null

View File

@ -320,7 +320,7 @@ fn load_config() -> Config {
} else {
server_config.redis.prefix.clone()
}
.unwrap_or(host.clone());
.unwrap_or(hostname.clone());
Config {
url: server_config.url,

View File

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

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

@ -2,8 +2,8 @@ import * as fs from "node:fs";
import * as stream from "node:stream";
import * as util from "node:util";
import got, * as Got from "got";
import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
import { config } from "@/config.js";
import { getAgentByHostname, StatusError } from "./fetch.js";
import chalk from "chalk";
import Logger from "@/services/logger.js";
import IPCIDR from "ip-cidr";
@ -40,10 +40,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpAgent,
https: httpsAgent,
},
agent: getAgentByHostname(new URL(url).hostname),
http2: false, // default
retry: {
limit: 0,

View File

@ -171,6 +171,25 @@ export function getAgentByUrl(url: URL, bypassProxy = false) {
}
}
/**
* Get agent by Hostname
* @param hostname Hostname
* @param bypassProxy Allways bypass proxy
*/
export function getAgentByHostname(hostname: string, bypassProxy = false) {
if (bypassProxy || (config.proxyBypassHosts || []).includes(hostname)) {
return {
http: _http,
https: _https,
};
} else {
return {
http: httpAgent,
https: httpsAgent,
};
}
}
export class StatusError extends Error {
public statusCode: number;
public statusMessage?: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
});
});
}

View File

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