Compare commits
33 Commits
ebc619c4ec
...
d51bfde68a
Author | SHA1 | Date |
---|---|---|
naskya | d51bfde68a | |
naskya | a6498b0491 | |
naskya | 9ced0d96ad | |
naskya | 9a4988eaad | |
naskya | fbdc068115 | |
naskya | 23ec206aee | |
naskya | 07444ae7c1 | |
naskya | 455ecdf743 | |
naskya | d98c564ead | |
naskya | 56aac15a6b | |
naskya | 280dddf464 | |
naskya | 1347c6ff04 | |
naskya | b3cc01c440 | |
naskya | ebaefb9697 | |
naskya | d9982a0b6a | |
naskya | 0cb2e94d99 | |
naskya | d1817d9a22 | |
naskya | c9de5f6095 | |
naskya | c4658801aa | |
naskya | a107d8c1ec | |
naskya | 4c91e8e37f | |
naskya | 0c9dc92f07 | |
naskya | ce672f4edd | |
Lhcfl | 241c824ab5 | |
Lhcfl | 54d9916fec | |
Lhcfl | f0a50bc288 | |
老周部落 | 5eff4da27b | |
老周部落 | f44a2937d4 | |
老周部落 | 46bf02cdd5 | |
老周部落 | f346d2e2f6 | |
naskya | 2267e90d3b | |
老周部落 | db0bd21edc | |
老周部落 | de4da3c1fd |
|
@ -42,6 +42,8 @@ cargo --version
|
||||||
|
|
||||||
### PostgreSQL and PGroonga
|
### 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/).
|
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,9 +1,36 @@
|
||||||
# Install Firefish
|
# 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 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.
|
Make sure that you can use the `sudo` command before proceeding.
|
||||||
|
|
||||||
## 1. Install dependencies
|
## 1. Install dependencies
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -1226,6 +1227,8 @@ publishTimelinesDescription: "If enabled, the Local and Global timelines will be
|
||||||
on {url} even when signed out."
|
on {url} even when signed out."
|
||||||
noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?"
|
noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?"
|
||||||
showNoAltTextWarning: "Show a warning if you attempt to post files without a description"
|
showNoAltTextWarning: "Show a warning if you attempt to post files without a description"
|
||||||
|
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description when you
|
||||||
|
attempt to post files without a description"
|
||||||
|
|
||||||
_emojiModPerm:
|
_emojiModPerm:
|
||||||
unauthorized: "None"
|
unauthorized: "None"
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -2053,6 +2054,7 @@ searchRangeDescription: "如果您要过滤时间段,请按以下格式输入
|
||||||
messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。"
|
messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。"
|
||||||
noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
|
noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
|
||||||
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告
|
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告
|
||||||
|
showAddFileDescriptionAtFirstPost: 当您首次尝试发布没有描述的帖子附件时自动弹出添加描述页面
|
||||||
autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告
|
autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告
|
||||||
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
|
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
|
||||||
noteEditHistory: "帖子编辑历史"
|
noteEditHistory: "帖子编辑历史"
|
||||||
|
|
|
@ -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 --",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -320,7 +320,7 @@ fn load_config() -> Config {
|
||||||
} else {
|
} else {
|
||||||
server_config.redis.prefix.clone()
|
server_config.redis.prefix.clone()
|
||||||
}
|
}
|
||||||
.unwrap_or(host.clone());
|
.unwrap_or(hostname.clone());
|
||||||
|
|
||||||
Config {
|
Config {
|
||||||
url: server_config.url,
|
url: server_config.url,
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ import * as fs from "node:fs";
|
||||||
import * as stream from "node:stream";
|
import * as stream from "node:stream";
|
||||||
import * as util from "node:util";
|
import * as util from "node:util";
|
||||||
import got, * as Got from "got";
|
import got, * as Got from "got";
|
||||||
import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
|
|
||||||
import { config } from "@/config.js";
|
import { config } from "@/config.js";
|
||||||
|
import { getAgentByHostname, StatusError } from "./fetch.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
import IPCIDR from "ip-cidr";
|
import IPCIDR from "ip-cidr";
|
||||||
|
@ -40,10 +40,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||||
send: timeout,
|
send: timeout,
|
||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
},
|
},
|
||||||
agent: {
|
agent: getAgentByHostname(new URL(url).hostname),
|
||||||
http: httpAgent,
|
|
||||||
https: httpsAgent,
|
|
||||||
},
|
|
||||||
http2: false, // default
|
http2: false, // default
|
||||||
retry: {
|
retry: {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
|
|
|
@ -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 {
|
export class StatusError extends Error {
|
||||||
public statusCode: number;
|
public statusCode: number;
|
||||||
public statusMessage?: string;
|
public statusMessage?: string;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -318,6 +318,8 @@ import XNoteSimple from "@/components/MkNoteSimple.vue";
|
||||||
import XNotePreview from "@/components/MkNotePreview.vue";
|
import XNotePreview from "@/components/MkNotePreview.vue";
|
||||||
import XPostFormAttaches from "@/components/MkPostFormAttaches.vue";
|
import XPostFormAttaches from "@/components/MkPostFormAttaches.vue";
|
||||||
import XPollEditor from "@/components/MkPollEditor.vue";
|
import XPollEditor from "@/components/MkPollEditor.vue";
|
||||||
|
import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
|
||||||
|
import XMediaCaption from "@/components/MkMediaCaption.vue";
|
||||||
import { host, url } from "@/config";
|
import { host, url } from "@/config";
|
||||||
import { erase, unique } from "@/scripts/array";
|
import { erase, unique } from "@/scripts/array";
|
||||||
import { extractMentions } from "@/scripts/extract-mentions";
|
import { extractMentions } from "@/scripts/extract-mentions";
|
||||||
|
@ -334,7 +336,6 @@ import { getAccounts, openAccountMenu as openAccountMenu_ } from "@/account";
|
||||||
import { me } from "@/me";
|
import { me } from "@/me";
|
||||||
import { uploadFile } from "@/scripts/upload";
|
import { uploadFile } from "@/scripts/upload";
|
||||||
import { deepClone } from "@/scripts/clone";
|
import { deepClone } from "@/scripts/clone";
|
||||||
import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
|
|
||||||
import preprocess from "@/scripts/preprocess";
|
import preprocess from "@/scripts/preprocess";
|
||||||
import { vibrate } from "@/scripts/vibrate";
|
import { vibrate } from "@/scripts/vibrate";
|
||||||
import { langmap } from "@/scripts/langmap";
|
import { langmap } from "@/scripts/langmap";
|
||||||
|
@ -508,6 +509,8 @@ const hashtags = computed(
|
||||||
defaultStore.makeGetterSetter("postFormHashtags"),
|
defaultStore.makeGetterSetter("postFormHashtags"),
|
||||||
) as Ref<string | null>;
|
) as Ref<string | null>;
|
||||||
|
|
||||||
|
let isFirstPostAttempt = true;
|
||||||
|
|
||||||
watch(text, () => {
|
watch(text, () => {
|
||||||
checkMissingMention();
|
checkMissingMention();
|
||||||
});
|
});
|
||||||
|
@ -1022,6 +1025,46 @@ function deleteDraft() {
|
||||||
localStorage.setItem("drafts", JSON.stringify(draftData));
|
localStorage.setItem("drafts", JSON.stringify(draftData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns whether the file is described
|
||||||
|
*/
|
||||||
|
function openFileDescriptionWindow(file: entities.DriveFile) {
|
||||||
|
return new Promise<boolean>((resolve, reject) => {
|
||||||
|
os.popup(
|
||||||
|
XMediaCaption,
|
||||||
|
{
|
||||||
|
title: i18n.ts.describeFile,
|
||||||
|
input: {
|
||||||
|
placeholder: i18n.ts.inputNewDescription,
|
||||||
|
default: file.comment !== null ? file.comment : "",
|
||||||
|
},
|
||||||
|
image: file,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
done: (result) => {
|
||||||
|
if (!result || result.canceled) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const comment = result.result?.length === 0 ? null : result.result;
|
||||||
|
os.api("drive/files/update", {
|
||||||
|
fileId: file.id,
|
||||||
|
comment,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
resolve(true);
|
||||||
|
file.comment = comment ?? null;
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"closed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function post() {
|
async function post() {
|
||||||
// For text that is too short, the false positive rate may be too high, so we don't show alarm.
|
// For text that is too short, the false positive rate may be too high, so we don't show alarm.
|
||||||
if (defaultStore.state.autocorrectNoteLanguage && text.value.length > 10) {
|
if (defaultStore.state.autocorrectNoteLanguage && text.value.length > 10) {
|
||||||
|
@ -1056,6 +1099,24 @@ async function post() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
defaultStore.state.showAddFileDescriptionAtFirstPost &&
|
||||||
|
files.value.some((f) => f.comment == null || f.comment.length === 0)
|
||||||
|
) {
|
||||||
|
if (isFirstPostAttempt) {
|
||||||
|
for (const file of files.value) {
|
||||||
|
if (file.comment == null || file.comment.length === 0) {
|
||||||
|
const described = await openFileDescriptionWindow(file);
|
||||||
|
if (!described) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFirstPostAttempt = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
defaultStore.state.showNoAltTextWarning &&
|
defaultStore.state.showNoAltTextWarning &&
|
||||||
files.value.some((f) => f.comment == null || f.comment.length === 0)
|
files.value.some((f) => f.comment == null || f.comment.length === 0)
|
||||||
|
@ -1064,12 +1125,22 @@ async function post() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: "warning",
|
type: "warning",
|
||||||
text: i18n.ts.noAltTextWarning,
|
text: i18n.ts.noAltTextWarning,
|
||||||
okText: i18n.ts.goBack,
|
okText: i18n.ts.describeFile,
|
||||||
cancelText: i18n.ts.toPost,
|
cancelText: i18n.ts.toPost,
|
||||||
isPlaintext: true,
|
isPlaintext: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!canceled) return;
|
if (!canceled) {
|
||||||
|
for (const file of files.value) {
|
||||||
|
if (file.comment == null || file.comment.length === 0) {
|
||||||
|
const described = await openFileDescriptionWindow(file);
|
||||||
|
if (!described) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedText = preprocess(text.value);
|
const processedText = preprocess(text.value);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,9 @@
|
||||||
<FormSwitch v-model="showNoAltTextWarning" class="_formBlock">{{
|
<FormSwitch v-model="showNoAltTextWarning" class="_formBlock">{{
|
||||||
i18n.ts.showNoAltTextWarning
|
i18n.ts.showNoAltTextWarning
|
||||||
}}</FormSwitch>
|
}}</FormSwitch>
|
||||||
|
<FormSwitch v-model="showAddFileDescriptionAtFirstPost" class="_formBlock">{{
|
||||||
|
i18n.ts.showAddFileDescriptionAtFirstPost
|
||||||
|
}}</FormSwitch>
|
||||||
<FormSwitch v-model="autocorrectNoteLanguage" class="_formBlock">{{
|
<FormSwitch v-model="autocorrectNoteLanguage" class="_formBlock">{{
|
||||||
i18n.ts.autocorrectNoteLanguage
|
i18n.ts.autocorrectNoteLanguage
|
||||||
}}</FormSwitch>
|
}}</FormSwitch>
|
||||||
|
@ -533,6 +536,9 @@ const pullToRefreshThreshold = computed(
|
||||||
const showNoAltTextWarning = computed(
|
const showNoAltTextWarning = computed(
|
||||||
defaultStore.makeGetterSetter("showNoAltTextWarning"),
|
defaultStore.makeGetterSetter("showNoAltTextWarning"),
|
||||||
);
|
);
|
||||||
|
const showAddFileDescriptionAtFirstPost = computed(
|
||||||
|
defaultStore.makeGetterSetter("showAddFileDescriptionAtFirstPost"),
|
||||||
|
);
|
||||||
const autocorrectNoteLanguage = computed(
|
const autocorrectNoteLanguage = computed(
|
||||||
defaultStore.makeGetterSetter("autocorrectNoteLanguage"),
|
defaultStore.makeGetterSetter("autocorrectNoteLanguage"),
|
||||||
);
|
);
|
||||||
|
|
|
@ -125,6 +125,7 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
|
||||||
"enablePullToRefresh",
|
"enablePullToRefresh",
|
||||||
"pullToRefreshThreshold",
|
"pullToRefreshThreshold",
|
||||||
"showNoAltTextWarning",
|
"showNoAltTextWarning",
|
||||||
|
"showAddFileDescriptionAtFirstPost",
|
||||||
"autocorrectNoteLanguage",
|
"autocorrectNoteLanguage",
|
||||||
];
|
];
|
||||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
||||||
|
|
|
@ -442,6 +442,10 @@ export const defaultStore = markRaw(
|
||||||
where: "account",
|
where: "account",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showAddFileDescriptionAtFirstPost: {
|
||||||
|
where: "account",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
autocorrectNoteLanguage: {
|
autocorrectNoteLanguage: {
|
||||||
where: "account",
|
where: "account",
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue