Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
commit
a8feed1eff
17 changed files with 338 additions and 16 deletions
|
@ -131,11 +131,20 @@ proxyBypassHosts:
|
||||||
|
|
||||||
# Media Proxy
|
# Media Proxy
|
||||||
# Reference Implementation: https://github.com/misskey-dev/media-proxy
|
# Reference Implementation: https://github.com/misskey-dev/media-proxy
|
||||||
|
# * Deliver a common cache between instances
|
||||||
|
# * Perform image compression (on a different server resource than the main process)
|
||||||
#mediaProxy: https://example.com/proxy
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
# Proxy remote files (default: false)
|
# Proxy remote files (default: false)
|
||||||
|
# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains.
|
||||||
#proxyRemoteFiles: true
|
#proxyRemoteFiles: true
|
||||||
|
|
||||||
|
# Movie Thumbnail Generation URL
|
||||||
|
# There is no reference implementation.
|
||||||
|
# For example, Misskey will point to the following URL:
|
||||||
|
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
|
||||||
|
#videoThumbnailGenerator: https://example.com
|
||||||
|
|
||||||
# Sign to ActivityPub GET request (default: true)
|
# Sign to ActivityPub GET request (default: true)
|
||||||
signToActivityPubGet: true
|
signToActivityPubGet: true
|
||||||
|
|
||||||
|
|
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1 @@
|
||||||
|
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18
|
11
.devcontainer/devcontainer.json
Normal file
11
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "Misskey",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "app",
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||||
|
},
|
||||||
|
"forwardPorts": [3000],
|
||||||
|
"postCreateCommand": ".devcontainer/init.sh"
|
||||||
|
}
|
146
.devcontainer/devcontainer.yml
Normal file
146
.devcontainer/devcontainer.yml
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# Misskey configuration
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
# ┌─────┐
|
||||||
|
#───┘ URL └─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Final accessible URL seen by a user.
|
||||||
|
url: http://127.0.0.1:3000/
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
# ┌───────────────────────┐
|
||||||
|
#───┘ Port and TLS settings └───────────────────────────────────
|
||||||
|
|
||||||
|
#
|
||||||
|
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||||
|
#
|
||||||
|
# +----- https://example.tld/ ------------+
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# +---------------------------------------+
|
||||||
|
#
|
||||||
|
# You need to set up a reverse proxy. (e.g. nginx)
|
||||||
|
# An encrypted connection with HTTPS is highly recommended
|
||||||
|
# because tokens may be transferred in GET requests.
|
||||||
|
|
||||||
|
# The port that your Misskey server should listen on.
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
# ┌──────────────────────────┐
|
||||||
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
|
|
||||||
|
db:
|
||||||
|
host: db
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
# Database name
|
||||||
|
db: misskey
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
user: postgres
|
||||||
|
pass: postgres
|
||||||
|
|
||||||
|
# Whether disable Caching queries
|
||||||
|
#disableCache: true
|
||||||
|
|
||||||
|
# Extra Connection options
|
||||||
|
#extra:
|
||||||
|
# ssl: true
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
#pass: example-pass
|
||||||
|
#prefix: example-prefix
|
||||||
|
#db: 1
|
||||||
|
|
||||||
|
# ┌─────────────────────────────┐
|
||||||
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
#elasticsearch:
|
||||||
|
# host: localhost
|
||||||
|
# port: 9200
|
||||||
|
# ssl: false
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
|
||||||
|
# ┌───────────────┐
|
||||||
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
||||||
|
# You can select the ID generation method.
|
||||||
|
# You don't usually need to change this setting, but you can
|
||||||
|
# change it according to your preferences.
|
||||||
|
|
||||||
|
# Available methods:
|
||||||
|
# aid ... Short, Millisecond accuracy
|
||||||
|
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||||
|
# ulid ... Millisecond accuracy
|
||||||
|
# objectid ... This is left for backward compatibility
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# ID SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
id: 'aid'
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# Whether disable HSTS
|
||||||
|
#disableHsts: true
|
||||||
|
|
||||||
|
# Number of worker processes
|
||||||
|
#clusterLimit: 1
|
||||||
|
|
||||||
|
# Job concurrency per worker
|
||||||
|
# deliverJobConcurrency: 128
|
||||||
|
# inboxJobConcurrency: 16
|
||||||
|
|
||||||
|
# Job rate limiter
|
||||||
|
# deliverJobPerSec: 128
|
||||||
|
# inboxJobPerSec: 16
|
||||||
|
|
||||||
|
# Job attempts
|
||||||
|
# deliverJobMaxAttempts: 12
|
||||||
|
# inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Proxy for HTTP/HTTPS
|
||||||
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
proxyBypassHosts:
|
||||||
|
- api.deepl.com
|
||||||
|
- api-free.deepl.com
|
||||||
|
- www.recaptcha.net
|
||||||
|
- hcaptcha.com
|
||||||
|
- challenges.cloudflare.com
|
||||||
|
|
||||||
|
# Proxy for SMTP/SMTPS
|
||||||
|
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||||
|
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||||
|
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||||
|
|
||||||
|
# Media Proxy
|
||||||
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
|
# Proxy remote files (default: false)
|
||||||
|
#proxyRemoteFiles: true
|
||||||
|
|
||||||
|
# Sign to ActivityPub GET request (default: true)
|
||||||
|
signToActivityPubGet: true
|
||||||
|
|
||||||
|
allowedPrivateNetworks: [
|
||||||
|
'127.0.0.1/32'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Upload or download file size limits (bytes)
|
||||||
|
#maxFileSize: 262144000
|
52
.devcontainer/docker-compose.yml
Normal file
52
.devcontainer/docker-compose.yml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ../..:/workspaces:cached
|
||||||
|
|
||||||
|
command: sleep infinity
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
- external_network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
restart: always
|
||||||
|
image: redis:7-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
volumes:
|
||||||
|
- ../redis:/data
|
||||||
|
healthcheck:
|
||||||
|
test: "redis-cli ping"
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
db:
|
||||||
|
restart: unless-stopped
|
||||||
|
image: postgres:15-alpine
|
||||||
|
networks:
|
||||||
|
- internal_network
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: misskey
|
||||||
|
volumes:
|
||||||
|
- ../db:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
|
||||||
|
interval: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal_network:
|
||||||
|
internal: true
|
||||||
|
external_network:
|
9
.devcontainer/init.sh
Executable file
9
.devcontainer/init.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
git submodule update --init
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||||
|
pnpm build
|
||||||
|
pnpm migrate
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -33,6 +33,7 @@ coverage
|
||||||
!/.config/docker_example.yml
|
!/.config/docker_example.yml
|
||||||
!/.config/docker_example.env
|
!/.config/docker_example.env
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
!/.devcontainer/docker-compose.yml
|
||||||
|
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
|
|
|
@ -12,6 +12,7 @@ You should also include the user name that made the change.
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化
|
- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化
|
||||||
|
- Backend: activitypub情報がcorsでブロックされないようヘッダーを追加
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Client: ユーザーページでアクティビティを見ることができない問題を修正
|
- Client: ユーザーページでアクティビティを見ることができない問題を修正
|
||||||
|
|
|
@ -111,6 +111,25 @@ command.
|
||||||
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
|
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
|
||||||
- Service Worker is watched by esbuild.
|
- Service Worker is watched by esbuild.
|
||||||
|
|
||||||
|
### Dev Container
|
||||||
|
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||||
|
To use Dev Container, open the project directory on VSCode with Dev Containers installed.
|
||||||
|
|
||||||
|
It will run the following command automatically inside the container.
|
||||||
|
``` bash
|
||||||
|
git submodule update --init
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cp .devcontainer/devcontainer.yml .config/default.yml
|
||||||
|
pnpm build
|
||||||
|
pnpm migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
After finishing the migration, run the `pnpm dev` command to start the development server.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
- Test codes are located in [`/packages/backend/test`](/packages/backend/test).
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ export type Source = {
|
||||||
|
|
||||||
mediaProxy?: string;
|
mediaProxy?: string;
|
||||||
proxyRemoteFiles?: boolean;
|
proxyRemoteFiles?: boolean;
|
||||||
|
videoThumbnailGenerator?: string;
|
||||||
|
|
||||||
signToActivityPubGet?: boolean;
|
signToActivityPubGet?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -89,6 +90,7 @@ export type Mixin = {
|
||||||
clientManifestExists: boolean;
|
clientManifestExists: boolean;
|
||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
externalMediaProxyEnabled: boolean;
|
externalMediaProxyEnabled: boolean;
|
||||||
|
videoThumbnailGenerator: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = Source & Mixin;
|
export type Config = Source & Mixin;
|
||||||
|
@ -144,6 +146,10 @@ export function loadConfig() {
|
||||||
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy;
|
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy;
|
||||||
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
|
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
|
||||||
|
|
||||||
|
mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ?
|
||||||
|
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||||
|
: null;
|
||||||
|
|
||||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||||
|
|
||||||
return Object.assign(config, mixin);
|
return Object.assign(config, mixin);
|
||||||
|
|
|
@ -250,6 +250,14 @@ export class DriveService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async generateAlts(path: string, type: string, generateWeb: boolean) {
|
public async generateAlts(path: string, type: string, generateWeb: boolean) {
|
||||||
if (type.startsWith('video/')) {
|
if (type.startsWith('video/')) {
|
||||||
|
if (this.config.videoThumbnailGenerator != null) {
|
||||||
|
// videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ
|
||||||
|
return {
|
||||||
|
webpublic: null,
|
||||||
|
thumbnail: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
|
const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ImageProcessingService } from '@/core/ImageProcessingService.js';
|
||||||
import type { IImage } from '@/core/ImageProcessingService.js';
|
import type { IImage } from '@/core/ImageProcessingService.js';
|
||||||
import { createTempDir } from '@/misc/create-temp.js';
|
import { createTempDir } from '@/misc/create-temp.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoProcessingService {
|
export class VideoProcessingService {
|
||||||
|
@ -41,5 +42,18 @@ export class VideoProcessingService {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getExternalVideoThumbnailUrl(url: string): string | null {
|
||||||
|
if (this.config.videoThumbnailGenerator == null) return null;
|
||||||
|
|
||||||
|
return appendQuery(
|
||||||
|
`${this.config.videoThumbnailGenerator}/thumbnail.webp`,
|
||||||
|
query({
|
||||||
|
thumbnail: '1',
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { deepClone } from '@/misc/clone.js';
|
||||||
import { UtilityService } from '../UtilityService.js';
|
import { UtilityService } from '../UtilityService.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
|
import { DriveFolderEntityService } from './DriveFolderEntityService.js';
|
||||||
|
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||||
|
|
||||||
type PackOptions = {
|
type PackOptions = {
|
||||||
detail?: boolean,
|
detail?: boolean,
|
||||||
|
@ -43,6 +44,7 @@ export class DriveFileEntityService {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private driveFolderEntityService: DriveFolderEntityService,
|
private driveFolderEntityService: DriveFolderEntityService,
|
||||||
|
private videoProcessingService: VideoProcessingService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,40 +74,63 @@ export class DriveFileEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail
|
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string | null {
|
||||||
const proxiedUrl = (url: string) => appendQuery(
|
return appendQuery(
|
||||||
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
||||||
query({
|
query({
|
||||||
url,
|
url,
|
||||||
...(mode ? { [mode]: '1' } : {}),
|
...(mode ? { [mode]: '1' } : {}),
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getThumbnailUrl(file: DriveFile): string | null {
|
||||||
|
if (file.type.startsWith('video')) {
|
||||||
|
if (file.thumbnailUrl) return file.thumbnailUrl;
|
||||||
|
|
||||||
|
if (this.config.videoThumbnailGenerator == null) {
|
||||||
|
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
|
||||||
|
}
|
||||||
|
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
|
// 動画ではなくリモートかつメディアプロキシ
|
||||||
|
return this.getProxiedUrl(file.uri, 'static');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
||||||
|
// リモートかつ期限切れはローカルプロキシを試みる
|
||||||
|
// 従来は/files/${thumbnailAccessKey}にアクセスしていたが、
|
||||||
|
// /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する
|
||||||
|
return this.getProxiedUrl(file.uri, 'static');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = file.webpublicUrl ?? file.url;
|
||||||
|
|
||||||
|
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getPublicUrl(file: DriveFile, mode?: 'avatar'): string | null { // static = thumbnail
|
||||||
// リモートかつメディアプロキシ
|
// リモートかつメディアプロキシ
|
||||||
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||||
if (!(mode === 'static' && file.type.startsWith('video'))) {
|
return this.getProxiedUrl(file.uri, mode);
|
||||||
return proxiedUrl(file.uri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートかつ期限切れはローカルプロキシを試みる
|
// リモートかつ期限切れはローカルプロキシを試みる
|
||||||
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
||||||
const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey;
|
const key = file.webpublicAccessKey;
|
||||||
|
|
||||||
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
||||||
const url = `${this.config.url}/files/${key}`;
|
const url = `${this.config.url}/files/${key}`;
|
||||||
if (mode === 'avatar') return proxiedUrl(file.uri);
|
if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar');
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = file.webpublicUrl ?? file.url;
|
const url = file.webpublicUrl ?? file.url;
|
||||||
|
|
||||||
if (mode === 'static') {
|
|
||||||
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
|
|
||||||
}
|
|
||||||
if (mode === 'avatar') {
|
if (mode === 'avatar') {
|
||||||
return proxiedUrl(url);
|
return this.getProxiedUrl(url, 'avatar');
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -183,7 +208,7 @@ export class DriveFileEntityService {
|
||||||
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),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
thumbnailUrl: this.getThumbnailUrl(file),
|
||||||
comment: file.comment,
|
comment: file.comment,
|
||||||
folderId: file.folderId,
|
folderId: file.folderId,
|
||||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||||
|
@ -218,7 +243,7 @@ export class DriveFileEntityService {
|
||||||
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),
|
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
thumbnailUrl: this.getThumbnailUrl(file),
|
||||||
comment: file.comment,
|
comment: file.comment,
|
||||||
folderId: file.folderId,
|
folderId: file.folderId,
|
||||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||||
|
|
|
@ -441,6 +441,14 @@ export class ActivityPubServerService {
|
||||||
fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||||
fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||||
|
|
||||||
|
fastify.addHook('onRequest', (request, reply, done) => {
|
||||||
|
reply.header('Access-Control-Allow-Headers', 'Accept');
|
||||||
|
reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
|
reply.header('Access-Control-Expose-Headers', 'Vary');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
//#region Routing
|
//#region Routing
|
||||||
// inbox (limit: 64kb)
|
// inbox (limit: 64kb)
|
||||||
fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||||
|
|
|
@ -150,6 +150,12 @@ export class FileServerService {
|
||||||
file.cleanup();
|
file.cleanup();
|
||||||
return await reply.redirect(301, url.toString());
|
return await reply.redirect(301, url.toString());
|
||||||
} else if (file.mime.startsWith('video/')) {
|
} else if (file.mime.startsWith('video/')) {
|
||||||
|
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
||||||
|
if (externalThumbnail) {
|
||||||
|
file.cleanup();
|
||||||
|
return await reply.redirect(301, externalThumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<div v-else ref="rootEl">
|
<div v-else ref="rootEl">
|
||||||
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
||||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
||||||
{{ i18n.ts.loadMore }}
|
{{ i18n.ts.loadMore }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
<MkLoading v-else class="loading"/>
|
<MkLoading v-else class="loading"/>
|
||||||
|
|
|
@ -60,11 +60,17 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
let ads: any[] = $ref([]);
|
let ads: any[] = $ref([]);
|
||||||
|
|
||||||
|
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
|
||||||
|
const localTime = new Date();
|
||||||
|
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||||
|
|
||||||
os.api('admin/ad/list').then(adsResponse => {
|
os.api('admin/ad/list').then(adsResponse => {
|
||||||
ads = adsResponse.map(r => {
|
ads = adsResponse.map(r => {
|
||||||
|
const date = new Date(r.expiresAt);
|
||||||
|
date.setMilliseconds(date.getMilliseconds() - localTimeDiff);
|
||||||
return {
|
return {
|
||||||
...r,
|
...r,
|
||||||
expiresAt: new Date(r.expiresAt).toISOString().slice(0, 16),
|
expiresAt: date.toISOString().slice(0, 16),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue