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
uses: FedericoCarboni/setup-ffmpeg@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
@ -116,7 +116,7 @@ jobs:
with:
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.2.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -115,25 +115,10 @@ pnpm dev
command.
- Server-side source files and automatically builds them if they are modified. Automatically start the server process(es).
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- Service Worker is watched by esbuild.
- The front end can be viewed by accessing `http://localhost:5173`.
- The backend listens on the port configured with `port` in .config/default.yml.
If you have not changed it from the default, it will be "http://localhost:3000".
If "port" in .config/default.yml is set to something other than 3000, you need to change the proxy settings in packages/frontend/vite.config.local-dev.ts.
### `MK_DEV_PREFER=backend pnpm dev`
pnpm dev has another mode with `MK_DEV_PREFER=backend`.
```
MK_DEV_PREFER=backend pnpm dev
```
- This mode is closer to the production environment than the default mode.
- Vite runs behind the backend (the backend will proxy Vite at /vite).
- Vite HMR (just the `vite` command) is available. The behavior may be different from production.
- Vite runs behind the backend (the backend will proxy Vite at /vite except for websocket used for HMR).
- You can see Misskey by accessing `http://localhost:3000` (Replace `3000` with the port configured with `port` in .config/default.yml).
- To change the port of Vite, specify with `VITE_PORT` environment variable.
- HMR may not work in some environments such as Windows.
### Dev Container
Instead of running `pnpm` locally, you can use Dev Container to set up your development environment.

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."
movedAndCannotBeUndone: "\nThis account has been migrated.\nMigration cannot be reversed."
postMigrationNote: "This account will unfollow all accounts it is currently following 24 hours after migration finishes.\nBoth the number of follows and followers will then become zero. To avoid your followers from being unable to see followers only posts of this account, they will however continue following this account."
movedTo: "New account:"
movedTo: "Migrated account:"
movedToServer: "Migrated server"
movedFromServer: "Original server"
_achievements:
earnedAt: "Unlocked at"
_types:

8
locales/index.d.ts vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -356,7 +356,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
*
*/
@bindThis
public async getUserBadgeRoles(userId: MiUser['id']) {
public async getUserBadgeRoles(userId: MiUser['id'], publicOnly: boolean) {
const now = Date.now();
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
// 期限切れのロールを除外
@ -368,12 +368,25 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
if (badgeCondRoles.length > 0) {
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
return this.sortAndMapBadgeRoles([...assignedBadgeRoles, ...matchedBadgeCondRoles], publicOnly);
} else {
return assignedBadgeRoles;
return this.sortAndMapBadgeRoles(assignedBadgeRoles, publicOnly);
}
}
@bindThis
private sortAndMapBadgeRoles(roles: MiRole[], publicOnly: boolean) {
return roles
.filter((r) => r.isPublic || !publicOnly)
.sort((a, b) => b.displayOrder - a.displayOrder)
.map((r) => ({
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
behavior: r.badgeBehavior ?? undefined,
}));
}
@bindThis
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch();

View file

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

View file

@ -518,16 +518,7 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
badgeRoles: this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
.filter((r) => r.isPublic || iAmModerator)
.sort((a, b) => b.displayOrder - a.displayOrder)
.map((r) => ({
name: r.name,
iconUrl: r.iconUrl,
displayOrder: r.displayOrder,
behavior: r.badgeBehavior ?? undefined,
})),
),
badgeRoles: this.roleService.getUserBadgeRoles(user.id, !iAmModerator),
...(isDetailed ? {
url: profile?.url,

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>);
return Object.entries(params)
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
.join('&');
}
export function appendQuery(url: string, query: string): string {
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
}
export function omitHttps(url: string): string {
if (url.startsWith('https://')) return url.slice(8);
if (url.startsWith('https%3A%2F%2F')) return url.slice(14);
return url;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -62,6 +62,8 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
movedFromId: { type: 'string', format: 'misskey:id', nullable: true },
movedToId: { type: 'string', format: 'misskey:id', nullable: true },
from: { type: 'string', enum: ['local', 'remote', 'all'], nullable: true },
to: { type: 'string', enum: ['local', 'remote', 'all'], nullable: true },
},
required: [],
} as const;
@ -86,6 +88,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('accountMoveLogs.movedToId = :movedToId', { movedToId: ps.movedToId });
}
if (ps.from != null || ps.to != null) {
query
.innerJoin('accountMoveLogs.movedFrom', 'movedFrom')
.innerJoin('accountMoveLogs.movedTo', 'movedTo');
if (ps.from === 'local') {
query.andWhere('movedFrom.host IS NULL');
}
if (ps.from === 'remote') {
query.andWhere('movedFrom.host IS NOT NULL');
}
if (ps.to === 'local') {
query.andWhere('movedTo.host IS NULL');
}
if (ps.to === 'remote') {
query.andWhere('movedTo.host IS NOT NULL');
}
}
const accountMoveLogs = await query.limit(ps.limit).getMany();
return await this.userAccountMoveLogEntityService.packMany(accountMoveLogs, me);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,7 +36,8 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons.min.css`)
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons-outline.min.css`)
link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons-filled.min.css`)
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

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/fedi.jpg?raw=true" as="image" type="image/jpeg" crossorigin="anonymous">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@3.5.0/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/@fontsource/m-plus-rounded-1c/index.css">
<style>
html {

View file

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

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;
waiting();
document.cookie.split(';').forEach((cookie) => {
const cookieName = cookie.split('=')[0].trim();
if (cookieName === 'token') {
document.cookie = `${cookieName}=; max-age=0; path=/`;
}
});
miLocalStorage.removeItem('account');
await removeAccount($i.id);
const accounts = await getAccounts();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -127,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
<i v-if=" (appearNote.reactionAcceptance === 'likeOnly' || !$i?.policies.canUseReaction) && appearNote.myReaction != null " class="ti-filled ti-filled-heart" style="color: var(--eventReactionHeart);"></i>
<i v-else-if="appearNote.myReaction != null " class="ti ti-minus" style="color: var(--accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || $i && !$i.policies.canUseReaction" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i>

View file

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

View file

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

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 v-for="ad in ads" class="_panel _gaps_m" :class="$style.ad">
<MkAd v-if="ad.url" :key="ad.id" :specify="ad"/>
<MkInput v-if="ad.id" v-model="ad.id" :readonly="true">
<template #label>ID</template>
</MkInput>
<MkInput v-model="ad.url" type="url">
<template #label>URL</template>
</MkInput>

View file

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

View file

@ -182,24 +182,24 @@ definePageMetadata(() => ({
}
.rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .body {
padding: 32px;
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
}
& + .file {
margin-top: 16px;
}
}
}
> .title {
font-weight: bold;
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-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
<div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
<i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti-filled ti-filled-circle' : 'ti ti-circle'"></i>
</div>
</div>
</div>

View file

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

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { query } from '@/scripts/url.js';
import { appendQuery, omitHttps, query } from '@/scripts/url.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
@ -12,18 +12,26 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji'
if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
// もう既にproxyっぽそうだったらurlを取り出す
imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
const url = (new URL(imageUrl)).searchParams.get('url');
if (url) {
imageUrl = url;
} else if (imageUrl.startsWith(instance.mediaProxy + '/')) {
imageUrl = imageUrl.slice(instance.mediaProxy.length + 1);
} else if (imageUrl.startsWith('/proxy/')) {
imageUrl = imageUrl.slice('/proxy/'.length);
} else if (imageUrl.startsWith(localProxy + '/')) {
imageUrl = imageUrl.slice(localProxy.length + 1);
}
}
return `${mustOrigin ? localProxy : instance.mediaProxy}/${
type === 'preview' ? 'preview.webp'
: 'image.webp'
}?${query({
url: imageUrl,
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
})}`;
return appendQuery(
`${mustOrigin ? localProxy : instance.mediaProxy}/${type === 'preview' ? 'preview' : 'image'}/${encodeURIComponent(omitHttps(imageUrl))}`,
query({
...(!noFallback ? { 'fallback': '1' } : {}),
...(type ? { [type]: '1' } : {}),
...(mustOrigin ? { origin: '1' } : {}),
}),
);
}
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
@ -46,8 +54,8 @@ export function getStaticImageUrl(baseUrl: string): string {
return u.href;
}
return `${instance.mediaProxy}/static.webp?${query({
url: u.href,
static: '1',
})}`;
return appendQuery(
`${instance.mediaProxy}/static/${encodeURIComponent(omitHttps(u.href))}`,
query({ static: '1' }),
);
}

View file

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

View file

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

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 typescript from '@rollup/plugin-typescript';
import { type UserConfig, defineConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
import locales from '../../locales/index.js';
import meta from '../../package.json';
@ -10,6 +12,9 @@ import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
/**
@ -65,6 +70,7 @@ export function getConfig(): UserConfig {
base: '/vite/',
server: {
host,
port: 5173,
},

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@types/node": "22.10.7",
"@types/node": "22.13.0",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.1",

View file

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

View file

@ -21,6 +21,16 @@ async function copyFrontendFonts() {
async function copyFrontendTablerIcons() {
await fs.cp('./packages/frontend/node_modules/@tabler/icons-webfont', `./built/_frontend_dist_/tabler-icons.${meta.version}`, { dereference: true, recursive: true });
for (const file of [
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.scss`,
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.css`,
`./built/_frontend_dist_/tabler-icons.${meta.version}/dist/tabler-icons-filled.min.css`,
]) {
let source = await fs.readFile(file, { encoding: 'utf-8' });
source = source.replaceAll('$ti-prefix: \'ti\'', '$ti-prefix: \'ti-filled\'');
source = source.replaceAll('.ti', '.ti-filled');
await fs.writeFile(file, source);
}
}
async function copyFrontendLocales() {
@ -91,13 +101,13 @@ async function build() {
await build();
if (process.argv.includes("--watch")) {
const watcher = fs.watch('./locales');
for await (const event of watcher) {
const filename = event.filename?.replaceAll('\\', '/');
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
console.log(`update ${filename} ...`)
locales = buildLocales();
await copyFrontendLocales()
}
}
const watcher = fs.watch('./locales');
for await (const event of watcher) {
const filename = event.filename?.replaceAll('\\', '/');
if (/^[a-z]+-[A-Z]+\.yml/.test(filename)) {
console.log(`update ${filename} ...`)
locales = buildLocales();
await copyFrontendLocales()
}
}
}

View file

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