1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-07 06:26:01 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
syuilo
5fd6d65042 wip 2025-05-12 09:29:08 +09:00
syuilo
c25f49ebb1 Merge branch 'develop' into ptr-sfx 2025-05-11 19:56:29 +09:00
syuilo
453c6d4180 wip 2025-05-05 19:41:31 +09:00
43 changed files with 191 additions and 707 deletions

View File

@@ -215,6 +215,20 @@ proxyBypassHosts:
# Media Proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: true)
proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -227,6 +227,12 @@ proxyBypassHosts:
# Media Proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: true)
proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
@@ -234,6 +240,14 @@ proxyBypassHosts:
# '127.0.0.1/32'
#]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
# Log settings
# logging:
# sql:

View File

@@ -319,12 +319,19 @@ proxyBypassHosts:
# * Perform image compression (on a different server resource than the main process)
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: true)
# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains.
proxyRemoteFiles: true
# Movie Thumbnail Generation URL
# There is no reference implementation.
# For example, Misskey will point to the following URL:
# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4
#videoThumbnailGenerator: https://example.com
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
@@ -332,6 +339,14 @@ proxyBypassHosts:
# '127.0.0.1/32'
#]
# Disable automatic redirect for ActivityPub object lookup. (default: false)
# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation.
# However it will make it impossible for other instances to lookup third-party user and notes through your URL.
#disallowExternalApRedirect: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
# PID File of master process
#pidFile: /tmp/misskey.pid

View File

@@ -202,6 +202,15 @@ proxyBypassHosts:
# Media Proxy
#mediaProxy: https://example.com/proxy
# Proxy remote files (default: true)
proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
allowedPrivateNetworks: [
'127.0.0.1/32'
]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -1,14 +1,5 @@
## 2025.5.1
### Note
- 設定ファイルの以下の項目がコントロールパネルから設定するようになりました
- signToActivityPubGet
- proxyRemoteFiles
- disallowExternalApRedirect
- 許可しないかどうかではなく、許可するかどうかの設定(allowExternalApRedirect)になりました
- 設定ファイルの maxFileSize が削除されました
- リクエストの最大ペイロードサイズは、nginxやCDNなどのMisskeyの前段で制限をするか、ロール設定で制限してください。
### General
- Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました
- モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます
@@ -23,8 +14,6 @@
- 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です
- 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました
- チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます
- Feat: 絵文字をミュート可能にする機能
- 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました
- Enhance: メモリ使用量を軽減しました
- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加
- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように

View File

@@ -221,6 +221,12 @@ id: "aidx"
# Media Proxy
#mediaProxy: https://example.com/proxy
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

76
locales/index.d.ts vendored
View File

