Merge upstream
This commit is contained in:
commit
bc9acabd6c
65 changed files with 502 additions and 500 deletions
4
.github/unused/test-backend.yml
vendored
4
.github/unused/test-backend.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
|||
- name: Install FFmpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -116,7 +116,7 @@ jobs:
|
|||
with:
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/api-misskey-js.yml
vendored
2
.github/workflows/api-misskey-js.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
|||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -54,7 +54,7 @@ jobs:
|
|||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -78,7 +78,7 @@ jobs:
|
|||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/test-frontend.yml
vendored
2
.github/workflows/test-frontend.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
|||
with:
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/test-misskey-js.yml
vendored
2
.github/workflows/test-misskey-js.yml
vendored
|
@ -36,7 +36,7 @@ jobs:
|
|||
run_install: false
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/test-production.yml
vendored
2
.github/workflows/test-production.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
with:
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
2
.github/workflows/validate-api-json.yml
vendored
2
.github/workflows/validate-api-json.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
with:
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.1.0
|
||||
uses: actions/setup-node@v4.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -115,25 +115,10 @@ pnpm dev
|
|||
command.
|
||||
|
||||
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
|
||||
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
|
||||
- Service Worker is watched by esbuild.
|
||||
- The front end can be viewed by accessing `http://localhost:5173`.
|
||||
- The backend listens on the port configured with `port` in .config/default.yml.
|
||||
If you have not changed it from the default, it will be "http://localhost:3000".
|
||||
If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts.
|
||||
|
||||
### `MK_DEV_PREFER=backend pnpm dev`
|
||||
pnpm dev has another mode with `MK_DEV_PREFER=backend`.
|
||||
|
||||
```
|
||||
MK_DEV_PREFER=backend pnpm dev
|
||||
```
|
||||
|
||||
- This mode is closer to the production environment than the default mode.
|
||||
- Vite runs behind the backend (the backend will proxy Vite at /vite).
|
||||
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
|
||||
- Vite runs behind the backend (the backend will proxy Vite at /vite except for websocket used for HMR).
|
||||
- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml).
|
||||
- To change the port of Vite, specify with `VITE_PORT` environment variable.
|
||||
- HMR may not work in some environments such as Windows.
|
||||
|
||||
### Dev Container
|
||||
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.
|
||||
|
|
|
@ -1530,7 +1530,9 @@ _accountMigration:
|
|||
migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore."
|
||||
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
|
||||
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
|
||||
movedTo: "New account:"
|
||||
movedTo: "Migrated account:"
|
||||
movedToServer: "Migrated server"
|
||||
movedFromServer: "Original server"
|
||||
_achievements:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
|
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
@ -6296,6 +6296,14 @@ export interface Locale extends ILocale {
|
|||
* 移行先のアカウント:
|
||||
*/
|
||||
"movedTo": string;
|
||||
/**
|
||||
* 移行先のサーバー
|
||||
*/
|
||||
"movedToServer": string;
|
||||
/**
|
||||
* 移行元のサーバー
|
||||
*/
|
||||
"movedFromServer": string;
|
||||
};
|
||||
"_achievements": {
|
||||
/**
|
||||
|
|
|
@ -1586,6 +1586,8 @@ _accountMigration:
|
|||
movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。"
|
||||
postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。"
|
||||
movedTo: "移行先のアカウント:"
|
||||
movedToServer: "移行先のサーバー"
|
||||
movedFromServer: "移行元のサーバー"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
|
|
|
@ -1568,6 +1568,8 @@ _accountMigration:
|
|||
movedAndCannotBeUndone: "\n이사한 계정입니다.\n이사는 취소할 수 없습니다."
|
||||
postMigrationNote: "이 계정의 팔로잉 해제는 이사 후 24시간 뒤에 실행됩니다.\n이 계정의 팔로우 및 팔로워 수는 0으로 표시됩니다. 팔로워 해제는 이루어지지 않으므로, 당신의 팔로워는 이 계정의 팔로워 한정 게시물을 계속해서 열람할 수 있습니다."
|
||||
movedTo: "이사할 계정:"
|
||||
movedToServer: "이사한 서버"
|
||||
movedFromServer: "기존 서버"
|
||||
_achievements:
|
||||
earnedAt: "달성 일시"
|
||||
_types:
|
||||
|
|
|
@ -33,16 +33,16 @@
|
|||
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.10.7",
|
||||
"@swc/core-darwin-x64": "1.10.7",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.10.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.10.7",
|
||||
"@swc/core-linux-arm64-musl": "1.10.7",
|
||||
"@swc/core-linux-x64-gnu": "1.10.7",
|
||||
"@swc/core-linux-x64-musl": "1.10.7",
|
||||
"@swc/core-win32-arm64-msvc": "1.10.7",
|
||||
"@swc/core-win32-ia32-msvc": "1.10.7",
|
||||
"@swc/core-win32-x64-msvc": "1.10.7",
|
||||
"@swc/core-darwin-arm64": "1.10.12",
|
||||
"@swc/core-darwin-x64": "1.10.12",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.10.12",
|
||||
"@swc/core-linux-arm64-gnu": "1.10.12",
|
||||
"@swc/core-linux-arm64-musl": "1.10.12",
|
||||
"@swc/core-linux-x64-gnu": "1.10.12",
|
||||
"@swc/core-linux-x64-musl": "1.10.12",
|
||||
"@swc/core-win32-arm64-msvc": "1.10.12",
|
||||
"@swc/core-win32-ia32-msvc": "1.10.12",
|
||||
"@swc/core-win32-x64-msvc": "1.10.12",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.0.9",
|
||||
|
@ -63,11 +63,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@authenio/samlify-node-xmllint": "2.0.0",
|
||||
"@aws-sdk/client-s3": "3.729.0",
|
||||
"@aws-sdk/lib-storage": "3.729.0",
|
||||
"@bull-board/api": "6.6.2",
|
||||
"@bull-board/fastify": "6.6.2",
|
||||
"@bull-board/ui": "6.6.2",
|
||||
"@aws-sdk/client-s3": "3.740.0",
|
||||
"@aws-sdk/lib-storage": "3.740.0",
|
||||
"@bull-board/api": "6.7.4",
|
||||
"@bull-board/fastify": "6.7.4",
|
||||
"@bull-board/ui": "6.7.4",
|
||||
"@discordapp/twemoji": "15.1.0",
|
||||
"@elastic/elasticsearch": "8.17.0",
|
||||
"@fastify/accepts": "5.0.2",
|
||||
|
@ -76,21 +76,21 @@
|
|||
"@fastify/express": "4.0.2",
|
||||
"@fastify/formbody": "8.0.2",
|
||||
"@fastify/http-proxy": "11.0.1",
|
||||
"@fastify/multipart": "9.0.2",
|
||||
"@fastify/multipart": "9.0.3",
|
||||
"@fastify/static": "8.0.4",
|
||||
"@fastify/view": "10.0.2",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||
"@napi-rs/canvas": "0.1.65",
|
||||
"@nestjs/common": "10.4.15",
|
||||
"@nestjs/core": "10.4.15",
|
||||
"@nestjs/testing": "10.4.15",
|
||||
"@nestjs/common": "11.0.7",
|
||||
"@nestjs/core": "11.0.7",
|
||||
"@nestjs/testing": "11.0.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "13.1.0",
|
||||
"@simplewebauthn/server": "13.1.1",
|
||||
"@sinonjs/fake-timers": "11.3.1",
|
||||
"@smithy/node-http-handler": "4.0.2",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/core": "1.10.7",
|
||||
"@swc/core": "1.10.12",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
|
@ -99,7 +99,7 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.3",
|
||||
"bullmq": "5.34.10",
|
||||
"bullmq": "5.39.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "10.0.3",
|
||||
"chalk": "5.4.1",
|
||||
|
@ -114,7 +114,7 @@
|
|||
"fastify-http-errors-enhanced": "6.0.1",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "20.0.0",
|
||||
"file-type": "20.0.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.1",
|
||||
"got": "14.4.5",
|
||||
|
@ -131,7 +131,7 @@
|
|||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.3",
|
||||
"jsrsasign": "11.1.0",
|
||||
"meilisearch": "0.48.0",
|
||||
"meilisearch": "0.48.2",
|
||||
"mfm-js": "0.24.0",
|
||||
"microformats-parser": "2.0.2",
|
||||
"mime-types": "2.1.35",
|
||||
|
@ -142,7 +142,7 @@
|
|||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-forge": "1.3.1",
|
||||
"nodemailer": "6.9.16",
|
||||
"nodemailer": "6.10.0",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
|
@ -191,7 +191,7 @@
|
|||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.4.15",
|
||||
"@nestjs/platform-express": "11.0.7",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.3",
|
||||
|
@ -208,14 +208,14 @@
|
|||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/node-forge": "1.3.11",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.11.10",
|
||||
"@types/pg": "8.11.11",
|
||||
"@types/psl": "1.1.3",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
|
@ -231,7 +231,7 @@
|
|||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
|
@ -243,7 +243,7 @@
|
|||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.9",
|
||||
"pid-port": "1.0.0",
|
||||
"pid-port": "1.0.2",
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -356,7 +356,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
* 指定ユーザーのバッジロール一覧取得
|
||||
*/
|
||||
@bindThis
|
||||
public async getUserBadgeRoles(userId: MiUser['id']) {
|
||||
public async getUserBadgeRoles(userId: MiUser['id'], publicOnly: boolean) {
|
||||
const now = Date.now();
|
||||
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||
// 期限切れのロールを除外
|
||||
|
@ -368,12 +368,25 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
if (badgeCondRoles.length > 0) {
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
return this.sortAndMapBadgeRoles([...assignedBadgeRoles, ...matchedBadgeCondRoles], publicOnly);
|
||||
} else {
|
||||
return assignedBadgeRoles;
|
||||
return this.sortAndMapBadgeRoles(assignedBadgeRoles, publicOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private sortAndMapBadgeRoles(roles: MiRole[], publicOnly: boolean) {
|
||||
return roles
|
||||
.filter((r) => r.isPublic || !publicOnly)
|
||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||
.map((r) => ({
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
behavior: r.badgeBehavior ?? undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||
import { appendQuery, omitHttps, query } from '@/misc/prelude/url.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
|
@ -77,9 +77,8 @@ export class DriveFileEntityService {
|
|||
@bindThis
|
||||
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
|
||||
return appendQuery(
|
||||
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
||||
`${this.config.mediaProxy}/${mode ?? 'image'}/${encodeURIComponent(omitHttps(url))}`,
|
||||
query({
|
||||
url,
|
||||
...(mode ? { [mode]: '1' } : {}),
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -518,16 +518,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
} : undefined) : undefined,
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
badgeRoles: this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||
.filter((r) => r.isPublic || iAmModerator)
|
||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||
.map((r) => ({
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
behavior: r.badgeBehavior ?? undefined,
|
||||
})),
|
||||
),
|
||||
badgeRoles: this.roleService.getUserBadgeRoles(user.id, !iAmModerator),
|
||||
|
||||
...(isDetailed ? {
|
||||
url: profile?.url,
|
||||
|
|
|
@ -14,10 +14,16 @@ export function query(obj: Record<string, unknown>): string {
|
|||
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
|
||||
|
||||
return Object.entries(params)
|
||||
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
|
||||
.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function appendQuery(url: string, query: string): string {
|
||||
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
|
||||
}
|
||||
|
||||
export function omitHttps(url: string): string {
|
||||
if (url.startsWith('https://')) return url.slice(8);
|
||||
if (url.startsWith('https%3A%2F%2F')) return url.slice(14);
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ class MyCustomLogger implements Logger {
|
|||
|
||||
@bindThis
|
||||
public logQuery(query: string, parameters?: any[]) {
|
||||
sqlLogger.info(this.highlight(query));
|
||||
sqlLogger.debug(this.highlight(query));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -26,6 +26,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
|
|||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { appendQuery, omitHttps, query } from '@/misc/prelude/url.js';
|
||||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
|
@ -35,6 +36,16 @@ const _dirname = dirname(_filename);
|
|||
|
||||
const assets = `${_dirname}/../../server/file/assets/`;
|
||||
|
||||
interface TransformQuery {
|
||||
origin?: string;
|
||||
fallback?: string;
|
||||
emoji?: string;
|
||||
avatar?: string;
|
||||
static?: string;
|
||||
preview?: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class FileServerService {
|
||||
private logger: Logger;
|
||||
|
@ -87,10 +98,18 @@ export class FileServerService {
|
|||
done();
|
||||
});
|
||||
|
||||
fastify.get<{
|
||||
Params: { type: string; url: string; };
|
||||
Querystring: { url?: string; } & TransformQuery;
|
||||
}>('/proxy/:type/:url', async (request, reply) => {
|
||||
return await this.proxyHandler(request, reply)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
||||
fastify.get<{
|
||||
Params: { url: string; };
|
||||
Querystring: { url?: string; };
|
||||
}>('/proxy/:url*', async (request, reply) => {
|
||||
Querystring: { url?: string; } & TransformQuery;
|
||||
}>('/proxy/:url', async (request, reply) => {
|
||||
return await this.proxyHandler(request, reply)
|
||||
.catch(err => this.errorHandler(request, reply, err));
|
||||
});
|
||||
|
@ -142,12 +161,15 @@ export class FileServerService {
|
|||
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/static.webp`);
|
||||
url.searchParams.set('url', file.url);
|
||||
url.searchParams.set('static', '1');
|
||||
const url = appendQuery(
|
||||
`${this.config.mediaProxy}/static/${encodeURIComponent(omitHttps(file.url))}`,
|
||||
query({
|
||||
static: '1',
|
||||
}),
|
||||
);
|
||||
|
||||
file.cleanup();
|
||||
return await reply.redirect(url.toString(), 301);
|
||||
return await reply.redirect(url, 301);
|
||||
} else if (file.mime.startsWith('video/')) {
|
||||
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
|
||||
if (externalThumbnail) {
|
||||
|
@ -163,11 +185,10 @@ export class FileServerService {
|
|||
if (['image/svg+xml'].includes(file.mime)) {
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
|
||||
url.searchParams.set('url', file.url);
|
||||
const url = `${this.config.mediaProxy}/svg/${encodeURIComponent(omitHttps(file.url))}`;
|
||||
|
||||
file.cleanup();
|
||||
return await reply.redirect(url.toString(), 301);
|
||||
return await reply.redirect(url, 301);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,30 +312,43 @@ export class FileServerService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) {
|
||||
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url;
|
||||
private async proxyHandler(request: FastifyRequest<{ Params: { type?: string; url: string; }; Querystring: { url?: string; } & TransformQuery; }>, reply: FastifyReply) {
|
||||
let url: string;
|
||||
if ('url' in request.query && request.query.url) {
|
||||
url = request.query.url;
|
||||
} else {
|
||||
url = request.params.url;
|
||||
}
|
||||
|
||||
if (typeof url !== 'string') {
|
||||
// noinspection HttpUrlsUsage
|
||||
if (url
|
||||
&& !url.startsWith('http://')
|
||||
&& !url.startsWith('https://')
|
||||
) {
|
||||
url = 'https://' + url;
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
// アバタークロップなど、どうしてもオリジンである必要がある場合
|
||||
const mustOrigin = 'origin' in request.query;
|
||||
const transformQuery = request.query as TransformQuery;
|
||||
|
||||
if (this.config.externalMediaProxyEnabled && !mustOrigin) {
|
||||
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
|
||||
|
||||
for (const [key, value] of Object.entries(request.query)) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
const redirectUrl = appendQuery(
|
||||
`${this.config.mediaProxy}/redirect/${encodeURIComponent(omitHttps(url))}`,
|
||||
query(transformQuery as Record<string, string>),
|
||||
);
|
||||
|
||||
return reply.redirect(
|
||||
url.toString(),
|
||||
redirectUrl,
|
||||
301,
|
||||
);
|
||||
}
|
||||
|
@ -344,11 +378,11 @@ export class FileServerService {
|
|||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
|
||||
|
||||
if (
|
||||
'emoji' in request.query ||
|
||||
'avatar' in request.query ||
|
||||
'static' in request.query ||
|
||||
'preview' in request.query ||
|
||||
'badge' in request.query
|
||||
'emoji' in transformQuery ||
|
||||
'avatar' in transformQuery ||
|
||||
'static' in transformQuery ||
|
||||
'preview' in transformQuery ||
|
||||
'badge' in transformQuery
|
||||
) {
|
||||
if (!isConvertibleImage) {
|
||||
// 画像でないなら404でお茶を濁す
|
||||
|
@ -357,17 +391,17 @@ export class FileServerService {
|
|||
}
|
||||
|
||||
let image: IImageStreamable | null = null;
|
||||
if ('emoji' in request.query || 'avatar' in request.query) {
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
if ('emoji' in transformQuery || 'avatar' in transformQuery) {
|
||||
if (!isAnimationConvertibleImage && !('static' in transformQuery)) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
} else {
|
||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
|
||||
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in transformQuery) }))
|
||||
.resize({
|
||||
height: 'emoji' in request.query ? 128 : 320,
|
||||
height: 'emoji' in transformQuery ? 128 : 320,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault);
|
||||
|
@ -378,11 +412,11 @@ export class FileServerService {
|
|||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query) {
|
||||
} else if ('static' in transformQuery) {
|
||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 498, 422);
|
||||
} else if ('preview' in request.query) {
|
||||
} else if ('preview' in transformQuery) {
|
||||
image = this.imageProcessingService.convertSharpToWebpStream(await sharpBmp(file.path, file.mime), 200, 200);
|
||||
} else if ('badge' in request.query) {
|
||||
} else if ('badge' in transformQuery) {
|
||||
const mask = (await sharpBmp(file.path, file.mime))
|
||||
.resize(96, 96, {
|
||||
fit: 'contain',
|
||||
|
|
|
@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import type Logger from '@/logger.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||
import { appendQuery, omitHttps, query } from '@/misc/prelude/url.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -77,8 +78,9 @@ export class ServerService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: true,
|
||||
logger: false,
|
||||
maxParamLength: 1024,
|
||||
trustProxy: true,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
||||
|
@ -162,22 +164,28 @@ export class ServerService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
let url: string;
|
||||
if ('badge' in request.query) {
|
||||
url = new URL(`${this.config.mediaProxy}/emoji.png`);
|
||||
url = appendQuery(
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||
url.searchParams.set('badge', '1');
|
||||
`${this.config.mediaProxy}/emoji/${encodeURIComponent(omitHttps(emoji.publicUrl || emoji.originalUrl))}`,
|
||||
query({
|
||||
badge: '1',
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
|
||||
url = appendQuery(
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||
url.searchParams.set('emoji', '1');
|
||||
if ('static' in request.query) url.searchParams.set('static', '1');
|
||||
`${this.config.mediaProxy}/emoji/${encodeURIComponent(omitHttps(emoji.publicUrl || emoji.originalUrl))}`,
|
||||
query({
|
||||
emoji: '1',
|
||||
...('static' in request.query ? { static: '1' } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return reply.redirect(
|
||||
url.toString(),
|
||||
url,
|
||||
301,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { UserService } from '@/core/UserService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/Connection.js';
|
||||
|
@ -40,6 +41,7 @@ export class StreamingApiServerService {
|
|||
private channelsService: ChannelsService,
|
||||
private notificationService: NotificationService,
|
||||
private usersService: UserService,
|
||||
private roleService: RoleService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
) {
|
||||
}
|
||||
|
@ -99,6 +101,7 @@ export class StreamingApiServerService {
|
|||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
this.roleService,
|
||||
this.channelFollowingService,
|
||||
user, app,
|
||||
);
|
||||
|
|
|
@ -62,6 +62,8 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
movedFromId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
movedToId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
from: { type: 'string', enum: ['local', 'remote', 'all'], nullable: true },
|
||||
to: { type: 'string', enum: ['local', 'remote', 'all'], nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -86,6 +88,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.andWhere('accountMoveLogs.movedToId = :movedToId', { movedToId: ps.movedToId });
|
||||
}
|
||||
|
||||
if (ps.from != null || ps.to != null) {
|
||||
query
|
||||
.innerJoin('accountMoveLogs.movedFrom', 'movedFrom')
|
||||
.innerJoin('accountMoveLogs.movedTo', 'movedTo');
|
||||
|
||||
if (ps.from === 'local') {
|
||||
query.andWhere('movedFrom.host IS NULL');
|
||||
}
|
||||
|
||||
if (ps.from === 'remote') {
|
||||
query.andWhere('movedFrom.host IS NOT NULL');
|
||||
}
|
||||
|
||||
if (ps.to === 'local') {
|
||||
query.andWhere('movedTo.host IS NULL');
|
||||
}
|
||||
|
||||
if (ps.to === 'remote') {
|
||||
query.andWhere('movedTo.host IS NOT NULL');
|
||||
}
|
||||
}
|
||||
|
||||
const accountMoveLogs = await query.limit(ps.limit).getMany();
|
||||
|
||||
return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me);
|
||||
|
|
|
@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { MiFollowing, MiUserProfile } from '@/models/_.js';
|
||||
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
|
@ -31,6 +32,7 @@ export default class Connection {
|
|||
private subscribingNotes: any = {};
|
||||
private cachedNotes: Packed<'Note'>[] = [];
|
||||
public userProfile: MiUserProfile | null = null;
|
||||
public isModerator = false;
|
||||
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
|
||||
public followingChannels: Set<string> = new Set();
|
||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||
|
@ -45,6 +47,7 @@ export default class Connection {
|
|||
private noteReadService: NoteReadService,
|
||||
private notificationService: NotificationService,
|
||||
private cacheService: CacheService,
|
||||
private roleService: RoleService,
|
||||
private channelFollowingService: ChannelFollowingService,
|
||||
|
||||
user: MiUser | null | undefined,
|
||||
|
@ -80,6 +83,7 @@ export default class Connection {
|
|||
public async init() {
|
||||
if (this.user != null) {
|
||||
await this.fetch();
|
||||
this.isModerator = await this.roleService.isModerator(this.user);
|
||||
|
||||
if (!this.fetchIntervalId) {
|
||||
this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
|
||||
|
|
|
@ -30,6 +30,10 @@ export default abstract class Channel {
|
|||
return this.connection.userProfile;
|
||||
}
|
||||
|
||||
protected get iAmModerator() {
|
||||
return this.connection.isModerator;
|
||||
}
|
||||
|
||||
protected get following() {
|
||||
return this.connection.following;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -18,6 +19,7 @@ class AntennaChannel extends Channel {
|
|||
private minimize: boolean;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
|
@ -64,11 +66,14 @@ class AntennaChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
@ -92,6 +97,7 @@ export class AntennaChannelService implements MiChannelService<true> {
|
|||
public readonly kind = AntennaChannel.kind;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
}
|
||||
|
@ -99,6 +105,7 @@ export class AntennaChannelService implements MiChannelService<true> {
|
|||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): AntennaChannel {
|
||||
return new AntennaChannel(
|
||||
this.roleService,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -18,6 +19,7 @@ class ChannelChannel extends Channel {
|
|||
private minimize: boolean;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
|
@ -70,11 +72,14 @@ class ChannelChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
@ -95,6 +100,7 @@ export class ChannelChannelService implements MiChannelService<false> {
|
|||
public readonly kind = ChannelChannel.kind;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
}
|
||||
|
@ -102,6 +108,7 @@ export class ChannelChannelService implements MiChannelService<false> {
|
|||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): ChannelChannel {
|
||||
return new ChannelChannel(
|
||||
this.roleService,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -100,11 +100,14 @@ class GlobalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -20,6 +21,7 @@ class HomeTimelineChannel extends Channel {
|
|||
private minimize: boolean;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
|
@ -101,11 +103,14 @@ class HomeTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
@ -126,6 +131,7 @@ export class HomeTimelineChannelService implements MiChannelService<true> {
|
|||
public readonly kind = HomeTimelineChannel.kind;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
}
|
||||
|
@ -133,6 +139,7 @@ export class HomeTimelineChannelService implements MiChannelService<true> {
|
|||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): HomeTimelineChannel {
|
||||
return new HomeTimelineChannel(
|
||||
this.roleService,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -117,11 +117,14 @@ class HybridTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -100,11 +100,14 @@ class LocalTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
@ -19,8 +19,8 @@ class RoleTimelineChannel extends Channel {
|
|||
private minimize: boolean;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
|
@ -42,7 +42,7 @@ class RoleTimelineChannel extends Channel {
|
|||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
|
||||
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
|
||||
if (!(await this.roleService.isExplorable({ id: this.roleId }))) {
|
||||
return;
|
||||
}
|
||||
if (note.visibility !== 'public') return;
|
||||
|
@ -86,11 +86,14 @@ class RoleTimelineChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
@ -114,16 +117,16 @@ export class RoleTimelineChannelService implements MiChannelService<false> {
|
|||
public readonly kind = RoleTimelineChannel.kind;
|
||||
|
||||
constructor(
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
|
||||
return new RoleTimelineChannel(
|
||||
this.roleService,
|
||||
this.noteEntityService,
|
||||
this.roleservice,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
|
@ -26,6 +27,7 @@ class UserListChannel extends Channel {
|
|||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
|
||||
id: string,
|
||||
|
@ -135,11 +137,14 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
|
||||
if (this.minimize && ['public', 'home'].includes(note.visibility)) {
|
||||
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
|
||||
|
||||
this.send('note', {
|
||||
id: note.id, myReaction: note.myReaction,
|
||||
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
|
||||
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
|
||||
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
|
||||
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
|
||||
});
|
||||
} else {
|
||||
this.send('note', note);
|
||||
|
@ -169,6 +174,7 @@ export class UserListChannelService implements MiChannelService<false> {
|
|||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
) {
|
||||
}
|
||||
|
@ -178,6 +184,7 @@ export class UserListChannelService implements MiChannelService<false> {
|
|||
return new UserListChannel(
|
||||
this.userListsRepository,
|
||||
this.userListMembershipsRepository,
|
||||
this.roleService,
|
||||
this.noteEntityService,
|
||||
id,
|
||||
connection,
|
||||
|
|
|
@ -303,9 +303,12 @@ export class ClientServerService {
|
|||
done();
|
||||
});
|
||||
} else {
|
||||
const configUrl = new URL(this.config.url);
|
||||
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
|
||||
|
||||
const port = (process.env.VITE_PORT ?? '5173');
|
||||
fastify.register(fastifyProxy, {
|
||||
upstream: 'http://localhost:' + port,
|
||||
upstream: urlOriginWithoutPort + ':' + port,
|
||||
prefix: '/vite',
|
||||
rewritePrefix: '/vite',
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
|
|||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import { appendQuery, omitHttps, query } from '@/misc/prelude/url.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
@ -36,14 +36,15 @@ export class UrlPreviewService {
|
|||
|
||||
@bindThis
|
||||
private wrap(url?: string | null): string | null {
|
||||
return url != null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `${this.config.mediaProxy}/preview.webp?${query({
|
||||
url,
|
||||
if (!url) return null;
|
||||
if (!RegExp(/^https?:\/\//).exec(url)) return url;
|
||||
|
||||
return appendQuery(
|
||||
`${this.config.mediaProxy}/preview/${encodeURIComponent(omitHttps(url))}`,
|
||||
query({
|
||||
preview: '1',
|
||||
})}`
|
||||
: url
|
||||
: null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -36,7 +36,8 @@ html
|
|||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons.min.css`)
|
||||
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons-outline.min.css`)
|
||||
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons-filled.min.css`)
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true" as="image" type="image/png" crossorigin="anonymous">
|
||||
<link rel="preload" href="https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.5.0/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
|
||||
<style>
|
||||
html {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||
"build": "vite build",
|
||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||
|
@ -27,7 +26,7 @@
|
|||
"@rollup/plugin-typescript": "12.1.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.28.1",
|
||||
"@tabler/icons-webfont": "3.29.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
|
@ -41,7 +40,7 @@
|
|||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "11.24.0",
|
||||
"chromatic": "11.25.2",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.0.0-rc.0",
|
||||
"date-fns": "4.1.0",
|
||||
|
@ -59,13 +58,13 @@
|
|||
"misskey-reversi": "workspace:*",
|
||||
"photoswipe": "5.4.4",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.30.1",
|
||||
"rollup": "4.34.0",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sass": "1.83.4",
|
||||
"shiki": "1.27.2",
|
||||
"shiki": "2.2.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.172.0",
|
||||
"three": "0.173.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.10",
|
||||
|
@ -73,7 +72,7 @@
|
|||
"typescript": "5.7.3",
|
||||
"uuid": "11.0.5",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.0.7",
|
||||
"vite": "6.0.11",
|
||||
"vue": "3.5.13",
|
||||
"vue-gtag": "2.0.1",
|
||||
"vuedraggable": "next",
|
||||
|
@ -82,49 +81,49 @@
|
|||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
|
||||
"@storybook/addon-actions": "8.5.0",
|
||||
"@storybook/addon-essentials": "8.5.0",
|
||||
"@storybook/addon-interactions": "8.5.0",
|
||||
"@storybook/addon-links": "8.5.0",
|
||||
"@storybook/addon-mdx-gfm": "8.5.0",
|
||||
"@storybook/addon-storysource": "8.5.0",
|
||||
"@storybook/blocks": "8.5.0",
|
||||
"@storybook/components": "8.5.0",
|
||||
"@storybook/core-events": "8.5.0",
|
||||
"@storybook/manager-api": "8.5.0",
|
||||
"@storybook/preview-api": "8.5.0",
|
||||
"@storybook/react": "8.5.0",
|
||||
"@storybook/react-vite": "8.5.0",
|
||||
"@storybook/test": "8.5.0",
|
||||
"@storybook/theming": "8.5.0",
|
||||
"@storybook/types": "8.5.0",
|
||||
"@storybook/vue3": "8.5.0",
|
||||
"@storybook/vue3-vite": "8.5.0",
|
||||
"@storybook/addon-actions": "8.5.2",
|
||||
"@storybook/addon-essentials": "8.5.2",
|
||||
"@storybook/addon-interactions": "8.5.2",
|
||||
"@storybook/addon-links": "8.5.2",
|
||||
"@storybook/addon-mdx-gfm": "8.5.2",
|
||||
"@storybook/addon-storysource": "8.5.2",
|
||||
"@storybook/blocks": "8.5.2",
|
||||
"@storybook/components": "8.5.2",
|
||||
"@storybook/core-events": "8.5.2",
|
||||
"@storybook/manager-api": "8.5.2",
|
||||
"@storybook/preview-api": "8.5.2",
|
||||
"@storybook/react": "8.5.2",
|
||||
"@storybook/react-vite": "8.5.2",
|
||||
"@storybook/test": "8.5.2",
|
||||
"@storybook/theming": "8.5.2",
|
||||
"@storybook/types": "8.5.2",
|
||||
"@storybook/vue3": "8.5.2",
|
||||
"@storybook/vue3-vite": "8.5.2",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "^1.6.4",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.6",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/three": "0.172.0",
|
||||
"@types/three": "0.173.0",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/ws": "8.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"@vitest/coverage-v8": "2.1.8",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"acorn": "8.14.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.17.0",
|
||||
"cypress": "14.0.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"happy-dom": "16.6.0",
|
||||
"happy-dom": "16.8.1",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.7.0",
|
||||
|
@ -134,10 +133,10 @@
|
|||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"storybook": "8.5.0",
|
||||
"storybook": "8.5.2",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "2.1.8",
|
||||
"vitest": "3.0.4",
|
||||
"vitest-fetch-mock": "0.3.0",
|
||||
"vue-component-type-helpers": "2.2.0",
|
||||
"vue-eslint-parser": "9.4.3",
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// devモードで起動される際(index.htmlを使うとき)はrouterが暴発してしまってうまく読み込めない。
|
||||
// よって、devモードとして起動されるときはビルド時に組み込む形としておく。
|
||||
// (pnpm start時はpugファイルの中で静的リソースとして読み込むようになっており、この問題は起こっていない)
|
||||
import '@tabler/icons-webfont/dist/tabler-icons.scss';
|
||||
|
||||
await main();
|
||||
|
||||
import('@/_boot_.js');
|
||||
|
||||
/**
|
||||
* backend/src/server/web/boot.jsで差し込まれている起動処理のうち、最低限必要なものを模倣するための処理
|
||||
*/
|
||||
async function main() {
|
||||
const forceError = localStorage.getItem('forceError');
|
||||
if (forceError != null) {
|
||||
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
|
||||
}
|
||||
|
||||
const metaRes = await window.fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
credentials: 'omit',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (metaRes.status !== 200) {
|
||||
renderError('META_FETCH');
|
||||
return;
|
||||
}
|
||||
const meta = await metaRes.json();
|
||||
//#region Detect language & fetch translations
|
||||
|
||||
// dev-modeの場合は常に取り直す
|
||||
const supportedLangs = _LANGS_.map(it => it[0]);
|
||||
let lang: string | null | undefined = localStorage.getItem('lang');
|
||||
if (lang == null || !supportedLangs.includes(lang)) {
|
||||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'ko-KR';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:今のままだと言語ファイル変更後はpnpm devをリスタートする必要があるので、chokidarを使ったり等で対応できるようにする
|
||||
const locale = _LANGS_FULL_.find(it => it[0] === lang);
|
||||
localStorage.setItem('lang', lang);
|
||||
localStorage.setItem('locale', JSON.stringify(locale[1]));
|
||||
localStorage.setItem('localeVersion', _VERSION_);
|
||||
//#endregion
|
||||
|
||||
//#region Theme
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
|
||||
// HTMLの theme-color 適用
|
||||
if (k === 'htmlThemeColor') {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const colorScheme = localStorage.getItem('colorScheme');
|
||||
if (colorScheme) {
|
||||
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const fontSize = localStorage.getItem('fontSize');
|
||||
if (fontSize) {
|
||||
document.documentElement.classList.add('f-' + fontSize);
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem('useSystemFont');
|
||||
if (useSystemFont) {
|
||||
document.documentElement.classList.add('useSystemFont');
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper') ?? meta.backgroundImageUrl;
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.background = `url(${wallpaper}) no-repeat fixed center`;
|
||||
document.documentElement.style.backgroundSize = 'cover';
|
||||
}
|
||||
|
||||
const customCss = localStorage.getItem('customCss');
|
||||
if (customCss && customCss.length > 0) {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = customCss;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
function renderError(code: string, details?: string) {
|
||||
console.log(code, details);
|
||||
}
|
|
@ -43,6 +43,12 @@ export async function signout() {
|
|||
if (!$i) return;
|
||||
|
||||
waiting();
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const cookieName = cookie.split('=')[0].trim();
|
||||
if (cookieName === 'token') {
|
||||
document.cookie = `${cookieName}=; max-age=0; path=/`;
|
||||
}
|
||||
});
|
||||
miLocalStorage.removeItem('account');
|
||||
await removeAccount($i.id);
|
||||
const accounts = await getAccounts();
|
||||
|
|
|
@ -100,6 +100,11 @@ export async function common(createVue: () => App<Element>) {
|
|||
// タッチデバイスでCSSの:hoverを機能させる
|
||||
document.addEventListener('touchend', () => {}, { passive: true });
|
||||
|
||||
// URLに#pswpを含む場合は取り除く
|
||||
if (location.hash === '#pswp') {
|
||||
history.replaceState(null, '', location.href.replace('#pswp', ''));
|
||||
}
|
||||
|
||||
// 一斉リロード
|
||||
reloadChannel.addEventListener('message', path => {
|
||||
if (path !== null) location.href = path;
|
||||
|
|
|
@ -26,22 +26,34 @@ let prevTime = 0;
|
|||
let angle1 = 0;
|
||||
let angle2 = 0;
|
||||
|
||||
let scene, camera, renderer, width, height, uniforms, texture, maskTexture, dataArray1, dataArray2, dataArrayOrigin, bufferLength: number;
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera();
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
|
||||
let width: number;
|
||||
let height: number;
|
||||
let uniforms: { [p: string]: THREE.IUniform };
|
||||
let texture: THREE.Texture;
|
||||
let maskTexture: THREE.Texture;
|
||||
let dataArray1: Uint8Array;
|
||||
let dataArray2: Uint8Array;
|
||||
let dataArrayOrigin: Uint8Array;
|
||||
let bufferLength: number;
|
||||
|
||||
const init = () => {
|
||||
const parent = container.value ?? { offsetWidth: 0 };
|
||||
width = parent.offsetWidth;
|
||||
height = Math.floor(width * 9 / 16);
|
||||
|
||||
scene = new THREE.Scene();
|
||||
camera = new THREE.OrthographicCamera();
|
||||
scene.clear();
|
||||
camera.clear();
|
||||
|
||||
camera.left = width / -2;
|
||||
camera.right = width / 2;
|
||||
camera.top = height / 2;
|
||||
camera.bottom = height / -2;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(width, height);
|
||||
|
||||
if (container.value) {
|
||||
|
@ -176,7 +188,7 @@ const animate = (time) => {
|
|||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
const resize = () => {
|
||||
const parent = container.value ?? { offsetWidth: 0 };
|
||||
width = parent.offsetWidth;
|
||||
height = Math.floor(width * 9 / 16);
|
||||
|
@ -189,17 +201,25 @@ const onResize = () => {
|
|||
uniforms.resolution.value.set(width, height);
|
||||
};
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
resize();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick().then(() => {
|
||||
init();
|
||||
window.addEventListener('resize', onResize);
|
||||
resize();
|
||||
});
|
||||
|
||||
if (!container.value) return;
|
||||
ro.observe(container.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (renderer) {
|
||||
renderer.dispose();
|
||||
}
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</audio>
|
||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
|
||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||
<i v-else class="ti ti-player-play-filled"></i>
|
||||
<i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
|
||||
<i v-else class="ti-filled ti-filled-player-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||
|
|
|
@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<source :src="video.url">
|
||||
</video>
|
||||
<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti ti-player-play-filled"></i></button>
|
||||
<button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti-filled ti-filled-player-play"></i></button>
|
||||
<div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
|
@ -75,8 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="videoControls" :class="$style.videoControls" @click.self="togglePlayPause">
|
||||
<div :class="[$style.controlsChild, $style.controlsLeft]">
|
||||
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
|
||||
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
|
||||
<i v-else class="ti ti-player-play-filled"></i>
|
||||
<i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
|
||||
<i v-else class="ti-filled ti-filled-player-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="[$style.controlsChild, $style.controlsRight]">
|
||||
|
|
|
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti-filled ti-filled-heart" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null " class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
|
@ -594,7 +594,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||
contain: content;
|
||||
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto none auto 128px;
|
||||
contain-intrinsic-size: none auto 128px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
|
|
@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti-filled ti-filled-heart" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null " class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
|
|
|
@ -220,7 +220,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||
contain: content;
|
||||
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto none auto 100px;
|
||||
contain-intrinsic-size: none auto 100px;
|
||||
}
|
||||
|
||||
.head {
|
||||
|
|
|
@ -24,7 +24,7 @@ import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
|||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { Paging } from '@/components/MkPagination.vue';
|
||||
|
@ -108,7 +108,6 @@ async function prepend(data) {
|
|||
let connection: Misskey.ChannelConnection | null = null;
|
||||
let connection2: Misskey.ChannelConnection | null = null;
|
||||
let paginationQuery: Paging | null = null;
|
||||
const minimize = !iAmModerator;
|
||||
|
||||
const stream = useStream();
|
||||
|
||||
|
@ -117,13 +116,13 @@ function connectChannel() {
|
|||
if (props.antenna == null) return;
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'home') {
|
||||
connection = stream.useChannel('homeTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
connection2 = stream.useChannel('main');
|
||||
} else if (props.src === 'local') {
|
||||
|
@ -131,27 +130,27 @@ function connectChannel() {
|
|||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'media') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: true,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'social') {
|
||||
connection = stream.useChannel('hybridTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'global') {
|
||||
connection = stream.useChannel('globalTimeline', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'mentions') {
|
||||
connection = stream.useChannel('main');
|
||||
|
@ -170,19 +169,19 @@ function connectChannel() {
|
|||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'channel') {
|
||||
if (props.channel == null) return;
|
||||
connection = stream.useChannel('channel', {
|
||||
channelId: props.channel,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
} else if (props.src === 'role') {
|
||||
if (props.role == null) return;
|
||||
connection = stream.useChannel('roleTimeline', {
|
||||
roleId: props.role,
|
||||
minimize: minimize,
|
||||
minimize: true,
|
||||
});
|
||||
}
|
||||
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<!--
|
||||
開発モードのviteはこのファイルを起点にサーバーを起動します。
|
||||
このファイルに書かれた [t]js のリンクと (s)cssのリンクと、その依存関係にあるファイルはビルドされます
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>[DEV] Loading...</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy-Report-Only"
|
||||
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/ https://fonts.gstatic.com/ https://www.google-analytics.com/ https://www.googletagmanager.com/;
|
||||
worker-src 'self';
|
||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://www.googletagmanager.com https://esm.sh;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://www.googletagmanager.com;
|
||||
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://fonts.gstatic.com https://www.googletagmanager.com;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com https://www.google-analytics.com https://analytics.google.com;
|
||||
frame-src *;"
|
||||
/>
|
||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="misskey_app"></div>
|
||||
<script type="module" src="./_dev_boot_.ts"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -18,6 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>
|
||||
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
|
||||
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
|
||||
<MkInput v-if="ad.id" v-model="ad.id" :readonly="true">
|
||||
<template #label>ID</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
|
|
|
@ -2,14 +2,30 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkInput v-model="movedFromId" style="margin: 0; flex: 1;">
|
||||
<div style="display: flex; flex-direction: column; gap: var(--margin); flex-wrap: wrap;">
|
||||
<div :class="$style.inputs">
|
||||
<MkSelect v-model="from" :class="$style.input">
|
||||
<template #label>{{ i18n.ts._accountMigration.movedFromServer }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="to" :class="$style.input">
|
||||
<template #label>{{ i18n.ts._accountMigration.movedToServer }}</template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="remote">{{ i18n.ts.remote }}</option>
|
||||
<option value="local">{{ i18n.ts.local }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<div :class="$style.inputs">
|
||||
<MkInput v-model="movedFromId" :class="$style.input">
|
||||
<template #label> {{ i18n.ts.moveFromId }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="movedToId" style="margin: 0; flex: 1;">
|
||||
<MkInput v-model="movedToId" :class="$style.input">
|
||||
<template #label> {{ i18n.ts.movedToId }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<div class="_gaps_s">
|
||||
|
@ -48,11 +64,15 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
|
||||
const logs = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const movedToId = ref('');
|
||||
const movedFromId = ref('');
|
||||
const from = ref('all');
|
||||
const to = ref('all');
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'admin/show-user-account-move-logs' as const,
|
||||
|
@ -60,6 +80,8 @@ const pagination = {
|
|||
params: computed(() => ({
|
||||
movedFromId: movedFromId.value === '' ? null : movedFromId.value,
|
||||
movedToId: movedToId.value === '' ? null : movedToId.value,
|
||||
from: from.value,
|
||||
to: to.value,
|
||||
})),
|
||||
};
|
||||
|
||||
|
@ -95,4 +117,14 @@ definePageMetadata(() => ({
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -182,6 +182,9 @@ definePageMetadata(() => ({
|
|||
}
|
||||
|
||||
.rkxwuolj {
|
||||
> .body {
|
||||
padding: 32px;
|
||||
|
||||
> .files {
|
||||
> .file {
|
||||
> img {
|
||||
|
@ -197,9 +200,6 @@ definePageMetadata(() => ({
|
|||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 32px;
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="game.map == null"><i class="ti ti-dice"></i></div>
|
||||
<div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
|
||||
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
|
||||
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
|
||||
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti-filled ti-filled-circle' : 'ti ti-circle'"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -145,7 +145,6 @@ import { i18n } from '@/i18n.js';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
@ -276,7 +275,7 @@ async function toggleBlockItem(item) {
|
|||
}
|
||||
|
||||
async function saveMutedWords(mutedWords: (string | string[])[]) {
|
||||
await misskeyApi('i/update', { mutedWords });
|
||||
await os.apiWithDialog('i/update', { mutedWords });
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { query } from '@/scripts/url.js';
|
||||
import { appendQuery, omitHttps, query } from '@/scripts/url.js';
|
||||
import { url } from '@/config.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
||||
|
@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'
|
|||
|
||||
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
|
||||
// もう既にproxyっぽそうだったらurlを取り出す
|
||||
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
|
||||
const url = (new URL(imageUrl)).searchParams.get('url');
|
||||
if (url) {
|
||||
imageUrl = url;
|
||||
} else if (imageUrl.startsWith(instance.mediaProxy + '/')) {
|
||||
imageUrl = imageUrl.slice(instance.mediaProxy.length + 1);
|
||||
} else if (imageUrl.startsWith('/proxy/')) {
|
||||
imageUrl = imageUrl.slice('/proxy/'.length);
|
||||
} else if (imageUrl.startsWith(localProxy + '/')) {
|
||||
imageUrl = imageUrl.slice(localProxy.length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
|
||||
type === 'preview' ? 'preview.webp'
|
||||
: 'image.webp'
|
||||
}?${query({
|
||||
url: imageUrl,
|
||||
return appendQuery(
|
||||
`${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(omitHttps(imageUrl))}`,
|
||||
query({
|
||||
...(!noFallback ? { 'fallback': '1' } : {}),
|
||||
...(type ? { [type]: '1' } : {}),
|
||||
...(mustOrigin ? { origin: '1' } : {}),
|
||||
})}`;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
|
||||
|
@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
|
|||
return u.href;
|
||||
}
|
||||
|
||||
return `${instance.mediaProxy}/static.webp?${query({
|
||||
url: u.href,
|
||||
static: '1',
|
||||
})}`;
|
||||
return appendQuery(
|
||||
`${instance.mediaProxy}/static/${encodeURIComponent(omitHttps(u.href))}`,
|
||||
query({ static: '1' }),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
* 2. プロパティがundefinedの時はクエリを付けない
|
||||
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
|
||||
*/
|
||||
export function query(obj: Record<string, any>): string {
|
||||
export function query(obj: Record<string, unknown>): string {
|
||||
const params = Object.entries(obj)
|
||||
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
||||
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
|
||||
|
@ -21,3 +21,9 @@ export function query(obj: Record<string, any>): string {
|
|||
export function appendQuery(url: string, query: string): string {
|
||||
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
|
||||
}
|
||||
|
||||
export function omitHttps(url: string): string {
|
||||
if (url.startsWith('https://')) return url.slice(8);
|
||||
if (url.startsWith('https%3A%2F%2F')) return url.slice(14);
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -177,6 +177,16 @@ rt {
|
|||
}
|
||||
}
|
||||
|
||||
.ti-filled {
|
||||
width: 1.28em;
|
||||
vertical-align: -12%;
|
||||
line-height: 1em;
|
||||
|
||||
&:before {
|
||||
font-size: 128%;
|
||||
}
|
||||
}
|
||||
|
||||
.ti-fw {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
import dns from 'dns';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { defineConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import locales from '../../locales/index.js';
|
||||
import { getConfig } from './vite.config.js';
|
||||
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
|
||||
const defaultConfig = getConfig();
|
||||
|
||||
const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
|
||||
|
||||
const httpUrl = `http://localhost:${port}/`;
|
||||
const websocketUrl = `ws://localhost:${port}/`;
|
||||
|
||||
const devConfig = {
|
||||
// 基本の設定は vite.config.js から引き継ぐ
|
||||
...defaultConfig,
|
||||
root: 'src',
|
||||
publicDir: '../assets',
|
||||
base: './',
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
target: httpUrl,
|
||||
},
|
||||
'/assets': httpUrl,
|
||||
'/static-assets': httpUrl,
|
||||
'/client-assets': httpUrl,
|
||||
'/files': httpUrl,
|
||||
'/twemoji': httpUrl,
|
||||
'/fluent-emoji': httpUrl,
|
||||
'/sw.js': httpUrl,
|
||||
'/streaming': {
|
||||
target: websocketUrl,
|
||||
ws: true,
|
||||
},
|
||||
'/favicon.ico': httpUrl,
|
||||
'/identicon': {
|
||||
target: httpUrl,
|
||||
rewrite(path) {
|
||||
return path.replace('@localhost:5173', '');
|
||||
},
|
||||
},
|
||||
'/url': httpUrl,
|
||||
'/proxy': httpUrl,
|
||||
'/_info_card_': httpUrl,
|
||||
'/bios': httpUrl,
|
||||
'/cli': httpUrl,
|
||||
'/inbox': httpUrl,
|
||||
'/emoji/': httpUrl,
|
||||
'/queue': httpUrl,
|
||||
'/notes': {
|
||||
target: httpUrl,
|
||||
headers: {
|
||||
'Accept': 'application/activity+json',
|
||||
},
|
||||
},
|
||||
'/users': {
|
||||
target: httpUrl,
|
||||
headers: {
|
||||
'Accept': 'application/activity+json',
|
||||
},
|
||||
},
|
||||
'/.well-known': {
|
||||
target: httpUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
...defaultConfig.build,
|
||||
rollupOptions: {
|
||||
...defaultConfig.build?.rollupOptions,
|
||||
input: 'index.html',
|
||||
},
|
||||
},
|
||||
|
||||
define: {
|
||||
...defaultConfig.define,
|
||||
_LANGS_FULL_: JSON.stringify(Object.entries(locales)),
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(({ command, mode }) => devConfig);
|
||||
|
|
@ -3,6 +3,8 @@ import pluginReplace from '@rollup/plugin-replace';
|
|||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { type UserConfig, defineConfig } from 'vite';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { promises as fsp } from 'fs';
|
||||
|
||||
import locales from '../../locales/index.js';
|
||||
import meta from '../../package.json';
|
||||
|
@ -10,6 +12,9 @@ import packageInfo from './package.json' with { type: 'json' };
|
|||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
|
||||
import pluginJson5 from './vite.json5.js';
|
||||
|
||||
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
|
||||
const host = url ? (new URL(url)).hostname : undefined;
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||
|
||||
/**
|
||||
|
@ -65,6 +70,7 @@ export function getConfig(): UserConfig {
|
|||
base: '/vite/',
|
||||
|
||||
server: {
|
||||
host,
|
||||
port: 5173,
|
||||
},
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/matter-js": "0.19.8",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.13.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@readme/openapi-parser": "2.6.0",
|
||||
"@types/node": "22.10.7",
|
||||
"@readme/openapi-parser": "2.7.0",
|
||||
"@types/node": "22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"eslint": "8.57.1",
|
||||
|
|
|
@ -9957,6 +9957,10 @@ export type operations = {
|
|||
movedFromId?: string | null;
|
||||
/** Format: misskey:id */
|
||||
movedToId?: string | null;
|
||||
/** @enum {string|null} */
|
||||
from?: 'local' | 'remote' | 'all';
|
||||
/** @enum {string|null} */
|
||||
to?: 'local' | 'remote' | 'all';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/node": "22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"eslint": "8.57.1",
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@types/serviceworker": "0.0.113",
|
||||
"@types/serviceworker": "0.0.118",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
|
|
|
@ -21,6 +21,16 @@ async function copyFrontendFonts() {
|
|||
|
||||
async function copyFrontendTablerIcons() {
|
||||
await fs.cp('./packages/frontend/node_modules/@tabler/icons-webfont', `./built/_frontend_dist_/tabler-icons.${meta.version}`, { dereference: true, recursive: true });
|
||||
for (const file of [
|
||||
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.scss`,
|
||||
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.css`,
|
||||
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.min.css`,
|
||||
]) {
|
||||
let source = await fs.readFile(file, { encoding: 'utf-8' });
|
||||
source = source.replaceAll('$ti-prefix: \'ti\'', '$ti-prefix: \'ti-filled\'');
|
||||
source = source.replaceAll('.ti', '.ti-filled');
|
||||
await fs.writeFile(file, source);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFrontendLocales() {
|
||||
|
|
|
@ -64,7 +64,7 @@ execa('pnpm', ['--filter', 'backend', 'dev'], {
|
|||
stderr: process.stderr,
|
||||
});
|
||||
|
||||
execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], {
|
||||
execa('pnpm', ['--filter', 'frontend', 'watch'], {
|
||||
cwd: _dirname + '/../',
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue