Compare commits

..

47 Commits

Author SHA1 Message Date
laozhoubuluo 85c1bce800 Merge branch 'docs/add_dependencies' into 'develop'
docs: add minimum dependencies

Co-authored-by: naskya <m@naskya.net>

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

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

See merge request firefish/firefish!10739
2024-04-21 21:27:55 +00:00
naskya b3cc01c440 Merge branch 'feat/show-MkRemoteCaution-in-history' into 'develop'
feat: show MkRemoteCaution in note history page

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

See merge request firefish/firefish!10743
2024-04-21 21:23:02 +00:00
naskya ebaefb9697
chore (minor, client): remove redundant attribute 2024-04-22 06:02:08 +09:00
naskya d9982a0b6a
Merge branch 'develop' into feat/show-MkRemoteCaution-in-history 2024-04-22 06:01:19 +09:00
naskya 0cb2e94d99 Merge branch 'fix/feed' into 'develop'
fix: notes in rss feed do not display HTML

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

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

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

Closes #10894

See merge request firefish/firefish!10740
2024-04-21 20:58:09 +00:00
naskya c9de5f6095
docs: update api-changes.md 2024-04-22 05:56:46 +09:00
naskya c4658801aa
chore: regenerate entities 2024-04-22 05:54:32 +09:00
naskya a107d8c1ec
fix (backend): update import 2024-04-22 05:52:56 +09:00
naskya 4c91e8e37f
Merge branch 'develop' into feat/antenna_limit 2024-04-22 05:51:22 +09:00
naskya ce672f4edd
dev: add cargo test to pnpm scripts
mocha test has been unmaintained for a long time and is very broken :(
2024-04-21 22:36:05 +09:00
naskya 131b3686d4 Merge branch 'feat/drive-file-usage-hints' into 'develop'
feat: Add usageHint field to DriveFile

Co-authored-by: yumeko <yumeko@mainichi.social>

See merge request firefish/firefish!10750
2024-04-21 12:58:37 +00:00
naskya 6b008c651a
chore (backend): remove (technically) incorrect TypeORM decorator field 2024-04-21 11:09:18 +09:00
naskya d2dbfb37c7
chore (backend): reflect entity changes to the schema and repository 2024-04-21 10:59:02 +09:00
naskya 96481f1353
chore: update downgrade.sql 2024-04-21 10:48:31 +09:00
naskya c936102a4c
chore (backend-rs): regenerate entities and index.js/d.ts 2024-04-21 10:45:47 +09:00
naskya 43570a54aa
chore: format 2024-04-21 10:44:54 +09:00
naskya 4d34e14dd8
Merge branch 'develop' into feat/drive-file-usage-hints 2024-04-21 10:42:25 +09:00
naskya 28f7ac1acd
fix (backend): typo 2024-04-21 10:31:00 +09:00
naskya 9f3396af21
chore (backend): translate Japanese comments into English 2024-04-21 10:30:13 +09:00
naskya dac4043dd9
v20240421 2024-04-21 10:09:45 +09:00
naskya d1e898c0d0
docs: update changelog 2024-04-21 09:32:05 +09:00
mei23 dc02a07774
fix (backend): add Cache-Control to Bull Dashboard 2024-04-21 09:29:00 +09:00
naskya 2760e7feee
chore (minor): use ** in lieu of Math.pow 2024-04-21 06:40:53 +09:00
naskya 488323cc8e
chore: format 2024-04-21 05:57:05 +09:00
naskya a2699e6687
chore (backend): fix imports 2024-04-20 23:04:12 +09:00
naskya dd3ad89b64 Merge branch 'refactor/types' into 'develop'
revert unnecessary MaybeRef in components

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

See merge request firefish/firefish!10751
2024-04-20 01:07:25 +00:00
naskya 4fb2cab617 Merge branch 'fix/emoji-picker' into 'develop'
fix: use settings from reactionPicker for non-reaction emoji picker

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

Closes #10905

See merge request firefish/firefish!10752
2024-04-20 01:06:53 +00:00
naskya 5c4a773ecf
chore (backend): qualify Node.js builtin modules 2024-04-20 03:09:18 +09:00
Lhcfl 207855b0e8 fix: use settings from reactionPicker for non-reaction emoji picker 2024-04-20 01:05:30 +08:00
Lhcfl 781c98dda7 revert unnecessary `.value` for MkLink 2024-04-20 00:18:36 +08:00
Lhcfl ab221c98a7 revert unnecessary MaybeRef in components 2024-04-20 00:05:37 +08:00
yumeko 6c46bb56fd
Switch DriveFile's usageHint field to an enum type 2024-04-19 18:24:48 +03:00
naskya 1be5373dfc
chore (backend-rs): make exported enum compatible w/ TypeScript's string enum 2024-04-19 21:59:35 +09:00
yumeko 968657d26e
Run format 2024-04-19 07:54:11 +03:00
yumeko 913de651db
When updating (remote) user avatar/banner, clear usageHint for the previous drivefile, if any 2024-04-19 07:25:42 +03:00
yumeko 4aeb0d95cc
Add DriveFile usageHint field to rust model as well 2024-04-19 07:03:09 +03:00
yumeko c0f93de94b
Set file usage hints on local avatar/banner uploads as well + export "valid" values as type 2024-04-19 06:29:28 +03:00
yumeko 4823abd3a9
Add usageHint field to DriveFile, and fill accordingly when operating on Persons 2024-04-19 03:41:36 +03:00
Lhcfl 241c824ab5 fix: use better `]]>` replacer 2024-04-14 16:44:12 +08:00
Lhcfl 54d9916fec fix: rss feed no HTML 2024-04-14 16:34:33 +08:00
Lhcfl f0a50bc288 feat: show MkRemoteCaution in note history page 2024-04-14 13:59:27 +08:00
老周部落 5eff4da27b
feat: antenna limit 2024-04-13 10:02:32 +08:00
老周部落 f44a2937d4
fix: download-url should use proxy bypass hosts 2024-04-13 09:00:08 +08:00
59 changed files with 458 additions and 270 deletions

View File

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

View File

@ -2,6 +2,10 @@
Breaking changes are indicated by the :warning: icon.
## Unreleased
- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
## v20240413
- :warning: Removed `patrons` endpoint.

View File

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

View File

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

View File

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

View File

@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
invite: "Invite"
driveCapacityPerLocalAccount: "Drive capacity per local user"
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
antennaLimit: "The maximum number of antennas that each user can create"
inMb: "In megabytes"
iconUrl: "Icon URL"
bannerUrl: "Banner image URL"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class antennaLimit1712937600000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "meta" ADD "antennaLimit" integer NOT NULL DEFAULT 5`,
undefined,
);
await queryRunner.query(
`COMMENT ON COLUMN "meta"."antennaLimit" IS 'Antenna Limit'`,
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "meta" DROP COLUMN "antennaLimit"`,
undefined,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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への直リンクか否か
*/

View File

@ -276,6 +276,12 @@ export class Meta {
})
public remoteDriveCapacityMb: number;
@Column("integer", {
default: 5,
comment: "Antenna Limit",
})
public antennaLimit: number;
@Column("varchar", {
length: 128,
nullable: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,11 @@ export const meta = {
optional: false,
nullable: false,
},
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: {
type: "boolean",
optional: false,
@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => {
enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,

View File

@ -94,6 +94,7 @@ export const paramDef = {
defaultDarkTheme: { type: "string", nullable: true },
localDriveCapacityMb: { type: "integer" },
remoteDriveCapacityMb: { type: "integer" },
antennaLimit: { type: "integer" },
cacheRemoteFiles: { type: "boolean" },
markLocalFilesNsfwByDefault: { type: "boolean" },
emailRequiredForSignup: { type: "boolean" },
@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
}
if (ps.antennaLimit !== undefined) {
set.antennaLimit = ps.antennaLimit;
}
if (ps.cacheRemoteFiles !== undefined) {
set.cacheRemoteFiles = ps.cacheRemoteFiles;
}

View File

@ -1,5 +1,5 @@
import define from "@/server/api/define.js";
import { genId } from "backend-rs";
import { fetchMeta, genId } from "backend-rs";
import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
import { ApiError } from "@/server/api/error.js";
import { publishInternalEvent } from "@/services/stream.js";
@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => {
let userList;
let userGroupJoining;
const instance = await fetchMeta(true);
const antennas = await Antennas.findBy({
userId: user.id,
});
if (antennas.length > 5 && !user.isAdmin) {
if (antennas.length >= instance.antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}

View File

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

View File

@ -126,6 +126,11 @@ export const meta = {
optional: false,
nullable: false,
},
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: {
type: "boolean",
optional: false,
@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => {
enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[]> {

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ import { defaultStore } from "@/store";
withDefaults(
defineProps<{
manualShowing?: boolean | null;
src?: HTMLElement;
src?: HTMLElement | null;
showPinned?: boolean;
asReactionPicker?: boolean;
}>(),

View File

@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
os.popup(
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
{
showing: showing.value,
showing,
url: props.url,
source: el.value,
},

View File

@ -1188,7 +1188,7 @@ async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(
(ev.currentTarget ?? ev.target) as HTMLElement,
{},
textareaEl.value,
textareaEl.value!,
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -350,6 +350,19 @@
</FormSplit>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.antennas }}</template>
<FormInput
v-model="antennaLimit"
type="number"
class="_formBlock"
>
<template #label>{{
i18n.ts.antennaLimit
}}</template>
</FormInput>
</FormSection>
<FormSection>
<template #label>ServiceWorker</template>
@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false);
const markLocalFilesNsfwByDefault = ref(false);
const localDriveCapacityMb = ref(0);
const remoteDriveCapacityMb = ref(0);
const antennaLimit = ref(0);
const enableRegistration = ref(false);
const emailRequiredForSignup = ref(false);
const enableServiceWorker = ref(false);
@ -579,6 +593,7 @@ async function init() {
markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
antennaLimit.value = meta.antennaLimit;
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
enableServiceWorker.value = meta.enableServiceWorker;
@ -631,6 +646,7 @@ function save() {
markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
localDriveCapacityMb: localDriveCapacityMb.value,
remoteDriveCapacityMb: remoteDriveCapacityMb.value,
antennaLimit: antennaLimit.value,
disableRegistration: !enableRegistration.value,
emailRequiredForSignup: emailRequiredForSignup.value,
enableServiceWorker: enableServiceWorker.value,

View File

@ -4,30 +4,35 @@
><MkPageHeader :display-back-button="true"
/></template>
<MkSpacer :content-max="800">
<MkLoading v-if="!loaded" />
<MkPagination
v-else
ref="pagingComponent"
v-slot="{ items }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
>
<XNote
:key="item.id"
class="qtqtichx"
:note="item"
:hide-footer="true"
:detailed-view="true"
/>
</XList>
</div>
</MkPagination>
<MkLoading v-if="note == null" />
<div v-else>
<MkRemoteCaution
v-if="note.user.host != null"
:href="note.url ?? note.uri!"
/>
<MkPagination
ref="pagingComponent"
v-slot="{ items }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
>
<XNote
:key="item.id"
class="qtqtichx"
:note="item"
:hide-footer="true"
:detailed-view="true"
/>
</XList>
</div>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import icon from "@/scripts/icon";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
const pagingComponent = ref<MkPaginationType<
typeof pagination.endpoint
@ -69,8 +75,7 @@ definePageMetadata(
})),
);
const note = ref<entities.Note>({} as entities.Note);
const loaded = ref(false);
const note = ref<entities.Note | null>(null);
onMounted(() => {
api("notes/show", {
@ -83,20 +88,19 @@ onMounted(() => {
res.replyId = null;
note.value = res;
loaded.value = true;
});
});
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
const now: entities.NoteEdit = {
id: "EditionNow",
noteId: note.value.id,
updatedAt: note.value.createdAt,
text: note.value.text,
cw: note.value.cw,
files: note.value.files,
fileIds: note.value.fileIds,
emojis: note.value.emojis,
noteId: note.value!.id,
updatedAt: note.value!.createdAt,
text: note.value!.text,
cw: note.value!.cw,
files: note.value!.files,
fileIds: note.value!.fileIds,
emojis: note.value!.emojis,
};
return [now]
@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
_shouldInsertAd_: false,
files: noteEdit.files,
fileIds: noteEdit.fileIds,
emojis: note.value.emojis.concat(noteEdit.emojis),
emojis: note.value!.emojis.concat(noteEdit.emojis),
});
});
}

View File

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

View File

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