Merge upstream

This commit is contained in:
ASTRO:? 2025-02-13 16:13:52 +09:00
commit bc9acabd6c
No known key found for this signature in database
GPG key ID: 8947F3AF5B0B4BFE
65 changed files with 502 additions and 500 deletions

View file

@ -57,7 +57,7 @@ jobs:
- name: Install FFmpeg - name: Install FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v3 uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -116,7 +116,7 @@ jobs:
with: with:
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -26,7 +26,7 @@ jobs:
run_install: false run_install: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v4.1.0 - uses: actions/setup-node@v4.2.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -54,7 +54,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v4.1.0 - uses: actions/setup-node@v4.2.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -78,7 +78,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v4.1.0 - uses: actions/setup-node@v4.2.0
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View file

@ -37,7 +37,7 @@ jobs:
with: with:
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -36,7 +36,7 @@ jobs:
run_install: false run_install: false
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -27,7 +27,7 @@ jobs:
with: with:
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -28,7 +28,7 @@ jobs:
with: with:
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0 uses: actions/setup-node@v4.2.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View file

@ -115,25 +115,10 @@ pnpm dev
command. command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). - 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. - Service Worker is watched by esbuild.
- The front end can be viewed by accessing `http://localhost:5173`. - Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- The backend listens on the port configured with `port` in .config/default.yml. - Vite runs behind the backend (the backend will proxy Vite at /vite except for websocket used for HMR).
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).
- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml). - 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 ### Dev Container
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.

View file

@ -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." 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." 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." 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: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:

8
locales/index.d.ts vendored
View file

@ -6296,6 +6296,14 @@ export interface Locale extends ILocale {
* : * :
*/ */
"movedTo": string; "movedTo": string;
/**
*
*/
"movedToServer": string;
/**
*
*/
"movedFromServer": string;
}; };
"_achievements": { "_achievements": {
/** /**

View file

@ -1586,6 +1586,8 @@ _accountMigration:
movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。" movedAndCannotBeUndone: "\nアカウントは移行されています。\n移行を取り消すことはできません。"
postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。" postMigrationNote: "このアカウントからのフォロー解除は移行操作から24時間後に実行されます。\nこのアカウントのフォロー・フォロワー数は0になっています。フォロワーの解除はされないため、あなたのフォロワーはこのアカウントのフォロワー向け投稿を引き続き閲覧できます。"
movedTo: "移行先のアカウント:" movedTo: "移行先のアカウント:"
movedToServer: "移行先のサーバー"
movedFromServer: "移行元のサーバー"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"

View file

@ -1568,6 +1568,8 @@ _accountMigration:
movedAndCannotBeUndone: "\n이사한 계정입니다.\n이사는 취소할 수 없습니다." movedAndCannotBeUndone: "\n이사한 계정입니다.\n이사는 취소할 수 없습니다."
postMigrationNote: "이 계정의 팔로잉 해제는 이사 후 24시간 뒤에 실행됩니다.\n이 계정의 팔로우 및 팔로워 수는 0으로 표시됩니다. 팔로워 해제는 이루어지지 않으므로, 당신의 팔로워는 이 계정의 팔로워 한정 게시물을 계속해서 열람할 수 있습니다." postMigrationNote: "이 계정의 팔로잉 해제는 이사 후 24시간 뒤에 실행됩니다.\n이 계정의 팔로우 및 팔로워 수는 0으로 표시됩니다. 팔로워 해제는 이루어지지 않으므로, 당신의 팔로워는 이 계정의 팔로워 한정 게시물을 계속해서 열람할 수 있습니다."
movedTo: "이사할 계정:" movedTo: "이사할 계정:"
movedToServer: "이사한 서버"
movedFromServer: "기존 서버"
_achievements: _achievements:
earnedAt: "달성 일시" earnedAt: "달성 일시"
_types: _types:

View file

@ -33,16 +33,16 @@
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js" "generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-darwin-arm64": "1.10.7", "@swc/core-darwin-arm64": "1.10.12",
"@swc/core-darwin-x64": "1.10.7", "@swc/core-darwin-x64": "1.10.12",
"@swc/core-linux-arm-gnueabihf": "1.10.7", "@swc/core-linux-arm-gnueabihf": "1.10.12",
"@swc/core-linux-arm64-gnu": "1.10.7", "@swc/core-linux-arm64-gnu": "1.10.12",
"@swc/core-linux-arm64-musl": "1.10.7", "@swc/core-linux-arm64-musl": "1.10.12",
"@swc/core-linux-x64-gnu": "1.10.7", "@swc/core-linux-x64-gnu": "1.10.12",
"@swc/core-linux-x64-musl": "1.10.7", "@swc/core-linux-x64-musl": "1.10.12",
"@swc/core-win32-arm64-msvc": "1.10.7", "@swc/core-win32-arm64-msvc": "1.10.12",
"@swc/core-win32-ia32-msvc": "1.10.7", "@swc/core-win32-ia32-msvc": "1.10.12",
"@swc/core-win32-x64-msvc": "1.10.7", "@swc/core-win32-x64-msvc": "1.10.12",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9", "bufferutil": "4.0.9",
@ -63,11 +63,11 @@
}, },
"dependencies": { "dependencies": {
"@authenio/samlify-node-xmllint": "2.0.0", "@authenio/samlify-node-xmllint": "2.0.0",
"@aws-sdk/client-s3": "3.729.0", "@aws-sdk/client-s3": "3.740.0",
"@aws-sdk/lib-storage": "3.729.0", "@aws-sdk/lib-storage": "3.740.0",
"@bull-board/api": "6.6.2", "@bull-board/api": "6.7.4",
"@bull-board/fastify": "6.6.2", "@bull-board/fastify": "6.7.4",
"@bull-board/ui": "6.6.2", "@bull-board/ui": "6.7.4",
"@discordapp/twemoji": "15.1.0", "@discordapp/twemoji": "15.1.0",
"@elastic/elasticsearch": "8.17.0", "@elastic/elasticsearch": "8.17.0",
"@fastify/accepts": "5.0.2", "@fastify/accepts": "5.0.2",
@ -76,21 +76,21 @@
"@fastify/express": "4.0.2", "@fastify/express": "4.0.2",
"@fastify/formbody": "8.0.2", "@fastify/formbody": "8.0.2",
"@fastify/http-proxy": "11.0.1", "@fastify/http-proxy": "11.0.1",
"@fastify/multipart": "9.0.2", "@fastify/multipart": "9.0.3",
"@fastify/static": "8.0.4", "@fastify/static": "8.0.4",
"@fastify/view": "10.0.2", "@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3", "@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
"@napi-rs/canvas": "0.1.65", "@napi-rs/canvas": "0.1.65",
"@nestjs/common": "10.4.15", "@nestjs/common": "11.0.7",
"@nestjs/core": "10.4.15", "@nestjs/core": "11.0.7",
"@nestjs/testing": "10.4.15", "@nestjs/testing": "11.0.7",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "13.1.0", "@simplewebauthn/server": "13.1.1",
"@sinonjs/fake-timers": "11.3.1", "@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "4.0.2", "@smithy/node-http-handler": "4.0.2",
"@swc/cli": "0.6.0", "@swc/cli": "0.6.0",
"@swc/core": "1.10.7", "@swc/core": "1.10.12",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.17.1", "ajv": "8.17.1",
@ -99,7 +99,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.34.10", "bullmq": "5.39.1",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "10.0.3", "cbor": "10.0.3",
"chalk": "5.4.1", "chalk": "5.4.1",
@ -114,7 +114,7 @@
"fastify-http-errors-enhanced": "6.0.1", "fastify-http-errors-enhanced": "6.0.1",
"fastify-raw-body": "5.0.0", "fastify-raw-body": "5.0.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "20.0.0", "file-type": "20.0.1",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.1", "form-data": "4.0.1",
"got": "14.4.5", "got": "14.4.5",
@ -131,7 +131,7 @@
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.3", "jsonld": "8.3.3",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"meilisearch": "0.48.0", "meilisearch": "0.48.2",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
@ -142,7 +142,7 @@
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"nodemailer": "6.9.16", "nodemailer": "6.10.0",
"nsfwjs": "4.2.0", "nsfwjs": "4.2.0",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
@ -191,7 +191,7 @@
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.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", "@swc/jest": "0.2.37",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
"@types/archiver": "6.0.3", "@types/archiver": "6.0.3",
@ -208,14 +208,14 @@
"@types/jsonld": "1.5.15", "@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "2.1.0",
"@types/node": "22.10.7", "@types/node": "22.13.0",
"@types/node-forge": "1.3.11", "@types/node-forge": "1.3.11",
"@types/nodemailer": "6.4.17", "@types/nodemailer": "6.4.17",
"@types/oauth": "0.9.6", "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.10", "@types/pg": "8.11.11",
"@types/psl": "1.1.3", "@types/psl": "1.1.3",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/punycode.js": "npm:@types/punycode@2.1.4",
@ -231,7 +231,7 @@
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@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/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.10.0",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.1.0",
@ -243,7 +243,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.9", "nodemon": "3.1.9",
"pid-port": "1.0.0", "pid-port": "1.0.2",
"simple-oauth2": "5.1.0" "simple-oauth2": "5.1.0"
} }
} }

View file

@ -356,7 +356,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
* *
*/ */
@bindThis @bindThis
public async getUserBadgeRoles(userId: MiUser['id']) { public async getUserBadgeRoles(userId: MiUser['id'], publicOnly: boolean) {
const now = Date.now(); const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId })); 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) { if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null; 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)); const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles]; return this.sortAndMapBadgeRoles([...assignedBadgeRoles, ...matchedBadgeCondRoles], publicOnly);
} else { } 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 @bindThis
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> { public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();

View file

@ -12,7 +12,7 @@ import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiDriveFile } from '@/models/DriveFile.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 { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js'; import { isMimeImage } from '@/misc/is-mime-image.js';
@ -77,9 +77,8 @@ export class DriveFileEntityService {
@bindThis @bindThis
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string { private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
return appendQuery( return appendQuery(
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`, `${this.config.mediaProxy}/${mode ?? 'image'}/${encodeURIComponent(omitHttps(url))}`,
query({ query({
url,
...(mode ? { [mode]: '1' } : {}), ...(mode ? { [mode]: '1' } : {}),
}), }),
); );

View file

@ -518,16 +518,7 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined, } : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
badgeRoles: this.roleService.getUserBadgeRoles(user.id).then((rs) => rs badgeRoles: this.roleService.getUserBadgeRoles(user.id, !iAmModerator),
.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,
})),
),
...(isDetailed ? { ...(isDetailed ? {
url: profile?.url, url: profile?.url,

View file

@ -14,10 +14,16 @@ export function query(obj: Record<string, unknown>): string {
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
return Object.entries(params) return Object.entries(params)
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
.join('&'); .join('&');
} }
export function appendQuery(url: string, query: string): string { export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; 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;
}

View file

@ -108,7 +108,7 @@ class MyCustomLogger implements Logger {
@bindThis @bindThis
public logQuery(query: string, parameters?: any[]) { public logQuery(query: string, parameters?: any[]) {
sqlLogger.info(this.highlight(query)); sqlLogger.debug(this.highlight(query));
} }
@bindThis @bindThis

View file

@ -26,6 +26,7 @@ import { FileInfoService } from '@/core/FileInfoService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.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 { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
@ -35,6 +36,16 @@ const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`; const assets = `${_dirname}/../../server/file/assets/`;
interface TransformQuery {
origin?: string;
fallback?: string;
emoji?: string;
avatar?: string;
static?: string;
preview?: string;
badge?: string;
}
@Injectable() @Injectable()
export class FileServerService { export class FileServerService {
private logger: Logger; private logger: Logger;
@ -87,10 +98,18 @@ export class FileServerService {
done(); 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<{ fastify.get<{
Params: { url: string; }; Params: { url: string; };
Querystring: { url?: string; }; Querystring: { url?: string; } & TransformQuery;
}>('/proxy/:url*', async (request, reply) => { }>('/proxy/:url', async (request, reply) => {
return await this.proxyHandler(request, reply) return await this.proxyHandler(request, reply)
.catch(err => this.errorHandler(request, reply, err)); .catch(err => this.errorHandler(request, reply, err));
}); });
@ -142,12 +161,15 @@ export class FileServerService {
if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) { if (isMimeImage(file.mime, 'sharp-convertible-image-with-bmp')) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/static.webp`); const url = appendQuery(
url.searchParams.set('url', file.url); `${this.config.mediaProxy}/static/${encodeURIComponent(omitHttps(file.url))}`,
url.searchParams.set('static', '1'); query({
static: '1',
}),
);
file.cleanup(); file.cleanup();
return await reply.redirect(url.toString(), 301); return await reply.redirect(url, 301);
} else if (file.mime.startsWith('video/')) { } else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) { if (externalThumbnail) {
@ -163,11 +185,10 @@ export class FileServerService {
if (['image/svg+xml'].includes(file.mime)) { if (['image/svg+xml'].includes(file.mime)) {
reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Cache-Control', 'max-age=31536000, immutable');
const url = new URL(`${this.config.mediaProxy}/svg.webp`); const url = `${this.config.mediaProxy}/svg/${encodeURIComponent(omitHttps(file.url))}`;
url.searchParams.set('url', file.url);
file.cleanup(); file.cleanup();
return await reply.redirect(url.toString(), 301); return await reply.redirect(url, 301);
} }
} }
@ -291,30 +312,43 @@ export class FileServerService {
} }
@bindThis @bindThis
private async proxyHandler(request: FastifyRequest<{ Params: { url: string; }; Querystring: { url?: string; }; }>, reply: FastifyReply) { private async proxyHandler(request: FastifyRequest<{ Params: { type?: string; url: string; }; Querystring: { url?: string; } & TransformQuery; }>, reply: FastifyReply) {
const url = 'url' in request.query ? request.query.url : 'https://' + request.params.url; 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); reply.code(400);
return; return;
} }
// アバタークロップなど、どうしてもオリジンである必要がある場合 // アバタークロップなど、どうしてもオリジンである必要がある場合
const mustOrigin = 'origin' in request.query; const mustOrigin = 'origin' in request.query;
const transformQuery = request.query as TransformQuery;
if (this.config.externalMediaProxyEnabled && !mustOrigin) { if (this.config.externalMediaProxyEnabled && !mustOrigin) {
// 外部のメディアプロキシが有効なら、そちらにリダイレクト // 外部のメディアプロキシが有効なら、そちらにリダイレクト
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`); const redirectUrl = appendQuery(
`${this.config.mediaProxy}/redirect/${encodeURIComponent(omitHttps(url))}`,
for (const [key, value] of Object.entries(request.query)) { query(transformQuery as Record<string, string>),
url.searchParams.append(key, value); );
}
return reply.redirect( return reply.redirect(
url.toString(), redirectUrl,
301, 301,
); );
} }
@ -344,11 +378,11 @@ export class FileServerService {
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp'); const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image-with-bmp');
if ( if (
'emoji' in request.query || 'emoji' in transformQuery ||
'avatar' in request.query || 'avatar' in transformQuery ||
'static' in request.query || 'static' in transformQuery ||
'preview' in request.query || 'preview' in transformQuery ||
'badge' in request.query 'badge' in transformQuery
) { ) {
if (!isConvertibleImage) { if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す // 画像でないなら404でお茶を濁す
@ -357,17 +391,17 @@ export class FileServerService {
} }
let image: IImageStreamable | null = null; let image: IImageStreamable | null = null;
if ('emoji' in request.query || 'avatar' in request.query) { if ('emoji' in transformQuery || 'avatar' in transformQuery) {
if (!isAnimationConvertibleImage && !('static' in request.query)) { if (!isAnimationConvertibleImage && !('static' in transformQuery)) {
image = { image = {
data: fs.createReadStream(file.path), data: fs.createReadStream(file.path),
ext: file.ext, ext: file.ext,
type: file.mime, type: file.mime,
}; };
} else { } 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({ .resize({
height: 'emoji' in request.query ? 128 : 320, height: 'emoji' in transformQuery ? 128 : 320,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.webp(webpDefault); .webp(webpDefault);
@ -378,11 +412,11 @@ export class FileServerService {
type: 'image/webp', 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); 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); 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)) const mask = (await sharpBmp(file.path, file.mime))
.resize(96, 96, { .resize(96, 96, {
fit: 'contain', fit: 'contain',

View file

@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { genIdenticon } from '@/misc/gen-identicon.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -77,8 +78,9 @@ export class ServerService implements OnApplicationShutdown {
@bindThis @bindThis
public async launch(): Promise<void> { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true,
logger: false, logger: false,
maxParamLength: 1024,
trustProxy: true,
}); });
this.#fastify = fastify; this.#fastify = fastify;
@ -162,22 +164,28 @@ export class ServerService implements OnApplicationShutdown {
} }
} }
let url: URL; let url: string;
if ('badge' in request.query) { if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`); url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); `${this.config.mediaProxy}/emoji/${encodeURIComponent(omitHttps(emoji.publicUrl || emoji.originalUrl))}`,
url.searchParams.set('badge', '1'); query({
badge: '1',
}),
);
} else { } else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`); url = appendQuery(
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl); `${this.config.mediaProxy}/emoji/${encodeURIComponent(omitHttps(emoji.publicUrl || emoji.originalUrl))}`,
url.searchParams.set('emoji', '1'); query({
if ('static' in request.query) url.searchParams.set('static', '1'); emoji: '1',
...('static' in request.query ? { static: '1' } : {}),
}),
);
} }
return reply.redirect( return reply.redirect(
url.toString(), url,
301, 301,
); );
}); });

View file

@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js'; import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js'; import { UserService } from '@/core/UserService.js';
import { RoleService } from '@/core/RoleService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js'; import MainStreamConnection from './stream/Connection.js';
@ -40,6 +41,7 @@ export class StreamingApiServerService {
private channelsService: ChannelsService, private channelsService: ChannelsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private usersService: UserService, private usersService: UserService,
private roleService: RoleService,
private channelFollowingService: ChannelFollowingService, private channelFollowingService: ChannelFollowingService,
) { ) {
} }
@ -99,6 +101,7 @@ export class StreamingApiServerService {
this.noteReadService, this.noteReadService,
this.notificationService, this.notificationService,
this.cacheService, this.cacheService,
this.roleService,
this.channelFollowingService, this.channelFollowingService,
user, app, user, app,
); );