@@ -707,7 +707,7 @@ export interface Locale extends ILocale {
*/
"cacheRemoteFiles": string;
/**
* この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。
* この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします
*/
"cacheRemoteFilesDescription": string;
/**
@@ -3186,10 +3186,6 @@ export interface Locale extends ILocale {
* 反映には再起動が必要です。
*/
"needReloadToApply": string;
/**
* 反映にはサーバーの再起動が必要です。
*/
"needToRestartServerToApply": string;
/**
* タイトルバーを表示する
*/
@@ -5429,22 +5425,6 @@ export interface Locale extends ILocale {
* オフにする
*/
"turnItOff": string;
/**
* 絵文字ミュート
*/
"emojiMute": string;
/**
* 絵文字ミュート解除
*/
"emojiUnmute": string;
/**
* {x}をミュート
*/
"muteX": ParameterizedString<"x">;
/**
* {x}のミュートを解除
*/
"unmuteX": ParameterizedString<"x">;
"_chat": {
/**
* まだメッセージはありません
@@ -6456,30 +6436,6 @@ export interface Locale extends ILocale {
* このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。
*/
"singleUserMode_description": string;
/**
* GETリクエストに署名する
*/
"signToActivityPubGet": string;
/**
* 通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。
*/
"signToActivityPubGet_description": string;
/**
* リモートファイルをプロキシする
*/
"proxyRemoteFiles": string;
/**
* 有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。
*/
"proxyRemoteFiles_description": string;
/**
* ActivityPub経由の照会にリダイレクトを許可する
*/
"allowExternalApRedirect": string;
/**
* 有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。
*/
"allowExternalApRedirect_description": string;
/**
* 非利用者に対するユーザー作成コンテンツの公開範囲
*/
@@ -11898,36 +11854,6 @@ export interface Locale extends ILocale {
"text3": string;
};
};
"_clientPerformanceIssueTip": {
/**
* バッテリー消費が多いと感じたら
*/
"title": string;
/**
* アドブロッカーを無効にしてください
*/
"makeSureDisabledAdBlocker": string;
/**
* アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。
*/
"makeSureDisabledAdBlocker_description": string;
/**
* カスタムCSSを無効にしてください
*/
"makeSureDisabledCustomCss": string;
/**
* スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。
*/
"makeSureDisabledCustomCss_description": string;
/**
* 拡張機能を無効にしてください
*/
"makeSureDisabledAddons": string;
/**
* 一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。
*/
"makeSureDisabledAddons_description": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@@ -172,7 +172,7 @@ emojiUrl: "絵文字画像URL"
addEmoji: "絵文字を追加"
settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。"
cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。"
youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。"
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
@@ -792,7 +792,6 @@ wide: "広い"
narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されます。"
needReloadToApply: "反映には再起動が必要です。"
needToRestartServerToApply: "反映にはサーバーの再起動が必要です。"
showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"
onlineUsersCount: "{n}人がオンライン"
@@ -1352,10 +1351,6 @@ advice: "アドバイス"
realtimeMode: "リアルタイムモード"
turnItOn: "オンにする"
turnItOff: "オフにする"
emojiMute: "絵文字ミュート"
emojiUnmute: "絵文字ミュート解除"
muteX: "{x}をミュート"
unmuteX: "{x}のミュートを解除"
_chat:
noMessagesYet: "まだメッセージはありません"
@@ -1640,12 +1635,6 @@ _serverSettings:
deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。"
singleUserMode: "お一人様モード"
singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。"
signToActivityPubGet: "GETリクエストに署名する"
signToActivityPubGet_description: "通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。"
proxyRemoteFiles: "リモートファイルをプロキシする"
proxyRemoteFiles_description: "有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。"
allowExternalApRedirect: "ActivityPub経由の照会にリダイレクトを許可する"
allowExternalApRedirect_description: "有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。"
userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲"
userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。"
userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。"
@@ -3181,12 +3170,3 @@ _serverSetupWizard:
text1: "Misskeyは有志によって開発されている無料のソフトウェアです。"
text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。"
text3: "支援者向け特典もあります!"
_clientPerformanceIssueTip:
title: "バッテリー消費が多いと感じたら"
makeSureDisabledAdBlocker: "アドブロッカーを無効にしてください"
makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。"
makeSureDisabledCustomCss: "カスタムCSSを無効にしてください"
makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。"
makeSureDisabledAddons: "拡張機能を無効にしてください"
makeSureDisabledAddons_description: "一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.5.1-alpha.2",
"version": "2025.5.1-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {loadConfig} from "./js/migration-config.js";
export class MigrateSomeConfigFileSettingsToMeta1746949539915 {
name = 'MigrateSomeConfigFileSettingsToMeta1746949539915'
async up(queryRunner) {
const config = loadConfig();
// $1 cannot be used in ALTER TABLE queries
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`);
await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`);
await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowExternalApRedirect"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "signToActivityPubGet"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`);
}
}

View File

@@ -3,29 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { path as configYamlPath } from '../../built/config.js';
import * as yaml from 'js-yaml';
import fs from "node:fs";
export function isConcurrentIndexMigrationEnabled() {
return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
}
let loadedConfigCache = undefined;
function loadConfigInternal() {
const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8'));
return {
disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false),
proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false),
signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true),
}
}
export function loadConfig() {
if (loadedConfigCache === undefined) {
loadedConfigCache = loadConfigInternal();
}
return loadedConfigCache;
}

View File

@@ -79,6 +79,9 @@ type Source = {
proxyBypassHosts?: string[];
allowedPrivateNetworks?: string[];
disallowExternalApRedirect?: boolean;
maxFileSize?: number;
clusterLimit?: number;
@@ -97,8 +100,11 @@ type Source = {
inboxJobMaxAttempts?: number;
mediaProxy?: string;
proxyRemoteFiles?: boolean;
videoThumbnailGenerator?: string;
signToActivityPubGet?: boolean;
perChannelMaxNoteCacheCount?: number;
perUserNotificationsMaxCount?: number;
deactivateAntennaThreshold?: number;
@@ -150,6 +156,8 @@ export type Config = {
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
disallowExternalApRedirect: boolean;
maxFileSize: number;
clusterLimit: number | undefined;
id: string;
outgoingAddress: string | undefined;
@@ -162,6 +170,8 @@ export type Config = {
relationshipJobPerSec: number | undefined;
deliverJobMaxAttempts: number | undefined;
inboxJobMaxAttempts: number | undefined;
proxyRemoteFiles: boolean | undefined;
signToActivityPubGet: boolean | undefined;
logging?: {
sql?: {
disableQueryTruncation?: boolean,
@@ -219,7 +229,7 @@ const dir = `${_dirname}/../../../.config`;
/**
* Path of configuration file
*/
export const path = process.env.MISSKEY_CONFIG_YML
const path = process.env.MISSKEY_CONFIG_YML
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
: process.env.NODE_ENV === 'test'
? resolve(dir, 'test.yml')
@@ -290,6 +300,8 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
disallowExternalApRedirect: config.disallowExternalApRedirect ?? false,
maxFileSize: config.maxFileSize ?? 262144000,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,
outgoingAddressFamily: config.outgoingAddressFamily,
@@ -301,6 +313,8 @@ export function loadConfig(): Config {
relationshipJobPerSec: config.relationshipJobPerSec,
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
proxyRemoteFiles: config.proxyRemoteFiles,
signToActivityPubGet: config.signToActivityPubGet ?? true,
mediaProxy: externalMediaProxy ?? internalMediaProxy,
externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy,
videoThumbnailGenerator: config.videoThumbnailGenerator ?

View File

@@ -41,7 +41,7 @@ export class DownloadService {
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const maxSize = 1024 * 1024 * 1024; // 1GB
const maxSize = this.config.maxFileSize;
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';

View File

@@ -104,7 +104,7 @@ export class Resolver {
throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked');
}
if (this.meta.signToActivityPubGet && !this.user) {
if (this.config.signToActivityPubGet && !this.user) {
this.user = await this.systemAccountService.fetch('actor');
}

View File

@@ -6,7 +6,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, MiMeta } from '@/models/_.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
@@ -34,9 +34,6 @@ export class DriveFileEntityService {
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@@ -98,7 +95,7 @@ export class DriveFileEntityService {
return this.getProxiedUrl(file.uri, 'static');
}
if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) {
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
// リモートかつ期限切れはローカルプロキシを試みる
// 従来は/files/${thumbnailAccessKey}にアクセスしていたが、
// /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する
@@ -118,7 +115,7 @@ export class DriveFileEntityService {
}
// リモートかつ期限切れはローカルプロキシを試みる
if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) {
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
const key = file.webpublicAccessKey;
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外

View File

@@ -131,6 +131,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
maxFileSize: this.config.maxFileSize,
federation: this.meta.federation,
};

View File

@@ -680,21 +680,6 @@ export class MiMeta {
default: false,
})
public singleUserMode: boolean;
@Column('boolean', {
default: true,
})
public proxyRemoteFiles: boolean;
@Column('boolean', {
default: true,
})
public signToActivityPubGet: boolean;
@Column('boolean', {
default: true,
})
public allowExternalApRedirect: boolean;
}
export type SoftwareSuspension = {

View File

@@ -293,6 +293,10 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
default: 'local',
},
maxFileSize: {
type: 'number',
optional: false, nullable: false,
},
federation: {
type: 'string',
enum: ['all', 'specified', 'none'],

View File

@@ -108,7 +108,7 @@ export class ServerService implements OnApplicationShutdown {
// this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com
//
// this is not required by standard but protect us from peers that did not validate final URL.
if (!this.meta.allowExternalApRedirect) {
if (this.config.disallowExternalApRedirect) {
const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i;
fastify.addHook('onSend', (request, reply, _, done) => {
const location = reply.getHeader('location');
@@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown {
reply.header('content-type', 'text/plain; charset=utf-8');
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
done(null, [
'Refusing to relay remote ActivityPub object lookup.',
'',
"Refusing to relay remote ActivityPub object lookup.",
"",
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
].join('\n'));
});

View File

@@ -51,6 +51,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
fileSize: this.config.maxFileSize,
files: 1,
},
});

View File

@@ -555,18 +555,6 @@ export const meta = {
enum: ['all', 'local', 'none'],
optional: false, nullable: false,
},
proxyRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
signToActivityPubGet: {
type: 'boolean',
optional: false, nullable: false,
},
allowExternalApRedirect: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
} as const;
@@ -714,9 +702,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
singleUserMode: instance.singleUserMode,
ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor,
proxyRemoteFiles: instance.proxyRemoteFiles,
signToActivityPubGet: instance.signToActivityPubGet,
allowExternalApRedirect: instance.allowExternalApRedirect,
};
});
}

View File

@@ -201,9 +201,6 @@ export const paramDef = {
type: 'string',
enum: ['all', 'local', 'none'],
},
proxyRemoteFiles: { type: 'boolean' },
signToActivityPubGet: { type: 'boolean' },
allowExternalApRedirect: { type: 'boolean' },
},
required: [],
} as const;
@@ -706,18 +703,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor;
}
if (ps.proxyRemoteFiles !== undefined) {
set.proxyRemoteFiles = ps.proxyRemoteFiles;
}
if (ps.signToActivityPubGet !== undefined) {
set.signToActivityPubGet = ps.signToActivityPubGet;
}
if (ps.allowExternalApRedirect !== undefined) {
set.allowExternalApRedirect = ps.allowExternalApRedirect;
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);

View File

@@ -17,6 +17,8 @@ proxyBypassHosts:
- www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
proxyRemoteFiles: true
signToActivityPubGet: true
allowedPrivateNetworks:
- 127.0.0.1/32
- 172.20.0.0/16

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -27,6 +27,7 @@ import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js';
import { i18n } from '@/i18n.js';
import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
import * as sound from '@/utility/sound.js';
const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
@@ -200,7 +201,13 @@ function moving(event: MouseEvent | TouchEvent) {
const moveHeight = moveScreenY - startScreenY!;
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
let before = isPulledEnough.value;
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
if (before === false && isPulledEnough.value === true) {
sound.playUrl('/client-assets/sounds/syuilo/sho.mp3', {
volume: 1,
});
}
}
/**
@@ -209,6 +216,9 @@ function moving(event: MouseEvent | TouchEvent) {
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
*/
function refreshFinished() {
sound.playUrl('/client-assets/sounds/syuilo/pa2.mp3', {
volume: 1,
});
closeContent().then(() => {
isPulling.value = false;
isRefreshing.value = false;

View File

@@ -22,7 +22,6 @@ import { computed, inject, onMounted, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { getUnicodeEmoji } from '@@/js/emojilist.js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import type { MenuItem } from '@/types/menu';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import * as os from '@/os.js';
@@ -37,7 +36,6 @@ import { customEmojisMap } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { noteEvents } from '@/composables/use-note-capture.js';
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as isEmojiMuted } from '@/utility/emoji-mute.js';
const props = defineProps<{
noteId: Misskey.entities.Note['id'];
@@ -65,7 +63,6 @@ const canToggle = computed(() => {
return !props.reaction.match(/@\w/) && $i && emoji.value;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
async function toggleReaction() {
if (!canToggle.value) return;
@@ -142,55 +139,21 @@ async function toggleReaction() {
}
async function menu(ev) {
let menuItems: MenuItem[] = [];
if (!canGetInfo.value) return;
if (canGetInfo.value) {
menuItems.push({
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
}),
}, {
closed: () => dispose(),
});
},
});
}
if (isEmojiMuted(props.reaction).value) {
menuItems.push({
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-smile',
action: () => {
os.confirm({
type: 'question',
title: i18n.tsx.unmuteX({ x: isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction }),
}).then(({ canceled }) => {
if (canceled) return;
unmuteEmoji(props.reaction);
});
},
});
} else {
menuItems.push({
text: i18n.ts.emojiMute,
icon: 'ti ti-mood-off',
action: () => {
os.confirm({
type: 'question',
title: i18n.tsx.muteX({ x: isLocalCustomEmoji ? `:${emojiName.value}:` : props.reaction }),
}).then(({ canceled }) => {
if (canceled) return;
muteEmoji(props.reaction);
});
},
});
}
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
os.popupMenu([{
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
}),
}, {
closed: () => dispose(),
});
},
}], ev.currentTarget ?? ev.target);
}
function anime() {

View File

@@ -5,16 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<img
v-if="shouldMute"
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
src="/client-assets/unknown.png"
:title="alt"
draggable="false"
style="-webkit-user-drag: none;"
@click="onClick"
/>
<img
v-else-if="errored && fallbackToImage"
v-if="errored && fallbackToImage"
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
src="/client-assets/dummy.png"
:title="alt"
@@ -49,7 +40,6 @@ import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialo
import { $i } from '@/i.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { makeEmojiMuteKey, mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkEmojiMuted } from '@/utility/emoji-mute';
const props = defineProps<{
name: string;
@@ -61,16 +51,12 @@ const props = defineProps<{
menu?: boolean;
menuReaction?: boolean;
fallbackToImage?: boolean;
ignoreMuted?: boolean;
}>();
const react = inject(DI.mfmEmojiReactCallback);
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
const emojiCodeToMute = makeEmojiMuteKey(props);
const isMuted = checkEmojiMuted(emojiCodeToMute);
const shouldMute = computed(() => !props.ignoreMuted && isMuted.value);
const rawUrl = computed(() => {
if (props.url) {
@@ -109,18 +95,14 @@ function onClick(ev: MouseEvent) {
menuItems.push({
type: 'label',
text: `:${props.name}:`,
}, {
text: i18n.ts.copy,
icon: 'ti ti-copy',
action: () => {
copyToClipboard(`:${props.name}:`);
},
});
if (isLocal.value) {
menuItems.push({
text: i18n.ts.copy,
icon: 'ti ti-copy',
action: () => {
copyToClipboard(`:${props.name}:`);
},
});
}
if (props.menuReaction && react) {
menuItems.push({
text: i18n.ts.doReaction,
@@ -131,43 +113,21 @@ function onClick(ev: MouseEvent) {
});
}
if (isLocal.value) {
menuItems.push({
type: 'divider',
}, {
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: customEmojiName.value,
}),
}, {
closed: () => dispose(),
});
},
});
}
menuItems.push({
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: async () => {
const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', {
name: customEmojiName.value,
}),
}, {
closed: () => dispose(),
});
},
});
if (isMuted.value) {
menuItems.push({
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-smile',
action: async () => {
await unmute();
},
});
} else {
menuItems.push({
text: i18n.ts.emojiMute,
icon: 'ti ti-mood-off',
action: async () => {
await mute();
},
});
}
if (($i?.isModerator ?? $i?.isAdmin) && isLocal.value) {
if ($i?.isModerator ?? $i?.isAdmin) {
menuItems.push({
text: i18n.ts.edit,
icon: 'ti ti-pencil',
@@ -192,36 +152,6 @@ async function edit(name: string) {
});
}
function mute() {
const titleEmojiName = isLocal.value
? `:${customEmojiName.value}:`
: emojiCodeToMute;
os.confirm({
type: 'question',
title: i18n.tsx.muteX({ x: titleEmojiName }),
}).then(({ canceled }) => {
if (canceled) {
return;
}
muteEmoji(emojiCodeToMute);
});
}
function unmute() {
const titleEmojiName = isLocal.value
? `:${customEmojiName.value}:`
: emojiCodeToMute;
os.confirm({
type: 'question',
title: i18n.tsx.unmuteX({ x: titleEmojiName }),
}).then(({ canceled }) => {
if (canceled) {
return;
}
unmuteEmoji(emojiCodeToMute);
});
}
</script>
<style lang="scss" module>

View File

@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<img v-if="shouldMute" :class="$style.root" src="/client-assets/unknown.png" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<img v-else-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
</template>
@@ -19,13 +18,11 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
import { mute as muteEmoji, unmute as unmuteEmoji, checkMuted as checkMutedEmoji } from '@/utility/emoji-mute.js';
const props = defineProps<{
emoji: string;
menu?: boolean;
menuReaction?: boolean;
ignoreMuted?: boolean;
}>();
const react = inject(DI.mfmEmojiReactCallback, null);
@@ -35,38 +32,12 @@ const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : cha
const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
const url = computed(() => char2path(props.emoji));
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
const isMuted = checkMutedEmoji(props.emoji);
const shouldMute = computed(() => isMuted.value && !props.ignoreMuted);
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
function computeTitle(event: PointerEvent): void {
(event.target as HTMLElement).title = getEmojiName(props.emoji);
}
function mute() {
os.confirm({
type: 'question',
title: i18n.tsx.muteX({ x: props.emoji }),
}).then(({ canceled }) => {
if (canceled) {
return;
}
muteEmoji(props.emoji);
});
}
function unmute() {
os.confirm({
type: 'question',
title: i18n.tsx.unmuteX({ x: props.emoji }),
}).then(({ canceled }) => {
if (canceled) {
return;
}
unmuteEmoji(props.emoji);
});
}
function onClick(ev: MouseEvent) {
if (props.menu) {
const menuItems: MenuItem[] = [];
@@ -92,22 +63,6 @@ function onClick(ev: MouseEvent) {
});
}
menuItems.push({
type: 'divider',
}, isMuted.value ? {
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-smile',
action: () => {
unmute();
},
} : {
text: i18n.ts.emojiMute,
icon: 'ti ti-mood-off',
action: () => {
mute();
},
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
}

View File

@@ -435,8 +435,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
normal: props.plain,
host: props.author.host,
useOriginalSize: scale >= 2.5,
menu: props.enableEmojiMenu,
menuReaction: false,
})];
}
}

View File

@@ -35,15 +35,10 @@ export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoint
baseId?: MisskeyEntity['id'];
direction?: 'newer' | 'older';
// 一部のAPIはさらに遡れる場合でもパフォーマンス上の理由でlimit以下の結果を返す場合があり、その場合はsafe、それ以外はlimitにすることを推奨
canFetchDetection?: 'safe' | 'limit';
};
export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I : never>(props: {
ctx: PagingCtx<Endpoint>;
autoInit?: boolean;
autoReInit?: boolean;
useShallowRef?: boolean;
}) {
const items = props.useShallowRef ? shallowRef<T[]>([]) : ref<T[]>([]);
@@ -54,9 +49,8 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
const canFetchOlder = ref(false);
const error = ref(false);
if (props.autoReInit !== false) {
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
}
// パラメータに何らかの変更があった際、再読込したいチャンネル等のIDが変わったなど
watch(() => [props.ctx.endpoint, props.ctx.params], init, { deep: true });
function getNewestId(): string | null | undefined {
// 様々な要因により並び順は保証されないのでソートが必要
@@ -98,20 +92,12 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
if (i === 3) item._shouldInsertAd_ = true;
}
pushItems(res);
if (props.ctx.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
if (res.length === 0 || props.ctx.noPaging) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
if (res.length === 0 || props.ctx.noPaging) {
pushItems(res);
canFetchOlder.value = false;
} else {
pushItems(res);
canFetchOlder.value = true;
}
error.value = false;
@@ -144,22 +130,15 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
if (i === 10) item._shouldInsertAd_ = true;
}
pushItems(res);
if (props.ctx.canFetchDetection === 'limit') {
if (res.length < FIRST_FETCH_LIMIT) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
} else if (props.ctx.canFetchDetection === 'safe' || props.ctx.canFetchDetection == null) {
if (res.length === 0) {
canFetchOlder.value = false;
} else {
canFetchOlder.value = true;
}
if (res.length === 0) {
canFetchOlder.value = false;
fetchingOlder.value = false;
} else {
pushItems(res);
canFetchOlder.value = true;
fetchingOlder.value = false;
}
}).finally(() => {
}, err => {
fetchingOlder.value = false;
});
}
@@ -253,11 +232,9 @@ export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Miss
}
}
if (props.autoInit !== false) {
onMounted(() => {
init();
});
}
onMounted(() => {
init();
});
return {
items: items as DeepReadonly<ShallowRef<T[]>>,

View File

@@ -86,6 +86,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkTextarea>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.files }}</template>
<template v-if="filesForm.modified.value" #footer>
<MkFormFooter :form="filesForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="filesForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
<template v-if="filesForm.state.cacheRemoteFiles">
<MkSwitch v-model="filesForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-world-cog"></i></template>
<template #label>ServiceWorker</template>
@@ -233,36 +255,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkFolder>
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label>{{ i18n.ts._serverSettings.signToActivityPubGet }}<span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label>{{ i18n.ts._serverSettings.proxyRemoteFiles }}<span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label>{{ i18n.ts._serverSettings.allowExternalApRedirect }}<span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
<template v-if="federationForm.state.cacheRemoteFiles">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</MkFolder>
@@ -346,6 +338,17 @@ const pinnedUsersForm = useForm({
fetchInstance(true);
});
const filesForm = useForm({
cacheRemoteFiles: meta.cacheRemoteFiles,
cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
cacheRemoteFiles: state.cacheRemoteFiles,
cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles,
});
fetchInstance(true);
});
const serviceWorkerForm = useForm({
enableServiceWorker: meta.enableServiceWorker,
swPublicKey: meta.swPublickey ?? '',
@@ -391,21 +394,11 @@ const federationForm = useForm({
federation: meta.federation,
federationHosts: meta.federationHosts.join('\n'),
deliverSuspendedSoftware: meta.deliverSuspendedSoftware,
signToActivityPubGet: meta.signToActivityPubGet,
proxyRemoteFiles: meta.proxyRemoteFiles,
allowExternalApRedirect: meta.allowExternalApRedirect,
cacheRemoteFiles: meta.cacheRemoteFiles,
cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
federation: state.federation,
federationHosts: state.federationHosts.split('\n'),
deliverSuspendedSoftware: state.deliverSuspendedSoftware,
signToActivityPubGet: state.signToActivityPubGet,
proxyRemoteFiles: state.proxyRemoteFiles,
allowExternalApRedirect: state.allowExternalApRedirect,
cacheRemoteFiles: state.cacheRemoteFiles,
cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles,
});
fetchInstance(true);
});

View File

@@ -1,105 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.emojis">
<div v-for="emoji in emojis" :key="`emojiMute-${emoji}`" :class="$style.emoji" @click="onEmojiClick($event, emoji)">
<MkCustomEmoji
v-if="emoji.startsWith(':')"
:name="customEmojiName(emoji)"
:host="customEmojiHost(emoji)"
:normal="true"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
/>
<MkEmoji
v-else
:emoji="emoji"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
></MkEmoji>
</div>
</div>
<MkButton primary inline @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</template>
<script lang="ts" setup>
import type { MenuItem } from '@/types/menu';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import {
mute as muteEmoji,
unmute as unmuteEmoji,
extractCustomEmojiName as customEmojiName,
extractCustomEmojiHost as customEmojiHost,
} from '@/utility/emoji-mute.js';
const emojis = prefer.model('mutingEmojis');
function getHTMLElement(ev: MouseEvent): HTMLElement {
const target = ev.currentTarget ?? ev.target;
return target as HTMLElement;
}
function add(ev: MouseEvent) {
os.pickEmoji(getHTMLElement(ev), { showPinned: false }).then((emoji) => {
if (emoji) {
muteEmoji(emoji);
}
});
}
function onEmojiClick(ev: MouseEvent, emoji: string) {
const menuItems : MenuItem[] = [{
type: 'label',
text: emoji,
}, {
text: i18n.ts.emojiUnmute,
icon: 'ti ti-mood-off',
action: () => unmute(emoji),
}];
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
function unmute(emoji: string) {
os.confirm({
type: 'question',
title: i18n.tsx.unmuteX({ x: emoji }),
}).then(({ canceled }) => {
if (canceled) {
return;
}
unmuteEmoji(emoji);
});
}
</script>
<style module>
.emojis {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
&:empty {
display: none;
}
}
.emoji {
display: inline-flex;
height: 42px;
padding: 0 6px;
font-size: 1.5em;
border-radius: 6px;
align-items: center;
justify-content: center;
background: var(--MI_THEME-buttonBg);
}
</style>

View File

@@ -49,20 +49,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</SearchMarker>
<SearchMarker
:label="i18n.ts.emojiMute"
:keywords="['emoji', 'mute', 'hide']"
>
<MkFolder>
<template #icon><i class="ti ti-mood-off"></i></template>
<template #label>{{ i18n.ts.emojiMute }}</template>
<div class="_gaps_m">
<XEmojiMute/>
</div>
</mkfolder>
</SearchMarker>
<SearchMarker
:label="i18n.ts.instanceMute"
:keywords="['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide']"
@@ -177,7 +163,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import XEmojiMute from './mute-block.emoji-mute.vue';
import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';

View File

@@ -601,24 +601,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<MkInfo>
<div class="_gaps_s">
<div>{{ i18n.ts._clientPerformanceIssueTip.title }}</div>
<div>
<div><b>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledAdBlocker }}</b></div>
<div>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledAdBlocker_description }}</div>
</div>
<div>
<div><b>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledCustomCss }}</b></div>
<div>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledCustomCss_description }}</div>
</div>
<div>
<div><b>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledAddons }}</b></div>
<div>{{ i18n.ts._clientPerformanceIssueTip.makeSureDisabledAddons_description }}</div>
</div>
</div>
</MkInfo>
</div>
</MkFolder>
</SearchMarker>

