Compare commits
48 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 | |
老周部落 | de4da3c1fd |
|
@ -42,6 +42,8 @@ cargo --version
|
||||||
|
|
||||||
### PostgreSQL and PGroonga
|
### PostgreSQL and PGroonga
|
||||||
|
|
||||||
|
Firefish requires PostgreSQL v12 or later. We recommend that you install v12.x for the same reason as Node.js.
|
||||||
|
|
||||||
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
|
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
Breaking changes are indicated by the :warning: icon.
|
Breaking changes are indicated by the :warning: icon.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
|
||||||
|
|
||||||
## v20240413
|
## v20240413
|
||||||
|
|
||||||
- :warning: Removed `patrons` endpoint.
|
- :warning: Removed `patrons` endpoint.
|
||||||
|
|
|
@ -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.
|
- 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.
|
- 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)
|
## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
|
||||||
|
|
||||||
- Add "Media" tab to user page
|
- Add "Media" tab to user page
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
DELETE FROM "migrations" WHERE name IN (
|
DELETE FROM "migrations" WHERE name IN (
|
||||||
|
'AddDriveFileUsage1713451569342',
|
||||||
'ConvertCwVarcharToText1713225866247',
|
'ConvertCwVarcharToText1713225866247',
|
||||||
'FixChatFileConstraint1712855579316',
|
'FixChatFileConstraint1712855579316',
|
||||||
'DropTimeZone1712425488543',
|
'DropTimeZone1712425488543',
|
||||||
|
@ -23,7 +24,11 @@ DELETE FROM "migrations" WHERE name IN (
|
||||||
'RemoveNativeUtilsMigration1705877093218'
|
'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";
|
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
|
||||||
ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
|
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);
|
CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);
|
||||||
|
|
|
@ -1,9 +1,36 @@
|
||||||
# Install Firefish
|
# Install Firefish
|
||||||
|
|
||||||
This document shows an example procedure for installing Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
|
Firefish depends on the following software.
|
||||||
|
|
||||||
|
## Runtime dependencies
|
||||||
|
|
||||||
|
- At least [NodeJS](https://nodejs.org/en/) v18.17.0 (v20/v21 recommended)
|
||||||
|
- At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) with [PGroonga](https://pgroonga.github.io/) extension
|
||||||
|
- At least [Redis](https://redis.io/) v7
|
||||||
|
- Web Proxy (one of the following)
|
||||||
|
- Caddy (recommended)
|
||||||
|
- Nginx (recommended)
|
||||||
|
- Apache
|
||||||
|
- [FFmpeg](https://ffmpeg.org/) for video transcoding (**optional**)
|
||||||
|
- Caching server (**optional**, one of the following)
|
||||||
|
- [DragonflyDB](https://www.dragonflydb.io/)
|
||||||
|
- [KeyDB](https://keydb.dev/)
|
||||||
|
- Another [Redis](https://redis.io/) server
|
||||||
|
|
||||||
|
## Build dependencies
|
||||||
|
|
||||||
|
- At least [Rust](https://www.rust-lang.org/) v1.74
|
||||||
|
- C/C++ compiler & build tools
|
||||||
|
- `build-essential` on Debian/Ubuntu Linux
|
||||||
|
- `base-devel` on Arch Linux
|
||||||
|
- [Python 3](https://www.python.org/)
|
||||||
|
|
||||||
|
This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
|
||||||
|
|
||||||
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
|
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
|
||||||
|
|
||||||
|
If you do not prepare your environment as document, be sure to meet the minimum dependencies given at the bottom of the page.
|
||||||
|
|
||||||
Make sure that you can use the `sudo` command before proceeding.
|
Make sure that you can use the `sudo` command before proceeding.
|
||||||
|
|
||||||
## 1. Install dependencies
|
## 1. Install dependencies
|
||||||
|
|
|
@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
|
||||||
invite: "Invite"
|
invite: "Invite"
|
||||||
driveCapacityPerLocalAccount: "Drive capacity per local user"
|
driveCapacityPerLocalAccount: "Drive capacity per local user"
|
||||||
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
|
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
|
||||||
|
antennaLimit: "The maximum number of antennas that each user can create"
|
||||||
inMb: "In megabytes"
|
inMb: "In megabytes"
|
||||||
iconUrl: "Icon URL"
|
iconUrl: "Icon URL"
|
||||||
bannerUrl: "Banner image URL"
|
bannerUrl: "Banner image URL"
|
||||||
|
|
|
@ -340,6 +340,7 @@ invite: "邀请"
|
||||||
driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
|
driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
|
||||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||||
inMb: "以兆字节 (MegaByte) 为单位"
|
inMb: "以兆字节 (MegaByte) 为单位"
|
||||||
|
antennaLimit: "每个用户最多可以创建的天线数量"
|
||||||
iconUrl: "图标 URL"
|
iconUrl: "图标 URL"
|
||||||
bannerUrl: "横幅图 URL"
|
bannerUrl: "横幅图 URL"
|
||||||
backgroundImageUrl: "背景图 URL"
|
backgroundImageUrl: "背景图 URL"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "firefish",
|
"name": "firefish",
|
||||||
"version": "20240413",
|
"version": "20240421",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://firefish.dev/firefish/firefish.git"
|
"url": "https://firefish.dev/firefish/firefish.git"
|
||||||
|
@ -26,7 +26,9 @@
|
||||||
"debug": "pnpm run build:debug && pnpm run start",
|
"debug": "pnpm run build:debug && pnpm run start",
|
||||||
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
|
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
|
||||||
"mocha": "pnpm --filter backend run mocha",
|
"mocha": "pnpm --filter backend run mocha",
|
||||||
"test": "pnpm run mocha",
|
"test": "pnpm run test:ts && pnpm run test:rs",
|
||||||
|
"test:ts": "pnpm run mocha",
|
||||||
|
"test:rs": "cargo test",
|
||||||
"format": "pnpm run format:ts; pnpm run format:rs",
|
"format": "pnpm run format:ts; pnpm run format:rs",
|
||||||
"format:ts": "pnpm -r --parallel run format",
|
"format:ts": "pnpm -r --parallel run format",
|
||||||
"format:rs": "cargo fmt --all --",
|
"format:rs": "cargo fmt --all --",
|
||||||
|
|
|
@ -17,7 +17,7 @@ regenerate-entities:
|
||||||
attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \
|
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}"; \
|
sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \
|
||||||
done
|
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
|
src/model/entity/sea_orm_active_enums.rs
|
||||||
cargo fmt --all --
|
cargo fmt --all --
|
||||||
|
|
||||||
|
|
|
@ -348,6 +348,7 @@ export interface DriveFile {
|
||||||
webpublicType: string | null
|
webpublicType: string | null
|
||||||
requestHeaders: Json | null
|
requestHeaders: Json | null
|
||||||
requestIp: string | null
|
requestIp: string | null
|
||||||
|
usageHint: DriveFileUsageHintEnum | null
|
||||||
}
|
}
|
||||||
export interface DriveFolder {
|
export interface DriveFolder {
|
||||||
id: string
|
id: string
|
||||||
|
@ -491,6 +492,7 @@ export interface Meta {
|
||||||
recaptchaSecretKey: string | null
|
recaptchaSecretKey: string | null
|
||||||
localDriveCapacityMb: number
|
localDriveCapacityMb: number
|
||||||
remoteDriveCapacityMb: number
|
remoteDriveCapacityMb: number
|
||||||
|
antennaLimit: number
|
||||||
summalyProxy: string | null
|
summalyProxy: string | null
|
||||||
enableEmail: boolean
|
enableEmail: boolean
|
||||||
email: string | null
|
email: string | null
|
||||||
|
@ -772,81 +774,85 @@ export interface ReplyMuting {
|
||||||
muteeId: string
|
muteeId: string
|
||||||
muterId: string
|
muterId: string
|
||||||
}
|
}
|
||||||
export const enum AntennaSrcEnum {
|
export enum AntennaSrcEnum {
|
||||||
All = 0,
|
All = 'all',
|
||||||
Group = 1,
|
Group = 'group',
|
||||||
Home = 2,
|
Home = 'home',
|
||||||
Instances = 3,
|
Instances = 'instances',
|
||||||
List = 4,
|
List = 'list',
|
||||||
Users = 5
|
Users = 'users'
|
||||||
}
|
}
|
||||||
export const enum MutedNoteReasonEnum {
|
export enum DriveFileUsageHintEnum {
|
||||||
Manual = 0,
|
UserAvatar = 'userAvatar',
|
||||||
Other = 1,
|
UserBanner = 'userBanner'
|
||||||
Spam = 2,
|
|
||||||
Word = 3
|
|
||||||
}
|
}
|
||||||
export const enum NoteVisibilityEnum {
|
export enum MutedNoteReasonEnum {
|
||||||
Followers = 0,
|
Manual = 'manual',
|
||||||
Hidden = 1,
|
Other = 'other',
|
||||||
Home = 2,
|
Spam = 'spam',
|
||||||
Public = 3,
|
Word = 'word'
|
||||||
Specified = 4
|
|
||||||
}
|
}
|
||||||
export const enum NotificationTypeEnum {
|
export enum NoteVisibilityEnum {
|
||||||
App = 0,
|
Followers = 'followers',
|
||||||
Follow = 1,
|
Hidden = 'hidden',
|
||||||
FollowRequestAccepted = 2,
|
Home = 'home',
|
||||||
GroupInvited = 3,
|
Public = 'public',
|
||||||
Mention = 4,
|
Specified = 'specified'
|
||||||
PollEnded = 5,
|
|
||||||
PollVote = 6,
|
|
||||||
Quote = 7,
|
|
||||||
Reaction = 8,
|
|
||||||
ReceiveFollowRequest = 9,
|
|
||||||
Renote = 10,
|
|
||||||
Reply = 11
|
|
||||||
}
|
}
|
||||||
export const enum PageVisibilityEnum {
|
export enum NotificationTypeEnum {
|
||||||
Followers = 0,
|
App = 'app',
|
||||||
Public = 1,
|
Follow = 'follow',
|
||||||
Specified = 2
|
FollowRequestAccepted = 'followRequestAccepted',
|
||||||
|
GroupInvited = 'groupInvited',
|
||||||
|
Mention = 'mention',
|
||||||
|
PollEnded = 'pollEnded',
|
||||||
|
PollVote = 'pollVote',
|
||||||
|
Quote = 'quote',
|
||||||
|
Reaction = 'reaction',
|
||||||
|
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||||
|
Renote = 'renote',
|
||||||
|
Reply = 'reply'
|
||||||
}
|
}
|
||||||
export const enum PollNotevisibilityEnum {
|
export enum PageVisibilityEnum {
|
||||||
Followers = 0,
|
Followers = 'followers',
|
||||||
Home = 1,
|
Public = 'public',
|
||||||
Public = 2,
|
Specified = 'specified'
|
||||||
Specified = 3
|
|
||||||
}
|
}
|
||||||
export const enum RelayStatusEnum {
|
export enum PollNotevisibilityEnum {
|
||||||
Accepted = 0,
|
Followers = 'followers',
|
||||||
Rejected = 1,
|
Home = 'home',
|
||||||
Requesting = 2
|
Public = 'public',
|
||||||
|
Specified = 'specified'
|
||||||
}
|
}
|
||||||
export const enum UserEmojimodpermEnum {
|
export enum RelayStatusEnum {
|
||||||
Add = 0,
|
Accepted = 'accepted',
|
||||||
Full = 1,
|
Rejected = 'rejected',
|
||||||
Mod = 2,
|
Requesting = 'requesting'
|
||||||
Unauthorized = 3
|
|
||||||
}
|
}
|
||||||
export const enum UserProfileFfvisibilityEnum {
|
export enum UserEmojimodpermEnum {
|
||||||
Followers = 0,
|
Add = 'add',
|
||||||
Private = 1,
|
Full = 'full',
|
||||||
Public = 2
|
Mod = 'mod',
|
||||||
|
Unauthorized = 'unauthorized'
|
||||||
}
|
}
|
||||||
export const enum UserProfileMutingnotificationtypesEnum {
|
export enum UserProfileFfvisibilityEnum {
|
||||||
App = 0,
|
Followers = 'followers',
|
||||||
Follow = 1,
|
Private = 'private',
|
||||||
FollowRequestAccepted = 2,
|
Public = 'public'
|
||||||
GroupInvited = 3,
|
}
|
||||||
Mention = 4,
|
export enum UserProfileMutingnotificationtypesEnum {
|
||||||
PollEnded = 5,
|
App = 'app',
|
||||||
PollVote = 6,
|
Follow = 'follow',
|
||||||
Quote = 7,
|
FollowRequestAccepted = 'followRequestAccepted',
|
||||||
Reaction = 8,
|
GroupInvited = 'groupInvited',
|
||||||
ReceiveFollowRequest = 9,
|
Mention = 'mention',
|
||||||
Renote = 10,
|
PollEnded = 'pollEnded',
|
||||||
Reply = 11
|
PollVote = 'pollVote',
|
||||||
|
Quote = 'quote',
|
||||||
|
Reaction = 'reaction',
|
||||||
|
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||||
|
Renote = 'renote',
|
||||||
|
Reply = 'reply'
|
||||||
}
|
}
|
||||||
export interface Signin {
|
export interface Signin {
|
||||||
id: string
|
id: string
|
||||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
||||||
throw new Error(`Failed to load native binding`)
|
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.readEnvironmentConfig = readEnvironmentConfig
|
||||||
module.exports.readServerConfig = readServerConfig
|
module.exports.readServerConfig = readServerConfig
|
||||||
|
@ -339,6 +339,7 @@ module.exports.decodeReaction = decodeReaction
|
||||||
module.exports.countReactions = countReactions
|
module.exports.countReactions = countReactions
|
||||||
module.exports.toDbReaction = toDbReaction
|
module.exports.toDbReaction = toDbReaction
|
||||||
module.exports.AntennaSrcEnum = AntennaSrcEnum
|
module.exports.AntennaSrcEnum = AntennaSrcEnum
|
||||||
|
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
|
||||||
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
|
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
|
||||||
module.exports.NoteVisibilityEnum = NoteVisibilityEnum
|
module.exports.NoteVisibilityEnum = NoteVisibilityEnum
|
||||||
module.exports.NotificationTypeEnum = NotificationTypeEnum
|
module.exports.NotificationTypeEnum = NotificationTypeEnum
|
||||||
|
|
|
@ -33,8 +33,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"artifacts": "napi artifacts",
|
"artifacts": "napi artifacts",
|
||||||
"build": "napi build --features napi --platform --release ./built/",
|
"build": "napi build --features napi --no-const-enum --platform --release ./built/",
|
||||||
"build:debug": "napi build --features napi --platform ./built/",
|
"build:debug": "napi build --features napi --no-const-enum --platform ./built/",
|
||||||
"prepublishOnly": "napi prepublish -t npm",
|
"prepublishOnly": "napi prepublish -t npm",
|
||||||
"test": "pnpm run cargo:test && pnpm run build:debug && ava",
|
"test": "pnpm run cargo:test && pnpm run build:debug && ava",
|
||||||
"universal": "napi universal",
|
"universal": "napi universal",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
||||||
|
|
||||||
|
use super::sea_orm_active_enums::DriveFileUsageHintEnum;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
@ -52,6 +53,8 @@ pub struct Model {
|
||||||
pub request_headers: Option<Json>,
|
pub request_headers: Option<Json>,
|
||||||
#[sea_orm(column_name = "requestIp")]
|
#[sea_orm(column_name = "requestIp")]
|
||||||
pub request_ip: Option<String>,
|
pub request_ip: Option<String>,
|
||||||
|
#[sea_orm(column_name = "usageHint")]
|
||||||
|
pub usage_hint: Option<DriveFileUsageHintEnum>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -173,6 +173,8 @@ pub struct Model {
|
||||||
pub more_urls: Json,
|
pub more_urls: Json,
|
||||||
#[sea_orm(column_name = "markLocalFilesNsfwByDefault")]
|
#[sea_orm(column_name = "markLocalFilesNsfwByDefault")]
|
||||||
pub mark_local_files_nsfw_by_default: bool,
|
pub mark_local_files_nsfw_by_default: bool,
|
||||||
|
#[sea_orm(column_name = "antennaLimit")]
|
||||||
|
pub antenna_limit: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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")]
|
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "antenna_src_enum")]
|
||||||
pub enum AntennaSrcEnum {
|
pub enum AntennaSrcEnum {
|
||||||
#[sea_orm(string_value = "all")]
|
#[sea_orm(string_value = "all")]
|
||||||
|
@ -22,7 +22,21 @@ pub enum AntennaSrcEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -40,7 +54,7 @@ pub enum MutedNoteReasonEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -60,7 +74,7 @@ pub enum NoteVisibilityEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -94,7 +108,7 @@ pub enum NotificationTypeEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -110,7 +124,7 @@ pub enum PageVisibilityEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -128,7 +142,7 @@ pub enum PollNotevisibilityEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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")]
|
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status_enum")]
|
||||||
pub enum RelayStatusEnum {
|
pub enum RelayStatusEnum {
|
||||||
#[sea_orm(string_value = "accepted")]
|
#[sea_orm(string_value = "accepted")]
|
||||||
|
@ -140,7 +154,7 @@ pub enum RelayStatusEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -158,7 +172,7 @@ pub enum UserEmojimodpermEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
db_type = "Enum",
|
||||||
|
@ -174,7 +188,7 @@ pub enum UserProfileFfvisibilityEnum {
|
||||||
}
|
}
|
||||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||||
#[cfg_attr(not(feature = "napi"), derive(Clone))]
|
#[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(
|
#[sea_orm(
|
||||||
rs_type = "String",
|
rs_type = "String",
|
||||||
db_type = "Enum",
|
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 { redisClient } from "@/db/redis.js";
|
||||||
import { encode, decode } from "msgpackr";
|
import { encode, decode } from "msgpackr";
|
||||||
import { ChainableCommander } from "ioredis";
|
import type { ChainableCommander } from "ioredis";
|
||||||
|
|
||||||
export class Cache<T> {
|
export class Cache<T> {
|
||||||
private ttl: number;
|
private ttl: number;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
||||||
import * as stream from "node:stream";
|
import * as stream from "node:stream";
|
||||||
import * as util from "node:util";
|
import * as util from "node:util";
|
||||||
import got, * as Got from "got";
|
import got, * as Got from "got";
|
||||||
import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
|
import { getAgentByHostname, StatusError } from "./fetch.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Logger from "@/services/logger.js";
|
import Logger from "@/services/logger.js";
|
||||||
|
@ -40,10 +40,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||||
send: timeout,
|
send: timeout,
|
||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
},
|
},
|
||||||
agent: {
|
agent: getAgentByHostname(new URL(url).hostname),
|
||||||
http: httpAgent,
|
|
||||||
https: httpsAgent,
|
|
||||||
},
|
|
||||||
http2: false, // default
|
http2: false, // default
|
||||||
retry: {
|
retry: {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
|
|
|
@ -171,6 +171,25 @@ export function getAgentByUrl(url: URL, bypassProxy = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent by Hostname
|
||||||
|
* @param hostname Hostname
|
||||||
|
* @param bypassProxy Allways bypass proxy
|
||||||
|
*/
|
||||||
|
export function getAgentByHostname(hostname: string, bypassProxy = false) {
|
||||||
|
if (bypassProxy || (config.proxyBypassHosts || []).includes(hostname)) {
|
||||||
|
return {
|
||||||
|
http: _http,
|
||||||
|
https: _https,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
http: httpAgent,
|
||||||
|
https: httpsAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class StatusError extends Error {
|
export class StatusError extends Error {
|
||||||
public statusCode: number;
|
public statusCode: number;
|
||||||
public statusMessage?: string;
|
public statusMessage?: string;
|
||||||
|
|
|
@ -16,6 +16,8 @@ import { DriveFolder } from "./drive-folder.js";
|
||||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||||
import { NoteFile } from "./note-file.js";
|
import { NoteFile } from "./note-file.js";
|
||||||
|
|
||||||
|
export type DriveFileUsageHint = "userAvatar" | "userBanner" | null;
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(["userId", "folderId", "id"])
|
@Index(["userId", "folderId", "id"])
|
||||||
export class DriveFile {
|
export class DriveFile {
|
||||||
|
@ -177,6 +179,14 @@ export class DriveFile {
|
||||||
})
|
})
|
||||||
public isSensitive: boolean;
|
public isSensitive: boolean;
|
||||||
|
|
||||||
|
// Hint for what this file is used for
|
||||||
|
@Column({
|
||||||
|
type: "enum",
|
||||||
|
enum: ["userAvatar", "userBanner"],
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public usageHint: DriveFileUsageHint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 外部の(信頼されていない)URLへの直リンクか否か
|
* 外部の(信頼されていない)URLへの直リンクか否か
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -276,6 +276,12 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public remoteDriveCapacityMb: number;
|
public remoteDriveCapacityMb: number;
|
||||||
|
|
||||||
|
@Column("integer", {
|
||||||
|
default: 5,
|
||||||
|
comment: "Antenna Limit",
|
||||||
|
})
|
||||||
|
public antennaLimit: number;
|
||||||
|
|
||||||
@Column("varchar", {
|
@Column("varchar", {
|
||||||
length: 128,
|
length: 128,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -152,6 +152,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
md5: file.md5,
|
md5: file.md5,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
|
usageHint: file.usageHint,
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||||
|
@ -193,6 +194,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
md5: file.md5,
|
md5: file.md5,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
isSensitive: file.isSensitive,
|
isSensitive: file.isSensitive,
|
||||||
|
usageHint: file.usageHint,
|
||||||
blurhash: file.blurhash,
|
blurhash: file.blurhash,
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||||
|
|
|
@ -44,6 +44,12 @@ export const packedDriveFileSchema = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
usageHint: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: true,
|
||||||
|
enum: ["userAvatar", "userBanner"],
|
||||||
|
},
|
||||||
blurhash: {
|
blurhash: {
|
||||||
type: "string",
|
type: "string",
|
||||||
optional: false,
|
optional: false,
|
||||||
|
|
|
@ -34,7 +34,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
|
||||||
function apBackoff(attemptsMade: number, err: Error) {
|
function apBackoff(attemptsMade: number, err: Error) {
|
||||||
const baseDelay = 60 * 1000; // 1min
|
const baseDelay = 60 * 1000; // 1min
|
||||||
const maxBackoff = 8 * 60 * 60 * 1000; // 8hours
|
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.min(backoff, maxBackoff);
|
||||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||||
return backoff;
|
return backoff;
|
||||||
|
|
|
@ -3,7 +3,10 @@ import type { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
import Resolver from "../resolver.js";
|
import Resolver from "../resolver.js";
|
||||||
import { fetchMeta } from "backend-rs";
|
import { fetchMeta } from "backend-rs";
|
||||||
import { apLogger } from "../logger.js";
|
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 { DriveFiles } from "@/models/index.js";
|
||||||
import { truncate } from "@/misc/truncate.js";
|
import { truncate } from "@/misc/truncate.js";
|
||||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||||
|
@ -16,6 +19,7 @@ const logger = apLogger;
|
||||||
export async function createImage(
|
export async function createImage(
|
||||||
actor: CacheableRemoteUser,
|
actor: CacheableRemoteUser,
|
||||||
value: any,
|
value: any,
|
||||||
|
usage: DriveFileUsageHint,
|
||||||
): Promise<DriveFile> {
|
): Promise<DriveFile> {
|
||||||
// Skip if author is frozen.
|
// Skip if author is frozen.
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
|
@ -43,6 +47,7 @@ export async function createImage(
|
||||||
sensitive: image.sensitive,
|
sensitive: image.sensitive,
|
||||||
isLink: !instance.cacheRemoteFiles,
|
isLink: !instance.cacheRemoteFiles,
|
||||||
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
|
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||||
|
usageHint: usage,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file.isLink) {
|
if (file.isLink) {
|
||||||
|
@ -73,9 +78,10 @@ export async function createImage(
|
||||||
export async function resolveImage(
|
export async function resolveImage(
|
||||||
actor: CacheableRemoteUser,
|
actor: CacheableRemoteUser,
|
||||||
value: any,
|
value: any,
|
||||||
|
usage: DriveFileUsageHint,
|
||||||
): Promise<DriveFile> {
|
): Promise<DriveFile> {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
// Fetch from remote server and register
|
// 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(
|
await Promise.all(
|
||||||
note.attachment.map(
|
note.attachment.map(
|
||||||
(x) => limit(() => resolveImage(actor, x)) as Promise<DriveFile>,
|
(x) =>
|
||||||
|
limit(() => resolveImage(actor, x, null)) as Promise<DriveFile>,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
).filter((image) => image != null)
|
).filter((image) => image != null)
|
||||||
|
@ -616,7 +617,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
|
||||||
fileList.map(
|
fileList.map(
|
||||||
(x) =>
|
(x) =>
|
||||||
limit(async () => {
|
limit(async () => {
|
||||||
const file = await resolveImage(actor, x);
|
const file = await resolveImage(actor, x, null);
|
||||||
const update: Partial<DriveFile> = {};
|
const update: Partial<DriveFile> = {};
|
||||||
|
|
||||||
const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);
|
const altText = truncate(x.name, DB_MAX_IMAGE_COMMENT_LENGTH);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Followings,
|
Followings,
|
||||||
UserProfiles,
|
UserProfiles,
|
||||||
UserPublickeys,
|
UserPublickeys,
|
||||||
|
DriveFiles,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
|
import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js";
|
||||||
import { User } 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
|
//#region Fetch avatar and header image
|
||||||
const [avatar, banner] = await Promise.all(
|
const [avatar, banner] = await Promise.all(
|
||||||
[person.icon, person.image].map((img) =>
|
[person.icon, person.image].map((img, index) =>
|
||||||
img == null
|
img == null
|
||||||
? Promise.resolve(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
|
// Fetch avatar and header image
|
||||||
const [avatar, banner] = await Promise.all(
|
const [avatar, banner] = await Promise.all(
|
||||||
[person.icon, person.image].map((img) =>
|
[person.icon, person.image].map((img, index) =>
|
||||||
img == null
|
img == null
|
||||||
? Promise.resolve(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>;
|
} as Partial<User>;
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
|
if (user?.avatarId)
|
||||||
|
await DriveFiles.update(user.avatarId, { usageHint: null });
|
||||||
updates.avatarId = avatar.id;
|
updates.avatarId = avatar.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (banner) {
|
if (banner) {
|
||||||
|
if (user?.bannerId)
|
||||||
|
await DriveFiles.update(user.bannerId, { usageHint: null });
|
||||||
updates.bannerId = banner.id;
|
updates.bannerId = banner.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import Followers from "./activitypub/followers.js";
|
||||||
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
import Outbox, { packActivity } from "./activitypub/outbox.js";
|
||||||
import { serverLogger } from "./index.js";
|
import { serverLogger } from "./index.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import Koa from "koa";
|
import type Koa from "koa";
|
||||||
import * as crypto from "node:crypto";
|
import * as crypto from "node:crypto";
|
||||||
import { inspect } from "node:util";
|
import { inspect } from "node:util";
|
||||||
import type { IActivity } from "@/remote/activitypub/type.js";
|
import type { IActivity } from "@/remote/activitypub/type.js";
|
||||||
|
|
|
@ -24,6 +24,11 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
antennaLimit: {
|
||||||
|
type: "number",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
cacheRemoteFiles: {
|
cacheRemoteFiles: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
optional: false,
|
optional: false,
|
||||||
|
@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => {
|
||||||
enableGuestTimeline: instance.enableGuestTimeline,
|
enableGuestTimeline: instance.enableGuestTimeline,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
|
antennaLimit: instance.antennaLimit,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
|
|
@ -94,6 +94,7 @@ export const paramDef = {
|
||||||
defaultDarkTheme: { type: "string", nullable: true },
|
defaultDarkTheme: { type: "string", nullable: true },
|
||||||
localDriveCapacityMb: { type: "integer" },
|
localDriveCapacityMb: { type: "integer" },
|
||||||
remoteDriveCapacityMb: { type: "integer" },
|
remoteDriveCapacityMb: { type: "integer" },
|
||||||
|
antennaLimit: { type: "integer" },
|
||||||
cacheRemoteFiles: { type: "boolean" },
|
cacheRemoteFiles: { type: "boolean" },
|
||||||
markLocalFilesNsfwByDefault: { type: "boolean" },
|
markLocalFilesNsfwByDefault: { type: "boolean" },
|
||||||
emailRequiredForSignup: { type: "boolean" },
|
emailRequiredForSignup: { type: "boolean" },
|
||||||
|
@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
|
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.antennaLimit !== undefined) {
|
||||||
|
set.antennaLimit = ps.antennaLimit;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.cacheRemoteFiles !== undefined) {
|
if (ps.cacheRemoteFiles !== undefined) {
|
||||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
import { genId } from "backend-rs";
|
import { fetchMeta, genId } from "backend-rs";
|
||||||
import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
|
import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import { publishInternalEvent } from "@/services/stream.js";
|
import { publishInternalEvent } from "@/services/stream.js";
|
||||||
|
@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
let userList;
|
let userList;
|
||||||
let userGroupJoining;
|
let userGroupJoining;
|
||||||
|
|
||||||
|
const instance = await fetchMeta(true);
|
||||||
|
|
||||||
const antennas = await Antennas.findBy({
|
const antennas = await Antennas.findBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
if (antennas.length > 5 && !user.isAdmin) {
|
if (antennas.length >= instance.antennaLimit) {
|
||||||
throw new ApiError(meta.errors.tooManyAntennas);
|
throw new ApiError(meta.errors.tooManyAntennas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
|
||||||
import { verifyLink } from "@/services/fetch-rel-me.js";
|
import { verifyLink } from "@/services/fetch-rel-me.js";
|
||||||
import { ApiError } from "@/server/api/error.js";
|
import { ApiError } from "@/server/api/error.js";
|
||||||
import define from "@/server/api/define.js";
|
import define from "@/server/api/define.js";
|
||||||
|
import { DriveFile } from "@/models/entities/drive-file";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ["account"],
|
tags: ["account"],
|
||||||
|
@ -241,8 +242,9 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
if (ps.emailNotificationTypes !== undefined)
|
if (ps.emailNotificationTypes !== undefined)
|
||||||
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||||
|
|
||||||
|
let avatar: DriveFile | null = null;
|
||||||
if (ps.avatarId) {
|
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)
|
if (avatar == null || avatar.userId !== user.id)
|
||||||
throw new ApiError(meta.errors.noSuchAvatar);
|
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);
|
throw new ApiError(meta.errors.avatarNotAnImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let banner: DriveFile | null = null;
|
||||||
if (ps.bannerId) {
|
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)
|
if (banner == null || banner.userId !== user.id)
|
||||||
throw new ApiError(meta.errors.noSuchBanner);
|
throw new ApiError(meta.errors.noSuchBanner);
|
||||||
|
@ -328,6 +331,20 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
updateUsertags(user, tags);
|
updateUsertags(user, tags);
|
||||||
//#endregion
|
//#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(updates).length > 0) await Users.update(user.id, updates);
|
||||||
if (Object.keys(profileUpdates).length > 0)
|
if (Object.keys(profileUpdates).length > 0)
|
||||||
await UserProfiles.update(user.id, profileUpdates);
|
await UserProfiles.update(user.id, profileUpdates);
|
||||||
|
|
|
@ -126,6 +126,11 @@ export const meta = {
|
||||||
optional: false,
|
optional: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
},
|
},
|
||||||
|
antennaLimit: {
|
||||||
|
type: "number",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
cacheRemoteFiles: {
|
cacheRemoteFiles: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
optional: false,
|
optional: false,
|
||||||
|
@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
enableGuestTimeline: instance.enableGuestTimeline,
|
enableGuestTimeline: instance.enableGuestTimeline,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
|
antennaLimit: instance.antennaLimit,
|
||||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type * as http from "node:http";
|
import type * as http from "node:http";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { ParsedUrlQuery } from "querystring";
|
import type { ParsedUrlQuery } from "node:querystring";
|
||||||
import * as websocket from "websocket";
|
import * as websocket from "websocket";
|
||||||
|
|
||||||
import { subscriber as redisClient } from "@/db/redis.js";
|
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 { Buffer } from "node:buffer";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,16 @@ import { Feed } from "feed";
|
||||||
import { In, IsNull } from "typeorm";
|
import { In, IsNull } from "typeorm";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
|
import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
|
||||||
|
import getNoteHtml from "@/remote/activitypub/misc/get-note-html.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is this part in the note, it will cause CDATA to be terminated early.
|
||||||
|
*/
|
||||||
|
function escapeCDATA(str: string) {
|
||||||
|
return str.replaceAll("]]>", "]]]]><![CDATA[>");
|
||||||
|
}
|
||||||
|
|
||||||
export default async function (
|
export default async function (
|
||||||
user: User,
|
user: User,
|
||||||
|
@ -15,7 +24,7 @@ export default async function (
|
||||||
const author = {
|
const author = {
|
||||||
link: `${config.url}/@${user.username}`,
|
link: `${config.url}/@${user.username}`,
|
||||||
email: `${user.username}@${config.host}`,
|
email: `${user.username}@${config.host}`,
|
||||||
name: user.name || user.username,
|
name: escapeCDATA(user.name || user.username),
|
||||||
};
|
};
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
@ -44,11 +53,13 @@ export default async function (
|
||||||
title: `${author.name} (@${user.username}@${config.host})`,
|
title: `${author.name} (@${user.username}@${config.host})`,
|
||||||
updated: notes[0].createdAt,
|
updated: notes[0].createdAt,
|
||||||
generator: "Firefish",
|
generator: "Firefish",
|
||||||
description: `${user.notesCount} Notes, ${
|
description: escapeCDATA(
|
||||||
profile.ffVisibility === "public" ? user.followingCount : "?"
|
`${user.notesCount} Notes, ${
|
||||||
} Following, ${
|
profile.ffVisibility === "public" ? user.followingCount : "?"
|
||||||
profile.ffVisibility === "public" ? user.followersCount : "?"
|
} Following, ${
|
||||||
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
|
profile.ffVisibility === "public" ? user.followersCount : "?"
|
||||||
|
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
|
||||||
|
),
|
||||||
link: author.link,
|
link: author.link,
|
||||||
image: await Users.getAvatarUrl(user),
|
image: await Users.getAvatarUrl(user),
|
||||||
feedLinks: {
|
feedLinks: {
|
||||||
|
@ -88,19 +99,23 @@ export default async function (
|
||||||
}
|
}
|
||||||
|
|
||||||
feed.addItem({
|
feed.addItem({
|
||||||
title: title
|
title: escapeCDATA(
|
||||||
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
title
|
||||||
.substring(0, 100),
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
||||||
|
.substring(0, 100),
|
||||||
|
),
|
||||||
link: `${config.url}/notes/${note.id}`,
|
link: `${config.url}/notes/${note.id}`,
|
||||||
date: note.createdAt,
|
date: note.createdAt,
|
||||||
description: note.cw
|
description: note.cw
|
||||||
? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
? escapeCDATA(note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""))
|
||||||
: undefined,
|
: undefined,
|
||||||
content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
|
content: escapeCDATA(
|
||||||
|
contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function noteToString(note, isTheNote = false) {
|
async function noteToString(note: Note, isTheNote = false) {
|
||||||
const author = isTheNote
|
const author = isTheNote
|
||||||
? null
|
? null
|
||||||
: await Users.findOneBy({ id: note.userId });
|
: await Users.findOneBy({ id: note.userId });
|
||||||
|
@ -135,7 +150,10 @@ export default async function (
|
||||||
}">${file.name}</a>`;
|
}">${file.name}</a>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
|
|
||||||
|
outstr += `${note.cw ? note.cw + "<br>" : ""}${
|
||||||
|
getNoteHtml(note) || ""
|
||||||
|
}${fileEle}`;
|
||||||
if (isTheNote) {
|
if (isTheNote) {
|
||||||
outstr += ` <span class="${
|
outstr += ` <span class="${
|
||||||
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
|
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
|
||||||
|
|
|
@ -54,6 +54,10 @@ app.use(async (ctx, next) => {
|
||||||
const url = decodeURI(ctx.path);
|
const url = decodeURI(ctx.path);
|
||||||
|
|
||||||
if (url === bullBoardPath || url.startsWith(`${bullBoardPath}/`)) {
|
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");
|
const token = ctx.cookies.get("token");
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
ctx.status = 401;
|
ctx.status = 401;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import type { KVs } from "../core.js";
|
|
||||||
import Chart from "../core.js";
|
import Chart from "../core.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import { name, schema } from "./entities/active-users.js";
|
import { name, schema } from "./entities/active-users.js";
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
UserProfiles,
|
UserProfiles,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import { DriveFile } from "@/models/entities/drive-file.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 type { IRemoteUser, User } from "@/models/entities/user.js";
|
||||||
import { genId } from "backend-rs";
|
import { genId } from "backend-rs";
|
||||||
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
|
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
|
||||||
|
@ -65,6 +66,7 @@ function urlPathJoin(
|
||||||
* @param type Content-Type for original
|
* @param type Content-Type for original
|
||||||
* @param hash Hash for original
|
* @param hash Hash for original
|
||||||
* @param size Size for original
|
* @param size Size for original
|
||||||
|
* @param usage Optional usage hint for file (f.e. "userAvatar")
|
||||||
*/
|
*/
|
||||||
async function save(
|
async function save(
|
||||||
file: DriveFile,
|
file: DriveFile,
|
||||||
|
@ -73,6 +75,7 @@ async function save(
|
||||||
type: string,
|
type: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
size: number,
|
size: number,
|
||||||
|
usage: DriveFileUsageHint = null,
|
||||||
): Promise<DriveFile> {
|
): Promise<DriveFile> {
|
||||||
// thunbnail, webpublic を必要なら生成
|
// thunbnail, webpublic を必要なら生成
|
||||||
const alts = await generateAlts(path, type, !file.uri);
|
const alts = await generateAlts(path, type, !file.uri);
|
||||||
|
@ -161,6 +164,7 @@ async function save(
|
||||||
file.md5 = hash;
|
file.md5 = hash;
|
||||||
file.size = size;
|
file.size = size;
|
||||||
file.storedInternal = false;
|
file.storedInternal = false;
|
||||||
|
file.usageHint = usage ?? null;
|
||||||
|
|
||||||
return await DriveFiles.insert(file).then((x) =>
|
return await DriveFiles.insert(file).then((x) =>
|
||||||
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
||||||
|
@ -204,6 +208,7 @@ async function save(
|
||||||
file.type = type;
|
file.type = type;
|
||||||
file.md5 = hash;
|
file.md5 = hash;
|
||||||
file.size = size;
|
file.size = size;
|
||||||
|
file.usageHint = usage ?? null;
|
||||||
|
|
||||||
return await DriveFiles.insert(file).then((x) =>
|
return await DriveFiles.insert(file).then((x) =>
|
||||||
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
DriveFiles.findOneByOrFail(x.identifiers[0]),
|
||||||
|
@ -450,6 +455,9 @@ type AddFileArgs = {
|
||||||
|
|
||||||
requestIp?: string | null;
|
requestIp?: string | null;
|
||||||
requestHeaders?: Record<string, 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,
|
sensitive = null,
|
||||||
requestIp = null,
|
requestIp = null,
|
||||||
requestHeaders = null,
|
requestHeaders = null,
|
||||||
|
usageHint = null,
|
||||||
}: AddFileArgs): Promise<DriveFile> {
|
}: AddFileArgs): Promise<DriveFile> {
|
||||||
const info = await getFileInfo(path);
|
const info = await getFileInfo(path);
|
||||||
logger.info(`${JSON.stringify(info)}`);
|
logger.info(`${JSON.stringify(info)}`);
|
||||||
|
@ -581,6 +590,7 @@ export async function addFile({
|
||||||
file.isLink = isLink;
|
file.isLink = isLink;
|
||||||
file.requestIp = requestIp;
|
file.requestIp = requestIp;
|
||||||
file.requestHeaders = requestHeaders;
|
file.requestHeaders = requestHeaders;
|
||||||
|
file.usageHint = usageHint;
|
||||||
file.isSensitive = user
|
file.isSensitive = user
|
||||||
? Users.isLocalUser(user) &&
|
? Users.isLocalUser(user) &&
|
||||||
(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
|
(instance!.markLocalFilesNsfwByDefault || profile!.alwaysMarkNsfw)
|
||||||
|
@ -639,6 +649,7 @@ export async function addFile({
|
||||||
info.type.mime,
|
info.type.mime,
|
||||||
info.md5,
|
info.md5,
|
||||||
info.size,
|
info.size,
|
||||||
|
usageHint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@ import type { User } from "@/models/entities/user.js";
|
||||||
import { createTemp } from "@/misc/create-temp.js";
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
|
import { downloadUrl, isPrivateIp } from "@/misc/download-url.js";
|
||||||
import type { DriveFolder } from "@/models/entities/drive-folder.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 { DriveFiles } from "@/models/index.js";
|
||||||
import { driveLogger } from "./logger.js";
|
import { driveLogger } from "./logger.js";
|
||||||
import { addFile } from "./add-file.js";
|
import { addFile } from "./add-file.js";
|
||||||
|
@ -13,7 +16,11 @@ const logger = driveLogger.createSubLogger("downloader");
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
url: string;
|
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;
|
folderId?: DriveFolder["id"] | null;
|
||||||
uri?: string | null;
|
uri?: string | null;
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
|
@ -22,6 +29,7 @@ type Args = {
|
||||||
comment?: string | null;
|
comment?: string | null;
|
||||||
requestIp?: string | null;
|
requestIp?: string | null;
|
||||||
requestHeaders?: Record<string, string> | null;
|
requestHeaders?: Record<string, string> | null;
|
||||||
|
usageHint?: DriveFileUsageHint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function uploadFromUrl({
|
export async function uploadFromUrl({
|
||||||
|
@ -35,6 +43,7 @@ export async function uploadFromUrl({
|
||||||
comment = null,
|
comment = null,
|
||||||
requestIp = null,
|
requestIp = null,
|
||||||
requestHeaders = null,
|
requestHeaders = null,
|
||||||
|
usageHint = null,
|
||||||
}: Args): Promise<DriveFile> {
|
}: Args): Promise<DriveFile> {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
if (
|
if (
|
||||||
|
@ -75,9 +84,10 @@ export async function uploadFromUrl({
|
||||||
sensitive,
|
sensitive,
|
||||||
requestIp,
|
requestIp,
|
||||||
requestHeaders,
|
requestHeaders,
|
||||||
|
usageHint,
|
||||||
});
|
});
|
||||||
logger.succ(`Got: ${driveFile.id}`);
|
logger.succ(`Got: ${driveFile.id}`);
|
||||||
return driveFile!;
|
return driveFile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to create drive file:\n${inspect(e)}`);
|
logger.error(`Failed to create drive file:\n${inspect(e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Window } from "happy-dom";
|
import { Window } from "happy-dom";
|
||||||
|
import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
|
|
||||||
async function getRelMeLinks(url: string): Promise<string[]> {
|
async function getRelMeLinks(url: string): Promise<string[]> {
|
||||||
|
|
|
@ -28,9 +28,9 @@ export default class Logger {
|
||||||
|
|
||||||
if (config.syslog) {
|
if (config.syslog) {
|
||||||
this.syslogClient = new SyslogPro.RFC5424({
|
this.syslogClient = new SyslogPro.RFC5424({
|
||||||
applacationName: "Firefish",
|
applicationName: "Firefish",
|
||||||
timestamp: true,
|
timestamp: true,
|
||||||
encludeStructuredData: true,
|
includeStructuredData: true,
|
||||||
color: true,
|
color: true,
|
||||||
extendedColor: true,
|
extendedColor: true,
|
||||||
server: {
|
server: {
|
||||||
|
@ -144,12 +144,12 @@ export default class Logger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used when the process can't continue (fatal error)
|
||||||
public error(
|
public error(
|
||||||
x: string | Error,
|
x: string | Error,
|
||||||
data?: Record<string, any> | null,
|
data?: Record<string, any> | null,
|
||||||
important = false,
|
important = false,
|
||||||
): void {
|
): void {
|
||||||
// 実行を継続できない状況で使う
|
|
||||||
if (x instanceof Error) {
|
if (x instanceof Error) {
|
||||||
data = data || {};
|
data = data || {};
|
||||||
data.e = x;
|
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(
|
public warn(
|
||||||
message: string,
|
message: string,
|
||||||
data?: Record<string, any> | null,
|
data?: Record<string, any> | null,
|
||||||
important = false,
|
important = false,
|
||||||
): void {
|
): void {
|
||||||
// 実行を継続できるが改善すべき状況で使う
|
|
||||||
this.log("warning", message, data, important);
|
this.log("warning", message, data, important);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used when something is successful
|
||||||
public succ(
|
public succ(
|
||||||
message: string,
|
message: string,
|
||||||
data?: Record<string, any> | null,
|
data?: Record<string, any> | null,
|
||||||
important = false,
|
important = false,
|
||||||
): void {
|
): void {
|
||||||
// 何かに成功した状況で使う
|
|
||||||
this.log("success", message, data, important);
|
this.log("success", message, data, important);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for debugging (information necessary for developers but unnecessary for users)
|
||||||
public debug(
|
public debug(
|
||||||
message: string,
|
message: string,
|
||||||
data?: Record<string, any> | null,
|
data?: Record<string, any> | null,
|
||||||
important = false,
|
important = false,
|
||||||
): void {
|
): void {
|
||||||
// Used for debugging (information necessary for developers but unnecessary for users)
|
|
||||||
// Fixed if statement is ignored when logLevel includes debug
|
// Fixed if statement is ignored when logLevel includes debug
|
||||||
if (
|
if (
|
||||||
config.logLevel?.includes("debug") ||
|
config.logLevel?.includes("debug") ||
|
||||||
|
@ -200,12 +200,12 @@ export default class Logger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Other generic logs
|
||||||
public info(
|
public info(
|
||||||
message: string,
|
message: string,
|
||||||
data?: Record<string, any> | null,
|
data?: Record<string, any> | null,
|
||||||
important = false,
|
important = false,
|
||||||
): void {
|
): void {
|
||||||
// それ以外
|
|
||||||
this.log("info", message, data, important);
|
this.log("info", message, data, important);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { publishMainStream } from "@/services/stream.js";
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import {
|
import { NoteUnreads, Followings, ChannelFollowings } from "@/models/index.js";
|
||||||
NoteUnreads,
|
|
||||||
Users,
|
|
||||||
Followings,
|
|
||||||
ChannelFollowings,
|
|
||||||
} from "@/models/index.js";
|
|
||||||
import { Not, IsNull, In } from "typeorm";
|
import { Not, IsNull, In } from "typeorm";
|
||||||
import type { Channel } from "@/models/entities/channel.js";
|
import type { Channel } from "@/models/entities/channel.js";
|
||||||
import { readNotificationByQuery } from "@/server/api/common/read-notification.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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from "vue";
|
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: Ref<boolean>;
|
showing: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
|
@ -231,15 +231,9 @@ const unicodeEmojiSkinToneLabels = [
|
||||||
i18n.ts._skinTones?.dark ?? "Dark",
|
i18n.ts._skinTones?.dark ?? "Dark",
|
||||||
];
|
];
|
||||||
|
|
||||||
const size = computed(() =>
|
const size = reactionPickerSize;
|
||||||
props.asReactionPicker ? reactionPickerSize.value : 1,
|
const width = reactionPickerWidth;
|
||||||
);
|
const height = reactionPickerHeight;
|
||||||
const width = computed(() =>
|
|
||||||
props.asReactionPicker ? reactionPickerWidth.value : 3,
|
|
||||||
);
|
|
||||||
const height = computed(() =>
|
|
||||||
props.asReactionPicker ? reactionPickerHeight.value : 2,
|
|
||||||
);
|
|
||||||
const customEmojiCategories = emojiCategories;
|
const customEmojiCategories = emojiCategories;
|
||||||
const customEmojis = instance.emojis;
|
const customEmojis = instance.emojis;
|
||||||
const q = ref<string | null>(null);
|
const q = ref<string | null>(null);
|
||||||
|
|
|
@ -39,7 +39,7 @@ import { defaultStore } from "@/store";
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
src?: HTMLElement;
|
src?: HTMLElement | null;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
|
|
|
@ -42,7 +42,7 @@ useTooltip(el, (showing) => {
|
||||||
os.popup(
|
os.popup(
|
||||||
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
defineAsyncComponent(() => import("@/components/MkUrlPreviewPopup.vue")),
|
||||||
{
|
{
|
||||||
showing: showing.value,
|
showing,
|
||||||
url: props.url,
|
url: props.url,
|
||||||
source: el.value,
|
source: el.value,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1188,7 +1188,7 @@ async function insertEmoji(ev: MouseEvent) {
|
||||||
os.openEmojiPicker(
|
os.openEmojiPicker(
|
||||||
(ev.currentTarget ?? ev.target) as HTMLElement,
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
{},
|
{},
|
||||||
textareaEl.value,
|
textareaEl.value!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: Ref<boolean>;
|
showing: boolean;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
emojis: entities.EmojiLite[];
|
emojis: entities.EmojiLite[];
|
||||||
targetElement: HTMLElement;
|
targetElement: HTMLElement;
|
||||||
|
|
|
@ -30,13 +30,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: Ref<boolean>;
|
showing: boolean;
|
||||||
reaction: string;
|
reaction: string;
|
||||||
users: entities.User[]; // TODO
|
users: entities.User[]; // TODO
|
||||||
count: number;
|
count: number;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
@after-leave="emit('closed')"
|
@after-leave="emit('closed')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-show="unref(showing)"
|
v-show="showing"
|
||||||
ref="el"
|
ref="el"
|
||||||
class="buebdbiu _acrylic _shadow"
|
class="buebdbiu _acrylic _shadow"
|
||||||
:style="{ zIndex, maxWidth: maxWidth + 'px' }"
|
:style="{ zIndex, maxWidth: maxWidth + 'px' }"
|
||||||
|
@ -19,21 +19,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||||
type MaybeRef,
|
|
||||||
nextTick,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
ref,
|
|
||||||
unref,
|
|
||||||
} from "vue";
|
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { calcPopupPosition } from "@/scripts/popup-position";
|
import { calcPopupPosition } from "@/scripts/popup-position";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: MaybeRef<boolean>;
|
showing: boolean;
|
||||||
targetElement?: HTMLElement | null;
|
targetElement?: HTMLElement | null;
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
|
|
|
@ -19,12 +19,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from "vue";
|
|
||||||
import type { entities } from "firefish-js";
|
import type { entities } from "firefish-js";
|
||||||
import MkTooltip from "./MkTooltip.vue";
|
import MkTooltip from "./MkTooltip.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: Ref<boolean>;
|
showing: boolean;
|
||||||
users: entities.User[];
|
users: entities.User[];
|
||||||
count: number;
|
count: number;
|
||||||
targetElement?: HTMLElement;
|
targetElement?: HTMLElement;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
v-if="unref(success)"
|
v-if="success"
|
||||||
:class="[$style.icon, $style.success, iconify('ph-check')]"
|
:class="[$style.icon, $style.success, iconify('ph-check')]"
|
||||||
></i>
|
></i>
|
||||||
<MkLoading
|
<MkLoading
|
||||||
|
@ -29,16 +29,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MaybeRef } from "vue";
|
import { shallowRef, watch } from "vue";
|
||||||
import { shallowRef, unref, watch } from "vue";
|
|
||||||
import MkModal from "@/components/MkModal.vue";
|
import MkModal from "@/components/MkModal.vue";
|
||||||
import iconify from "@/scripts/icon";
|
import iconify from "@/scripts/icon";
|
||||||
|
|
||||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
success: MaybeRef<boolean>;
|
success: boolean;
|
||||||
showing: MaybeRef<boolean>;
|
showing: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
|
import { type Endpoints, type entities, api as firefishApi } from "firefish-js";
|
||||||
import insertTextAtCursor from "insert-text-at-cursor";
|
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 { defineAsyncComponent, markRaw, ref } from "vue";
|
||||||
import { i18n } from "./i18n";
|
import { i18n } from "./i18n";
|
||||||
import MkDialog from "@/components/MkDialog.vue";
|
import MkDialog from "@/components/MkDialog.vue";
|
||||||
|
@ -213,9 +213,13 @@ interface VueComponentConstructor<P, E> {
|
||||||
|
|
||||||
type NonArrayAble<A> = A extends Array<unknown> ? never : A;
|
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>(
|
export async function popup<Props, Emits>(
|
||||||
component: VueComponentConstructor<Props, Emits>,
|
component: VueComponentConstructor<Props, Emits>,
|
||||||
props: Props,
|
props: CanUseRef<Props>,
|
||||||
events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
|
events: Partial<NonArrayAble<NonNullable<Emits>>> = {},
|
||||||
disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
|
disposeEvent?: keyof Partial<NonArrayAble<NonNullable<Emits>>>,
|
||||||
) {
|
) {
|
||||||
|
@ -240,6 +244,7 @@ export async function popup<Props, Emits>(
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hint: Vue will automatically resolve ref here, so it is safe to use ref in props
|
||||||
popups.value.push(state);
|
popups.value.push(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -350,6 +350,19 @@
|
||||||
</FormSplit>
|
</FormSplit>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ i18n.ts.antennas }}</template>
|
||||||
|
<FormInput
|
||||||
|
v-model="antennaLimit"
|
||||||
|
type="number"
|
||||||
|
class="_formBlock"
|
||||||
|
>
|
||||||
|
<template #label>{{
|
||||||
|
i18n.ts.antennaLimit
|
||||||
|
}}</template>
|
||||||
|
</FormInput>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>ServiceWorker</template>
|
<template #label>ServiceWorker</template>
|
||||||
|
|
||||||
|
@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false);
|
||||||
const markLocalFilesNsfwByDefault = ref(false);
|
const markLocalFilesNsfwByDefault = ref(false);
|
||||||
const localDriveCapacityMb = ref(0);
|
const localDriveCapacityMb = ref(0);
|
||||||
const remoteDriveCapacityMb = ref(0);
|
const remoteDriveCapacityMb = ref(0);
|
||||||
|
const antennaLimit = ref(0);
|
||||||
const enableRegistration = ref(false);
|
const enableRegistration = ref(false);
|
||||||
const emailRequiredForSignup = ref(false);
|
const emailRequiredForSignup = ref(false);
|
||||||
const enableServiceWorker = ref(false);
|
const enableServiceWorker = ref(false);
|
||||||
|
@ -579,6 +593,7 @@ async function init() {
|
||||||
markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
|
markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
|
||||||
localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
|
localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
|
||||||
remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
|
remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
|
||||||
|
antennaLimit.value = meta.antennaLimit;
|
||||||
enableRegistration.value = !meta.disableRegistration;
|
enableRegistration.value = !meta.disableRegistration;
|
||||||
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
emailRequiredForSignup.value = meta.emailRequiredForSignup;
|
||||||
enableServiceWorker.value = meta.enableServiceWorker;
|
enableServiceWorker.value = meta.enableServiceWorker;
|
||||||
|
@ -631,6 +646,7 @@ function save() {
|
||||||
markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
|
markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
|
||||||
localDriveCapacityMb: localDriveCapacityMb.value,
|
localDriveCapacityMb: localDriveCapacityMb.value,
|
||||||
remoteDriveCapacityMb: remoteDriveCapacityMb.value,
|
remoteDriveCapacityMb: remoteDriveCapacityMb.value,
|
||||||
|
antennaLimit: antennaLimit.value,
|
||||||
disableRegistration: !enableRegistration.value,
|
disableRegistration: !enableRegistration.value,
|
||||||
emailRequiredForSignup: emailRequiredForSignup.value,
|
emailRequiredForSignup: emailRequiredForSignup.value,
|
||||||
enableServiceWorker: enableServiceWorker.value,
|
enableServiceWorker: enableServiceWorker.value,
|
||||||
|
|
|
@ -4,30 +4,35 @@
|
||||||
><MkPageHeader :display-back-button="true"
|
><MkPageHeader :display-back-button="true"
|
||||||
/></template>
|
/></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<MkLoading v-if="!loaded" />
|
<MkLoading v-if="note == null" />
|
||||||
<MkPagination
|
<div v-else>
|
||||||
v-else
|
<MkRemoteCaution
|
||||||
ref="pagingComponent"
|
v-if="note.user.host != null"
|
||||||
v-slot="{ items }"
|
:href="note.url ?? note.uri!"
|
||||||
:pagination="pagination"
|
/>
|
||||||
>
|
<MkPagination
|
||||||
<div ref="tlEl" class="giivymft noGap">
|
ref="pagingComponent"
|
||||||
<XList
|
v-slot="{ items }"
|
||||||
v-slot="{ item }"
|
:pagination="pagination"
|
||||||
:items="convertNoteEditsToNotes(items)"
|
>
|
||||||
class="notes"
|
<div ref="tlEl" class="giivymft noGap">
|
||||||
:no-gap="true"
|
<XList
|
||||||
>
|
v-slot="{ item }"
|
||||||
<XNote
|
:items="convertNoteEditsToNotes(items)"
|
||||||
:key="item.id"
|
class="notes"
|
||||||
class="qtqtichx"
|
:no-gap="true"
|
||||||
:note="item"
|
>
|
||||||
:hide-footer="true"
|
<XNote
|
||||||
:detailed-view="true"
|
:key="item.id"
|
||||||
/>
|
class="qtqtichx"
|
||||||
</XList>
|
:note="item"
|
||||||
</div>
|
:hide-footer="true"
|
||||||
</MkPagination>
|
:detailed-view="true"
|
||||||
|
/>
|
||||||
|
</XList>
|
||||||
|
</div>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||||
import icon from "@/scripts/icon";
|
import icon from "@/scripts/icon";
|
||||||
|
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
|
||||||
|
|
||||||
const pagingComponent = ref<MkPaginationType<
|
const pagingComponent = ref<MkPaginationType<
|
||||||
typeof pagination.endpoint
|
typeof pagination.endpoint
|
||||||
|
@ -69,8 +75,7 @@ definePageMetadata(
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const note = ref<entities.Note>({} as entities.Note);
|
const note = ref<entities.Note | null>(null);
|
||||||
const loaded = ref(false);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
api("notes/show", {
|
api("notes/show", {
|
||||||
|
@ -83,20 +88,19 @@ onMounted(() => {
|
||||||
res.replyId = null;
|
res.replyId = null;
|
||||||
|
|
||||||
note.value = res;
|
note.value = res;
|
||||||
loaded.value = true;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
|
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
|
||||||
const now: entities.NoteEdit = {
|
const now: entities.NoteEdit = {
|
||||||
id: "EditionNow",
|
id: "EditionNow",
|
||||||
noteId: note.value.id,
|
noteId: note.value!.id,
|
||||||
updatedAt: note.value.createdAt,
|
updatedAt: note.value!.createdAt,
|
||||||
text: note.value.text,
|
text: note.value!.text,
|
||||||
cw: note.value.cw,
|
cw: note.value!.cw,
|
||||||
files: note.value.files,
|
files: note.value!.files,
|
||||||
fileIds: note.value.fileIds,
|
fileIds: note.value!.fileIds,
|
||||||
emojis: note.value.emojis,
|
emojis: note.value!.emojis,
|
||||||
};
|
};
|
||||||
|
|
||||||
return [now]
|
return [now]
|
||||||
|
@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
|
||||||
_shouldInsertAd_: false,
|
_shouldInsertAd_: false,
|
||||||
files: noteEdit.files,
|
files: noteEdit.files,
|
||||||
fileIds: noteEdit.fileIds,
|
fileIds: noteEdit.fileIds,
|
||||||
emojis: note.value.emojis.concat(noteEdit.emojis),
|
emojis: note.value!.emojis.concat(noteEdit.emojis),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,14 +24,14 @@ class ReactionPicker {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
done: (reaction) => {
|
done: (reaction) => {
|
||||||
this.onChosen!(reaction);
|
this.onChosen?.(reaction);
|
||||||
},
|
},
|
||||||
close: () => {
|
close: () => {
|
||||||
this.manualShowing.value = false;
|
this.manualShowing.value = false;
|
||||||
},
|
},
|
||||||
closed: () => {
|
closed: () => {
|
||||||
this.src.value = null;
|
this.src.value = null;
|
||||||
this.onClosed!();
|
this.onClosed?.();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -356,6 +356,7 @@ export type LiteInstanceMetadata = {
|
||||||
disableGlobalTimeline: boolean;
|
disableGlobalTimeline: boolean;
|
||||||
driveCapacityPerLocalUserMb: number;
|
driveCapacityPerLocalUserMb: number;
|
||||||
driveCapacityPerRemoteUserMb: number;
|
driveCapacityPerRemoteUserMb: number;
|
||||||
|
antennaLimit: number;
|
||||||
enableHcaptcha: boolean;
|
enableHcaptcha: boolean;
|
||||||
hcaptchaSiteKey: string | null;
|
hcaptchaSiteKey: string | null;
|
||||||
enableRecaptcha: boolean;
|
enableRecaptcha: boolean;
|
||||||
|
|
Loading…
Reference in New Issue