Compare commits
47 Commits
8d0128e879
...
85c1bce800
Author | SHA1 | Date |
---|---|---|
laozhoubuluo | 85c1bce800 | |
naskya | d98c564ead | |
naskya | 56aac15a6b | |
naskya | 280dddf464 | |
naskya | b3cc01c440 | |
naskya | ebaefb9697 | |
naskya | d9982a0b6a | |
naskya | 0cb2e94d99 | |
naskya | d1817d9a22 | |
naskya | c9de5f6095 | |
naskya | c4658801aa | |
naskya | a107d8c1ec | |
naskya | 4c91e8e37f | |
naskya | ce672f4edd | |
naskya | 131b3686d4 | |
naskya | 6b008c651a | |
naskya | d2dbfb37c7 | |
naskya | 96481f1353 | |
naskya | c936102a4c | |
naskya | 43570a54aa | |
naskya | 4d34e14dd8 | |
naskya | 28f7ac1acd | |
naskya | 9f3396af21 | |
naskya | dac4043dd9 | |
naskya | d1e898c0d0 | |
mei23 | dc02a07774 | |
naskya | 2760e7feee | |
naskya | 488323cc8e | |
naskya | a2699e6687 | |
naskya | dd3ad89b64 | |
naskya | 4fb2cab617 | |
naskya | 5c4a773ecf | |
Lhcfl | 207855b0e8 | |
Lhcfl | 781c98dda7 | |
Lhcfl | ab221c98a7 | |
yumeko | 6c46bb56fd | |
naskya | 1be5373dfc | |
yumeko | 968657d26e | |
yumeko | 913de651db | |
yumeko | 4aeb0d95cc | |
yumeko | c0f93de94b | |
yumeko | 4823abd3a9 | |
Lhcfl | 241c824ab5 | |
Lhcfl | 54d9916fec | |
Lhcfl | f0a50bc288 | |
老周部落 | 5eff4da27b | |
老周部落 | f44a2937d4 |
|
@ -42,7 +42,7 @@ cargo --version
|
|||
|
||||
### PostgreSQL and PGroonga
|
||||
|
||||
Firefish requires PostgreSQL v12 or later. While you can choose any versions between v12 and the latest version (v16.2 as of writing), we recommend that you install v12.x so as not to use new features inadvertently and introduce incompatibility issues.
|
||||
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/).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon.
|
|||
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
||||
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
||||
|
||||
## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
||||
## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
|
||||
|
||||
- Add "Media" tab to user page
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
'FixChatFileConstraint1712855579316',
|
||||
'DropTimeZone1712425488543',
|
||||
|
@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'RemoveNativeUtilsMigration1705877093218'
|
||||
);
|
||||
|
||||
--convert-cw-varchar-to-text
|
||||
-- AddDriveFileUsage
|
||||
ALTER TABLE "drive_file" DROP COLUMN "usageHint";
|
||||
DROP TYPE "drive_file_usage_hint_enum";
|
||||
|
||||
-- convert-cw-varchar-to-text
|
||||
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
|
||||
ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
|
||||
CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);
|
||||
|
|
|
@ -1,6 +1,31 @@
|
|||
# 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).
|
||||
|
||||
|
@ -318,31 +343,3 @@ cd ~/firefish
|
|||
- Run `psql -d firefish` (or whatever the database name is)
|
||||
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace `999999` with the copied ID)
|
||||
- Restart your Firefish server
|
||||
|
||||
## Dependencies
|
||||
|
||||
**We only recommend that you use components that are still within the upstream support cycle for better performance and security, and it is recommended that new sites meet the recommended dependency version requirements.**
|
||||
|
||||
- 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 for new users)
|
||||
- Nginx (recommended)
|
||||
- Apache
|
||||
|
||||
### Optional dependencies
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) for video transcoding
|
||||
- Caching server (one of the following)
|
||||
- [DragonflyDB](https://www.dragonflydb.io/) (recommended)
|
||||
- [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/)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240413",
|
||||
"version": "20240421",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
|
@ -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 --",
|
||||
|
|
|
@ -17,7 +17,7 @@ regenerate-entities:
|
|||
attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \
|
||||
sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \
|
||||
done
|
||||
sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \
|
||||
sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \
|
||||
src/model/entity/sea_orm_active_enums.rs
|
||||
cargo fmt --all --
|
||||
|
||||
|
|
|
@ -348,6 +348,7 @@ export interface DriveFile {
|
|||
webpublicType: string | null
|
||||
requestHeaders: Json | null
|
||||
requestIp: string | null
|
||||
usageHint: DriveFileUsageHintEnum | null
|
||||
}
|
||||
export interface DriveFolder {
|
||||
id: string
|
||||
|
@ -491,6 +492,7 @@ export interface Meta {
|
|||
recaptchaSecretKey: string | null
|
||||
localDriveCapacityMb: number
|
||||
remoteDriveCapacityMb: number
|
||||
antennaLimit: number
|
||||
summalyProxy: string | null
|
||||
enableEmail: boolean
|
||||
email: string | null
|
||||
|
@ -772,81 +774,85 @@ export interface ReplyMuting {
|
|||
muteeId: string
|
||||
muterId: string
|
||||
}
|
||||
export const enum AntennaSrcEnum {
|
||||
All = 0,
|
||||
Group = 1,
|
||||
Home = 2,
|
||||
Instances = 3,
|
||||
List = 4,
|
||||
Users = 5
|
||||
export enum AntennaSrcEnum {
|
||||
All = 'all',
|
||||
Group = 'group',
|
||||
Home = 'home',
|
||||
Instances = 'instances',
|
||||
List = 'list',
|
||||
Users = 'users'
|
||||
}
|
||||
export const enum MutedNoteReasonEnum {
|
||||
Manual = 0,
|
||||
Other = 1,
|
||||
Spam = 2,
|
||||
Word = 3
|
||||
export enum DriveFileUsageHintEnum {
|
||||
UserAvatar = 'userAvatar',
|
||||
UserBanner = 'userBanner'
|
||||
}
|
||||
export const enum NoteVisibilityEnum {
|
||||
Followers = 0,
|
||||
Hidden = 1,
|
||||
Home = 2,
|
||||
Public = 3,
|
||||
Specified = 4
|
||||
export enum MutedNoteReasonEnum {
|
||||
Manual = 'manual',
|
||||
Other = 'other',
|
||||
Spam = 'spam',
|
||||
Word = 'word'
|
||||
}
|
||||
export const enum NotificationTypeEnum {
|
||||
App = 0,
|
||||
Follow = 1,
|
||||
FollowRequestAccepted = 2,
|
||||
GroupInvited = 3,
|
||||
Mention = 4,
|
||||
PollEnded = 5,
|
||||
PollVote = 6,
|
||||
Quote = 7,
|
||||
Reaction = 8,
|
||||
ReceiveFollowRequest = 9,
|
||||
Renote = 10,
|
||||
Reply = 11
|
||||
export enum NoteVisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Hidden = 'hidden',
|
||||
Home = 'home',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum PageVisibilityEnum {
|
||||
Followers = 0,
|
||||
Public = 1,
|
||||
Specified = 2
|
||||
export enum NotificationTypeEnum {
|
||||
App = 'app',
|
||||
Follow = 'follow',
|
||||
FollowRequestAccepted = 'followRequestAccepted',
|
||||
GroupInvited = 'groupInvited',
|
||||
Mention = 'mention',
|
||||
PollEnded = 'pollEnded',
|
||||
PollVote = 'pollVote',
|
||||
Quote = 'quote',
|
||||
Reaction = 'reaction',
|
||||
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||
Renote = 'renote',
|
||||
Reply = 'reply'
|
||||
}
|
||||
export const enum PollNotevisibilityEnum {
|
||||
Followers = 0,
|
||||
Home = 1,
|
||||
Public = 2,
|
||||
Specified = 3
|
||||
export enum PageVisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum RelayStatusEnum {
|
||||
Accepted = 0,
|
||||
Rejected = 1,
|
||||
Requesting = 2
|
||||
export enum PollNotevisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Home = 'home',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum UserEmojimodpermEnum {
|
||||
Add = 0,
|
||||
Full = 1,
|
||||
Mod = 2,
|
||||
Unauthorized = 3
|
||||
export enum RelayStatusEnum {
|
||||
Accepted = 'accepted',
|
||||
Rejected = 'rejected',
|
||||
Requesting = 'requesting'
|
||||
}
|
||||
export const enum UserProfileFfvisibilityEnum {
|
||||
Followers = 0,
|
||||
Private = 1,
|
||||
Public = 2
|
||||
export enum UserEmojimodpermEnum {
|
||||
Add = 'add',
|
||||
Full = 'full',
|
||||
Mod = 'mod',
|
||||
Unauthorized = 'unauthorized'
|
||||
}
|
||||
export const enum UserProfileMutingnotificationtypesEnum {
|
||||
App = 0,
|
||||
Follow = 1,
|
||||
FollowRequestAccepted = 2,
|
||||
GroupInvited = 3,
|
||||
Mention = 4,
|
||||
PollEnded = 5,
|
||||
PollVote = 6,
|
||||
Quote = 7,
|
||||
Reaction = 8,
|
||||
ReceiveFollowRequest = 9,
|
||||
Renote = 10,
|
||||
Reply = 11
|
||||
export enum UserProfileFfvisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Private = 'private',
|
||||
Public = 'public'
|
||||
}
|
||||
export enum UserProfileMutingnotificationtypesEnum {
|
||||
App = 'app',
|
||||
Follow = 'follow',
|
||||
FollowRequestAccepted = 'followRequestAccepted',
|
||||
GroupInvited = 'groupInvited',
|
||||
Mention = 'mention',
|
||||
PollEnded = 'pollEnded',
|
||||
PollVote = 'pollVote',
|
||||
Quote = 'quote',
|
||||
Reaction = 'reaction',
|
||||
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||
Renote = 'renote',
|
||||
Reply = 'reply'
|
||||
}
|
||||
export interface Signin {
|
||||
id: string
|
||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
|
||||
const { readEnvironmentConfig, readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
|
||||
|
||||
module.exports.readEnvironmentConfig = readEnvironmentConfig
|
||||
module.exports.readServerConfig = readServerConfig
|
||||
|
@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction
|
|||
module.exports.countReactions = countReactions
|
||||
module.exports.toDbReaction = toDbReaction
|
||||
module.exports.AntennaSrcEnum = AntennaSrcEnum
|
||||
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
|
||||
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
|
||||
module.exports.NoteVisibilityEnum = NoteVisibilityEnum
|
||||
module.exports.NotificationTypeEnum = NotificationTypeEnum
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "napi build --features napi --platform --release ./built/",
|
||||
"build:debug": "napi build --features napi --platform ./built/",
|
||||
"build": "napi build --features napi --no-const-enum --platform --release ./built/",
|
||||
"build:debug": "napi build --features napi --no-const-enum --platform ./built/",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"test": "pnpm run cargo:test && pnpm run build:debug && ava",
|
||||
"universal": "napi universal",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
||||
|
||||
use super::sea_orm_active_enums::DriveFileUsageHintEnum;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
|
@ -52,6 +53,8 @@ pub struct Model {
|
|||
pub request_headers: Option<Json>,
|
||||
#[sea_orm(column_name = "requestIp")]
|
||||
pub request_ip: Option<String>,
|
||||
#[sea_orm(column_name = "usageHint")]
|
||||
pub usage_hint: Option<DriveFileUsageHintEnum>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -173,6 +173,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)]
|
||||
|
|
|
@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
|
|||
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")]
|
||||
pub enum AntennaSrcEnum {
|
||||
#[sea_orm(string_value = "all")]
|
||||
|
@ -22,7 +22,21 @@ pub enum AntennaSrcEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
enum_name = "drive_file_usage_hint_enum"
|
||||
)]
|
||||
pub enum DriveFileUsageHintEnum {
|
||||
#[sea_orm(string_value = "userAvatar")]
|
||||
UserAvatar,
|
||||
#[sea_orm(string_value = "userBanner")]
|
||||
UserBanner,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -40,7 +54,7 @@ pub enum MutedNoteReasonEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -60,7 +74,7 @@ pub enum NoteVisibilityEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -94,7 +108,7 @@ pub enum NotificationTypeEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -110,7 +124,7 @@ pub enum PageVisibilityEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -128,7 +142,7 @@ pub enum PollNotevisibilityEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")]
|
||||
pub enum RelayStatusEnum {
|
||||
#[sea_orm(string_value = "accepted")]
|
||||
|
@ -140,7 +154,7 @@ pub enum RelayStatusEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -158,7 +172,7 @@ pub enum UserEmojimodpermEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
@ -174,7 +188,7 @@ pub enum UserProfileFfvisibilityEnum {
|
|||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi)]
|
||||
#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddDriveFileUsage1713451569342 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE drive_file_usage_hint_enum AS ENUM ('userAvatar', 'userBanner')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "drive_file" ADD "usageHint" drive_file_usage_hint_enum DEFAULT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "usageHint"`);
|
||||
await queryRunner.query(`DROP TYPE drive_file_usage_hint_enum`);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { redisClient } from "@/db/redis.js";
|
||||
import { encode, decode } from "msgpackr";
|
||||
import { ChainableCommander } from "ioredis";
|
||||
import type { ChainableCommander } from "ioredis";
|
||||
|
||||
export class Cache<T> {
|
||||
private ttl: number;
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { getAgentByHostname, StatusError } from "./fetch.js";
|
||||
import config from "@/config/index.js";
|
||||
import chalk from "chalk";
|
||||
import Logger from "@/services/logger.js";
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js";
|
|||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||
import { NoteFile } from "./note-file.js";
|
||||
|
||||
export type DriveFileUsageHint = "userAvatar" | "userBanner" | null;
|
||||
|
||||
@Entity()
|
||||
@Index(["userId", "folderId", "id"])
|
||||
export class DriveFile {
|
||||
|
@ -177,6 +179,14 @@ export class DriveFile {
|
|||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
// Hint for what this file is used for
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: ["userAvatar", "userBanner"],
|
||||
nullable: true,
|
||||
})
|
||||
public usageHint: DriveFileUsageHint;
|
||||
|
||||
/**
|
||||
* 外部の(信頼されていない)URLへの直リンクか否か
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
|||
md5: file.md5,
|
||||
size: file.size,
|
||||
isSensitive: file.isSensitive,
|
||||
usageHint: file.usageHint,
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
|
@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
|||
md5: file.md5,
|
||||
size: file.size,
|
||||
isSensitive: file.isSensitive,
|
||||
usageHint: file.usageHint,
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
|
|
|
@ -44,6 +44,12 @@ export const packedDriveFileSchema = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
usageHint: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
nullable: true,
|
||||
enum: ["userAvatar", "userBanner"],
|
||||
},
|
||||
blurhash: {
|
||||
type: "string",
|
||||
optional: false,
|
||||
|
|
|
@ -34,7 +34,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
|
|||
function apBackoff(attemptsMade: number, err: Error) {
|
||||
const baseDelay = 60 * 1000; // 1min
|
||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||
let backoff = (2 ** attemptsMade - 1) * baseDelay;
|
||||
backoff = Math.min(backoff, maxBackoff);
|
||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||
return backoff;
|
||||
|
|
|
@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
|||
import Resolver from "../resolver.js";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import { apLogger } from "../logger.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type {
|
||||
DriveFile,
|
||||
DriveFileUsageHint,
|
||||
} from "@/models/entities/drive-file.js";
|
||||
import { DriveFiles } from "@/models/index.js";
|
||||
import { truncate } from "@/misc/truncate.js";
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||
|
@ -16,6 +19,7 @@ const logger = apLogger;
|
|||
export async function createImage(
|
||||
actor: CacheableRemoteUser,
|
||||
value: any,
|
||||
usage: DriveFileUsageHint,
|
||||
): Promise<DriveFile> {
|
||||
// Skip if author is frozen.
|
||||
if (actor.isSuspended) {
|
||||
|
@ -43,6 +47,7 @@ export async function createImage(
|
|||
sensitive: image.sensitive,
|
||||
isLink: !instance.cacheRemoteFiles,
|
||||
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||
usageHint: usage,
|
||||
});
|
||||
|
||||
if (file.isLink) {
|
||||
|
@ -73,9 +78,10 @@ export async function createImage(
|
|||
export async function resolveImage(
|
||||
actor: CacheableRemoteUser,
|
||||
value: any,
|
||||
usage: DriveFileUsageHint,
|
||||
): Promise<DriveFile> {
|
||||
// TODO
|
||||
|
||||
// Fetch from remote server and register
|
||||
return await createImage(actor, value);
|
||||
return await createImage(actor, value, usage);
|
||||
}
|
||||
|
|
|
@ -213,7 +213,8 @@ export async function createNote(
|
|||
? (
|
||||
await Promise.all(
|
||||
note.attachment.map(
|
||||
(x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>,
|
||||
(x) =>
|
||||
limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
|
||||
),
|
||||
)
|
||||
).filter((image) => image != null)
|
||||
|
@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
|||
fileList.map(
|
||||
(x) =>
|
||||
limit(async () => {
|
||||
const file = await resolveImage(actor, x);
|
||||
const file = await resolveImage(actor, x, null);
|
||||
const update: Partial<DriveFile> = {};
|
||||
|
||||
const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
Followings,
|
||||
UserProfiles,
|
||||
UserPublickeys,
|
||||
DriveFiles,
|
||||
} from "@/models/index.js";
|
||||
import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
|
||||
import { User } from "@/models/entities/user.js";
|
||||
|
@ -362,10 +363,14 @@ export async function createPerson(
|
|||
|
||||
//#region Fetch avatar and header image
|
||||
const [avatar, banner] = await Promise.all(
|
||||
[person.icon, person.image].map((img) =>
|
||||
[person.icon, person.image].map((img, index) =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(user!, img).catch(() => null),
|
||||
: resolveImage(
|
||||
user,
|
||||
img,
|
||||
index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
|
||||
).catch(() => null),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -438,10 +443,14 @@ export async function updatePerson(
|
|||
|
||||
// Fetch avatar and header image
|
||||
const [avatar, banner] = await Promise.all(
|
||||
[person.icon, person.image].map((img) =>
|
||||
[person.icon, person.image].map((img, index) =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: resolveImage(user, img).catch(() => null),
|
||||
: resolveImage(
|
||||
user,
|
||||
img,
|
||||
index === 0 ? "userAvatar" : index === 1 ? "userBanner" : null,
|
||||
).catch(() => null),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -561,10 +570,14 @@ export async function updatePerson(
|
|||
} as Partial<User>;
|
||||
|
||||
if (avatar) {
|
||||
if (user?.avatarId)
|
||||
await DriveFiles.update(user.avatarId, { usageHint: null });
|
||||
updates.avatarId = avatar.id;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
if (user?.bannerId)
|
||||
await DriveFiles.update(user.bannerId, { usageHint: null });
|
||||
updates.bannerId = banner.id;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import Followers from "./activitypub/followers.js";
|
|||
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
||||
import { serverLogger } from "./index.js";
|
||||
import config from "@/config/index.js";
|
||||
import Koa from "koa";
|
||||
import type Koa from "koa";
|
||||
import * as crypto from "node:crypto";
|
||||
import { inspect } from "node:util";
|
||||
import type { IActivity } from "@/remote/activitypub/type.js";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
|
|||
import { verifyLink } from "@/services/fetch-rel-me.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file";
|
||||
|
||||
export const meta = {
|
||||
tags: ["account"],
|
||||
|
@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
if (ps.emailNotificationTypes !== undefined)
|
||||
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||
|
||||
let avatar: DriveFile | null = null;
|
||||
if (ps.avatarId) {
|
||||
const avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
|
||||
avatar = await DriveFiles.findOneBy({ id: ps.avatarId });
|
||||
|
||||
if (avatar == null || avatar.userId !== user.id)
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
|
@ -250,8 +252,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
throw new ApiError(meta.errors.avatarNotAnImage);
|
||||
}
|
||||
|
||||
let banner: DriveFile | null = null;
|
||||
if (ps.bannerId) {
|
||||
const banner = await DriveFiles.findOneBy({ id: ps.bannerId });
|
||||
banner = await DriveFiles.findOneBy({ id: ps.bannerId });
|
||||
|
||||
if (banner == null || banner.userId !== user.id)
|
||||
throw new ApiError(meta.errors.noSuchBanner);
|
||||
|
@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
updateUsertags(user, tags);
|
||||
//#endregion
|
||||
|
||||
// Update old/new avatar usage hints
|
||||
if (avatar) {
|
||||
if (user.avatarId)
|
||||
await DriveFiles.update(user.avatarId, { usageHint: null });
|
||||
await DriveFiles.update(avatar.id, { usageHint: "userAvatar" });
|
||||
}
|
||||
|
||||
// Update old/new banner usage hints
|
||||
if (banner) {
|
||||
if (user.bannerId)
|
||||
await DriveFiles.update(user.bannerId, { usageHint: null });
|
||||
await DriveFiles.update(banner.id, { usageHint: "userBanner" });
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
|
||||
if (Object.keys(profileUpdates).length > 0)
|
||||
await UserProfiles.update(user.id, profileUpdates);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type * as http from "node:http";
|
||||
import { EventEmitter } from "events";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ParsedUrlQuery } from "node:querystring";
|
||||
import * as websocket from "websocket";
|
||||
|
||||
import { subscriber as redisClient } from "@/db/redis.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Readable, ReadableOptions } from "node:stream";
|
||||
import { Readable, type ReadableOptions } from "node:stream";
|
||||
import { Buffer } from "node:buffer";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
|
|
|
@ -2,7 +2,16 @@ import { Feed } from "feed";
|
|||
import { In, IsNull } from "typeorm";
|
||||
import config from "@/config/index.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"
|
||||
|
|
|
@ -54,6 +54,10 @@ app.use(async (ctx, next) => {
|
|||
const url = decodeURI(ctx.path);
|
||||
|
||||
if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
|
||||
if (!url.startsWith(`${bullBoardPath}/static/`)) {
|
||||
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
|
||||
}
|
||||
|
||||
const token = ctx.cookies.get("token");
|
||||
if (token == null) {
|
||||
ctx.status = 401;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { KVs } from "../core.js";
|
||||
import Chart from "../core.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import { name, schema } from "./entities/active-users.js";
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
UserProfiles,
|
||||
} from "@/models/index.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type { DriveFileUsageHint } from "@/models/entities/drive-file.js";
|
||||
import type { IRemoteUser, User } from "@/models/entities/user.js";
|
||||
import { genId } from "backend-rs";
|
||||
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
|
||||
|
@ -65,6 +66,7 @@ function urlPathJoin(
|
|||
* @param type Content-Type for original
|
||||
* @param hash Hash for original
|
||||
* @param size Size for original
|
||||
* @param usage Optional usage hint for file (f.e. "userAvatar")
|
||||
*/
|
||||
async function save(
|
||||
file: DriveFile,
|
||||
|
@ -73,6 +75,7 @@ async function save(
|
|||
type: string,
|
||||
hash: string,
|
||||
size: number,
|
||||
usage: DriveFileUsageHint = null,
|
||||
): Promise<DriveFile> {
|
||||
// thunbnail, webpublic を必要なら生成
|
||||
const alts = await generateAlts(path, type, !file.uri);
|
||||
|
@ -161,6 +164,7 @@ async function save(
|
|||
file.md5 = hash;
|
||||
file.size = size;
|
||||
file.storedInternal = false;
|
||||
file.usageHint = usage ?? null;
|
||||
|
||||
return await DriveFiles.insert(file).then((x) =>
|
||||
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
||||
|
@ -204,6 +208,7 @@ async function save(
|
|||
file.type = type;
|
||||
file.md5 = hash;
|
||||
file.size = size;
|
||||
file.usageHint = usage ?? null;
|
||||
|
||||
return await DriveFiles.insert(file).then((x) =>
|
||||
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
||||
|
@ -450,6 +455,9 @@ type AddFileArgs = {
|
|||
|
||||
requestIp?: string | null;
|
||||
requestHeaders?: Record<string, string> | null;
|
||||
|
||||
/** Whether this file has a known use case, like user avatar or instance icon */
|
||||
usageHint?: DriveFileUsageHint;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -469,6 +477,7 @@ export async function addFile({
|
|||
sensitive = null,
|
||||
requestIp = null,
|
||||
requestHeaders = null,
|
||||
usageHint = null,
|
||||
}: AddFileArgs): Promise<DriveFile> {
|
||||
const info = await getFileInfo(path);
|
||||
logger.info(`${JSON.stringify(info)}`);
|
||||
|
@ -581,6 +590,7 @@ export async function addFile({
|
|||
file.isLink = isLink;
|
||||
file.requestIp = requestIp;
|
||||
file.requestHeaders = requestHeaders;
|
||||
file.usageHint = usageHint;
|
||||
file.isSensitive = user
|
||||
? Users.isLocalUser(user) &&
|
||||
(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
|
||||
|
@ -639,6 +649,7 @@ export async function addFile({
|
|||
info.type.mime,
|
||||
info.md5,
|
||||
info.size,
|
||||
usageHint,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js";
|
|||
import { createTemp } from "@/misc/create-temp.js";
|
||||
import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
|
||||
import type { DriveFolder } from "@/models/entities/drive-folder.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type {
|
||||
DriveFile,
|
||||
DriveFileUsageHint,
|
||||
} from "@/models/entities/drive-file.js";
|
||||
import { DriveFiles } from "@/models/index.js";
|
||||
import { driveLogger } from "./logger.js";
|
||||
import { addFile } from "./add-file.js";
|
||||
|
@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader");
|
|||
|
||||
type Args = {
|
||||
url: string;
|
||||
user: { id: User["id"]; host: User["host"] } | null;
|
||||
user: {
|
||||
id: User["id"];
|
||||
host: User["host"];
|
||||
driveCapacityOverrideMb: User["driveCapacityOverrideMb"];
|
||||
} | null;
|
||||
folderId?: DriveFolder["id"] | null;
|
||||
uri?: string | null;
|
||||
sensitive?: boolean;
|
||||
|
@ -22,6 +29,7 @@ type Args = {
|
|||
comment?: string | null;
|
||||
requestIp?: string | null;
|
||||
requestHeaders?: Record<string, string> | null;
|
||||
usageHint?: DriveFileUsageHint;
|
||||
};
|
||||
|
||||
export async function uploadFromUrl({
|
||||
|
@ -35,6 +43,7 @@ export async function uploadFromUrl({
|
|||
comment = null,
|
||||
requestIp = null,
|
||||
requestHeaders = null,
|
||||
usageHint = null,
|
||||
}: Args): Promise<DriveFile> {
|
||||
const parsedUrl = new URL(url);
|
||||
if (
|
||||
|
@ -75,9 +84,10 @@ export async function uploadFromUrl({
|
|||
sensitive,
|
||||
requestIp,
|
||||
requestHeaders,
|
||||
usageHint,
|
||||
});
|
||||
logger.succ(`Got: ${driveFile.id}`);
|
||||
return driveFile!;
|
||||
return driveFile;
|
||||
} catch (e) {
|
||||
logger.error(`Failed to create drive file:\n${inspect(e)}`);
|
||||
throw e;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Window } from "happy-dom";
|
||||
import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
async function getRelMeLinks(url: string): Promise<string[]> {
|
||||
|
|
|
@ -28,9 +28,9 @@ export default class Logger {
|
|||
|
||||
if (config.syslog) {
|
||||
this.syslogClient = new SyslogPro.RFC5424({
|
||||
applacationName: "Firefish",
|
||||
applicationName: "Firefish",
|
||||
timestamp: true,
|
||||
encludeStructuredData: true,
|
||||
includeStructuredData: true,
|
||||
color: true,
|
||||
extendedColor: true,
|
||||
server: {
|
||||
|
@ -144,12 +144,12 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
// Used when the process can't continue (fatal error)
|
||||
public error(
|
||||
x: string | Error,
|
||||
data?: Record<string, any> | null,
|
||||
important = false,
|
||||
): void {
|
||||
// 実行を継続できない状況で使う
|
||||
if (x instanceof Error) {
|
||||
data = data || {};
|
||||
data.e = x;
|
||||
|
@ -166,30 +166,30 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
// Used when the process can continue but some action should be taken
|
||||
public warn(
|
||||
message: string,
|
||||
data?: Record<string, any> | null,
|
||||
important = false,
|
||||
): void {
|
||||
// 実行を継続できるが改善すべき状況で使う
|
||||
this.log("warning", message, data, important);
|
||||
}
|
||||
|
||||
// Used when something is successful
|
||||
public succ(
|
||||
message: string,
|
||||
data?: Record<string, any> | null,
|
||||
important = false,
|
||||
): void {
|
||||
// 何かに成功した状況で使う
|
||||
this.log("success", message, data, important);
|
||||
}
|
||||
|
||||
// Used for debugging (information necessary for developers but unnecessary for users)
|
||||
public debug(
|
||||
message: string,
|
||||
data?: Record<string, any> | null,
|
||||
important = false,
|
||||
): void {
|
||||
// Used for debugging (information necessary for developers but unnecessary for users)
|
||||
// Fixed if statement is ignored when logLevel includes debug
|
||||
if (
|
||||
config.logLevel?.includes("debug") ||
|
||||
|
@ -200,12 +200,12 @@ export default class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
// Other generic logs
|
||||
public info(
|
||||
message: string,
|
||||
data?: Record<string, any> | null,
|
||||
important = false,
|
||||
): void {
|
||||
// それ以外
|
||||
this.log("info", message, data, important);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { publishMainStream } from "@/services/stream.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import {
|
||||
NoteUnreads,
|
||||
Users,
|
||||
Followings,
|
||||
ChannelFollowings,
|
||||
} from "@/models/index.js";
|
||||
import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js";
|
||||
import { Not, IsNull, In } from "typeorm";
|
||||
import type { Channel } from "@/models/entities/channel.js";
|
||||
import { readNotificationByQuery } from "@/server/api/common/read-notification.js";
|
||||
|
@ -120,34 +115,4 @@ export default async function (
|
|||
]),
|
||||
});
|
||||
}
|
||||
|
||||
// if (readAntennaNotes.length > 0) {
|
||||
// await AntennaNotes.update(
|
||||
// {
|
||||
// antennaId: In(myAntennas.map((a) => a.id)),
|
||||
// noteId: In(readAntennaNotes.map((n) => n.id)),
|
||||
// },
|
||||
// {
|
||||
// read: true,
|
||||
// },
|
||||
// );
|
||||
|
||||
// // TODO: まとめてクエリしたい
|
||||
// for (const antenna of myAntennas) {
|
||||
// const count = await AntennaNotes.countBy({
|
||||
// antennaId: antenna.id,
|
||||
// read: false,
|
||||
// });
|
||||
|
||||
// if (count === 0) {
|
||||
// publishMainStream(userId, "readAntenna", antenna);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Users.getHasUnreadAntenna(userId).then((unread) => {
|
||||
// if (!unread) {
|
||||
// publishMainStream(userId, "readAllAntennas");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -28,11 +28,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import MkTooltip from "./MkTooltip.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
showing: Ref<boolean>;
|
||||
showing: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
title?: string;
|
||||
|
|
|
@ -231,15 +231,9 @@ const unicodeEmojiSkinToneLabels = [
|
|||
i18n.ts._skinTones?.dark ?? "Dark",
|
||||
];
|
||||
|
||||
const size = computed(() =>
|
||||
props.asReactionPicker ? reactionPickerSize.value : 1,
|
||||
);
|
||||
const width = computed(() =>
|
||||
props.asReactionPicker ? reactionPickerWidth.value : 3,
|
||||
);
|
||||
const height = computed(() =>
|
||||
props.asReactionPicker ? reactionPickerHeight.value : 2,
|
||||
);
|
||||
const size = reactionPickerSize;
|
||||
const width = reactionPickerWidth;
|
||||
const height = reactionPickerHeight;
|
||||
const customEmojiCategories = emojiCategories;
|
||||
const customEmojis = instance.emojis;
|
||||
const q = ref<string | null>(null);
|
||||
|
|
|
@ -39,7 +39,7 @@ import { defaultStore } from "@/store";
|
|||
withDefaults(
|
||||
defineProps<{
|
||||
manualShowing?: boolean | null;
|
||||
src?: HTMLElement;
|
||||
src?: HTMLElement | null;
|
||||
showPinned?: boolean;
|
||||
asReactionPicker?: boolean;
|
||||
}>(),
|
||||
|
|
|
@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
|
|||
os.popup(
|
||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||
{
|
||||
showing: showing.value,
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
},
|
||||
|
|
|
@ -1188,7 +1188,7 @@ async function insertEmoji(ev: MouseEvent) {
|
|||
os.openEmojiPicker(
|
||||
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||
{},
|
||||
textareaEl.value,
|
||||
textareaEl.value!,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,13 +19,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
import MkTooltip from "./MkTooltip.vue";
|
||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
showing: Ref<boolean>;
|
||||
showing: boolean;
|
||||
reaction: string;
|
||||
emojis: entities.EmojiLite[];
|
||||
targetElement: HTMLElement;
|
||||
|
|
|
@ -30,13 +30,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
import MkTooltip from "./MkTooltip.vue";
|
||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||
|
||||
defineProps<{
|
||||
showing: Ref<boolean>;
|
||||
showing: boolean;
|
||||
reaction: string;
|
||||
users: entities.User[]; // TODO
|
||||
count: number;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@after-leave="emit('closed')"
|
||||
>
|
||||
<div
|
||||
v-show="unref(showing)"
|
||||
v-show="showing"
|
||||
ref="el"
|
||||
class="buebdbiu _acrylic _shadow"
|
||||
:style="{ zIndex, maxWidth: maxWidth + 'px' }"
|
||||
|
@ -19,21 +19,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
type MaybeRef,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
unref,
|
||||
} from "vue";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import * as os from "@/os";
|
||||
import { calcPopupPosition } from "@/scripts/popup-position";
|
||||
import { defaultStore } from "@/store";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showing: MaybeRef<boolean>;
|
||||
showing: boolean;
|
||||
targetElement?: HTMLElement | null;
|
||||
x?: number;
|
||||
y?: number;
|
||||
|
|
|
@ -19,12 +19,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
import MkTooltip from "./MkTooltip.vue";
|
||||
|
||||
defineProps<{
|
||||
showing: Ref<boolean>;
|
||||
showing: boolean;
|
||||
users: entities.User[];
|
||||
count: number;
|
||||
targetElement?: HTMLElement;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
]"
|
||||
>
|
||||
<i
|
||||
v-if="unref(success)"
|
||||
v-if="success"
|
||||
:class="[$style.icon, $style.success, iconify('ph-check')]"
|
||||
></i>
|
||||
<MkLoading
|
||||
|
@ -29,16 +29,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MaybeRef } from "vue";
|
||||
import { shallowRef, unref, watch } from "vue";
|
||||
import { shallowRef, watch } from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import iconify from "@/scripts/icon";
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const props = defineProps<{
|
||||
success: MaybeRef<boolean>;
|
||||
showing: MaybeRef<boolean>;
|
||||
success: boolean;
|
||||
showing: boolean;
|
||||
text?: string;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { EventEmitter } from "eventemitter3";
|
||||
import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
|
||||
import insertTextAtCursor from "insert-text-at-cursor";
|
||||
import type { Component, Ref } from "vue";
|
||||
import type { Component, MaybeRef, Ref } from "vue";
|
||||
import { defineAsyncComponent, markRaw, ref } from "vue";
|
||||
import { i18n } from "./i18n";
|
||||
import MkDialog from "@/components/MkDialog.vue";
|
||||
|
@ -213,9 +213,13 @@ interface VueComponentConstructor<P, E> {
|
|||
|
||||
type NonArrayAble<A> = A extends Array<unknown> ? never : A;
|
||||
|
||||
type CanUseRef<T> = {
|
||||
[K in keyof T]: MaybeRef<T[K]>;
|
||||
};
|
||||
|
||||
export async function popup<Props, Emits>(
|
||||
component: VueComponentConstructor<Props, Emits>,
|
||||
props: Props,
|
||||
props: CanUseRef<Props>,
|
||||
events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
|
||||
disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
|
||||
) {
|
||||
|
@ -240,6 +244,7 @@ export async function popup<Props, Emits>(
|
|||
id,
|
||||
};
|
||||
|
||||
// Hint: Vue will automatically resolve ref here, so it is safe to use ref in props
|
||||
popups.value.push(state);
|
||||
|
||||
return {
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -24,14 +24,14 @@ class ReactionPicker {
|
|||
},
|
||||
{
|
||||
done: (reaction) => {
|
||||
this.onChosen!(reaction);
|
||||
this.onChosen?.(reaction);
|
||||
},
|
||||
close: () => {
|
||||
this.manualShowing.value = false;
|
||||
},
|
||||
closed: () => {
|
||||
this.src.value = null;
|
||||
this.onClosed!();
|
||||
this.onClosed?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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 New Issue