View File

@@ -345,9 +345,6 @@ export const PREF_DEF = {
plugins: {
default: [] as Plugin[],
},
mutingEmojis: {
default: [] as string[],
},
'sound.masterVolume': {
default: 0.3,

View File

@@ -407,7 +407,6 @@ function menuEdit() {
z-index: 1;
display: flex;
height: var(--top-height);
padding-left: 6px;
}
.instance {

View File

@@ -1,61 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed } from 'vue';
import { prefer } from '@/preferences.js';
// custom絵文字の情報からキーを作成する
export function makeEmojiMuteKey(props: { name: string; host?: string | null }) {
return props.name.startsWith(':') ? props.name : `:${props.name}${props.host ? `@${props.host}` : ''}:`;
}
export function extractCustomEmojiName (name:string) {
return (name[0] === ':' ? name.substring(1, name.length - 1) : name).replace('@.', '').split('@')[0];
}
export function extractCustomEmojiHost (name:string) {
// nameは:emojiName@host:の形式
// 取り出したい部分はhostなので、@以降を取り出す
const index = name.indexOf('@');
if (index === -1) {
return null;
}
const host = name.substring(index + 1, name.length - 1);
if (host === '' || host === '.') {
return null;
}
return host;
}
export function mute(emoji: string) {
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
const emojiMuteKey = isCustomEmoji ?
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
emoji;
const mutedEmojis = prefer.r.mutingEmojis.value;
if (!mutedEmojis.includes(emojiMuteKey)) {
return prefer.commit('mutingEmojis', [...mutedEmojis, emojiMuteKey]);
}
throw new Error('Emoji is already muted', { cause: `${emojiMuteKey} is Already Muted` });
}
export function unmute(emoji:string) {
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
const emojiMuteKey = isCustomEmoji ?
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
emoji;
const mutedEmojis = prefer.r.mutingEmojis.value;
console.log('unmute', emoji, emojiMuteKey);
console.log('mutedEmojis', mutedEmojis);
prefer.commit('mutingEmojis', mutedEmojis.filter((e) => e !== emojiMuteKey));
}
export function checkMuted(emoji: string) {
const isCustomEmoji = emoji.startsWith(':') && emoji.endsWith(':');
const emojiMuteKey = isCustomEmoji ?
makeEmojiMuteKey({ name: extractCustomEmojiName(emoji), host: extractCustomEmojiHost(emoji) }) :
emoji;
return computed(() => prefer.r.mutingEmojis.value.includes(emojiMuteKey));
}

View File

@@ -5,8 +5,6 @@
import { vi } from 'vitest';
import createFetchMock from 'vitest-fetch-mock';
import type { Ref } from 'vue';
import { ref } from 'vue';
const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();
@@ -29,24 +27,13 @@ export const preferState: Record<string, unknown> = {
code: false,
},
mutingEmojis: [],
};
export let preferReactive: Record<string, Ref<unknown>> = {};
for (const key in preferState) {
if (preferState[key] !== undefined) {
preferReactive[key] = ref(preferState[key]);
}
}
// XXX: store somehow becomes undefined in vitest?
vi.mock('@/preferences.js', () => {
return {
prefer: {
s: preferState,
r: preferReactive,
},
};
});

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.5.1-alpha.2",
"version": "2025.5.1-alpha.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@@ -8785,9 +8785,6 @@ export type operations = {
singleUserMode: boolean;
/** @enum {string} */
ugcVisibilityForVisitor: 'all' | 'local' | 'none';
proxyRemoteFiles: boolean;
signToActivityPubGet: boolean;
allowExternalApRedirect: boolean;
};
};
};
@@ -11461,9 +11458,6 @@ export type operations = {
singleUserMode?: boolean;
/** @enum {string} */
ugcVisibilityForVisitor?: 'all' | 'local' | 'none';
proxyRemoteFiles?: boolean;
signToActivityPubGet?: boolean;
allowExternalApRedirect?: boolean;
};
};
};