Compare commits

..
Author SHA1 Message Date
Kisaragi 9d6386f5b8 chore(issue-template): トラブルシューティングをDiscussionsへ誘導 2024-08-26 15:14:26 +09:00
syuilo 44f62160cb enhance(frontend): error message i18n 2024-08-24 16:59:17 +09:00
syuilo 8032a4e12a enhance(frontend): サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように 2024-08-23 16:40:11 +09:00
syuilo 2f009f7d49 add note 2024-08-23 16:31:16 +09:00
syuilo f85aa7b641 fix(backend): ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 2024-08-23 16:29:05 +09:00
syuilo 1008fa32a0 better boot error screen 2024-08-22 14:03:11 +09:00
atsuchan 043ab1f69b fix(backend): Fix chart generation non-matching blockedHosts (#14441) 2024-08-20 13:49:42 +09:00
かっこかりanddakkar 21a3095eb0 fix button translation (#14444)
that Japanese string exactly matches that i18n key

(cherry picked from commit a408d32bb72ada9a4ad6bd1afe6e3fadb9b403db)

Co-authored-by: dakkar <dakkar@thenautilus.net>
2024-08-20 12:39:01 +09:00
syuilo 1b5f0571f7 🎨 2024-08-20 10:51:02 +09:00
syuilo 59e83605ac refactor 2024-08-19 19:41:06 +09:00
syuilo 130ff361c3 refactor 2024-08-19 17:32:27 +09:00
syuilo e78110a5cd refactor 2024-08-19 13:13:32 +09:00
github-actions[bot] 6c5593d456 [skip ci] Update CHANGELOG.md (prepend template) 2024-08-18 08:08:49 +00:00
github-actions[bot] 621626aad3 Release: 2024.8.0 2024-08-18 08:08:38 +00:00
syuilo f4f55ef012 New translations ja-jp.yml (English) (#14432) 2024-08-18 16:03:07 +09:00
github-actions[bot] 2e8a1029a4 Bump version to 2024.8.0-rc.5 2024-08-18 05:21:24 +00:00
かっこかり b53ee54e4f fix(frontend): focustrapがzindexを考慮するように (#14431) 2024-08-18 14:18:46 +09:00
github-actions[bot] b708b27bc8 Bump version to 2024.8.0-rc.4 2024-08-18 04:37:19 +00:00
Hazel Kandかっこかり 9ce44b24b8 fix(backend): memory leak in memory caches (#14363)
* encapsulate `MemoryKVCache<T>`

* remove infinity caches

* encapsulate other caches

* add missing awaits to internally synchronize caches

* implement pull-through caching

* tune cache lifetimes

* optimize cache GC by stopping early

* summarize changes in CHANGELOG.md

* Fix timeout comments

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* add comments about awaiting the redis write

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-08-18 13:34:01 +09:00
syuilo 3cd5f86510 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-08-18 10:12:51 +09:00
syuilo 9b78ce8047 🎨 2024-08-18 10:12:42 +09:00
syuilo 1629c0e50d New Crowdin updates (#14426)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Simplified)
2024-08-18 10:11:50 +09:00
syuilo 427f4a2cda Update about-misskey.vue 2024-08-18 10:10:06 +09:00
woxtu ba9c5c37b8 Remove undefined style (#14427) 2024-08-18 03:40:21 +09:00
syuilo e790aa0548 [skip ci] New Crowdin updates (#14423)
* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)
2024-08-17 20:25:15 +09:00
taichan bf8c42eecd Fix(beckend): html content-type detection on signedGet (#14424)
* fix(backend): contenttype detection of html in signedGet

* code style

* fix by review
2024-08-17 19:51:56 +09:00
かっこかり 129af06198 Update packages/backend/src/core/activitypub/ApRequestService.ts 2024-08-17 18:25:46 +09:00
かっこかり 83c04c55ad fix(misskey-js): eliminate any (follow-up of #13523) (#14422) 2024-08-17 18:15:46 +09:00
かっこかりandtaichan 0b98554319 Update CHANGELOG.md
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
2024-08-17 18:05:29 +09:00
かっこかり 4e0d57000c fix(frontend): 以前のpopupの呼び出し方を修正 (#14421) 2024-08-17 18:04:52 +09:00
syuilo c0de57c08d Update about-misskey.vue 2024-08-17 17:57:27 +09:00
かっこかり 75b0315ace Update timeline.vue (refactor) 2024-08-17 15:24:29 +09:00
github-actions[bot] 6cdecd72ee Bump version to 2024.8.0-rc.3 2024-08-17 06:17:02 +00:00
taichanandsyuilo 9fbc1b7f7b enhance(backend): headタグ内にrel=alternateの指定のあるlinkタグがある場合、記述されたURLを参照して照会できるように (#14371)
* signedGet時にhttpかつalternate属性のlinkがある場合に一回だけfollowして照会する

* Fix: validation position

* Fix import

* Fix tagname

* Update CHANGELOG

* Fix code style

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-08-17 15:12:23 +09:00
zyoshoka fd744f44c1 enhance(backend): ページ、ギャラリー、Playのモデレーション強化 (#13523)
* enhance(backend): Page、ギャラリー、Playのモデレーション強化

* Update CHANGELOG.md

* fix: update misskey-js

* refactor(frontend): use `MkA`

* Update CHANGELOG.md

* fix(i18n): Page -> ページ
2024-08-17 15:01:08 +09:00
syuilo 383c41bdb6 🎨 2024-08-17 14:57:26 +09:00
73 changed files with 704 additions and 237 deletions
+1
View File
@@ -9,6 +9,7 @@ body:
Thanks for reporting!
First, in order to avoid duplicate Issues, please search to see if the problem you found has already been reported.
Also, If you are NOT owner/admin of server, PLEASE DONT REPORT SERVER SPECIFIC ISSUES TO HERE! (e.g. feature XXX is not working in misskey.example) Please try with another misskey servers, and if your issue is only reproducible with specific server, contact your server's owner/admin first.
Finally, this is not place to ask trouble shooting. Please use Duscussions for it.
- type: textarea
attributes:
+17
View File
@@ -1,12 +1,26 @@
## Unreleased
### General
-
### Client
- サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
### Server
- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
## 2024.8.0
### General
- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
- Enhance: アカウントの削除のモデレーションログを残すように
- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正
### Client
- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
- Fix: ページ遷移に失敗することがある問題を修正
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
@@ -15,6 +29,7 @@
- Fix: 特定の条件下でノートの削除ボタンが出ないのを修正
### Server
- Enhance: 照会時にURLがhtmlかつheadタグ内に`rel="alternate"`, `type="application/activity+json"`の`link`タグがある場合に追ってリンク先を照会できるように
- Enhance: 凍結されたアカウントのフォローリクエストを表示しないように
- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374
- 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。
@@ -30,6 +45,8 @@
- Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正
- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700)
- Fix: Prevent memory leak from memory caches (#14310)
- Fix: More reliable memory cache eviction (#14311)
## 2024.7.0
+5
View File
@@ -2316,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "Set thumbnail"
eyeCatchingImageRemove: "Delete thumbnail"
chooseBlock: "Add a block"
enterSectionTitle: "Enter a section title"
selectType: "Select a type"
contentBlocks: "Content"
inputBlocks: "Input"
@@ -2499,6 +2500,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
deleteAccount: "Delete the account"
deletePage: "Delete the page"
deleteFlash: "Delete Play"
deleteGalleryPost: "Delete the gallery post"
_fileViewer:
title: "File details"
type: "File type"
+17 -1
View File
@@ -2829,7 +2829,7 @@ export interface Locale extends ILocale {
*/
"reportAbuseOf": ParameterizedString<"name">;
/**
* 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。
* 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。
*/
"fillAbuseReportDescription": string;
/**
@@ -5068,6 +5068,10 @@ export interface Locale extends ILocale {
* 作成したアンテナ
*/
"createdAntennas": string;
/**
* これ以上このクリップにノートを追加できません。
*/
"clipNoteLimitExceeded": string;
"_delivery": {
/**
* 配信状態
@@ -9687,6 +9691,18 @@ export interface Locale extends ILocale {
* アカウントを削除
*/
"deleteAccount": string;
/**
* ページを削除
*/
"deletePage": string;
/**
* Playを削除
*/
"deleteFlash": string;
/**
* ギャラリーの投稿を削除
*/
"deleteGalleryPost": string;
};
"_fileViewer": {
/**
+5 -1
View File
@@ -703,7 +703,7 @@ abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノートを通報"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください。"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください。"
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
reporter: "通報者"
reporteeOrigin: "通報先"
@@ -1263,6 +1263,7 @@ confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
_delivery:
status: "配信状態"
@@ -2569,6 +2570,9 @@ _moderationLogTypes:
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
deleteAccount: "アカウントを削除"
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
_fileViewer:
title: "ファイルの詳細"
+5
View File
@@ -2316,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "设置封面图片"
eyeCatchingImageRemove: "删除封面图片"
chooseBlock: "添加块"
enterSectionTitle: "输入会话标题"
selectType: "选择类型"
contentBlocks: "内容"
inputBlocks: "输入"
@@ -2499,6 +2500,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "新建了举报通知"
updateAbuseReportNotificationRecipient: "更新了举报通知"
deleteAbuseReportNotificationRecipient: "删除了举报通知"
deleteAccount: "删除了账户"
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件"
_fileViewer:
title: "文件信息"
type: "文件类型"
+5
View File
@@ -2316,6 +2316,7 @@ _pages:
eyeCatchingImageSet: "設定封面影像"
eyeCatchingImageRemove: "刪除封面影像"
chooseBlock: "新增方塊"
enterSectionTitle: "輸入區段的標題"
selectType: "選擇類型"
contentBlocks: "內容"
inputBlocks: "輸入"
@@ -2499,6 +2500,10 @@ _moderationLogTypes:
createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
deleteAccount: "刪除帳戶"
deletePage: "刪除頁面"
deleteFlash: "刪除 Play"
deleteGalleryPost: "刪除相簿的貼文"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.8.0-beta.2",
"version": "2024.8.0",
"codename": "nasubi",
"repository": {
"type": "git",
+2 -2
View File
@@ -133,7 +133,7 @@ export type Config = {
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
allowedPrivateNetworks: string[] | undefined;
maxFileSize: number | undefined;
maxFileSize: number;
clusterLimit: number | undefined;
id: string;
outgoingAddress: string | undefined;
@@ -250,7 +250,7 @@ export function loadConfig(): Config {
proxySmtp: config.proxySmtp,
proxyBypassHosts: config.proxyBypassHosts,
allowedPrivateNetworks: config.allowedPrivateNetworks,
maxFileSize: config.maxFileSize,
maxFileSize: config.maxFileSize ?? 262144000,
clusterLimit: config.clusterLimit,
outgoingAddress: config.outgoingAddress,
outgoingAddressFamily: config.outgoingAddressFamily,
@@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30);
this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s
this.redisForSub.on('message', this.onMessage);
}
+6 -6
View File
@@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
this.userByIdCache = new MemoryKVCache<MiUser>(Infinity);
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity);
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity);
this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity);
this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m
this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m
this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m
this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m
this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', {
lifetime: 1000 * 60 * 30, // 30m
@@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown {
if (user == null) {
this.userByIdCache.delete(body.id);
this.localUserByIdCache.delete(body.id);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
for (const [k, v] of this.uriPersonCache.entries) {
if (v.value?.id === body.id) {
this.uriPersonCache.delete(k);
}
}
} else {
this.userByIdCache.set(user.id, user);
for (const [k, v] of this.uriPersonCache.cache.entries()) {
for (const [k, v] of this.uriPersonCache.entries) {
if (v.value?.id === user.id) {
this.uriPersonCache.set(k, user);
}
@@ -24,7 +24,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
private cache: MemoryKVCache<MiEmoji | null>;
private emojisCache: MemoryKVCache<MiEmoji | null>;
public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>;
constructor(
@@ -40,7 +40,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12);
this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h
this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', {
lifetime: 1000 * 60 * 30, // 30m
@@ -334,7 +334,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
host,
})) ?? null;
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);
const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull);
if (emoji == null) return null;
return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
@@ -361,7 +361,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
*/
@bindThis
public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null);
const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null);
const emojisQuery: any[] = [];
const hosts = new Set(notCachedEmojis.map(e => e.host));
for (const host of hosts) {
@@ -376,7 +376,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
select: ['name', 'host', 'originalUrl', 'publicUrl'],
}) : [];
for (const emoji of _emojis) {
this.cache.set(`${emoji.name} ${emoji.host}`, emoji);
this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji);
}
}
@@ -401,7 +401,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public dispose(): void {
this.cache.dispose();
this.emojisCache.dispose();
}
@bindThis
+1 -1
View File
@@ -42,7 +42,7 @@ export class DownloadService {
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const maxSize = this.config.maxFileSize ?? 262144000;
const maxSize = this.config.maxFileSize;
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop() ?? 'untitled';
+1 -1
View File
@@ -35,7 +35,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10);
this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m
}
@bindThis
+2 -4
View File
@@ -127,10 +127,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
this.redisForSub.on('message', this.onMessage);
}
@@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown {
) {
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity,
memoryCacheLifetime: 1000 * 60 * 60, // 1h
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
@@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown {
private cacheService: CacheService,
private apPersonService: ApPersonService,
) {
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity);
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
}
@bindThis
@@ -6,6 +6,7 @@
import * as crypto from 'node:crypto';
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { Window } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
@@ -180,7 +181,8 @@ export class ApRequestService {
* @param url URL to fetch
*/
@bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> {
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
const req = ApRequestCreator.createSignedGet({
@@ -198,9 +200,29 @@ export class ApRequestService {
headers: req.request.headers,
}, {
throwErrorWhenResponseNotOk: true,
validators: [validateContentTypeSetAsActivityPub],
});
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
const contentType = res.headers.get('content-type');
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
const html = await res.text();
const window = new Window();
const document = window.document;
document.documentElement.innerHTML = html;
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
if (alternate) {
const href = alternate.getAttribute('href');
if (href) {
return await this.signedGet(href, user, false);
}
}
}
//#endregion
validateContentTypeSetAsActivityPub(res);
return await res.json();
}
}
@@ -65,21 +65,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followerHost)')
.where('following.followerHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.getRawOne()
.then(x => parseInt(x.count, 10)),
this.followingsRepository.createQueryBuilder('following')
.select('COUNT(DISTINCT following.followeeHost)')
.where('following.followeeHost IS NOT NULL')
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
.setParameters(pubsubSubQuery.getParameters())
@@ -88,7 +88,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
@@ -96,7 +96,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
this.instancesRepository.createQueryBuilder('instance')
.select('COUNT(instance.id)')
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
.andWhere('instance.suspensionState = \'none\'')
.andWhere('instance.isNotResponding = false')
.getRawOne()
@@ -129,6 +129,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,
};
return packed;
+77 -60
View File
@@ -7,23 +7,23 @@ import * as Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
export class RedisKVCache<T> {
private redisClient: Redis.Redis;
private name: string;
private lifetime: number;
private memoryCache: MemoryKVCache<T>;
private fetcher: (key: string) => Promise<T>;
private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T | undefined;
private readonly lifetime: number;
private readonly memoryCache: MemoryKVCache<T>;
private readonly fetcher: (key: string) => Promise<T>;
private readonly toRedisConverter: (value: T) => string;
private readonly fromRedisConverter: (value: string) => T | undefined;
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
lifetime: RedisKVCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisKVCache<T>['fetcher'];
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
}) {
this.redisClient = redisClient;
this.name = name;
constructor(
private redisClient: Redis.Redis,
private name: string,
opts: {
lifetime: RedisKVCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisKVCache<T>['fetcher'];
toRedisConverter: RedisKVCache<T>['toRedisConverter'];
fromRedisConverter: RedisKVCache<T>['fromRedisConverter'];
},
) {
this.lifetime = opts.lifetime;
this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
@@ -55,7 +55,13 @@ export class RedisKVCache<T> {
const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
const value = this.fromRedisConverter(cached);
if (value !== undefined) {
this.memoryCache.set(key, value);
}
return value;
}
@bindThis
@@ -66,6 +72,10 @@ export class RedisKVCache<T> {
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
*/
@bindThis
public async fetch(key: string): Promise<T> {
@@ -77,14 +87,14 @@ export class RedisKVCache<T> {
// Cache MISS
const value = await this.fetcher(key);
this.set(key, value);
await this.set(key, value);
return value;
}
@bindThis
public async refresh(key: string) {
const value = await this.fetcher(key);
this.set(key, value);
await this.set(key, value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
@@ -101,23 +111,23 @@ export class RedisKVCache<T> {
}
export class RedisSingleCache<T> {
private redisClient: Redis.Redis;
private name: string;
private lifetime: number;
private memoryCache: MemorySingleCache<T>;
private fetcher: () => Promise<T>;
private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T | undefined;
private readonly lifetime: number;
private readonly memoryCache: MemorySingleCache<T>;
private readonly fetcher: () => Promise<T>;
private readonly toRedisConverter: (value: T) => string;
private readonly fromRedisConverter: (value: string) => T | undefined;
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
lifetime: RedisSingleCache<T>['lifetime'];
memoryCacheLifetime: number;
fetcher: RedisSingleCache<T>['fetcher'];
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
}) {
this.redisClient = redisClient;
this.name = name;
constructor(
private redisClient: Redis.Redis,
private name: string,
opts: {
lifetime: number;
memoryCacheLifetime: number;
fetcher: RedisSingleCache<T>['fetcher'];
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
},
) {
this.lifetime = opts.lifetime;
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
this.fetcher = opts.fetcher;
@@ -149,7 +159,13 @@ export class RedisSingleCache<T> {
const cached = await this.redisClient.get(`singlecache:${this.name}`);
if (cached == null) return undefined;
return this.fromRedisConverter(cached);
const value = this.fromRedisConverter(cached);
if (value !== undefined) {
this.memoryCache.set(value);
}
return value;
}
@bindThis
@@ -160,6 +176,10 @@ export class RedisSingleCache<T> {
/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
* This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons:
* * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster.
* * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value.
* * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses.
*/
@bindThis
public async fetch(): Promise<T> {
@@ -171,14 +191,14 @@ export class RedisSingleCache<T> {
// Cache MISS
const value = await this.fetcher();
this.set(value);
await this.set(value);
return value;
}
@bindThis
public async refresh() {
const value = await this.fetcher();
this.set(value);
await this.set(value);
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
}
@@ -187,22 +207,12 @@ export class RedisSingleCache<T> {
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
export class MemoryKVCache<T> {
/**
* データを持つマップ
* @deprecated これを直接操作するべきではない
*/
public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
private gcIntervalHandle: NodeJS.Timeout;
private readonly cache = new Map<string, { date: number; value: T; }>();
private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
this.gcIntervalHandle = setInterval(() => {
this.gc();
}, 1000 * 60 * 3);
}
constructor(
private readonly lifetime: number,
) {}
@bindThis
/**
@@ -287,10 +297,14 @@ export class MemoryKVCache<T> {
@bindThis
public gc(): void {
const now = Date.now();
for (const [key, { date }] of this.cache.entries()) {
if ((now - date) > this.lifetime) {
this.cache.delete(key);
}
// The map is ordered from oldest to youngest.
// We can stop once we find an entry that's still active, because all following entries must *also* be active.
const age = now - date;
if (age < this.lifetime) break;
this.cache.delete(key);
}
}
@@ -298,16 +312,19 @@ export class MemoryKVCache<T> {
public dispose(): void {
clearInterval(this.gcIntervalHandle);
}
public get entries() {
return this.cache.entries();
}
}
export class MemorySingleCache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
this.lifetime = lifetime;
}
constructor(
private lifetime: number,
) {}
@bindThis
public set(value: T): void {
@@ -253,6 +253,10 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
default: 'local',
},
maxFileSize: {
type: 'number',
optional: false, nullable: false,
},
},
} as const;
@@ -45,7 +45,7 @@ export class DeliverProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60);
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
}
@bindThis
@@ -134,7 +134,7 @@ export class NodeinfoServerService {
return document;
};
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m
fastify.get(nodeinfo2_1path, async (request, reply) => {
const base = await cache.fetch(() => nodeinfo2(21));
@@ -199,9 +199,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
const [path] = await createTemp();
const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartData.file.truncated) {
cleanup();
reply.code(413);
reply.send();
return;
}
const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@@ -49,7 +49,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
fileSize: this.config.maxFileSize ?? 262144000,
fileSize: this.config.maxFileSize,
files: 1,
},
});
@@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown {
private cacheService: CacheService,
) {
this.appCache = new MemoryKVCache<MiApp>(Infinity);
this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w
}
@bindThis
@@ -4,9 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { FlashsRepository } from '@/models/_.js';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
if (flash == null) {
throw new ApiError(meta.errors.noSuchFlash);
}
if (flash.userId !== me.id) {
if (!await this.roleService.isModerator(me) && flash.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.flashsRepository.delete(flash.id);
if (flash.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: flash.userId });
this.moderationLogService.log(me, 'deleteFlash', {
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
flash,
});
}
});
}
}
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository } from '@/models/_.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -22,6 +24,12 @@ export const meta = {
code: 'NO_SUCH_POST',
id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496',
},
},
} as const;
@@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const post = await this.galleryPostsRepository.findOneBy({
id: ps.postId,
userId: me.id,
});
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });
if (post == null) {
throw new ApiError(meta.errors.noSuchPost);
}
if (!await this.roleService.isModerator(me) && post.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.galleryPostsRepository.delete(post.id);
if (post.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: post.userId });
this.moderationLogService.log(me, 'deleteGalleryPost', {
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
post,
});
}
});
}
}
@@ -4,9 +4,11 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { PagesRepository } from '@/models/_.js';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId !== me.id) {
if (!await this.roleService.isModerator(me) && page.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.pagesRepository.delete(page.id);
if (page.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: page.userId });
this.moderationLogService.log(me, 'deletePage', {
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
page,
});
}
});
}
}
+3 -3
View File
@@ -176,10 +176,10 @@
<span class="button-label-big">Reload / リロード</span>
</button>
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
<p>Disable an adblocker / アドブロッカーを無効にする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
<p>&#40;Tor Browser&#41; Set dom.webaudio.enabled to true / dom.webaudio.enabledをtrueに設定する</p>
<details style="color: #86b300;">
<summary>Other options / その他のオプション</summary>
<a href="/flush">
@@ -212,7 +212,7 @@
<summary>
<code>ERROR CODE: ${code}</code>
</summary>
<code>${JSON.stringify(details)}</code>`;
<code>${details.toString()} ${JSON.stringify(details)}</code>`;
errorsElement.appendChild(detailsElement);
addStyle(`
* {
+21
View File
@@ -97,6 +97,9 @@ export const moderationLogTypes = [
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
] as const;
export type ModerationLogPayloads = {
@@ -320,6 +323,24 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
deletePage: {
pageId: string;
pageUserId: string;
pageUserUsername: string;
page: any;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
flash: any;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
post: any;
};
};
export type Serialized<T> = {
@@ -39,7 +39,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
user: Misskey.entities.UserLite;
initialComment?: string;
}>();
@@ -171,11 +171,11 @@ function onMousedown(evt: MouseEvent): void {
background: var(--accent);
&:not(:disabled):hover {
background: var(--X8);
background: hsl(from var(--accent) h s calc(l + 5));
}
&:not(:disabled):active {
background: var(--X8);
background: hsl(from var(--accent) h s calc(l + 5));
}
}
@@ -220,11 +220,11 @@ function onMousedown(evt: MouseEvent): void {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
&:not(:disabled):active {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
@@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
> .name {
@@ -216,7 +216,7 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
+1 -1
View File
@@ -859,7 +859,7 @@ function emitUpdReaction(emoji: string, delta: number) {
z-index: 2;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
+1 -1
View File
@@ -62,7 +62,7 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
@@ -1128,13 +1128,13 @@ defineExpose({
&:not(:disabled):hover {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
&:not(:disabled):active {
> .inner {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
}
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.preview">
<div :class="$style.preview__content1">
<div>
<MkInput v-model="text">
<template #label>Text</template>
</MkInput>
@@ -4,25 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<div :class="$style.title">
<span :class="$style.icon">
<template v-if="role.iconUrl">
<img :class="$style.badge" :src="role.iconUrl"/>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i>
</template>
<div v-adaptive-bg class="_panel" :class="$style.body">
<div :class="$style.bodyTitle">
<span :class="$style.bodyIcon">
<template v-if="role.iconUrl">
<img :class="$style.bodyBadge" :src="role.iconUrl"/>
</template>
<template v-else>
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
</template>
</span>
<span :class="$style.bodyName">{{ role.name }}</span>
<template v-if="detailed">
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
</template>
<template v-else>
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
</template>
</span>
<span :class="$style.name">{{ role.name }}</span>
<template v-if="detailed">
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
</template>
</div>
<div :class="$style.bodyDescription">{{ role.description }}</div>
</div>
<div :class="$style.description">{{ role.description }}</div>
</MkA>
</template>
@@ -42,34 +49,44 @@ const props = withDefaults(defineProps<{
<style lang="scss" module>
.root {
display: block;
padding: 16px 20px;
border-left: solid 6px var(--color);
}
.title {
display: flex;
align-items: center;
}
.icon {
margin: 0 12px;
}
.body {
display: block;
padding: 16px 20px;
flex: 1;
border-left: solid 6px var(--color);
}
.bodyTitle {
display: flex;
}
.bodyIcon {
margin-right: 8px;
}
.badge {
.bodyBadge {
height: 1.3em;
vertical-align: -20%;
}
.name {
.bodyName {
font-weight: bold;
}
.users {
.bodyUsers {
margin-left: auto;
opacity: 0.7;
}
.description {
.bodyDescription {
opacity: 0.7;
font-size: 85%;
}
@@ -62,7 +62,7 @@ const collapsed = ref(isLong);
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
@@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js';
import { defaultStore } from '@/store.js';
defineProps<{
user: Misskey.entities.User;
user: Misskey.entities.UserLite;
detail?: boolean;
}>();
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="showDecoration">
<img
v-for="decoration in decorations ?? user.avatarDecorations"
:class="[$style.decoration]"
:class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
:src="getDecorationUrl(decoration)"
:style="{
rotate: getDecorationAngle(decoration),
@@ -60,7 +60,7 @@ const props = withDefaults(defineProps<{
link?: boolean;
preview?: boolean;
indicator?: boolean;
decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
forceShowDecoration?: boolean;
}>(), {
target: null,
@@ -330,4 +330,17 @@ watch(() => props.user.avatarBlurhash, () => {
width: 200%;
pointer-events: none;
}
.decorationBlink {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% {
filter: brightness(2);
}
50% {
filter: brightness(1);
}
}
</style>
@@ -72,10 +72,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<img src="https://avatars.githubusercontent.com/u/4439005?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@syuilo</span>
</a>
<a href="https://github.com/tamaina" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/7973572?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@tamaina</span>
</a>
<a href="https://github.com/acid-chicken" target="_blank" :class="$style.contributor">
<img src="https://avatars.githubusercontent.com/u/20679825?v=4" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">@acid-chicken</span>
@@ -267,6 +263,9 @@ const patronsWithIcon = [{
}, {
name: 'Macop',
icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg',
}, {
name: 'なっかあ',
icon: 'https://assets.misskey-hub.net/patrons/c2f5f3e394e74a64912284a2f4ca710e.jpg',
}];
const patrons = [
+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="900">
<div :class="$style.root" class="_gaps">
<div :class="$style.subMenus" class="_gaps">
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ "通知設定" }}</MkButton>
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div>
<div :class="$style.inputs" class="_gaps">
@@ -37,6 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
'deleteSystemWebhook',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
].includes(log.type)
}"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
@@ -74,6 +77,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
</template>
<template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
+1 -1
View File
@@ -310,7 +310,7 @@ definePageMetadata(() => ({
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
.bannerStatus {
+50 -1
View File
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
<MkButton v-if="$i && $i.id !== flash.user.id" class="button" rounded @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></MkButton>
</div>
</div>
</div>
@@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue';
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { Interpreter, Parser, values } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
@@ -79,6 +80,7 @@ import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { MenuItem } from '@/types/menu';
import { pleaseLogin } from '@/scripts/please-login.js';
const props = defineProps<{
@@ -229,6 +231,53 @@ async function run() {
}
}
function reportAbuse() {
if (!flash.value) return;
const pageUrl = `${url}/play/${flash.value.id}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: flash.value.user,
initialComment: `Play: ${pageUrl}\n-----\n`,
}, {
closed: () => dispose(),
});
}
function showMenu(ev: MouseEvent) {
if (!flash.value) return;
const menu: MenuItem[] = [
...($i && $i.id !== flash.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !flash.value) return;
os.apiWithDialog('flash/delete', { flashId: flash.value.id });
}),
},
] : []),
] : []),
];
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function reset() {
if (aiscript.value) aiscript.value.abort();
started.value = false;
+51 -6
View File
@@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button>
<button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
<button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button>
<button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
</div>
</div>
<div class="user">
@@ -62,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@@ -79,6 +80,7 @@ import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { MenuItem } from '@/types/menu';
const router = useRouter();
@@ -153,13 +155,56 @@ function edit() {
router.push(`/gallery/${post.value.id}/edit`);
}
function reportAbuse() {
if (!post.value) return;
const pageUrl = `${url}/gallery/${post.value.id}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: post.value.user,
initialComment: `Post: ${pageUrl}\n-----\n`,
}, {
closed: () => dispose(),
});
}
function showMenu(ev: MouseEvent) {
if (!post.value) return;
const menu: MenuItem[] = [
...($i && $i.id !== post.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !post.value) return;
os.apiWithDialog('gallery/posts/delete', { postId: post.value.id });
}),
},
] : []),
] : []),
];
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
watch(() => props.postId, fetchPost, { immediate: true });
const headerActions = computed(() => [{
icon: 'ti ti-pencil',
text: i18n.ts.edit,
handler: edit,
}]);
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
+70 -9
View File
@@ -62,8 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton>
</div>
<div :class="$style.other">
<MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA>
<button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button>
<button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button>
<button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button>
</div>
</div>
<div :class="$style.pageUser">
@@ -78,14 +80,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
<div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div>
<div :class="$style.pageLinks">
<MkA v-if="!$i || $i.id !== page.userId" :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA>
<template v-if="$i && $i.id === page.userId">
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA>
<button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button>
<button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button>
</template>
</div>
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
@@ -104,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { computed, watch, ref, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import XPage from '@/components/page/page.vue';
import MkButton from '@/components/MkButton.vue';
@@ -126,6 +120,10 @@ import { isSupportShare } from '@/scripts/navigator.js';
import { instance } from '@/instance.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { useRouter } from '@/router/supplier.js';
import { MenuItem } from '@/types/menu';
const router = useRouter();
const props = defineProps<{
pageName: string;
@@ -242,6 +240,69 @@ function pin(pin) {
});
}
function reportAbuse() {
if (!page.value) return;
const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: page.value.user,
initialComment: `Page: ${pageUrl}\n-----\n`,
}, {
closed: () => dispose(),
});
}
function showMenu(ev: MouseEvent) {
if (!page.value) return;
const menu: MenuItem[] = [
...($i && $i.id === page.value.userId ? [
{
icon: 'ti ti-code',
text: i18n.ts._pages.viewSource,
action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`),
},
...($i.pinnedPageId === page.value.id ? [{
icon: 'ti ti-pinned-off',
text: i18n.ts.unpin,
action: () => pin(false),
}] : [{
icon: 'ti ti-pin',
text: i18n.ts.pin,
action: () => pin(true),
}]),
] : []),
...($i && $i.id !== page.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !page.value) return;
os.apiWithDialog('pages/delete', { pageId: page.value.id });
}),
},
] : []),
] : []),
];
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
watch(() => path.value, fetchPage, { immediate: true });
const headerActions = computed(() => []);
@@ -96,6 +96,7 @@ const decorationsForPreview = computed(() => {
flipH: flipH.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
blink: true,
};
const decorations = [...$i.avatarDecorations];
if (props.usingIndex != null) {
+1 -1
View File
@@ -240,7 +240,7 @@ function closeTutorial(): void {
}
function switchTlIfNeeded() {
if (isBasicTimeline(src.value) && !availableBasicTimelines().includes(src.value)) {
if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
src.value = availableBasicTimelines()[0];
}
}
@@ -84,7 +84,7 @@ onUpdated(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
}
}
+60 -4
View File
@@ -16,21 +16,57 @@ function containsFocusTrappedElements(el: HTMLElement): boolean {
});
}
function getZIndex(el: HTMLElement): number {
const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10);
if (isNaN(zIndex)) {
return 0;
}
return zIndex;
}
function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null {
let highestZIndexElement: HTMLElement | null = null;
let highestZIndex = -Infinity;
focusTrapElements.forEach((el) => {
const zIndex = getZIndex(el);
if (zIndex > highestZIndex) {
highestZIndex = zIndex;
highestZIndexElement = el;
}
});
return highestZIndexElement == null ? null : {
el: highestZIndexElement,
zIndex: highestZIndex,
};
}
function releaseFocusTrap(el: HTMLElement): void {
focusTrapElements.delete(el);
if (el.inert === true) {
el.inert = false;
}
const highestZIndexElement = getHighestZIndexElement();
if (el.parentElement != null && el !== document.body) {
el.parentElement.childNodes.forEach((siblingNode) => {
const siblingEl = getHTMLElementOrNull(siblingNode);
if (!siblingEl) return;
if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) {
if (
siblingEl !== el &&
(
highestZIndexElement == null ||
siblingEl === highestZIndexElement.el ||
siblingEl.contains(highestZIndexElement.el)
)
) {
siblingEl.inert = false;
} else if (
focusTrapElements.size > 0 &&
!containsFocusTrappedElements(siblingEl) &&
!focusTrapElements.has(siblingEl) &&
highestZIndexElement != null &&
siblingEl !== highestZIndexElement.el &&
!siblingEl.contains(highestZIndexElement.el) &&
!ignoreElements.includes(siblingEl.tagName.toLowerCase())
) {
siblingEl.inert = true;
@@ -45,9 +81,29 @@ function releaseFocusTrap(el: HTMLElement): void {
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void;
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; };
export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void {
const highestZIndexElement = getHighestZIndexElement();
const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex;
const zIndex = getZIndex(el);
// If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead
// Focus trapping for this element will be done in the release function
if (!parent && zIndex < highestZIndex) {
focusTrapElements.add(el);
if (highestZIndexElement) {
focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls);
}
return {
release: () => {
releaseFocusTrap(el);
},
};
}
if (el.inert === true) {
el.inert = false;
}
if (el.parentElement != null && el !== document.body) {
el.parentElement.childNodes.forEach((siblingNode) => {
const siblingEl = getHTMLElementOrNull(siblingNode);
@@ -66,6 +66,11 @@ export async function getNoteClipMenu(props: {
});
if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
}
} else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') {
os.alert({
type: 'error',
text: i18n.ts.clipNoteLimitExceeded,
});
} else {
os.alert({
type: 'error',
+10
View File
@@ -13,6 +13,7 @@ import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import { alert } from '@/os.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
type Uploading = {
id: string;
@@ -39,6 +40,15 @@ export function uploadFile(
if (folder && typeof folder === 'object') folder = folder.id;
if (file.size > instance.maxFileSize) {
alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
});
return Promise.reject();
}
return new Promise((resolve, reject) => {
const id = uuid();
+4 -4
View File
@@ -255,11 +255,11 @@ rt {
background: var(--accent);
&:not(:disabled):hover {
background: var(--X8);
background: hsl(from var(--accent) h s calc(l + 5));
}
&:not(:disabled):active {
background: var(--X9);
background: hsl(from var(--accent) h s calc(l - 5));
}
}
@@ -269,11 +269,11 @@ rt {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
&:not(:disabled):active {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
-8
View File
@@ -77,22 +77,14 @@
codeBoolean: '#c59eff',
deckBg: '#000',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
codeHighlighter: {
@@ -77,22 +77,14 @@
codeBoolean: '#62b70c',
deckBg: ':darken<3<@bg',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
codeHighlighter: {
@@ -57,20 +57,13 @@
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.3)',
X12: 'rgba(255, 255, 255, 0.1)',
X13: 'rgba(255, 255, 255, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
},
}
-3
View File
@@ -3,14 +3,11 @@
base: 'dark',
name: 'Mi U0 Dark',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#172426',
fg: '#dadada',
X10: ':alpha<0.4<@accent',
-3
View File
@@ -3,14 +3,11 @@
base: 'light',
name: 'Mi U0 Light',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
X4: 'rgba(255, 255, 255, 0.1)',
X5: 'rgba(255, 255, 255, 0.05)',
X6: 'rgba(255, 255, 255, 0.15)',
X7: 'rgba(255, 255, 255, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
bg: '#e7e7eb',
fg: '#5f5f5f',
X10: ':alpha<0.4<@accent',
@@ -60,21 +60,13 @@
fgTransparentWeak: ':alpha<0.75<@fg',
panelHeaderDivider: '@divider',
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
X4: 'rgba(0, 0, 0, 0.1)',
X5: 'rgba(0, 0, 0, 0.05)',
X6: 'rgba(0, 0, 0, 0.25)',
X7: 'rgba(0, 0, 0, 0.05)',
X8: ':lighten<5<@accent',
X9: ':darken<5<@accent',
X10: ':alpha<0.4<@accent',
X11: 'rgba(0, 0, 0, 0.1)',
X12: 'rgba(0, 0, 0, 0.1)',
X13: 'rgba(0, 0, 0, 0.15)',
X14: ':alpha<0.5<@navBg',
X15: ':alpha<0<@panel',
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
}
@@ -82,6 +82,8 @@ function more() {
<style lang="scss" module>
.root {
--nav-bg-transparent: color-mix(in srgb, var(--navBg), transparent 50%);
display: flex;
flex-direction: column;
}
@@ -91,7 +93,7 @@ function more() {
top: 0;
z-index: 1;
padding: 20px 0;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
@@ -125,7 +127,7 @@ function more() {
position: sticky;
bottom: 0;
padding: 20px 0;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
+5 -4
View File
@@ -111,6 +111,7 @@ function more(ev: MouseEvent) {
.root {
--nav-width: 250px;
--nav-icon-only-width: 80px;
--nav-bg-transparent: color-mix(in srgb, var(--navBg), transparent 50%);
flex: 0 0 var(--nav-width);
width: var(--nav-width);
@@ -144,7 +145,7 @@ function more(ev: MouseEvent) {
top: 0;
z-index: 1;
padding: 20px 0;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
@@ -187,7 +188,7 @@ function more(ev: MouseEvent) {
position: sticky;
bottom: 0;
padding-top: 20px;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
@@ -378,7 +379,7 @@ function more(ev: MouseEvent) {
top: 0;
z-index: 1;
padding: 20px 0;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
@@ -408,7 +409,7 @@ function more(ev: MouseEvent) {
position: sticky;
bottom: 0;
padding-top: 20px;
background: var(--X14);
background: var(--nav-bg-transparent);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
+3 -3
View File
@@ -450,7 +450,7 @@ body {
}
&:active {
background: var(--X2);
background: hsl(from var(--panel) h s calc(l - 2));
}
}
@@ -460,11 +460,11 @@ body {
color: var(--fgOnAccent);
&:hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
&:active {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
+3 -3
View File
@@ -427,7 +427,7 @@ $widgets-hide-threshold: 1090px;
}
&:active {
background: var(--X2);
background: hsl(from var(--panel) h s calc(l - 2));
}
}
@@ -437,11 +437,11 @@ $widgets-hide-threshold: 1090px;
color: var(--fgOnAccent);
&:hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
&:active {
background: linear-gradient(90deg, var(--X8), var(--X8));
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
}
}
+13 -1
View File
@@ -2464,6 +2464,9 @@ type ModerationLog = {
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
type: 'unsetUserBanner';
info: ModerationLogPayloads['unsetUserBanner'];
} | {
type: 'createSystemWebhook';
info: ModerationLogPayloads['createSystemWebhook'];
@@ -2485,10 +2488,19 @@ type ModerationLog = {
} | {
type: 'deleteAccount';
info: ModerationLogPayloads['deleteAccount'];
} | {
type: 'deletePage';
info: ModerationLogPayloads['deletePage'];
} | {
type: 'deleteFlash';
info: ModerationLogPayloads['deleteFlash'];
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.8.0-beta.2",
"version": "2024.8.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
+1
View File
@@ -4947,6 +4947,7 @@ export type components = {
* @enum {string}
*/
noteSearchableScope: 'local' | 'global';
maxFileSize: number;
};
MetaDetailedOnly: {
features?: {
+24
View File
@@ -4,9 +4,12 @@ import type {
Ad,
Announcement,
EmojiDetailed,
Flash,
GalleryPost,
InviteCode,
MetaDetailed,
Note,
Page,
Role,
ReversiGameDetailed,
SystemWebhook,
@@ -155,6 +158,9 @@ export const moderationLogTypes = [
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410
@@ -398,4 +404,22 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
deletePage: {
pageId: string;
pageUserId: string;
pageUserUsername: string;
page: Page;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
flash: Flash;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
post: GalleryPost;
};
};
+12
View File
@@ -155,6 +155,9 @@ export type ModerationLog = {
} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
type: 'unsetUserBanner';
info: ModerationLogPayloads['unsetUserBanner'];
} | {
type: 'createSystemWebhook';
info: ModerationLogPayloads['createSystemWebhook'];
@@ -176,6 +179,15 @@ export type ModerationLog = {
} | {
type: 'deleteAccount';
info: ModerationLogPayloads['deleteAccount'];
} | {
type: 'deletePage';
info: ModerationLogPayloads['deletePage'];
} | {
type: 'deleteFlash';
info: ModerationLogPayloads['deleteFlash'];
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
});
export type ServerStats = {