View file

@ -62,6 +62,8 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
movedFromId: { type: 'string', format: 'misskey:id', nullable: true }, movedFromId: { type: 'string', format: 'misskey:id', nullable: true },
movedToId: { 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: [], required: [],
} as const; } as const;
@ -86,6 +88,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('accountMoveLogs.movedToId = :movedToId', { movedToId: ps.movedToId }); 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(); const accountMoveLogs = await query.limit(ps.limit).getMany();
return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me); return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me);

View file

@ -14,6 +14,7 @@ import { CacheService } from '@/core/CacheService.js';
import { MiFollowing, MiUserProfile } from '@/models/_.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js';
import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { RoleService } from '@/core/RoleService.js';
import type { ChannelsService } from './ChannelsService.js'; import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
import type Channel from './channel.js'; import type Channel from './channel.js';
@ -31,6 +32,7 @@ export default class Connection {
private subscribingNotes: any = {}; private subscribingNotes: any = {};
private cachedNotes: Packed<'Note'>[] = []; private cachedNotes: Packed<'Note'>[] = [];
public userProfile: MiUserProfile | null = null; public userProfile: MiUserProfile | null = null;
public isModerator = false;
public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {};
public followingChannels: Set<string> = new Set(); public followingChannels: Set<string> = new Set();
public userIdsWhoMeMuting: Set<string> = new Set(); public userIdsWhoMeMuting: Set<string> = new Set();
@ -45,6 +47,7 @@ export default class Connection {
private noteReadService: NoteReadService, private noteReadService: NoteReadService,
private notificationService: NotificationService, private notificationService: NotificationService,
private cacheService: CacheService, private cacheService: CacheService,
private roleService: RoleService,
private channelFollowingService: ChannelFollowingService, private channelFollowingService: ChannelFollowingService,
user: MiUser | null | undefined, user: MiUser | null | undefined,
@ -80,6 +83,7 @@ export default class Connection {
public async init() { public async init() {
if (this.user != null) { if (this.user != null) {
await this.fetch(); await this.fetch();
this.isModerator = await this.roleService.isModerator(this.user);
if (!this.fetchIntervalId) { if (!this.fetchIntervalId) {
this.fetchIntervalId = setInterval(this.fetch, 1000 * 10); this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);

View file

@ -30,6 +30,10 @@ export default abstract class Channel {
return this.connection.userProfile; return this.connection.userProfile;
} }
protected get iAmModerator() {
return this.connection.isModerator;
}
protected get following() { protected get following() {
return this.connection.following; return this.connection.following;
} }

View file

@ -4,8 +4,9 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.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 type { GlobalEvents } from '@/core/GlobalEventService.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -18,6 +19,7 @@ class AntennaChannel extends Channel {
private minimize: boolean; private minimize: boolean;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
id: string, id: string,
@ -64,11 +66,14 @@ class AntennaChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);
@ -92,6 +97,7 @@ export class AntennaChannelService implements MiChannelService<true> {
public readonly kind = AntennaChannel.kind; public readonly kind = AntennaChannel.kind;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
) { ) {
} }
@ -99,6 +105,7 @@ export class AntennaChannelService implements MiChannelService<true> {
@bindThis @bindThis
public create(id: string, connection: Channel['connection']): AntennaChannel { public create(id: string, connection: Channel['connection']): AntennaChannel {
return new AntennaChannel( return new AntennaChannel(
this.roleService,
this.noteEntityService, this.noteEntityService,
id, id,
connection, connection,

View file

@ -4,9 +4,10 @@
*/ */
import { Injectable } from '@nestjs/common'; 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 { 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 { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -18,6 +19,7 @@ class ChannelChannel extends Channel {
private minimize: boolean; private minimize: boolean;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
id: string, id: string,
@ -70,11 +72,14 @@ class ChannelChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);
@ -95,6 +100,7 @@ export class ChannelChannelService implements MiChannelService<false> {
public readonly kind = ChannelChannel.kind; public readonly kind = ChannelChannel.kind;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
) { ) {
} }
@ -102,6 +108,7 @@ export class ChannelChannelService implements MiChannelService<false> {
@bindThis @bindThis
public create(id: string, connection: Channel['connection']): ChannelChannel { public create(id: string, connection: Channel['connection']): ChannelChannel {
return new ChannelChannel( return new ChannelChannel(
this.roleService,
this.noteEntityService, this.noteEntityService,
id, id,
connection, connection,

View file

@ -4,11 +4,11 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.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 { RoleService } from '@/core/RoleService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -100,11 +100,14 @@ class GlobalTimelineChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);

View file

@ -4,9 +4,10 @@
*/ */
import { Injectable } from '@nestjs/common'; 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 { 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 { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -20,6 +21,7 @@ class HomeTimelineChannel extends Channel {
private minimize: boolean; private minimize: boolean;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
id: string, id: string,
@ -101,11 +103,14 @@ class HomeTimelineChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);
@ -126,6 +131,7 @@ export class HomeTimelineChannelService implements MiChannelService<true> {
public readonly kind = HomeTimelineChannel.kind; public readonly kind = HomeTimelineChannel.kind;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
) { ) {
} }
@ -133,6 +139,7 @@ export class HomeTimelineChannelService implements MiChannelService<true> {
@bindThis @bindThis
public create(id: string, connection: Channel['connection']): HomeTimelineChannel { public create(id: string, connection: Channel['connection']): HomeTimelineChannel {
return new HomeTimelineChannel( return new HomeTimelineChannel(
this.roleService,
this.noteEntityService, this.noteEntityService,
id, id,
connection, connection,

View file

@ -4,11 +4,11 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.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 { RoleService } from '@/core/RoleService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -117,11 +117,14 @@ class HybridTimelineChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);

View file

@ -4,11 +4,11 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.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 { RoleService } from '@/core/RoleService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -100,11 +100,14 @@ class LocalTimelineChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);

View file

@ -4,9 +4,9 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -19,8 +19,8 @@ class RoleTimelineChannel extends Channel {
private minimize: boolean; private minimize: boolean;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private roleservice: RoleService,
id: string, id: string,
connection: Channel['connection'], connection: Channel['connection'],
@ -42,7 +42,7 @@ class RoleTimelineChannel extends Channel {
if (data.type === 'note') { if (data.type === 'note') {
const note = data.body; const note = data.body;
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) { if (!(await this.roleService.isExplorable({ id: this.roleId }))) {
return; return;
} }
if (note.visibility !== 'public') return; if (note.visibility !== 'public') return;
@ -86,11 +86,14 @@ class RoleTimelineChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);
@ -114,16 +117,16 @@ export class RoleTimelineChannelService implements MiChannelService<false> {
public readonly kind = RoleTimelineChannel.kind; public readonly kind = RoleTimelineChannel.kind;
constructor( constructor(
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private roleservice: RoleService,
) { ) {
} }
@bindThis @bindThis
public create(id: string, connection: Channel['connection']): RoleTimelineChannel { public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
return new RoleTimelineChannel( return new RoleTimelineChannel(
this.roleService,
this.noteEntityService, this.noteEntityService,
this.roleservice,
id, id,
connection, connection,
); );

View file

@ -4,11 +4,12 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; 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 { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.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 { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js'; import Channel, { type MiChannelService } from '../channel.js';
@ -26,6 +27,7 @@ class UserListChannel extends Channel {
constructor( constructor(
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
private userListMembershipsRepository: UserListMembershipsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
id: string, id: string,
@ -135,11 +137,14 @@ class UserListChannel extends Channel {
} }
if (this.minimize && ['public', 'home'].includes(note.visibility)) { if (this.minimize && ['public', 'home'].includes(note.visibility)) {
const badgeRoles = this.iAmModerator ? await this.roleService.getUserBadgeRoles(note.userId, false) : undefined;
this.send('note', { this.send('note', {
id: note.id, myReaction: note.myReaction, id: note.id, myReaction: note.myReaction,
poll: note.poll?.choices ? { choices: note.poll.choices } : undefined, poll: note.poll?.choices ? { choices: note.poll.choices } : undefined,
reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined, reply: note.reply?.myReaction ? { myReaction: note.reply.myReaction } : undefined,
renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined, renote: note.renote?.myReaction ? { myReaction: note.renote.myReaction } : undefined,
...(badgeRoles?.length ? { user: { badgeRoles } } : {}),
}); });
} else { } else {
this.send('note', note); this.send('note', note);
@ -169,6 +174,7 @@ export class UserListChannelService implements MiChannelService<false> {
@Inject(DI.userListMembershipsRepository) @Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository, private userListMembershipsRepository: UserListMembershipsRepository,
private roleService: RoleService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
) { ) {
} }
@ -178,6 +184,7 @@ export class UserListChannelService implements MiChannelService<false> {
return new UserListChannel( return new UserListChannel(
this.userListsRepository, this.userListsRepository,
this.userListMembershipsRepository, this.userListMembershipsRepository,
this.roleService,
this.noteEntityService, this.noteEntityService,
id, id,
connection, connection,

View file

@ -303,9 +303,12 @@ export class ClientServerService {
done(); done();
}); });
} else { } else {
const configUrl = new URL(this.config.url);
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
const port = (process.env.VITE_PORT ?? '5173'); const port = (process.env.VITE_PORT ?? '5173');
fastify.register(fastifyProxy, { fastify.register(fastifyProxy, {
upstream: 'http://localhost:' + port, upstream: urlOriginWithoutPort + ':' + port,
prefix: '/vite', prefix: '/vite',
rewritePrefix: '/vite', rewritePrefix: '/vite',
}); });

View file

@ -12,7 +12,7 @@ import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.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 { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApiError } from '@/server/api/error.js'; import { ApiError } from '@/server/api/error.js';
@ -36,14 +36,15 @@ export class UrlPreviewService {
@bindThis @bindThis
private wrap(url?: string | null): string | null { private wrap(url?: string | null): string | null {
return url != null if (!url) return null;
? url.match(/^https?:\/\//) if (!RegExp(/^https?:\/\//).exec(url)) return url;
? `${this.config.mediaProxy}/preview.webp?${query({
url, return appendQuery(
preview: '1', `${this.config.mediaProxy}/preview/${encodeURIComponent(omitHttps(url))}`,
})}` query({
: url preview: '1',
: null; }),
);
} }
@bindThis @bindThis

View file

@ -36,7 +36,8 @@ html
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842 //- 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}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists if !config.clientManifestExists

View file

@ -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/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="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"> <link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style> <style>
html { html {

View file

@ -4,7 +4,6 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"watch": "vite", "watch": "vite",
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build", "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\"", "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", "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/plugin-typescript": "12.1.2",
"@rollup/pluginutils": "5.1.4", "@rollup/pluginutils": "5.1.4",
"@syuilo/aiscript": "0.19.0", "@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.28.1", "@tabler/icons-webfont": "3.29.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13", "@vue/compiler-sfc": "3.5.13",
@ -41,7 +40,7 @@
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.24.0", "chromatic": "11.25.2",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.0", "cropperjs": "2.0.0-rc.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
@ -59,13 +58,13 @@
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.30.1", "rollup": "4.34.0",
"sanitize-html": "2.14.0", "sanitize-html": "2.14.0",
"sass": "1.83.4", "sass": "1.83.4",
"shiki": "1.27.2", "shiki": "2.2.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.172.0", "three": "0.173.0",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
@ -73,7 +72,7 @@
"typescript": "5.7.3", "typescript": "5.7.3",
"uuid": "11.0.5", "uuid": "11.0.5",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "6.0.7", "vite": "6.0.11",
"vue": "3.5.13", "vue": "3.5.13",
"vue-gtag": "2.0.1", "vue-gtag": "2.0.1",
"vuedraggable": "next", "vuedraggable": "next",
@ -82,49 +81,49 @@
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3", "@misskey-dev/summaly": "github:MisskeyIO/summaly#5.1.3",
"@storybook/addon-actions": "8.5.0", "@storybook/addon-actions": "8.5.2",
"@storybook/addon-essentials": "8.5.0", "@storybook/addon-essentials": "8.5.2",
"@storybook/addon-interactions": "8.5.0", "@storybook/addon-interactions": "8.5.2",
"@storybook/addon-links": "8.5.0", "@storybook/addon-links": "8.5.2",
"@storybook/addon-mdx-gfm": "8.5.0", "@storybook/addon-mdx-gfm": "8.5.2",
"@storybook/addon-storysource": "8.5.0", "@storybook/addon-storysource": "8.5.2",
"@storybook/blocks": "8.5.0", "@storybook/blocks": "8.5.2",
"@storybook/components": "8.5.0", "@storybook/components": "8.5.2",
"@storybook/core-events": "8.5.0", "@storybook/core-events": "8.5.2",
"@storybook/manager-api": "8.5.0", "@storybook/manager-api": "8.5.2",
"@storybook/preview-api": "8.5.0", "@storybook/preview-api": "8.5.2",
"@storybook/react": "8.5.0", "@storybook/react": "8.5.2",
"@storybook/react-vite": "8.5.0", "@storybook/react-vite": "8.5.2",
"@storybook/test": "8.5.0", "@storybook/test": "8.5.2",
"@storybook/theming": "8.5.0", "@storybook/theming": "8.5.2",
"@storybook/types": "8.5.0", "@storybook/types": "8.5.2",
"@storybook/vue3": "8.5.0", "@storybook/vue3": "8.5.2",
"@storybook/vue3-vite": "8.5.0", "@storybook/vue3-vite": "8.5.2",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4", "@types/canvas-confetti": "^1.6.4",
"@types/escape-regexp": "0.0.3", "@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.6", "@types/estree": "1.0.6",
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.13.0", "@types/sanitize-html": "2.13.0",
"@types/three": "0.172.0", "@types/three": "0.173.0",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.5.13", "@types/ws": "8.5.14",
"@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "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", "@vue/runtime-core": "3.5.13",
"acorn": "8.14.0", "acorn": "8.14.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.17.0", "cypress": "14.0.1",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.32.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "16.6.0", "happy-dom": "16.8.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.7.0", "msw": "2.7.0",
@ -134,10 +133,10 @@
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"start-server-and-test": "2.0.10", "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", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "2.1.8", "vitest": "3.0.4",
"vitest-fetch-mock": "0.3.0", "vitest-fetch-mock": "0.3.0",
"vue-component-type-helpers": "2.2.0", "vue-component-type-helpers": "2.2.0",
"vue-eslint-parser": "9.4.3", "vue-eslint-parser": "9.4.3",

View file

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

View file

@ -43,6 +43,12 @@ export async function signout() {
if (!$i) return; if (!$i) return;
waiting(); 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'); miLocalStorage.removeItem('account');
await removeAccount($i.id); await removeAccount($i.id);
const accounts = await getAccounts(); const accounts = await getAccounts();

View file

@ -100,6 +100,11 @@ export async function common(createVue: () => App<Element>) {
// タッチデバイスでCSSの:hoverを機能させる // タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true }); document.addEventListener('touchend', () => {}, { passive: true });
// URLに#pswpを含む場合は取り除く
if (location.hash === '#pswp') {
history.replaceState(null, '', location.href.replace('#pswp', ''));
}
// 一斉リロード // 一斉リロード
reloadChannel.addEventListener('message', path => { reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path; if (path !== null) location.href = path;

View file

@ -26,22 +26,34 @@ let prevTime = 0;
let angle1 = 0; let angle1 = 0;
let angle2 = 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 init = () => {
const parent = container.value ?? { offsetWidth: 0 }; const parent = container.value ?? { offsetWidth: 0 };
width = parent.offsetWidth; width = parent.offsetWidth;
height = Math.floor(width * 9 / 16); height = Math.floor(width * 9 / 16);
scene = new THREE.Scene(); scene.clear();
camera = new THREE.OrthographicCamera(); camera.clear();
camera.left = width / -2; camera.left = width / -2;
camera.right = width / 2; camera.right = width / 2;
camera.top = height / 2; camera.top = height / 2;
camera.bottom = height / -2; camera.bottom = height / -2;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height); renderer.setSize(width, height);
if (container.value) { if (container.value) {
@ -176,7 +188,7 @@ const animate = (time) => {
renderer.render(scene, camera); renderer.render(scene, camera);
}; };
const onResize = () => { const resize = () => {
const parent = container.value ?? { offsetWidth: 0 }; const parent = container.value ?? { offsetWidth: 0 };
width = parent.offsetWidth; width = parent.offsetWidth;
height = Math.floor(width * 9 / 16); height = Math.floor(width * 9 / 16);
@ -189,17 +201,25 @@ const onResize = () => {
uniforms.resolution.value.set(width, height); uniforms.resolution.value.set(width, height);
}; };
const ro = new ResizeObserver((entries, observer) => {
resize();
});
onMounted(async () => { onMounted(async () => {
nextTick().then(() => { nextTick().then(() => {
init(); init();
window.addEventListener('resize', onResize); resize();
}); });
if (!container.value) return;
ro.observe(container.value);
}); });
onUnmounted(() => { onUnmounted(() => {
if (renderer) { if (renderer) {
renderer.dispose(); renderer.dispose();
} }
ro.disconnect();
}); });
defineExpose({ defineExpose({

View file

@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</audio> </audio>
<div :class="[$style.controlsChild, $style.controlsLeft]"> <div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause"> <button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i> <i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
<i v-else class="ti ti-player-play-filled"></i> <i v-else class="ti-filled ti-filled-player-play"></i>
</button> </button>
</div> </div>
<div :class="[$style.controlsChild, $style.controlsRight]"> <div :class="[$style.controlsChild, $style.controlsRight]">

View file

@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<source :src="video.url"> <source :src="video.url">
</video> </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"> <div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
<MkLoading/> <MkLoading/>
</div> </div>
@ -75,8 +75,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="videoControls" :class="$style.videoControls" @click.self="togglePlayPause"> <div v-if="videoControls" :class="$style.videoControls" @click.self="togglePlayPause">
<div :class="[$style.controlsChild, $style.controlsLeft]"> <div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause"> <button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i> <i v-if="isPlaying" class="ti-filled ti-filled-player-pause"></i>
<i v-else class="ti ti-player-play-filled"></i> <i v-else class="ti-filled ti-filled-player-play"></i>
</button> </button>
</div> </div>
<div :class="[$style.controlsChild, $style.controlsRight]"> <div :class="[$style.controlsChild, $style.controlsRight]">

View file

@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i> <i class="ti ti-ban"></i>
</button> </button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> <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.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-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>
@ -594,7 +594,7 @@ function emitUpdReaction(emoji: string, delta: number) {
contain: content; contain: content;
content-visibility: auto; content-visibility: auto;
contain-intrinsic-size: auto none auto 128px; contain-intrinsic-size: none auto 128px;
&:focus-visible { &:focus-visible {
outline: none; outline: none;

View file

@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i> <i class="ti ti-ban"></i>
</button> </button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> <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.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-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>

View file

@ -220,7 +220,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
contain: content; contain: content;
content-visibility: auto; content-visibility: auto;
contain-intrinsic-size: auto none auto 100px; contain-intrinsic-size: none auto 100px;
} }
.head { .head {

View file

@ -24,7 +24,7 @@ import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { deepMerge } from '@/scripts/merge.js'; import { deepMerge } from '@/scripts/merge.js';
import { $i, iAmModerator } from '@/account.js'; import { $i } from '@/account.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue'; import { Paging } from '@/components/MkPagination.vue';
@ -108,7 +108,6 @@ async function prepend(data) {
let connection: Misskey.ChannelConnection | null = null; let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null; let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null; let paginationQuery: Paging | null = null;
const minimize = !iAmModerator;
const stream = useStream(); const stream = useStream();
@ -117,13 +116,13 @@ function connectChannel() {
if (props.antenna == null) return; if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connection = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'home') { } else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', { connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
minimize: minimize, minimize: true,
}); });
connection2 = stream.useChannel('main'); connection2 = stream.useChannel('main');
} else if (props.src === 'local') { } else if (props.src === 'local') {
@ -131,27 +130,27 @@ function connectChannel() {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'media') { } else if (props.src === 'media') {
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: true, withFiles: true,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'social') { } else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', { connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withReplies: props.withReplies, withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'global') { } else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', { connection = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'mentions') { } else if (props.src === 'mentions') {
connection = stream.useChannel('main'); connection = stream.useChannel('main');
@ -170,19 +169,19 @@ function connectChannel() {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return; if (props.channel == null) return;
connection = stream.useChannel('channel', { connection = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
minimize: minimize, minimize: true,
}); });
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return; if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connection = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
minimize: minimize, minimize: true,
}); });
} }
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend); if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);

View file

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

View file

@ -18,6 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div> <div>
<div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad"> <div v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
<MkAd v-if="ad.url" :key="ad.id" :specify="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"> <MkInput v-model="ad.url" type="url">
<template #label>URL</template> <template #label>URL</template>
</MkInput> </MkInput>

View file

@ -2,13 +2,29 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900"> <MkSpacer :contentMax="900">
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <div style="display: flex; flex-direction: column; gap: var(--margin); flex-wrap: wrap;">
<MkInput v-model="movedFromId" style="margin: 0; flex: 1;"> <div :class="$style.inputs">
<template #label> {{ i18n.ts.moveFromId }}</template> <MkSelect v-model="from" :class="$style.input">
</MkInput> <template #label>{{ i18n.ts._accountMigration.movedFromServer }}</template>
<MkInput v-model="movedToId" style="margin: 0; flex: 1;"> <option value="all">{{ i18n.ts.all }}</option>
<template #label> {{ i18n.ts.movedToId }}</template> <option value="remote">{{ i18n.ts.remote }}</option>
</MkInput> <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" :class="$style.input">
<template #label> {{ i18n.ts.movedToId }}</template>
</MkInput>
</div>
</div> </div>
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);"> <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
@ -48,11 +64,15 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import MkFolder from '@/components/MkFolder.vue'; 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 logs = shallowRef<InstanceType<typeof MkPagination>>();
const movedToId = ref(''); const movedToId = ref('');
const movedFromId = ref(''); const movedFromId = ref('');
const from = ref('all');
const to = ref('all');
const pagination = { const pagination = {
endpoint: 'admin/show-user-account-move-logs' as const, endpoint: 'admin/show-user-account-move-logs' as const,
@ -60,6 +80,8 @@ const pagination = {
params: computed(() => ({ params: computed(() => ({
movedFromId: movedFromId.value === '' ? null : movedFromId.value, movedFromId: movedFromId.value === '' ? null : movedFromId.value,
movedToId: movedToId.value === '' ? null : movedToId.value, movedToId: movedToId.value === '' ? null : movedToId.value,
from: from.value,
to: to.value,
})), })),
}; };
@ -95,4 +117,14 @@ definePageMetadata(() => ({
flex-direction: column; flex-direction: column;
} }
.inputs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.input {
margin: 0;
flex: 1;
}
</style> </style>

View file

@ -182,24 +182,24 @@ definePageMetadata(() => ({
} }
.rkxwuolj { .rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .body { > .body {
padding: 32px; padding: 32px;
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .title { > .title {
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;

View file

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

View file

@ -145,7 +145,6 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js'; import { signinRequired } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
@ -276,7 +275,7 @@ async function toggleBlockItem(item) {
} }
async function saveMutedWords(mutedWords: (string | string[])[]) { async function saveMutedWords(mutedWords: (string | string[])[]) {
await misskeyApi('i/update', { mutedWords }); await os.apiWithDialog('i/update', { mutedWords });
} }
const headerActions = computed(() => []); const headerActions = computed(() => []);

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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 { url } from '@/config.js';
import { instance } from '@/instance.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 + '/')) { if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す // もう既に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}/${ return appendQuery(
type === 'preview' ? 'preview.webp' `${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(omitHttps(imageUrl))}`,
: 'image.webp' query({
}?${query({ ...(!noFallback ? { 'fallback': '1' } : {}),
url: imageUrl, ...(type ? { [type]: '1' } : {}),
...(!noFallback ? { 'fallback': '1' } : {}), ...(mustOrigin ? { origin: '1' } : {}),
...(type ? { [type]: '1' } : {}), }),
...(mustOrigin ? { origin: '1' } : {}), );
})}`;
} }
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { 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 u.href;
} }
return `${instance.mediaProxy}/static.webp?${query({ return appendQuery(
url: u.href, `${instance.mediaProxy}/static/${encodeURIComponent(omitHttps(u.href))}`,
static: '1', query({ static: '1' }),
})}`; );
} }

View file

@ -8,7 +8,7 @@
* 2. undefinedの時はクエリを付けない * 2. undefinedの時はクエリを付けない
* new URLSearchParams(obj) * new URLSearchParams(obj)
*/ */
export function query(obj: Record<string, any>): string { export function query(obj: Record<string, unknown>): string {
const params = Object.entries(obj) const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); .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 { export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; 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;
}

View file

@ -177,6 +177,16 @@ rt {
} }
} }
.ti-filled {
width: 1.28em;
vertical-align: -12%;
line-height: 1em;
&:before {
font-size: 128%;
}
}
.ti-fw { .ti-fw {
display: inline-block; display: inline-block;
text-align: center; text-align: center;

View file

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

View file

@ -3,6 +3,8 @@ import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue'; import pluginVue from '@vitejs/plugin-vue';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { type UserConfig, defineConfig } from 'vite'; 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 locales from '../../locales/index.js';
import meta from '../../package.json'; 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 pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.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']; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
/** /**
@ -65,6 +70,7 @@ export function getConfig(): UserConfig {
base: '/vite/', base: '/vite/',
server: { server: {
host,
port: 5173, port: 5173,
}, },

View file

@ -26,7 +26,7 @@
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.8",
"@types/node": "22.10.7", "@types/node": "22.13.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.10.0",

View file

@ -8,8 +8,8 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@readme/openapi-parser": "2.6.0", "@readme/openapi-parser": "2.7.0",
"@types/node": "22.10.7", "@types/node": "22.13.0",
"@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.1", "eslint": "8.57.1",

View file

@ -9957,6 +9957,10 @@ export type operations = {
movedFromId?: string | null; movedFromId?: string | null;
/** Format: misskey:id */ /** Format: misskey:id */
movedToId?: string | null; movedToId?: string | null;
/** @enum {string|null} */
from?: 'local' | 'remote' | 'all';
/** @enum {string|null} */
to?: 'local' | 'remote' | 'all';
}; };
}; };
}; };

View file

@ -25,7 +25,7 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@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/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.1", "eslint": "8.57.1",

View file

@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/eslint-plugin": "1.0.0",
"@types/serviceworker": "0.0.113", "@types/serviceworker": "0.0.118",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.1", "eslint": "8.57.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",

View file

@ -21,6 +21,16 @@ async function copyFrontendFonts() {
async function copyFrontendTablerIcons() { async function copyFrontendTablerIcons() {
await fs.cp('./packages/frontend/node_modules/@tabler/icons-webfont', `./built/_frontend_dist_/tabler-icons.${meta.version}`, { dereference: true, recursive: true }); 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() { async function copyFrontendLocales() {
@ -91,13 +101,13 @@ async function build() {
await build(); await build();
if (process.argv.includes("--watch")) { if (process.argv.includes("--watch")) {
const watcher = fs.watch('./locales'); const watcher = fs.watch('./locales');
for await (const event of watcher) { for await (const event of watcher) {
const filename = event.filename?.replaceAll('\\', '/'); const filename = event.filename?.replaceAll('\\', '/');
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) { if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
console.log(`update ${filename} ...`) console.log(`update ${filename} ...`)
locales = buildLocales(); locales = buildLocales();
await copyFrontendLocales() await copyFrontendLocales()
} }
} }
} }

View file

@ -64,7 +64,7 @@ execa('pnpm', ['--filter', 'backend', 'dev'], {
stderr: process.stderr, stderr: process.stderr,
}); });
execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], { execa('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../', cwd: _dirname + '/../',
stdout: process.stdout, stdout: process.stdout,
stderr: process.stderr, stderr: process.stderr,