Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9b5e66cdb |
+2
-11
@@ -110,19 +110,10 @@ port: 3000
|
||||
# Changes how the server interpret the origin IP of the request.
|
||||
#
|
||||
# Any format supported by Fastify is accepted.
|
||||
# Default: trust all proxies (i.e. trustProxy: true)
|
||||
# Default: do not trust any proxies (i.e. trustProxy: false)
|
||||
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
|
||||
# To improve security, we recommend that you configure your settings appropriately.
|
||||
# Incorrect configuration can cause issues such as difficulty signing in,
|
||||
# so please configure your settings carefully.
|
||||
#
|
||||
#trustProxy:
|
||||
# - '10.0.0.0/8'
|
||||
# - '172.16.0.0/12'
|
||||
# - '192.168.0.0/16'
|
||||
# - '127.0.0.1/32'
|
||||
# - '::1/128'
|
||||
# - 'fc00::/7'
|
||||
# trustProxy: false
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
+6
-10
@@ -1,17 +1,13 @@
|
||||
## 2025.12.2
|
||||
|
||||
### Note
|
||||
v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`に変更」について、正しく環境に応じた設定を行わないとサインインが困難になるといった状態を緩和するために、以前のデフォルト値に戻す暫定対応を行いました。
|
||||
|
||||
**セキュリティを向上させるためには適切な設定を行うことを推奨しますが、間違った設定値を入れると上述のような不具合の原因となりますので、慎重に行ってください。**
|
||||
## Unreleased
|
||||
|
||||
### General
|
||||
- 依存関係の更新
|
||||
-
|
||||
|
||||
### Client
|
||||
- Enhance: デッキのUI説明を追加
|
||||
- Fix: バージョン表記のないPlayが正しく動作しない問題を修正
|
||||
バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。
|
||||
-
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
|
||||
## 2025.12.1
|
||||
|
||||
@@ -2890,15 +2890,6 @@ _deck:
|
||||
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
|
||||
flexible: "幅を自動調整"
|
||||
enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
|
||||
showHowToUse: "UIの説明を見る"
|
||||
|
||||
_howToUse:
|
||||
addColumn_title: "カラム追加"
|
||||
addColumn_description: "カラムの種類を選んで追加できます。"
|
||||
settings_title: "UI設定"
|
||||
settings_description: "デッキUIの詳細設定を行えます。"
|
||||
switchProfile_title: "プロファイル切り替え"
|
||||
switchProfile_description: "UIのレイアウトをプロファイルとして保存し、いつでも切り替えられるようにできます。"
|
||||
|
||||
_columns:
|
||||
main: "メイン"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.12.2-beta.2",
|
||||
"version": "2025.12.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.948.0",
|
||||
"@aws-sdk/lib-storage": "3.948.0",
|
||||
"@aws-sdk/client-s3": "3.947.0",
|
||||
"@aws-sdk/lib-storage": "3.947.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/cors": "11.1.0",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "11.4.1",
|
||||
"@fastify/multipart": "9.3.0",
|
||||
@@ -166,7 +166,7 @@
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.27.14",
|
||||
"systeminformation": "5.27.12",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
@@ -207,7 +207,7 @@
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/simple-oauth2": "5.0.8",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "15.0.1",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
|
||||
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public async launch(): Promise<void> {
|
||||
const fastify = Fastify({
|
||||
trustProxy: this.config.trustProxy ?? true,
|
||||
trustProxy: this.config.trustProxy ?? false,
|
||||
logger: false,
|
||||
});
|
||||
this.#fastify = fastify;
|
||||
|
||||
@@ -313,16 +313,12 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
if (ep.meta.limit) {
|
||||
let limitActor: string | null;
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
limitActor = null;
|
||||
} else {
|
||||
limitActor = getIpHash(request.ip);
|
||||
}
|
||||
limitActor = getIpHash(request.ip);
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
@@ -334,7 +330,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
||||
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
||||
|
||||
if (limitActor != null && factor > 0) {
|
||||
if (factor > 0) {
|
||||
// Rate limit
|
||||
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
|
||||
if (rateLimit != null) {
|
||||
|
||||
@@ -89,21 +89,17 @@ export class SigninApiService {
|
||||
return { error };
|
||||
}
|
||||
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
|
||||
@@ -84,23 +84,19 @@ export class SigninWithPasskeyApiService {
|
||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||
};
|
||||
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
try {
|
||||
try {
|
||||
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
|
||||
// NOTE: 1 Sign-in require 2 API calls
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Initiate Passkey Auth challenge with context
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -12,8 +12,6 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -81,7 +79,6 @@ export const paramDef = {
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
query: { type: 'string', nullable: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -103,7 +100,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private followingEntityService: FollowingEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||
@@ -142,21 +138,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
.andWhere('following.followeeId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('following.follower', 'follower');
|
||||
|
||||
if (ps.query) {
|
||||
const searchQuery = ps.query;
|
||||
const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1;
|
||||
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.where('follower.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' });
|
||||
|
||||
if (isUsername) {
|
||||
qb.orWhere('follower.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' });
|
||||
} else if (this.userEntityService.validateLocalUsername(searchQuery)) {
|
||||
qb.orWhere('follower.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const followings = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { birthdaySchema } from '@/models/User.js';
|
||||
@@ -13,8 +13,6 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -88,21 +86,9 @@ export const paramDef = {
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
birthday: { ...birthdaySchema, nullable: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
oneOf: [{
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
birthday: { ...birthdaySchema, nullable: true },
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
@@ -122,7 +108,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private followingEntityService: FollowingEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||
@@ -161,21 +146,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
// query takes priority over birthday
|
||||
if ('query' in ps && ps.query != null) {
|
||||
const searchQuery = ps.query;
|
||||
const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1;
|
||||
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.where('followee.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' });
|
||||
|
||||
if (isUsername) {
|
||||
qb.orWhere('followee.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' });
|
||||
} else if (this.userEntityService.validateLocalUsername(searchQuery)) {
|
||||
qb.orWhere('followee.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' });
|
||||
}
|
||||
}));
|
||||
} else if ('birthday' in ps && ps.birthday != null) {
|
||||
if (ps.birthday) {
|
||||
try {
|
||||
const birthday = ps.birthday.substring(5, 10);
|
||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.53.3",
|
||||
"sass": "1.95.1",
|
||||
"sass": "1.95.0",
|
||||
"shiki": "3.19.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "13.0.0",
|
||||
@@ -61,8 +61,8 @@
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.1.8",
|
||||
"vue-component-type-helpers": "3.1.7",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.8"
|
||||
"vue-tsc": "3.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.25.8",
|
||||
"mediabunny": "1.25.7",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -66,7 +66,7 @@
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.53.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.95.1",
|
||||
"sass": "1.95.0",
|
||||
"shiki": "3.19.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.181.2",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "10.1.5",
|
||||
"@storybook/addon-links": "10.1.4",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
@@ -90,13 +90,13 @@
|
||||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "10.1.5",
|
||||
"@storybook/react-vite": "10.1.5",
|
||||
"@storybook/react": "10.1.4",
|
||||
"@storybook/react-vite": "10.1.4",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "10.1.5",
|
||||
"@storybook/vue3-vite": "10.1.5",
|
||||
"@storybook/vue3": "10.1.4",
|
||||
"@storybook/vue3-vite": "10.1.4",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
@@ -133,7 +133,7 @@
|
||||
"react-dom": "19.2.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"storybook": "10.1.5",
|
||||
"storybook": "10.1.4",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
@@ -141,7 +141,7 @@
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "4.0.15",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.1.8",
|
||||
"vue-component-type-helpers": "3.1.7",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.8"
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="$style.controls">
|
||||
<div class="_spacer _gaps">
|
||||
<MkButton inline rounded primary @click="chooseFile">{{ i18n.ts.selectFile }}</MkButton>
|
||||
|
||||
<MkRange v-model="params.borderThickness" :min="0" :max="0.2" :step="0.01" :continuousUpdate="true">
|
||||
<template #label>{{ i18n.ts._imageFrameEditor.borderThickness }}</template>
|
||||
</MkRange>
|
||||
@@ -175,6 +177,7 @@ import { ensureSignin } from '@/i.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { selectFile } from '@/utility/drive.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
@@ -216,6 +219,7 @@ const params = reactive<ImageFrameParams>(deepClone(props.params) ?? {
|
||||
bgColor: [1, 1, 1],
|
||||
fgColor: [0, 0, 0],
|
||||
font: 'sans-serif',
|
||||
imageUrl: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -409,6 +413,28 @@ function getRgb(hex: string | number): [number, number, number] | null {
|
||||
if (m == null) return [0, 0, 0];
|
||||
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
|
||||
}
|
||||
|
||||
function chooseFile(ev: MouseEvent) {
|
||||
selectFile({
|
||||
anchorElement: ev.currentTarget ?? ev.target,
|
||||
multiple: false,
|
||||
label: i18n.ts.selectFile,
|
||||
features: {
|
||||
watermark: false,
|
||||
},
|
||||
}).then((file) => {
|
||||
if (!file.type.startsWith('image')) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
title: i18n.ts._watermarkEditor.driveFileTypeWarn,
|
||||
text: i18n.ts._watermarkEditor.driveFileTypeWarnDescription,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
params.imageUrl = file.url;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
|
||||
@@ -193,7 +193,7 @@ function start() {
|
||||
}
|
||||
|
||||
function getIsLegacy(version: string | null): boolean {
|
||||
if (version == null) return true;
|
||||
if (version == null) return false;
|
||||
try {
|
||||
return compareVersions(version, '1.0.0') < 0;
|
||||
} catch {
|
||||
@@ -206,7 +206,7 @@ async function run() {
|
||||
if (!flash.value) return;
|
||||
|
||||
const version = utils.getLangVersion(flash.value.script);
|
||||
const isLegacy = getIsLegacy(version);
|
||||
const isLegacy = version != null && getIsLegacy(version);
|
||||
|
||||
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
|
||||
<FormSection>
|
||||
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
|
||||
<MkPagination :paginator="paginator" withControl :forceDisableInfiniteScroll="true">
|
||||
<MkPagination :paginator="paginator" withControl>
|
||||
<template #default="{items}">
|
||||
<div>
|
||||
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
||||
|
||||
@@ -25,16 +25,11 @@ const props = defineProps<{
|
||||
type: 'following' | 'followers';
|
||||
}>();
|
||||
|
||||
const followingPaginator = markRaw(new Paginator<'users/following', {
|
||||
req: Misskey.entities.UsersFollowingRequest & { query?: string; };
|
||||
res: Misskey.entities.UsersFollowingResponse;
|
||||
}>('users/following', {
|
||||
const followingPaginator = markRaw(new Paginator('users/following', {
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
canSearch: true,
|
||||
searchParamName: 'query',
|
||||
}));
|
||||
|
||||
const followersPaginator = markRaw(new Paginator('users/followers', {
|
||||
@@ -42,8 +37,6 @@ const followersPaginator = markRaw(new Paginator('users/followers', {
|
||||
computedParams: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
canSearch: true,
|
||||
searchParamName: 'query',
|
||||
}));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -257,23 +257,20 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
|
||||
this.rewriteRawState(key, v);
|
||||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
this.emit('committed', {
|
||||
key,
|
||||
value: v,
|
||||
oldValue: this.s[key],
|
||||
});
|
||||
|
||||
const _save = () => {
|
||||
this.save();
|
||||
this.emit('committed', {
|
||||
key,
|
||||
value: v,
|
||||
oldValue: this.s[key],
|
||||
});
|
||||
};
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
server: host,
|
||||
account: currentAccount.id,
|
||||
}), v, {}]);
|
||||
_save();
|
||||
this.save();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -281,12 +278,12 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
server: host,
|
||||
}), v, {}]);
|
||||
_save();
|
||||
this.save();
|
||||
return;
|
||||
}
|
||||
|
||||
record[1] = v;
|
||||
_save();
|
||||
this.save();
|
||||
|
||||
if (record[2].sync) {
|
||||
// awaitの必要なし
|
||||
|
||||
@@ -12,7 +12,6 @@ export const TIPS = [
|
||||
'clips',
|
||||
'userLists',
|
||||
'postForm',
|
||||
'deck',
|
||||
'tl.home',
|
||||
'tl.local',
|
||||
'tl.social',
|
||||
|
||||
@@ -38,39 +38,36 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@headerWheel="onWheel"
|
||||
/>
|
||||
</section>
|
||||
<div v-if="layout.length === 0" class="_panel _gaps" :class="$style.onboarding">
|
||||
<div v-if="layout.length === 0" class="_panel" :class="$style.onboarding">
|
||||
<div>{{ i18n.ts._deck.introduction }}</div>
|
||||
<div>{{ i18n.ts._deck.introduction2 }}</div>
|
||||
<MkInfo v-if="!store.r.tips.value.deck" closable @close="closeTip('deck')">
|
||||
<button class="_textButton" @click="showTour">{{ i18n.ts._deck.showHowToUse }}</button>
|
||||
</MkInfo>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prefer.r['deck.menuPosition'].value === 'right'" :class="$style.sideMenu">
|
||||
<div :class="$style.sideMenuTop">
|
||||
<button ref="swicthProfileButtonEl" v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
|
||||
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
|
||||
</div>
|
||||
<div :class="$style.sideMenuMiddle">
|
||||
<button ref="addColumnButtonEl" v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
|
||||
</div>
|
||||
<div :class="$style.sideMenuBottom">
|
||||
<button ref="settingsButtonEl" v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="prefer.r['deck.menuPosition'].value === 'bottom'" :class="$style.bottomMenu">
|
||||
<div :class="$style.bottomMenuLeft">
|
||||
<button ref="swicthProfileButtonEl" v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
|
||||
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
|
||||
</div>
|
||||
<div :class="$style.bottomMenuMiddle">
|
||||
<button ref="addColumnButtonEl" v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
|
||||
</div>
|
||||
<div :class="$style.bottomMenuRight">
|
||||
<button ref="settingsButtonEl" v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
|
||||
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,7 +96,6 @@ import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deviceKind } from '@/utility/device-kind.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import XMainColumn from '@/ui/deck/main-column.vue';
|
||||
import XTlColumn from '@/ui/deck/tl-column.vue';
|
||||
import XAntennaColumn from '@/ui/deck/antenna-column.vue';
|
||||
@@ -111,13 +107,10 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
||||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||
import XChatColumn from '@/ui/deck/chat-column.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
|
||||
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
|
||||
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
|
||||
import { startTour } from '@/utility/tour.js';
|
||||
import { closeTip } from '@/tips.js';
|
||||
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
@@ -170,9 +163,6 @@ function showSettings() {
|
||||
}
|
||||
|
||||
const columnsEl = useTemplateRef('columnsEl');
|
||||
const addColumnButtonEl = useTemplateRef('addColumnButtonEl');
|
||||
const settingsButtonEl = useTemplateRef('settingsButtonEl');
|
||||
const swicthProfileButtonEl = useTemplateRef('swicthProfileButtonEl');
|
||||
|
||||
const addColumn = async (ev) => {
|
||||
const { canceled, result: column } = await os.select({
|
||||
@@ -228,30 +218,6 @@ async function deleteProfile() {
|
||||
os.success();
|
||||
}
|
||||
|
||||
function showTour() {
|
||||
if (addColumnButtonEl.value == null ||
|
||||
settingsButtonEl.value == null ||
|
||||
swicthProfileButtonEl.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTour([{
|
||||
element: addColumnButtonEl.value,
|
||||
title: i18n.ts._deck._howToUse.addColumn_title,
|
||||
description: i18n.ts._deck._howToUse.addColumn_description,
|
||||
}, {
|
||||
element: settingsButtonEl.value,
|
||||
title: i18n.ts._deck._howToUse.settings_title,
|
||||
description: i18n.ts._deck._howToUse.settings_description,
|
||||
}, {
|
||||
element: swicthProfileButtonEl.value,
|
||||
title: i18n.ts._deck._howToUse.switchProfile_title,
|
||||
description: i18n.ts._deck._howToUse.switchProfile_description,
|
||||
}]).then(() => {
|
||||
closeTip('deck');
|
||||
});
|
||||
}
|
||||
|
||||
window.document.documentElement.style.overflowY = 'hidden';
|
||||
window.document.documentElement.style.scrollBehavior = 'auto';
|
||||
</script>
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ImageFrameParams = {
|
||||
fgColor: [r: number, g: number, b: number];
|
||||
font: 'serif' | 'sans-serif';
|
||||
borderRadius: number; // TODO
|
||||
imageUrl: string | null;
|
||||
};
|
||||
|
||||
export type ImageFramePreset = {
|
||||
@@ -241,6 +242,17 @@ export class ImageFrameRenderer {
|
||||
this.compositor.registerTexture('bottomLabel', bottomLabelImage);
|
||||
}
|
||||
|
||||
if (params.imageUrl != null) {
|
||||
const frameImage = new Image();
|
||||
frameImage.crossOrigin = 'anonymous';
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
frameImage.onload = () => resolve();
|
||||
frameImage.onerror = () => reject(new Error('Failed to load frame image'));
|
||||
frameImage.src = params.imageUrl!;
|
||||
});
|
||||
this.compositor.registerTexture('frameImage', frameImage);
|
||||
}
|
||||
|
||||
this.compositor.changeResolution(renderWidth, renderHeight);
|
||||
|
||||
this.compositor.render([{
|
||||
@@ -248,6 +260,7 @@ export class ImageFrameRenderer {
|
||||
id: 'a',
|
||||
params: {
|
||||
image: 'image',
|
||||
frameImage: 'frameImage',
|
||||
topLabel: 'topLabel',
|
||||
bottomLabel: 'bottomLabel',
|
||||
topLabelEnabled: params.labelTop.enabled,
|
||||
|
||||
@@ -10,6 +10,7 @@ in vec2 in_uv;
|
||||
uniform sampler2D in_texture;
|
||||
uniform vec2 in_resolution;
|
||||
uniform sampler2D u_image;
|
||||
uniform sampler2D u_frameImage;
|
||||
uniform sampler2D u_topLabel;
|
||||
uniform sampler2D u_bottomLabel;
|
||||
uniform bool u_topLabelEnabled;
|
||||
@@ -37,6 +38,9 @@ void main() {
|
||||
remap(in_uv.y, u_paddingTop, 1.0 - u_paddingBottom, 0.0, 1.0)
|
||||
));
|
||||
|
||||
vec4 imageFrame_color = texture(u_frameImage, in_uv);
|
||||
image_color = vec4(blendAlpha(image_color.rgb, imageFrame_color), 1.0);
|
||||
|
||||
vec4 topLabel_color = u_topLabelEnabled ? texture(u_topLabel, vec2(
|
||||
in_uv.x,
|
||||
remap(in_uv.y, 0.0, u_paddingTop, 0.0, 1.0)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { defineImageCompositorFunction } from '@/lib/ImageCompositor.js';
|
||||
|
||||
export const FN_frame = defineImageCompositorFunction<{
|
||||
image: string | null;
|
||||
frameImage: string | null;
|
||||
topLabel: string | null;
|
||||
bottomLabel: string | null;
|
||||
topLabelEnabled: boolean;
|
||||
@@ -36,21 +37,30 @@ export const FN_frame = defineImageCompositorFunction<{
|
||||
gl.uniform1f(u.paddingRight, params.paddingRight);
|
||||
gl.uniform3f(u.bg, params.bg[0], params.bg[1], params.bg[2]);
|
||||
|
||||
if (params.frameImage != null) {
|
||||
const frameImage = textures.get(params.frameImage);
|
||||
if (frameImage) {
|
||||
gl.activeTexture(gl.TEXTURE2);
|
||||
gl.bindTexture(gl.TEXTURE_2D, frameImage.texture);
|
||||
gl.uniform1i(u.frameImage, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.topLabelEnabled && params.topLabel != null) {
|
||||
const topLabel = textures.get(params.topLabel);
|
||||
if (topLabel) {
|
||||
gl.activeTexture(gl.TEXTURE2);
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.bindTexture(gl.TEXTURE_2D, topLabel.texture);
|
||||
gl.uniform1i(u.topLabel, 2);
|
||||
gl.uniform1i(u.topLabel, 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.bottomLabelEnabled && params.bottomLabel != null) {
|
||||
const bottomLabel = textures.get(params.bottomLabel);
|
||||
if (bottomLabel) {
|
||||
gl.activeTexture(gl.TEXTURE3);
|
||||
gl.activeTexture(gl.TEXTURE4);
|
||||
gl.bindTexture(gl.TEXTURE_2D, bottomLabel.texture);
|
||||
gl.uniform1i(u.bottomLabel, 3);
|
||||
gl.uniform1i(u.bottomLabel, 4);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10936,36 +10936,6 @@ export interface Locale extends ILocale {
|
||||
* プロファイル情報のデバイス間同期を有効にする
|
||||
*/
|
||||
"enableSyncBetweenDevicesForProfiles": string;
|
||||
/**
|
||||
* UIの説明を見る
|
||||
*/
|
||||
"showHowToUse": string;
|
||||
"_howToUse": {
|
||||
/**
|
||||
* カラム追加
|
||||
*/
|
||||
"addColumn_title": string;
|
||||
/**
|
||||
* カラムの種類を選んで追加できます。
|
||||
*/
|
||||
"addColumn_description": string;
|
||||
/**
|
||||
* UI設定
|
||||
*/
|
||||
"settings_title": string;
|
||||
/**
|
||||
* デッキUIの詳細設定を行えます。
|
||||
*/
|
||||
"settings_description": string;
|
||||
/**
|
||||
* プロファイル切り替え
|
||||
*/
|
||||
"switchProfile_title": string;
|
||||
/**
|
||||
* UIのレイアウトをプロファイルとして保存し、いつでも切り替えられるようにできます。
|
||||
*/
|
||||
"switchProfile_description": string;
|
||||
};
|
||||
"_columns": {
|
||||
/**
|
||||
* メイン
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.12.2-beta.2",
|
||||
"version": "2025.12.1",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
@@ -34768,7 +34768,6 @@ export interface operations {
|
||||
untilDate?: number;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
query?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -34848,11 +34847,8 @@ export interface operations {
|
||||
untilDate?: number;
|
||||
/** @default 10 */
|
||||
limit?: number;
|
||||
} & ({
|
||||
query?: string;
|
||||
} | {
|
||||
birthday?: string | null;
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
||||
Generated
+314
-294
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -35,4 +35,5 @@ ignorePatchFailures: false
|
||||
minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
|
||||
minimumReleaseAgeExclude:
|
||||
- '@syuilo/aiscript'
|
||||
- systeminformation # 脆弱性対応。そのうち消すこと
|
||||
- vue-tsc
|
||||
- '@vue/language-core'
|
||||
|
||||
Reference in New Issue
Block a user