Compare commits

..

21 Commits

Author SHA1 Message Date
14cd43d0d8
chore: bump version to 2024.5.0-oscar.18
Some checks failed
Publish Docker Image (Misskey) TeamCity build failed
2024-11-06 20:37:09 +09:00
b8dcaecd2e
fix(create): add withNotification 2024-11-06 20:34:12 +09:00
03c64c296b
feat: notification muting 2024-11-06 20:32:41 +09:00
ab84c9afa0
wip: notification muting 2024-11-06 18:37:39 +09:00
4671fc4139
Merge upstream 2024-11-06 18:27:42 +09:00
cda3d93db8
feat: 안되겠소, 쏩시다! 2024-11-06 17:53:25 +09:00
3e90f8995e
fix(announcement): locale text 2024-11-06 17:33:35 +09:00
あわわわとーにゅ
410b36b5a0
Bump up version to 2024.5.0-io.4 (MisskeyIO#789) 2024-11-06 10:37:46 +09:00
あわわわとーにゅ
7553e64faf
Merge pull request MisskeyIO#787 from upstream
Cherry-picked from upstream
2024-11-06 10:37:22 +09:00
あわわわとーにゅ
57753225ea
fix(backend): ノートを連合する際にリモートユーザーのacctの大小文字を区別して処理している問題を修正 (misskey-dev#14880)
Cherry-picked from 6718a54f6fce29edbe2755c31a119e4468fc56e2

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
2024-11-06 09:09:10 +09:00
かっこかり
45faba3567
fix(backend): /@ にアクセスするとサーバーエラーが発生する問題を修正 (misskey-dev#13884)
Cherry-picked from 1df8ea824e5dace883f0d6855d7342984c8032d0

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-11-06 09:09:10 +09:00
あわわわとーにゅ
613e0a8aa3
enhance: アイコンデコレーション管理画面の改善
Cherry-picked from 74847bce303449124282a748fc50b1c6588288fc

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-11-06 09:09:09 +09:00
あわわわとーにゅ
7cc8c2a22b
fix(misskey-js): WebSocketの型定義をReconnectingWebsocketに依存するように (misskey-dev#14850)
Cherry-picked from ec4358d1e8c9a59a0702d19182c37d91510b3736

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-11-06 09:09:09 +09:00
あわわわとーにゅ
1328420f99
enhance(frontend): Self-XSS防止用のメッセージを追加 (misskey-dev#14839)
Cherry-picked from a6a1e3d733e192504986e6e91b5aca9211c331ce

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-11-06 09:09:09 +09:00
かっこかり
0f21232828
🎨
https://github.com/misskey-dev/misskey/pull/14828 のデザイン修正
2024-11-06 09:09:08 +09:00
あわわわとーにゅ
d4bbae8d45
enhance(frontend): 外部アプリ認証画面の改良 (misskey-dev#14828)
Cherry-picked from 076cc953e2bcd9f7335e2d9799cdf902829816cb

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2024-11-06 09:09:08 +09:00
anatawa12
62e801bf3a
もともとセンシティブではないと連合されていたファイルがセンシティブとして連合された場合にセンシティブとしてそのファイルを扱うように (misskey-dev#13879)
Cherry-picked from a7a8dc4dbbab075cdee140f468fd7e3559cde475

Co-authored-by: anatawa12 <anatawa12@icloud.com>
Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2024-11-06 09:09:07 +09:00
あわわわとーにゅ
5c9ea07e2b
fix(typescript): vue-gtagのタイプ定義の修正 (MisskeyIO#788) 2024-11-06 09:07:17 +09:00
あわわわとーにゅ
557f45b9d1
update deps (MisskeyIO#786) 2024-11-06 05:21:24 +09:00
あわわわとーにゅ
fcfd004c38
feat(analytics): Google Analytics・同意モード・一部機能のトラッキング実装 (MisskeyIO#784) 2024-11-06 01:28:14 +09:00
あわわわとーにゅ
2f4c48bbe6
spec(frontend): みつけるに表示される項目の調整 (MisskeyIO#783) 2024-11-05 23:58:03 +09:00
106 changed files with 4197 additions and 4172 deletions

View File

@ -195,4 +195,4 @@ signToActivityPubGet: true
#maxFileSize: 262144000
# Value of Content-Security-Policy header
#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';"
#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/; base-uri 'self'; object-src 'self';"

View File

@ -118,7 +118,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@ -214,7 +214,7 @@ proxyRemoteFiles: 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".
# 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)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
@ -227,4 +227,4 @@ signToActivityPubGet: true
#pidFile: /tmp/misskey.pid
# Value of Content-Security-Policy header
#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';"
#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/; base-uri 'self'; object-src 'self';"

View File

@ -87,6 +87,8 @@
- Fix: `/i/notifications``includeTypes`か`excludeTypes`を指定しているとき、通知が存在するのに空配列を返すことがある問題を修正
- Fix: 複数idを指定する`users/show`が関係ないユーザを返すことがある問題を修正
- Fix: `/tags``/user-tags` が検索エンジンにインデックスされないように
- Fix: もともとセンシティブではないと連合されていたファイルがセンシティブとして連合された場合にセンシティブとしてそのファイルを扱うように
- センシティブとして連合したファイルは非センシティブとして連合されてもセンシティブとして扱われます
## 2024.3.1

View File

@ -1272,6 +1272,33 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
noDescription: "There is not the explanation"
alwaysConfirmFollow: "Always confirm when following"
inquiry: "Contact"
tryAgain: "Please try again later"
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
createdLists: "Created lists"
createdAntennas: "Created antennas"
fromX: "From {x}"
genEmbedCode: "Generate embed code"
noteOfThisUser: "Notes by this user"
clipNoteLimitExceeded: "No more notes can be added to this clip."
performance: "Performance"
modified: "Modified"
discard: "Discard"
thereAreNChanges: "There are {n} change(s)"
signinWithPasskey: "Sign in with Passkey"
unknownWebAuthnKey: "Unknown Passkey"
passkeyVerificationFailed: "Passkey verification has failed."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
messageToFollower: "Message to followers"
target: "Target"
testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\n<strong>Do not use in a production environment.</strong>"
prohibitedWordsForNameOfUser: "Prohibited words for user names"
prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction."
yourNameContainsProhibitedWords: "Your name contains prohibited words"
yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator."
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
lockdown: "Lockdown"
pleaseSelectAccount: "Select an account"
here: "here"
credits: "Closing Credits"
timeWillCome: "Will this have your name on it someday?"
@ -1287,6 +1314,26 @@ autoRemoval: "Automatic note deletion"
autoRemovalDescription: "You can delete your note when it exceeds period you set."
CheckedByHIBP: "In addition to ensuring your passwords are secure, HIBP scans for password leaks."
changeUserName: "Change name"
gtagConsentCustomize: "Data Collection and Privacy Settings"
gtagConsentCustomizeDescription: "You can customize the scope of data collected by {host}.\nHowever, you cannot disable the collection of security-related information such as authentication features, fraud prevention, and other user protections."
gtagConsentAnalytics: "Collection of Statistical Information"
gtagConsentAnalyticsDescription: "Enable the storage (cookies, etc.) of analytics-related information such as site visit duration."
gtagConsentFunctionality: "Collection of Feature and Setting Usage"
gtagConsentFunctionalityDescription: "Enable the storage of information that supports website or app features, such as language settings."
gtagConsentPersonalization: "Collection of Personalized Information"
gtagConsentPersonalizationDescription: "Enable the storage of personalization-related information such as recommended posts."
helpUsImproveUserExperience: "To build the future of Misskey,\nplease help us by agreeing to data collection!"
pleaseConsentToTracking: "{host} may collect information that may include personal data such as your IP address, usage data, and device information during your use, based on our [Privacy Policy]({privacyPolicyUrl}), for the purpose of providing and operating the service and improving the user experience.\n\nThe collected data will be used for future feature development, operational policy decisions, and identifying areas for service improvement."
consentEssential: "Allow Essential Items"
consentAll: "Allow All Items"
consentSelected: "Allow Selected Items"
normalize: "Normalize"
normalizeConfirm: "After normalization, the account will be irreversible. Are you sure you want to do this?"
normalizeDescription: "Normalization is a feature for bulk data wiping and account suspension of users, which has a similar effect to deleting an account and closes all reports after it is run. Please note that this is an irreversible action."
useNormalization: "Show the Normalize menu"
alsoMuteNotification: "Also mute notifications from this user"
muteNotification: "Mute notifications"
unmuteNotification: "Unmute notifications"
_bubbleGame:
howToPlay: "How to play"
hold: "Hold"
@ -2205,8 +2252,11 @@ _auth:
permissionAsk: "This application requests the following permissions"
pleaseGoBack: "Please go back to the application"
callback: "Returning to the application"
accepted: "Access granted"
denied: "Access denied"
scopeUser: "Operate as the following user"
pleaseLogin: "Please log in to authorize applications."
byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL"
_antennaSources:
all: "All notes"
homeTimeline: "Notes from followed users"
@ -2578,6 +2628,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "Avatar decoration deleted"
unsetUserAvatar: "Unset this user's avatar"
unsetUserBanner: "Unset this user's banner"
normalize: "Normalization"
_fileViewer:
title: "File details"
type: "File type"

222
locales/index.d.ts vendored
View File

@ -5164,6 +5164,118 @@ export interface Locale extends ILocale {
*
*/
"inquiry": string;
/**
*
*/
"tryAgain": string;
/**
*
*/
"confirmWhenRevealingSensitiveMedia": string;
/**
*
*/
"sensitiveMediaRevealConfirm": string;
/**
*
*/
"createdLists": string;
/**
*
*/
"createdAntennas": string;
/**
* {x}
*/
"fromX": ParameterizedString<"x">;
/**
*
*/
"genEmbedCode": string;
/**
*
*/
"noteOfThisUser": string;
/**
*
*/
"clipNoteLimitExceeded": string;
/**
*
*/
"performance": string;
/**
*
*/
"modified": string;
/**
*
*/
"discard": string;
/**
* {n}
*/
"thereAreNChanges": ParameterizedString<"n">;
/**
*
*/
"signinWithPasskey": string;
/**
*
*/
"unknownWebAuthnKey": string;
/**
*
*/
"passkeyVerificationFailed": string;
/**
*
*/
"passkeyVerificationSucceededButPasswordlessLoginDisabled": string;
/**
*
*/
"messageToFollower": string;
/**
*
*/
"target": string;
/**
* CAPTCHAのテストを目的とした機能です<strong>使</strong>
*/
"testCaptchaWarning": string;
/**
*
*/
"prohibitedWordsForNameOfUser": string;
/**
*
*/
"prohibitedWordsForNameOfUserDescription": string;
/**
*
*/
"yourNameContainsProhibitedWords": string;
/**
* 使
*/
"yourNameContainsProhibitedWordsDescription": string;
/**
* 稿
*/
"thisContentsAreMarkedAsSigninRequiredByAuthor": string;
/**
*
*/
"lockdown": string;
/**
*
*/
"pleaseSelectAccount": string;
/**
*
*/
"availableRoles": string;
/**
*
*/
@ -5312,6 +5424,90 @@ export interface Locale extends ILocale {
*
*/
"changeUserName": string;
/**
*
*/
"normalize": string;
/**
*
*/
"normalizeConfirm": string;
/**
* 使
*/
"normalizeDescription": string;
/**
* 使
*/
"useNormalization": string;
/**
*
*/
"gtagConsentCustomize": string;
/**
* {host}
*
*/
"gtagConsentCustomizeDescription": ParameterizedString<"host">;
/**
*
*/
"gtagConsentAnalytics": string;
/**
* Cookie
*/
"gtagConsentAnalyticsDescription": string;
/**
*
*/
"gtagConsentFunctionality": string;
/**
*
*/
"gtagConsentFunctionalityDescription": string;
/**
*
*/
"gtagConsentPersonalization": string;
/**
* 稿
*/
"gtagConsentPersonalizationDescription": string;
/**
* Misskeyの明日を作るために
*
*/
"helpUsImproveUserExperience": string;
/**
* {host}[]({privacyPolicyUrl})IPアドレス
*
*
*/
"pleaseConsentToTracking": ParameterizedString<"host" | "privacyPolicyUrl">;
/**
*
*/
"consentEssential": string;
/**
*
*/
"consentAll": string;
/**
*
*/
"consentSelected": string;
/**
*
*/
"alsoMuteNotification": string;
/**
*
*/
"muteNotification": string;
/**
*
*/
"unmuteNotification": string;
"_bubbleGame": {
/**
*
@ -10283,6 +10479,10 @@ export interface Locale extends ILocale {
*
*/
"unsetUserBanner": string;
/**
*
*/
"normalize": string;
};
"_fileViewer": {
/**
@ -10880,6 +11080,28 @@ export interface Locale extends ILocale {
*/
"rolesDescription": string;
};
"_selfXssPrevention": {
/**
*
*/
"warning": string;
/**
*
*/
"title": string;
/**
*
*/
"description1": string;
/**
* %c今すぐ作業を中止してこのウィンドウを閉じてください
*/
"description2": string;
/**
* {link}
*/
"description3": ParameterizedString<"link">;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -1285,6 +1285,34 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
noDescription: "説明文はありません"
alwaysConfirmFollow: "フォローの際常に確認する"
inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
fromX: "{x}から"
genEmbedCode: "埋め込みコードを生成"
noteOfThisUser: "このユーザーのノート一覧"
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
performance: "パフォーマンス"
modified: "変更あり"
discard: "破棄"
thereAreNChanges: "{n}件の変更があります"
signinWithPasskey: "パスキーでログイン"
unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)"
prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。"
yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています"
yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。"
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
abuseReportCategory: "通報の種類"
selectCategory: "カテゴリを選択"
reportComplete: "通報完了"
@ -1322,6 +1350,26 @@ dangerZone: "危険区域"
dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。"
checkedByHIBP: "パスワードの安全性に加え、HIBPを通じてパスワードの漏洩を検査します。"
changeUserName: "名前を変更"
normalize: "正常化"
normalizeConfirm: "正常化すると元に戻せなくなり、これはアカウントの削除と同様の効力を持ちます。実行しますか?"
normalizeDescription: "正常化は、ユーザーの一括的なデータ抹消やアカウント制裁が必要な場合に使用する機能です。アカウントを正常化した後は取り返しのつかないことに留意してください。"
useNormalization: "正規化機能を使用する"
gtagConsentCustomize: "データ収集とプライバシー設定"
gtagConsentCustomizeDescription: "{host}が収集するデータの範囲をカスタマイズできます。\nただし、認証機能、不正行為防止、その他のユーザー保護など、セキュリティに関連する情報の収集は無効化できません。"
gtagConsentAnalytics: "統計情報の収集"
gtagConsentAnalyticsDescription: "サイトの滞在時間など、分析に関連する情報の保存Cookie など)を有効にします。"
gtagConsentFunctionality: "機能・設定の利用状況の収集"
gtagConsentFunctionalityDescription: "言語設定など、ウェブサイトやアプリの機能をサポートする情報の保存を有効にします。"
gtagConsentPersonalization: "パーソナライズされた情報の収集"
gtagConsentPersonalizationDescription: "おすすめの投稿など、パーソナライズに関連する情報の保存を有効にします。"
helpUsImproveUserExperience: "Misskeyの明日を作るために、\nデータ収集にご協力ください"
pleaseConsentToTracking: "{host}は[プライバシーポリシー]({privacyPolicyUrl})に基づき、サービスの提供・運営・ユーザー体験の向上のためにご利用中のIPアドレス、利用状況、デバイス情報等、個人情報を含む可能性のある情報を収集することがあります。\n\n収集されたデータは今後の機能の開発、運営の方針の決定、サービスの改善点の特定に利用されます。"
consentEssential: "必須項目のみ許可"
consentAll: "全て許可"
consentSelected: "選択した項目のみ許可"
alsoMuteNotification: "このユーザーが送信する通知も一緒にミュートする"
muteNotification: "通知をミュートする"
unmuteNotification: "通知をミュート解除する"
_bubbleGame:
howToPlay: "遊び方"
@ -2717,6 +2765,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
normalize: "正 常 化"
_fileViewer:
title: "ファイルの詳細"
@ -2890,3 +2939,10 @@ _hideSensitiveInformation:
roles: "ロール"
rolesUse: "割り当てられたロールを非表示にする"
rolesDescription: "このオプションを有効にすると、ユーザープロファイルにすべてのロールリストが表示されなくなります。"
_selfXssPrevention:
warning: "警告"
title: "「この画面に何か貼り付けろ」はすべて詐欺です。"
description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。"
description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。"
description3: "詳しくはこちらをご確認ください。 {link}"

View File

@ -610,12 +610,12 @@ removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합
userSuspended: "이 계정은 정지된 상태입니다."
userLimited: "이 계정은 제한된 상태입니다."
userSilenced: "이 계정은 사일런스된 상태입니다."
yourAccountSuspendedTitle: "계정이 정지되었습니다"
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
yourAccountSuspendedTitle: "당신의 계정이 정지되었습니다"
yourAccountSuspendedDescription: "당신의 계정이 규정 위반 또는 관리자 재량에 의해 정지되었습니다. 잘못되었다고 생각할 경우 관리자에게 문의해주십시오."
tokenRevoked: "유효하지 않은 토큰입니다"
tokenRevokedDescription: "로그인 토큰이 비활성화되었습니다. 다시 로그인하여 주십시오."
accountDeleted: "계정이 정지되었습니다"
accountDeletedDescription: "이 계정이 삭제되었습니다."
tokenRevokedDescription: "다른 곳에서 비밀번호를 변경했거나, 다른 모든 세션을 종료했습니다. 계속하려면 다시 로그인하세요."
accountDeleted: "당신의 계정이 정지되었습니다"
accountDeletedDescription: "당신의 계정이 일정 기간 이상 비활동 또는 관리자 재량에 의해 삭제되었습니다."
menu: "메뉴"
divider: "구분선"
addItem: "항목 추가"
@ -1271,6 +1271,35 @@ useTotp: "일회용 비밀번호 사용"
useBackupCode: "백업 코드 사용"
launchApp: "앱 실행"
useNativeUIForVideoAudioPlayer: "브라우저 UI에서 미디어 재생"
keepOriginalFilename: "원본 파일 이름을 유지"
keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파일 이름이 자동으로 무작위 문자열로 바뀝니다."
noDescription: "설명문이 없습니다"
inquiry: "문의하기"
tryAgain: "다시 시도해 주세요."
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
sensitiveMediaRevealConfirm: "민감한 미디어입니다. 표시할까요?"
createdLists: "만든 리스트"
createdAntennas: "만든 안테나"
fromX: "{x}부터"
genEmbedCode: "임베디드 코드 만들기"
noteOfThisUser: "이 유저의 노트 목록"
clipNoteLimitExceeded: "더 이상 이 클립에 노트를 추가 할 수 없습니다."
performance: "퍼포먼스"
modified: "변경 있음"
discard: "파기"
thereAreNChanges: "{n}건 변경이 있습니다."
signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지"
target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
lockdown: "잠금"
here: "여기"
alwaysConfirmFollow: "팔로우할 때 항상 확인하기"
credits: "엔딩 크레딧"
@ -1310,6 +1339,27 @@ dangerZoneDescription: "함부로 실행하면 어딘가 고장날 수 있는
checkedByHIBP: "비밀번호의 안전성과 더불어, HIBP를 통해 비밀번호 유출을 검사합니다."
changeUserName: "이름 변경"
pleaseSelectAccount: "사용할 계정을 선택해주십시오"
normalize: "정상화"
normalizeConfirm: "정상화 이후에는 계정을 되돌릴 수 없게 됩니다. 실행하시겠습니까?"
normalizeDescription: "정상화는 유저의 일괄적인 데이터 말소 및 계정 정지를 위한 기능으로, 계정 삭제와 비슷한 효과를 가지며, 실행 후에는 모든 신고가 닫히게 됩니다. 기능을 실행하고 나면 되돌릴 수 없는 점을 유의하시기 바랍니다."
useNormalization: "정상화 메뉴를 표시하기"
gtagConsentCustomize: "데이터 수집 및 개인정보 설정"
gtagConsentCustomizeDescription: "{host}에서 수집하는 데이터 범위를 사용자 지정할 수 있습니다.\n다만, 인증 기능, 부정 행위 방지, 기타 사용자 보호 등 보안과 관련된 정보 수집은 비활성화할 수 없습니다."
gtagConsentAnalytics: "통계 정보 수집"
gtagConsentAnalyticsDescription: "사이트 체류 시간 등 분석 관련 정보 저장(쿠키 등)을 활성화합니다."
gtagConsentFunctionality: "기능 및 설정 사용 정보 수집"
gtagConsentFunctionalityDescription: "언어 설정 등 웹사이트나 앱의 기능을 지원하는 정보 저장을 활성화합니다."
gtagConsentPersonalization: "개인 맞춤형 정보 수집"
gtagConsentPersonalizationDescription: "추천 게시물 등 개인화 관련 정보 저장을 활성화합니다."
helpUsImproveUserExperience: "Misskey의 미래를 위해,\n데이터 수집에 협조해 주세요!"
pleaseConsentToTracking: "{host}는 [개인정보 처리방침]({privacyPolicyUrl})에 따라 서비스 제공, 운영, 사용자 경험 향상을 위해 사용 중인 IP 주소, 이용 현황, 디바이스 정보 등 개인 정보를 포함할 수 있는 정보를 수집할 수 있습니다.\n\n수집된 데이터는 향후 기능 개발, 운영 방침 결정, 서비스 개선점 파악에 활용됩니다."
consentEssential: "필수 항목만 허용"
consentAll: "모두 허용"
consentSelected: "선택한 항목만 허용"
alsoMuteNotification: "이 유저가 보내는 알림도 같이 뮤트하기"
muteNotification: "알림을 뮤트하기"
unmuteNotification: "알림 뮤트를 해제하기"
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
@ -2609,6 +2659,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "아바타 장식 삭제"
unsetUserAvatar: "유저 아바타 제거"
unsetUserBanner: "유저 배너 제거"
normalize: "정 상 화"
_fileViewer:
title: "파일 상세"
type: "파일 유형"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2024.5.0-oscar.17a",
"version": "2024.5.0-oscar.18",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://git.psec.dev/oscar-surf/misskey.git"
},
"packageManager": "pnpm@9.7.1",
"packageManager": "pnpm@9.12.2",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -46,31 +46,35 @@
"cleanall": "pnpm clean-all"
},
"resolutions": {
"@tensorflow/tfjs-core": "4.20.0",
"chokidar": "3.6.0",
"esbuild": "0.23.0",
"@tensorflow/tfjs-core": "4.22.0",
"chokidar": "4.0.1",
"cookie": "1.0.1",
"cookie-signature": "1.2.2",
"debug": "4.3.7",
"esbuild": "0.24.0",
"lodash": "4.17.21",
"sharp": "0.33.4"
"sharp": "0.33.5",
"web-streams-polyfill": "4.0.0"
},
"dependencies": {
"cssnano": "7.0.5",
"execa": "9.3.0",
"cssnano": "7.0.6",
"execa": "9.5.1",
"js-yaml": "4.1.0",
"postcss": "8.4.41",
"terser": "5.31.5",
"typescript": "5.5.4"
"postcss": "8.4.47",
"terser": "5.36.0",
"typescript": "5.6.3"
},
"devDependencies": {
"@types/node": "22.2.0",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"cross-env": "7.0.3",
"cypress": "13.13.2",
"eslint": "8.57.0",
"cypress": "13.15.1",
"eslint": "8.57.1",
"ncp": "2.0.0",
"start-server-and-test": "2.0.5"
"start-server-and-test": "2.0.8"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.19.0"
"@tensorflow/tfjs-core": "4.22.0"
}
}

View File

@ -0,0 +1,11 @@
export class GoogleAnalyticsId1730629332694 {
name = 'GoogleAnalyticsId1730629332694'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsId" character varying(32)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsId"`);
}
}

View File

@ -0,0 +1,11 @@
export class NotificationMuting1730885274028 {
name = 'NotificationMuting1730885274028'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "muting" ADD "withNotification" boolean NOT NULL DEFAULT true`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "withNotification"`);
}
}

View File

@ -34,16 +34,16 @@
"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.7.39",
"@swc/core-darwin-x64": "1.7.39",
"@swc/core-linux-arm-gnueabihf": "1.7.39",
"@swc/core-linux-arm64-gnu": "1.7.39",
"@swc/core-linux-arm64-musl": "1.7.39",
"@swc/core-linux-x64-gnu": "1.7.39",
"@swc/core-linux-x64-musl": "1.7.39",
"@swc/core-win32-arm64-msvc": "1.7.39",
"@swc/core-win32-ia32-msvc": "1.7.39",
"@swc/core-win32-x64-msvc": "1.7.39",
"@swc/core-darwin-arm64": "1.8.0",
"@swc/core-darwin-x64": "1.8.0",
"@swc/core-linux-arm-gnueabihf": "1.8.0",
"@swc/core-linux-arm64-gnu": "1.8.0",
"@swc/core-linux-arm64-musl": "1.8.0",
"@swc/core-linux-x64-gnu": "1.8.0",
"@swc/core-linux-x64-musl": "1.8.0",
"@swc/core-win32-arm64-msvc": "1.8.0",
"@swc/core-win32-ia32-msvc": "1.8.0",
"@swc/core-win32-x64-msvc": "1.8.0",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.8",
@ -60,15 +60,15 @@
"slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "6.0.4"
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@authenio/samlify-node-xmllint": "2.0.0",
"@aws-sdk/client-s3": "3.676.0",
"@aws-sdk/lib-storage": "3.676.0",
"@bull-board/api": "6.2.4",
"@bull-board/fastify": "6.2.4",
"@bull-board/ui": "6.2.4",
"@aws-sdk/client-s3": "3.685.0",
"@aws-sdk/lib-storage": "3.685.0",
"@bull-board/api": "6.3.3",
"@bull-board/fastify": "6.3.3",
"@bull-board/ui": "6.3.3",
"@discordapp/twemoji": "15.1.0",
"@elastic/elasticsearch": "8.15.1",
"@fastify/accepts": "5.0.1",
@ -82,16 +82,16 @@
"@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.58",
"@nestjs/common": "10.4.5",
"@nestjs/core": "10.4.5",
"@nestjs/testing": "10.4.5",
"@napi-rs/canvas": "0.1.59",
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7",
"@nestjs/testing": "10.4.7",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "11.0.0",
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "3.2.5",
"@swc/cli": "0.4.0",
"@swc/core": "1.7.39",
"@swc/cli": "0.5.0",
"@swc/core": "1.8.0",
"@twemoji/parser": "15.1.1",
"accepts": "1.3.8",
"ajv": "8.17.1",
@ -100,9 +100,9 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.21.2",
"bullmq": "5.24.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"cbor": "10.0.2",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "4.0.1",
@ -118,8 +118,8 @@
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.1",
"got": "14.4.3",
"happy-dom": "15.7.4",
"got": "14.4.4",
"happy-dom": "15.9.0",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
@ -133,26 +133,26 @@
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
"meilisearch": "0.44.1",
"meilisearch": "0.45.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1",
"nanoid": "5.0.7",
"nanoid": "5.0.8",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-forge": "1.3.1",
"nodemailer": "6.9.15",
"nodemailer": "6.9.16",
"nsfwjs": "2.4.2",
"oauth": "0.10.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.3.4",
"parse5": "7.2.0",
"pg": "8.13.0",
"parse5": "7.2.1",
"pg": "8.13.1",
"pino": "9.5.0",
"pino-pretty": "11.3.0",
"pkce-challenge": "4.1.0",
@ -193,26 +193,26 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0",
"@nestjs/platform-express": "10.4.5",
"@nestjs/platform-express": "10.4.7",
"@simplewebauthn/types": "11.0.0",
"@swc/jest": "0.2.36",
"@swc/jest": "0.2.37",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.2",
"@types/archiver": "6.0.3",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.26",
"@types/fluent-ffmpeg": "2.1.27",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.13",
"@types/jest": "29.5.14",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.7.8",
"@types/node": "22.9.0",
"@types/node-forge": "1.3.11",
"@types/nodemailer": "6.4.16",
"@types/oauth": "0.9.6",
@ -233,14 +233,14 @@
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.5.12",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"execa": "9.4.1",
"execa": "9.5.1",
"fkill": "^9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",

View File

@ -4,10 +4,9 @@
*/
import { LoggerService } from '@nestjs/common';
import Logger from '@/logger.js';
import { coreLogger } from '@/logger.js';
const logger = new Logger('core', 'cyan');
const nestLogger = logger.createSubLogger('nest', 'green', false);
const nestLogger = coreLogger.createSubLogger('nest', 'green', false);
export class NestLogger implements LoggerService {
/**

View File

@ -12,7 +12,7 @@ import { EventEmitter } from 'node:events';
import process from 'node:process';
import chalk from 'chalk';
import Xev from 'xev';
import Logger from '@/logger.js';
import { coreLogger } from '@/logger.js';
import { envOption } from '../env.js';
import { masterMain } from './master.js';
import { workerMain } from './worker.js';
@ -24,8 +24,7 @@ process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`;
Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128;
const logger = new Logger('core', 'cyan');
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
const clusterLogger = coreLogger.createSubLogger('cluster', 'orange', false);
const ev = new Xev();
//#region Events
@ -53,12 +52,12 @@ if (cluster.isPrimary && !envOption.disableClustering) {
});
process.on('SIGINT', () => {
logger.warn(chalk.yellow('Process received SIGINT'));
coreLogger.warn(chalk.yellow('Process received SIGINT'));
isShuttingDown = true;
});
process.on('SIGTERM', () => {
logger.warn(chalk.yellow('Process received SIGTERM'));
coreLogger.warn(chalk.yellow('Process received SIGTERM'));
isShuttingDown = true;
});
}
@ -71,18 +70,18 @@ if (!envOption.quiet) {
// Display detail of uncaught exception
process.on('uncaughtException', err => {
try {
logger.error(`Uncaught exception: ${err.message}`, { error: err });
coreLogger.error(`Uncaught exception: ${err.message}`, { error: err });
} catch { }
});
// Dying away...
process.on('exit', code => {
logger.warn(chalk.yellow(`The process is going to exit with code ${code}`));
coreLogger.warn(chalk.yellow(`The process is going to exit with code ${code}`));
});
process.on('warning', warning => {
if ((warning as never)['code'] !== 'MISSKEY_SHUTDOWN') return;
logger.warn(chalk.yellow(`${warning.message}: ${(warning as never)['detail']}`));
coreLogger.warn(chalk.yellow(`${warning.message}: ${(warning as never)['detail']}`));
for (const id in cluster.workers) cluster.workers[id]?.process.kill('SIGTERM');
process.exit();
});

View File

@ -10,7 +10,7 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
import Logger from '@/logger.js';
import { coreLogger } from '@/logger.js';
import { loadConfig } from '@/config.js';
import type { Config } from '@/config.js';
import { showMachineInfo } from '@/misc/show-machine-info.js';
@ -22,8 +22,7 @@ const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
const bootLogger = coreLogger.createSubLogger('boot', 'magenta', false);
const themeColor = chalk.hex('#86b300');

View File

@ -21,6 +21,8 @@ export class CacheService implements OnApplicationShutdown {
public localUserByIdCache: MemoryKVCache<MiLocalUser>;
public uriPersonCache: MemoryKVCache<MiUser | null>;
public userProfileCache: RedisKVCache<MiUserProfile>;
public userMutingsWithNotificationCache: RedisKVCache<Set<string>>;
public userMutingsWithoutNotificationCache: RedisKVCache<Set<string>>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
@ -77,6 +79,22 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userMutingsWithoutNotificationCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key, withNotification: false }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userMutingsWithNotificationCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key, withNotification: true }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
});
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
@ -188,6 +206,8 @@ export class CacheService implements OnApplicationShutdown {
this.uriPersonCache.dispose();
this.userProfileCache.dispose();
this.userMutingsCache.dispose();
this.userMutingsWithoutNotificationCache.dispose();
this.userMutingsWithNotificationCache.dispose();
this.userBlockingCache.dispose();
this.userBlockedCache.dispose();
this.renoteMutingsCache.dispose();

View File

@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { LoggerService } from '@/core/LoggerService.js';
type AddFileArgs = {
/** User who wish to add file */
@ -123,12 +124,13 @@ export class DriveService {
private globalEventService: GlobalEventService,
private queueService: QueueService,
private roleService: RoleService,
private loggerService: LoggerService,
private moderationLogService: ModerationLogService,
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
) {
const logger = new Logger('drive', 'blue');
const logger = this.loggerService.getLogger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow');
this.downloaderLogger = logger.createSubLogger('downloader');
this.deleteLogger = logger.createSubLogger('delete');
@ -503,6 +505,12 @@ export class DriveService {
if (much) {
this.registerLogger.info(`file with same hash is found: ${much.id}`);
if (sensitive && !much.isSensitive) {
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
// Therefore, update the file to sensitive.
await this.driveFilesRepository.update({ id: much.id }, { isSensitive: true });
much.isSensitive = true;
}
return much;
}
}

View File

@ -159,7 +159,7 @@ export class FetchInstanceMetadataService {
throw err.statusCode ?? err.message;
});
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
this.logger.succ(`Successfully fetched nodeinfo of ${instance.host}`);
return info as NodeInfo;
} catch (err) {

View File

@ -4,7 +4,7 @@
*/
import { Injectable } from '@nestjs/common';
import Logger from '@/logger.js';
import { rootLogger } from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { KEYWORD } from 'color-convert/conversions.js';
@ -16,6 +16,6 @@ export class LoggerService {
@bindThis
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
return new Logger(domain);
return rootLogger.createSubLogger(domain, color, store);
}
}

View File

@ -410,7 +410,7 @@ export class MfmService {
mention: (node) => {
const a = doc.createElement('a');
const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
a.className = 'u-url mention';
a.textContent = acct;

View File

@ -257,7 +257,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const policies = await this.roleService.getUserPolicies(user.id);
if (!policies.canCreateContent) {
this.logger.error('Request rejected because user has no permission to create content', { user: user.id, note: data });
this.logger.error('Request rejected because user has no permission to create content', { userId: user.id, note: data });
throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.');
}
@ -265,7 +265,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
data.visibility = 'home';
this.logger.warn('Visibility changed to home because sensitive words are included', { user: user.id, note: data });
this.logger.warn('Visibility changed to home because sensitive words are included', { userId: user.id, note: data });
} else if (!policies.canPublicNote) {
data.visibility = 'home';
}
@ -281,7 +281,7 @@ export class NoteCreateService implements OnApplicationShutdown {
);
if (hasProhibitedWords) {
this.logger.error('Request rejected because prohibited words are included', { user: user.id, note: data });
this.logger.error('Request rejected because prohibited words are included', { userId: user.id, note: data });
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Notes including prohibited words are not allowed.');
}
@ -384,7 +384,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (process.env.MISSKEY_BLOCK_MENTIONS_FROM_UNFAMILIAR_REMOTE_USERS === 'true' && user.host !== null && willCauseNotification) {
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
if ((userEntity?.followersCount ?? 0) === 0) {
this.logger.error('Request rejected because user has no local followers', { user: user.id, note: data });
this.logger.error('Request rejected because user has no local followers', { userId: user.id, note: data });
throw new IdentifiableError('e11b3a16-f543-4885-8eb1-66cad131dbfd', 'Notes including mentions, replies, or renotes from remote users are not allowed until user has at least one local follower.');
}
}
@ -396,7 +396,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|| (data.visibility === 'specified' && data.visibleUsers?.some(u => u.id !== user.id))
|| (this.isQuote(data) && data.renote.userId !== user.id)
) {
this.logger.error('Request rejected because user has no permission to initiate conversation', { user: user.id, note: data });
this.logger.error('Request rejected because user has no permission to initiate conversation', { userId: user.id, note: data });
throw new IdentifiableError('332dd91b-6a00-430a-ac39-620cf60ad34b', 'Notes including mentions, replies, or renotes are not allowed.');
}
}

View File

@ -106,7 +106,7 @@ export class NotificationService implements OnApplicationShutdown {
return null;
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
const mutings = await this.cacheService.userMutingsWithNotificationCache.fetch(notifieeId);
if (mutings.has(notifierId)) {
return null;
}

View File

@ -50,7 +50,7 @@ export class UserBlockingService implements OnModuleInit {
private apRendererService: ApRendererService,
private loggerService: LoggerService,
) {
this.logger = this.loggerService.getLogger('user-block');
this.logger = this.loggerService.getLogger('user:block');
}
onModuleInit() {

View File

@ -30,9 +30,8 @@ import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
type Local = MiLocalUser | {
id: MiLocalUser['id'];
@ -50,6 +49,7 @@ type Both = Local | Remote;
@Injectable()
export class UserFollowingService implements OnModuleInit {
private userBlockingService: UserBlockingService;
private readonly logger: Logger;
constructor(
private moduleRef: ModuleRef,
@ -73,6 +73,7 @@ export class UserFollowingService implements OnModuleInit {
private instancesRepository: InstancesRepository,
private cacheService: CacheService,
private loggerService: LoggerService,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
@ -88,6 +89,7 @@ export class UserFollowingService implements OnModuleInit {
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
this.logger = this.loggerService.getLogger('user:following');
}
onModuleInit() {
@ -255,7 +257,7 @@ export class UserFollowingService implements OnModuleInit {
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
}).catch(err => {
if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
this.logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
alreadyFollowed = true;
} else {
throw err;
@ -378,7 +380,7 @@ export class UserFollowingService implements OnModuleInit {
});
if (following === null || !following.follower || !following.followee) {
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
this.logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
return;
}

View File

@ -24,15 +24,18 @@ export class UserMutingService {
}
@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null, withNotification = true): Promise<void> {
await this.mutingsRepository.insert({
id: this.idService.gen(),
expiresAt: expiresAt ?? null,
muterId: user.id,
muteeId: target.id,
withNotification: withNotification,
});
this.cacheService.userMutingsCache.refresh(user.id);
this.cacheService.userMutingsWithNotificationCache.refresh(user.id);
this.cacheService.userMutingsWithoutNotificationCache.refresh(user.id);
}
@bindThis
@ -46,6 +49,17 @@ export class UserMutingService {
const muterIds = [...new Set(mutings.map(m => m.muterId))];
for (const muterId of muterIds) {
this.cacheService.userMutingsCache.refresh(muterId);
this.cacheService.userMutingsWithNotificationCache.refresh(muterId);
this.cacheService.userMutingsWithoutNotificationCache.refresh(muterId);
}
}
@bindThis
public async editMute(muting: MiMuting, withNotification: boolean): Promise<void> {
await this.mutingsRepository.update(muting.id, { withNotification: withNotification });
this.cacheService.userMutingsCache.refresh(muting.muterId);
this.cacheService.userMutingsWithNotificationCache.refresh(muting.muterId);
this.cacheService.userMutingsWithoutNotificationCache.refresh(muting.muterId);
}
}

View File

@ -145,8 +145,7 @@ export class ApRequestService {
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
this.logger = this.loggerService.getLogger('ap:request');
}
@bindThis

View File

@ -45,7 +45,7 @@ export class Resolver {
private recursionLimit = 100,
) {
this.history = new Set();
this.logger = this.loggerService.getLogger('ap-resolve');
this.logger = this.loggerService.getLogger('ap:resolve');
}
@bindThis

View File

@ -78,6 +78,7 @@ export class MetaEntityService {
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
googleAnalyticsId: instance.googleAnalyticsId,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',

View File

@ -40,6 +40,7 @@ export class MutingEntityService {
mutee: this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
withNotification: muting.withNotification,
});
}

View File

@ -269,12 +269,12 @@ export class NotificationEntityService implements OnModuleInit {
*/
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
notification: T,
userIdsWhoMeMuting: Set<MiUser['id']>,
userIdsWhoMeMutingWithNotification: Set<MiUser['id']>,
userMutedInstances: Set<string>,
notifiers: MiUser[],
): boolean {
if (!('notifierId' in notification)) return true;
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;
if (userIdsWhoMeMutingWithNotification.has(notification.notifierId)) return false;
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
@ -303,10 +303,10 @@ export class NotificationEntityService implements OnModuleInit {
meId: MiUser['id'],
): Promise<T[]> {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingWithNotification,
userMutedInstances,
] = (await Promise.allSettled([
this.cacheService.userMutingsCache.fetch(meId),
this.cacheService.userMutingsWithNotificationCache.fetch(meId),
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
])).map(result => result.status === 'fulfilled' ? result.value : new Set<string>());
@ -316,7 +316,7 @@ export class NotificationEntityService implements OnModuleInit {
}) : [];
return ((await Promise.allSettled(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
const isValid = this.#validateNotifier(notification, userIdsWhoMeMutingWithNotification, userMutedInstances, notifiers);
return isValid ? notification : null;
}))).filter(result => result.status === 'fulfilled' && isNotNull(result.value))
.map(result => (result as PromiseFulfilledResult<T>).value));

View File

@ -22,13 +22,14 @@ const pinoPrettyStream = pinoPretty({
// eslint-disable-next-line import/no-default-export
export default class Logger {
private readonly domain: string;
private logger: pino.Logger;
private readonly domain: string | undefined;
private readonly logger: pino.Logger;
private context: Record<string, any> = {};
constructor(domain: string, _color?: KEYWORD, _store = true, parentLogger?: Logger) {
constructor(domain: string | undefined, _color?: KEYWORD, _store = true, parentLogger?: Logger) {
if (parentLogger) {
this.domain = parentLogger.domain + '.' + domain;
this.domain = [parentLogger.domain, domain].filter(x => x).join('.') || undefined;
this.context = { ...JSON.parse(JSON.stringify(parentLogger.context)) };
} else {
this.domain = domain;
}
@ -50,18 +51,20 @@ export default class Logger {
formatters: {
level: (label, number) => ({ severity: label, level: number }),
},
mixin: () => ({ cluster: cluster.isPrimary ? 'primary' : `worker#${cluster.worker!.id}`, ...this.context }),
mixin: () => this.mixin(),
}, !envOption.logJson ? pinoPrettyStream : undefined);
}
@bindThis
public createSubLogger(domain: string, _color?: KEYWORD, _store = true): Logger {
return new Logger(domain, undefined, false, this);
private mixin(): Record<string, any> {
return { cluster: cluster.isPrimary ? 'primary' : `worker#${cluster.worker!.id}`, ...this.context };
}
public createSubLogger(domain?: string, _color?: KEYWORD, _store = true): Logger {
return new Logger(domain, _color, _store, this);
}
@bindThis
public setContext(context: Record<string, any>): void {
this.context = context;
this.context = { ...this.context, ...JSON.parse(JSON.stringify(context)) };
}
@bindThis
@ -130,3 +133,6 @@ export default class Logger {
this.logger.info({ context, important }, message);
}
}
export const rootLogger = new Logger(undefined, undefined, false, undefined);
export const coreLogger = rootLogger.createSubLogger('core', 'cyan');

View File

@ -265,6 +265,12 @@ export class MiMeta {
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('varchar', {
length: 32,
nullable: true,
})
public googleAnalyticsId: string | null;
@Column('enum', {
enum: ['none', 'all', 'local', 'remote'],
default: 'none',

View File

@ -51,4 +51,10 @@ export class MiMuting {
})
@JoinColumn()
public muter: MiUser | null;
@Column('boolean', {
default: true,
comment: 'Whether to mute notification from mutee.',
})
public withNotification: boolean;
}

View File

@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
googleAnalyticsId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,

View File

@ -32,5 +32,9 @@ export const packedMutingSchema = {
optional: false, nullable: false,
ref: 'UserDetailedNotMe',
},
withNotification: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -13,7 +13,7 @@ import type {
} from '@/models/_.js';
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
import type * as Bull from "bullmq";
import type { DbUserSuspendJobData } from "@/queue/types.js";
import type { DbUserSuspendJobData } from '@/queue/types.js';
@Injectable()
export class UserSuspendProcessorService {

View File

@ -64,6 +64,7 @@ import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
import * as ep___admin_normalization from './endpoints/admin/normalization.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -280,6 +281,7 @@ import * as ep___emoji from './endpoints/emoji.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js';
import * as ep___mute_edit from './endpoints/mute/edit.js';
import * as ep___mute_list from './endpoints/mute/list.js';
import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
@ -462,6 +464,7 @@ const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', us
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
const $admin_normalization: Provider = { provide: 'ep:admin/normalization', useClass: ep___admin_normalization.default };
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
@ -678,6 +681,7 @@ const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
const $mute_edit: Provider = { provide: 'ep:mute/edit', useClass: ep___mute_edit.default };
const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default };
const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default };
const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default };
@ -864,6 +868,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_getUserIps,
$admin_invite_create,
$admin_invite_list,
$admin_normalization,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@ -1080,6 +1085,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$miauth_genToken,
$mute_create,
$mute_delete,
$mute_edit,
$mute_list,
$renoteMute_create,
$renoteMute_delete,
@ -1260,6 +1266,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_getUserIps,
$admin_invite_create,
$admin_invite_list,
$admin_normalization,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@ -1476,6 +1483,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$miauth_genToken,
$mute_create,
$mute_delete,
$mute_edit,
$mute_list,
$renoteMute_create,
$renoteMute_delete,

View File

@ -27,6 +27,7 @@ import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { randomUUID } from 'node:crypto';
@Injectable()
export class SigninApiService {
@ -71,7 +72,7 @@ export class SigninApiService {
reply: FastifyReply,
) {
const logger = this.loggerService.getLogger('api:signin');
logger.setContext({ username: request.body.username, ip: request.ip, headers: request.headers });
logger.setContext({ username: request.body.username, ip: request.ip, headers: request.headers, span: request.headers['x-client-transaction-id'] ?? randomUUID() });
logger.info('Requested to sign in.');
reply.header('Access-Control-Allow-Origin', this.config.url);

View File

@ -22,6 +22,7 @@ import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { LoggerService } from '@/core/LoggerService.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { randomUUID } from 'node:crypto';
@Injectable()
export class SignupApiService {
@ -73,7 +74,7 @@ export class SignupApiService {
reply: FastifyReply,
) {
const logger = this.loggerService.getLogger('api:signup');
logger.setContext({ username: request.body.username, email: request.body.emailAddress, ip: request.ip, headers: request.headers });
logger.setContext({ username: request.body.username, email: request.body.emailAddress, ip: request.ip, headers: request.headers, span: request.headers['x-client-transaction-id'] ?? randomUUID() });
logger.info('Requested to create user account.');
const body = request.body;

View File

@ -64,6 +64,7 @@ import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
import * as ep___admin_normalization from './endpoints/admin/normalization.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -280,6 +281,7 @@ import * as ep___emoji from './endpoints/emoji.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js';
import * as ep___mute_edit from './endpoints/mute/edit.js';
import * as ep___mute_list from './endpoints/mute/list.js';
import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
@ -460,6 +462,7 @@ const eps = [
['admin/get-user-ips', ep___admin_getUserIps],
['admin/invite/create', ep___admin_invite_create],
['admin/invite/list', ep___admin_invite_list],
['admin/normalization', ep___admin_normalization],
['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
@ -676,6 +679,7 @@ const eps = [
['miauth/gen-token', ep___miauth_genToken],
['mute/create', ep___mute_create],
['mute/delete', ep___mute_delete],
['mute/edit', ep___mute_edit],
['mute/list', ep___mute_list],
['renote-mute/create', ep___renoteMute_create],
['renote-mute/delete', ep___renoteMute_delete],

View File

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['admin'],
@ -13,6 +14,49 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: false,
},
description: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
},
roleIdsThatCanBeUsedThisDecoration: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
},
} as const;
export const paramDef = {
@ -32,14 +76,25 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private avatarDecorationService: AvatarDecorationService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
await this.avatarDecorationService.create({
const created = await this.avatarDecorationService.create({
name: ps.name,
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
}, me);
return {
id: created.id,
createdAt: this.idService.parse(created.id).date.toISOString(),
updatedAt: null,
name: created.name,
description: created.description,
url: created.url,
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
};
});
}
}

View File

@ -4,10 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
import type { MiAnnouncement } from '@/models/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';

View File

@ -73,6 +73,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
googleAnalyticsId: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
@ -562,6 +566,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
googleAnalyticsId: instance.googleAnalyticsId,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,

View File

@ -0,0 +1,139 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import type { AbuseUserReportsRepository, FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser, MiLocalUser } from '@/models/User.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:suspend-user',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
},
noModerator: {
message: 'Can\'t normalize user with moderator permission.',
code: 'NO_MODERATOR_NORMALIZATION',
id: '5b68a1d3-8ee3-4862-8294-6c7d2d2edd63',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
private userSuspendService: UserSuspendService,
private roleService: RoleService,
private moderationLogService: ModerationLogService,
private queueService: QueueService,
private deleteAccountService: DeleteAccountService,
private instanceActorService: InstanceActorService,
private apRendererService: ApRendererService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
if (await this.roleService.isModerator(user)) {
throw new ApiError(meta.errors.noModerator);
}
await this.usersRepository.update(user.id, {
isSuspended: true,
});
await this.moderationLogService.log(me, 'normalize', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
await this.resolveAllReports(user, me).catch(e => {});
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
await this.deleteAccountService.deleteAccount(user, true, me);
});
}
@bindThis
private async resolveAllReports(user: MiUser, me: MiLocalUser) {
const reports = await this.abuseUserReportsRepository.findBy({ targetUserId: user.id });
for (const report of reports) {
if (report.targetUserHost != null) {
const actor = await this.instanceActorService.getInstanceActor();
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
}
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
assigneeId: me.id,
forwarded: user.host !== null,
});
}
}
@bindThis
private async unFollowAll(user: MiUser) {
const followings = await this.followingsRepository.findBy({
followerId: user.id,
});
const followers = await this.followingsRepository.findBy({
followeeId: user.id,
});
const followingPairs = await Promise.all(followings.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }),
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
const followerPairs = await Promise.all(followers.map(f => Promise.all([
this.usersRepository.findOneByOrFail({ id: f.followerId }),
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
await this.queueService.createUnfollowJob(followingPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
await this.queueService.createUnfollowJob(followerPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
}
}

View File

@ -79,6 +79,7 @@ export const paramDef = {
enableTurnstile: { type: 'boolean' },
turnstileSiteKey: { type: 'string', nullable: true },
turnstileSecretKey: { type: 'string', nullable: true },
googleAnalyticsId: { type: 'string', nullable: true },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@ -379,6 +380,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.turnstileSecretKey = ps.turnstileSecretKey;
}
if (ps.googleAnalyticsId !== undefined) {
set.googleAnalyticsId = ps.googleAnalyticsId;
}
if (ps.sensitiveMediaDetection !== undefined) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
}

View File

@ -6,8 +6,8 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { EntityNotFoundError } from "typeorm";
import { ApiError } from "../error.js";
import { EntityNotFoundError } from 'typeorm';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['meta'],

View File

@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
const logger = this.loggerService.getLogger('api:federation:instances');
logger.setContext({ params: ps, user: me?.id, ip, headers });
logger.setContext({ params: ps, userId: me?.id, ip, headers });
logger.info('Requested to fetch federated instances.');
const query = this.instancesRepository.createQueryBuilder('instance');

View File

@ -8,7 +8,9 @@ import type { UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../error.js';
import { ApiError } from '@/server/api/error.js';
import { LoggerService } from '@/core/LoggerService.js';
import { randomUUID } from 'node:crypto';
export const meta = {
tags: ['account'],
@ -44,11 +46,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private loggerService: LoggerService,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, user, token) => {
super(meta, paramDef, async (ps, user, token, _file, _cleanup, ip, headers) => {
const isSecure = token == null;
const logger = this.loggerService.getLogger('api:account:i');
logger.setContext({ userId: user?.id, username: user?.username, client: isSecure ? 'misskey' : 'app', ip, headers, span: (headers ? headers['x-client-transaction-id'] : undefined) ?? randomUUID() });
logger.info('Fetching account information');
const now = new Date();
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
@ -71,11 +78,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
}
return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
schema: 'MeDetailed',
includeSecrets: isSecure,
userProfile,
});
try {
const result = await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
schema: 'MeDetailed',
includeSecrets: isSecure,
userProfile,
});
logger.info('Returning account information');
return result;
} catch (error) {
logger.error('Failed to pack user entity', { error });
throw error;
}
});
}
}

View File

@ -33,8 +33,8 @@ import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {

View File

@ -57,6 +57,10 @@ export const paramDef = {
nullable: true,
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
},
withNotification: {
type: 'boolean',
default: true,
},
},
required: ['userId'],
} as const;
@ -100,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return;
}
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null, ps.withNotification);
});
}
}

View File

@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:mutes',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: 'b851d00b-8ab1-4a56-8b1b-e24187cb48ef',
},
muteeIsYourself: {
message: 'Mutee is yourself.',
code: 'MUTEE_IS_YOURSELF',
id: 'f428b029-6b39-4d48-a1d2-cc1ae6dd5cf9',
},
notMuting: {
message: 'You are not muting that user.',
code: 'NOT_MUTING',
id: '5467d020-daa9-4553-81e1-135c0c35a96d',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
withNotification: { type: 'boolean', nullable: false },
},
required: ['userId', 'withNotification'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userMutingService: UserMutingService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
const muter = me;
// Check if the mutee is yourself
if (me.id === ps.userId) {
throw new ApiError(meta.errors.muteeIsYourself);
}
// Get mutee
const mutee = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
// Check not muting
const exist = await this.mutingsRepository.findOneBy({
muterId: muter.id,
muteeId: mutee.id,
});
if (exist == null) {
throw new ApiError(meta.errors.notMuting);
}
await this.userMutingService.editMute(exist, ps.withNotification);
});
}
}

View File

@ -282,7 +282,7 @@ export class ClientServerService {
};
const csp = this.config.contentSecurityPolicy
?? 'script-src \'self\' ' +
'https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ {scriptNonce}; ' +
'https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/ {scriptNonce}; ' +
'worker-src blob: \'self\'; ' +
'base-uri \'self\'; object-src \'self\'; report-uri /csp-error';
reply.header('Content-Security-Policy-Report-Only', csp.replace('{scriptNonce}', `'nonce-${scriptNonce}'`));
@ -478,7 +478,9 @@ export class ClientServerService {
};
// Atom
fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {
@ -491,7 +493,9 @@ export class ClientServerService {
});
// RSS
fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {
@ -504,7 +508,9 @@ export class ClientServerService {
});
// JSON
fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
if (request.params.user == null) return await renderBase(reply);
const feed = await getFeed(request.params.user);
if (feed) {

View File

@ -99,6 +99,7 @@ export const moderationLogTypes = [
'unsetUserAvatar',
'unsetUserBanner',
'unsetUserMutualLink',
'normalize',
] as const;
export type ModerationLogPayloads = {
@ -333,7 +334,12 @@ export type ModerationLogPayloads = {
userId: string;
userUsername: string;
userMutualLinkSections: { name: string | null; mutualLinks: { id: string; url: string; fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
}
};
normalize: {
userId: string;
userUsername: string;
userHost: string | null;
};
};
export type Serialized<T> = {

View File

@ -19,7 +19,7 @@ import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/te
import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
import { loadConfig } from '@/config.js';
import type { AppLockService } from '@/core/AppLockService.js';
import Logger from '@/logger.js';
import { coreLogger } from '@/logger.js';
describe('Chart', () => {
const config = loadConfig();
@ -63,7 +63,7 @@ describe('Chart', () => {
await db.initialize();
const logger = new Logger('chart'); // TODO: モックにする
const logger = coreLogger.createSubLogger('chart'); // TODO: モックにする
testChart = new TestChart(db, appLockService, logger);
testGroupedChart = new TestGroupedChart(db, appLockService, logger);
testUniqueChart = new TestUniqueChart(db, appLockService, logger);

14
packages/frontend/@types/vue-gtag.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
declare module 'vue-gtag' {
export type GtagConsent = (command: 'consent', arg: 'default' | 'update', params: GtagConsentParams) => void;
export interface GtagConsentParams {
ad_storage?: 'granted' | 'denied',
ad_user_data?: 'granted' | 'denied',
ad_personalization?: 'granted' | 'denied',
analytics_storage?: 'granted' | 'denied',
functionality_storage?: 'granted' | 'denied',
personalization_storage?: 'granted' | 'denied',
security_storage?: 'granted' | 'denied',
wait_for_update?: number
}
}

View File

@ -25,9 +25,9 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.1",
"@rollup/plugin-typescript": "12.1.1",
"@rollup/pluginutils": "5.1.2",
"@rollup/pluginutils": "5.1.3",
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.19.0",
"@tabler/icons-webfont": "3.21.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
"@vue/compiler-sfc": "3.5.12",
@ -36,12 +36,12 @@
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.4.5",
"chart.js": "4.4.6",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.14.0",
"chromatic": "11.16.5",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.0",
"date-fns": "4.1.0",
@ -59,86 +59,86 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.24.0",
"rollup": "4.24.4",
"sanitize-html": "2.13.1",
"sass": "1.80.3",
"shiki": "1.22.0",
"sass": "1.80.6",
"shiki": "1.22.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.169.0",
"three": "0.170.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.6.3",
"uuid": "10.0.0",
"uuid": "11.0.2",
"v-code-diff": "1.13.1",
"vite": "5.4.9",
"vite": "5.4.10",
"vue": "3.5.12",
"vue-gtag": "2.0.1",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.3.6",
"@storybook/addon-essentials": "8.3.6",
"@storybook/addon-interactions": "8.3.6",
"@storybook/addon-links": "8.3.6",
"@storybook/addon-mdx-gfm": "8.3.6",
"@storybook/addon-storysource": "8.3.6",
"@storybook/blocks": "8.3.6",
"@storybook/components": "8.3.6",
"@storybook/core-events": "8.3.6",
"@storybook/manager-api": "8.3.6",
"@storybook/preview-api": "8.3.6",
"@storybook/react": "8.3.6",
"@storybook/react-vite": "8.3.6",
"@storybook/test": "8.3.6",
"@storybook/theming": "8.3.6",
"@storybook/types": "8.3.6",
"@storybook/vue3": "8.3.6",
"@storybook/vue3-vite": "8.3.6",
"@storybook/addon-actions": "8.4.2",
"@storybook/addon-essentials": "8.4.2",
"@storybook/addon-interactions": "8.4.2",
"@storybook/addon-links": "8.4.2",
"@storybook/addon-mdx-gfm": "8.4.2",
"@storybook/addon-storysource": "8.4.2",
"@storybook/blocks": "8.4.2",
"@storybook/components": "8.4.2",
"@storybook/core-events": "8.4.2",
"@storybook/manager-api": "8.4.2",
"@storybook/preview-api": "8.4.2",
"@storybook/react": "8.4.2",
"@storybook/react-vite": "8.4.2",
"@storybook/test": "8.4.2",
"@storybook/theming": "8.4.2",
"@storybook/types": "8.4.2",
"@storybook/vue3": "8.4.2",
"@storybook/vue3-vite": "8.4.2",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "^1.6.4",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "22.7.8",
"@types/node": "22.9.0",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.13.0",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.12",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"@vitest/coverage-v8": "2.1.3",
"@vitest/coverage-v8": "2.1.4",
"@vue/runtime-core": "3.5.12",
"acorn": "8.13.0",
"acorn": "8.14.0",
"cross-env": "7.0.3",
"cypress": "13.15.0",
"cypress": "13.15.1",
"eslint": "8.57.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.29.1",
"eslint-plugin-vue": "9.30.0",
"fast-glob": "3.3.2",
"happy-dom": "15.7.4",
"happy-dom": "15.9.0",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.5.0",
"msw-storybook-addon": "2.0.3",
"msw": "2.6.0",
"msw-storybook-addon": "2.0.4",
"nodemon": "3.1.7",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"start-server-and-test": "2.0.8",
"storybook": "8.3.6",
"storybook": "8.4.2",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "2.1.3",
"vitest": "2.1.4",
"vitest-fetch-mock": "0.3.0",
"vue-component-type-helpers": "2.1.6",
"vue-component-type-helpers": "2.1.10",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.1.6"
"vue-tsc": "2.1.10"
}
}

View File

@ -10,7 +10,7 @@ import '@/style.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete', '/onboarding', '/oauth'];
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete', '/onboarding', '/oauth', '/sso'];
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
subBoot();

View File

@ -12,8 +12,10 @@ import { miLocalStorage } from '@/local-storage.js';
import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@/config.js';
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
import { set as gtagSet } from 'vue-gtag';
import { instance } from '@/instance.js';
// TODO: 他のタブと永続化されたstateを同期
@ -59,6 +61,7 @@ export async function signout() {
}),
headers: {
'Content-Type': 'application/json',
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
},
});
}
@ -109,6 +112,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
}),
headers: {
'Content-Type': 'application/json',
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
},
})
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
@ -171,6 +175,12 @@ export function updateAccount(accountData: Partial<Account>) {
$i[key] = value;
}
miLocalStorage.setItem('account', JSON.stringify($i));
if (instance.googleAnalyticsId) {
gtagSet({
'client_id': miLocalStorage.getItem('id'),
'user_id': $i.id,
});
}
}
export async function refreshAccount() {
@ -222,24 +232,6 @@ export async function openAccountMenu(opts: {
}, ev: MouseEvent) {
if (!$i) return;
function showSigninDialog() {
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
success();
},
}, 'closed');
}
function createAccount() {
popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
switchAccountWithToken(res.i);
},
}, 'closed');
}
async function switchAccount(account: Misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const found = storedAccounts.find(x => x.id === account.id);

View File

@ -11,7 +11,7 @@ import components from '@/components/index.js';
import { version, lang, updateLocale, locale } from '@/config.js';
import { applyTheme } from '@/scripts/theme.js';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
import { updateI18n } from '@/i18n.js';
import { updateI18n, i18n } from '@/i18n.js';
import { $i, iAmAdmin, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js';
@ -24,6 +24,8 @@ import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/definition.js';
import { mainRouter } from '@/router/main.js';
import VueGtag, { bootstrap as gtagBootstrap, GtagConsent, GtagConsentParams } from 'vue-gtag';
export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`);
@ -61,6 +63,10 @@ export async function common(createVue: () => App<Element>) {
});
}
if (miLocalStorage.getItem('id') === null) {
miLocalStorage.setItem('id', crypto.randomUUID());
}
let isClientUpdated = false;
//#region クライアントが更新されたかチェック
@ -275,6 +281,38 @@ export async function common(createVue: () => App<Element>) {
directives(app);
components(app);
if (instance.googleAnalyticsId) {
app.use(VueGtag, {
bootstrap: false,
appName: `Misskey v${version}`,
config: {
id: instance.googleAnalyticsId,
params: {
anonymize_ip: false,
send_page_view: true,
},
},
}, mainRouter);
const gtagConsent = miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams ?? {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'granted',
};
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'default', gtagConsent);
if (miLocalStorage.getItem('gaConsent') === 'true') {
// noinspection ES6MissingAwait
gtagBootstrap();
}
}
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜか2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = ((): HTMLElement => {
@ -301,6 +339,27 @@ export async function common(createVue: () => App<Element>) {
removeSplash();
//#region Self-XSS 対策メッセージ
console.log(
`%c${i18n.ts._selfXssPrevention.warning}`,
'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
);
console.log(
`%c${i18n.ts._selfXssPrevention.title}`,
'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
);
console.log(
`%c${i18n.ts._selfXssPrevention.description1}`,
'font-size: 16px; font-weight: 700;',
);
console.log(
`%c${i18n.ts._selfXssPrevention.description2}`,
'font-size: 16px;',
'font-size: 20px; font-weight: 700; color: #f00;',
);
console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
//#endregion
return {
isClientUpdated,
app,

View File

@ -20,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/router/main.js';
import { instance } from '@/instance.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@ -228,19 +229,25 @@ export async function mainBoot() {
}
miLocalStorage.setItem('lastUsed', Date.now().toString());
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
if (!location.pathname.startsWith('/miauth') && !location.pathname.startsWith('/sso') && !location.pathname.startsWith('/oauth')) {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
}
}
// const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
// if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
// }
if (instance.googleAnalyticsId && miLocalStorage.getItem('gaConsent') === null) {
popup(defineAsyncComponent(() => import('@/components/MkTrackingConsent.vue')), {}, {}, 'closed');
}
}
// const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
// if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
// }
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {

View File

@ -400,7 +400,7 @@ defineExpose({
}
&:checked + .accountSelectorItem {
background: var(--accent);
background: color-mix(in srgb, var(--accent), transparent 50%);
color: #fff;
}
}

View File

@ -26,12 +26,12 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
withOkButton: boolean;
withCloseButton: boolean;
okButtonDisabled: boolean;
escKeyDisabled: boolean;
width: number;
height: number;
withOkButton?: boolean;
withCloseButton?: boolean;
okButtonDisabled?: boolean;
escKeyDisabled?: boolean;
width?: number;
height?: number;
}>(), {
withOkButton: false,
withCloseButton: true,

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="500"
:height="600"
@close="dialog?.close()"
@close="onClose"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.signup }}</template>
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="!isAcceptedServerRule">
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
<XServerRules @done="isAcceptedServerRule = true" @cancel="onClose"/>
</template>
<template v-else>
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(ev: 'done', res: Misskey.entities.SigninResponse): void;
(ev: 'done', res: Misskey.entities.SignupResponse): void;
(ev: 'cancelled'): void;
(ev: 'closed'): void;
}>();
@ -56,7 +56,12 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
function onSignup(res: Misskey.entities.SigninResponse) {
function onClose() {
emit('cancelled');
dialog.value?.close();
}
function onSignup(res: Misskey.entities.SignupResponse) {
emit('done', res);
dialog.value?.close();
}

View File

@ -0,0 +1,177 @@
<template>
<div class="_panel _shadow" :class="$style.root">
<div :class="$style.main">
<div style="display: flex; align-items: center;">
<div :class="$style.headerIcon">
<i class="ti ti-report-analytics"></i>
</div>
<div :class="$style.headerTitle"><Mfm :text="i18n.ts.helpUsImproveUserExperience" /></div>
</div>
<div :class="$style.text">
<Mfm
:text="i18n.tsx.pleaseConsentToTracking({
host: instance.name ?? host,
privacyPolicyUrl: instance.privacyPolicyUrl,
})"
/>
</div>
<div class="_gaps_s">
<div class="_buttons" style="justify-content: right;">
<MkButton @click="consentEssential">{{ i18n.ts.consentEssential }}</MkButton>
<MkButton primary @click="consentAll">{{ i18n.ts.consentAll }}</MkButton>
</div>
<MkFolder>
<template #icon><i class="ti ti-lock-square"></i></template>
<template #label>{{ i18n.ts.gtagConsentCustomize }}</template>
<div class="_gaps_s">
<MkInfo>{{ i18n.tsx.gtagConsentCustomizeDescription({ host: instance.name ?? host }) }}</MkInfo>
<MkSwitch v-model="gtagConsentAnalytics">
{{ i18n.ts.gtagConsentAnalytics }}
<template #caption>{{ i18n.ts.gtagConsentAnalyticsDescription }}</template>
</MkSwitch>
<MkSwitch v-model="gtagConsentFunctionality">
{{ i18n.ts.gtagConsentFunctionality }}
<template #caption>{{ i18n.ts.gtagConsentFunctionalityDescription }}</template>
</MkSwitch>
<MkSwitch v-model="gtagConsentPersonalization">
{{ i18n.ts.gtagConsentPersonalization }}
<template #caption>{{ i18n.ts.gtagConsentPersonalizationDescription }}</template>
</MkSwitch>
<div class="_buttons" style="justify-content: right;">
<MkButton @click="consentSelected">{{ i18n.ts.consentSelected }}</MkButton>
</div>
</div>
</MkFolder>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import { miLocalStorage } from '@/local-storage.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import * as os from '@/os.js';
import {
bootstrap as gtagBootstrap,
GtagConsent,
GtagConsentParams,
set as gtagSet
} from 'vue-gtag';
const emit = defineEmits<(ev: 'closed') => void>();
const zIndex = os.claimZIndex('middle');
const gtagConsentAnalytics = ref(false);
const gtagConsentFunctionality = ref(false);
const gtagConsentPersonalization = ref(false);
function consentAll() {
miLocalStorage.setItem('gaConsent', 'true');
const gtagConsent = <GtagConsentParams>{
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'granted',
functionality_storage: 'granted',
personalization_storage: 'granted',
security_storage: 'granted',
};
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent);
bootstrap();
emit('closed');
}
function consentEssential() {
miLocalStorage.setItem('gaConsent', 'true');
const gtagConsent = <GtagConsentParams>{
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'granted',
};
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent);
bootstrap();
emit('closed');
}
function consentSelected() {
miLocalStorage.setItem('gaConsent', 'true');
const gtagConsent = <GtagConsentParams>{
ad_storage: gtagConsentAnalytics.value ? 'granted' : 'denied',
ad_user_data: gtagConsentFunctionality.value ? 'granted' : 'denied',
ad_personalization: gtagConsentPersonalization.value ? 'granted' : 'denied',
analytics_storage: gtagConsentAnalytics.value ? 'granted' : 'denied',
functionality_storage: gtagConsentFunctionality.value ? 'granted' : 'denied',
personalization_storage: gtagConsentPersonalization.value ? 'granted' : 'denied',
security_storage: 'granted',
};
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent);
bootstrap();
emit('closed');
}
function bootstrap() {
gtagBootstrap();
gtagSet({
'client_id': miLocalStorage.getItem('id'),
'user_id': $i?.id,
});
}
</script>
<style lang="scss" module>
.root {
position: fixed;
z-index: v-bind(zIndex);
bottom: var(--margin);
left: 0;
right: 0;
margin: auto;
box-sizing: border-box;
width: calc(100% - (var(--margin) * 2));
max-width: 500px;
display: flex;
}
.main {
padding: 25px 25px 25px 25px;
width: inherit;
flex: 1;
}
.headerIcon {
margin-right: 8px;
font-size: 40px;
color: var(--accent);
}
.headerTitle {
font-weight: bold;
font-size: 16px;
}
.text {
margin: 0.7em 0 1em 0;
}
</style>

View File

@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
rel: 'nofollow noopener',
target: '_blank',
}"
@click="onAdClicked"
>
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
@ -42,7 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
/* eslint-disable id-denylist */
import { ref, computed, onActivated, onMounted } from 'vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { url as local, host } from '@/config.js';
@ -50,6 +52,7 @@ import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { usageReport } from '@/scripts/usage-report.js';
type Ad = (typeof instance)['ads'][number];
@ -123,6 +126,36 @@ function reduceFrequency(): void {
chosen.value = choseAd();
showMenu.value = false;
}
function onAdClicked(): void {
if (chosen.value == null) return;
usageReport({
t: Math.floor(Date.now() / 1000),
e: 'a',
i: chosen.value.id,
a: 'c',
});
}
onMounted(() => {
if (chosen.value == null) return;
usageReport({
t: Math.floor(Date.now() / 1000),
e: 'a',
i: chosen.value.id,
a: 'v',
});
});
onActivated(() => {
if (chosen.value == null) return;
usageReport({
t: Math.floor(Date.now() / 1000),
e: 'a',
i: chosen.value.id,
a: 'v',
});
});
</script>
<style lang="scss" module>

View File

@ -16,13 +16,13 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/ https://fonts.gstatic.com/ https://www.google-analytics.com/ https://www.googletagmanager.com/;
worker-src 'self';
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://www.googletagmanager.com https://esm.sh;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://www.googletagmanager.com;
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://fonts.gstatic.com https://www.googletagmanager.com;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com;
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com https://www.google-analytics.com https://analytics.google.com;
frame-src *;"
/>
<meta property="og:site_name" content="[DEV BUILD] Misskey" />

View File

@ -4,6 +4,7 @@
*/
type Keys =
'id' |
'v' |
'lastVersion' |
'instance' |
@ -39,7 +40,10 @@ type Keys =
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
`channelLastReadedAt:${string}` |
'kawaii'
'kawaii' |
'gaConsent' |
'gtagConsent'
;
export const miLocalStorage = {
getItem: (key: Keys): string | null => window.localStorage.getItem(key),

View File

@ -71,6 +71,8 @@ export type Resolved = {
};
};
export type AfterNavigationHook = (to: RouteDef, from: RouteDef) => any;
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
@ -101,6 +103,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
currentRef: ShallowRef<Resolved>;
currentRoute: ShallowRef<RouteDef>;
navHook: ((path: string, flag?: any) => boolean) | null;
afterHooks: Array<AfterNavigationHook | null>;
/**
* eventListenerの定義後に必ず呼び出すこと
@ -109,10 +112,14 @@ export interface IRouter extends EventEmitter<RouterEvent> {
resolve(path: string): Resolved | null;
isReady(): Promise<boolean>;
getCurrentPath(): any;
getCurrentKey(): string;
afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined;
push(path: string, flag?: any): void;
replace(path: string, key?: string | null): void;
@ -191,6 +198,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
private redirectCount = 0;
public navHook: ((path: string, flag?: any) => boolean) | null = null;
public afterHooks: Array<AfterNavigationHook | null> = [];
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
super();
@ -339,6 +347,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
const beforePath = this.currentPath;
const beforeRoute = this.currentRoute.value;
this.currentPath = path;
const res = this.resolve(this.currentPath);
@ -382,6 +391,12 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
});
}
if (this.afterHooks.length > 0) {
for (const hook of this.afterHooks) {
if (hook) hook(res.route, beforeRoute);
}
}
this.redirectCount = 0;
return {
...res,
@ -389,6 +404,10 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
};
}
public isReady() {
return Promise.resolve(true);
}
public getCurrentPath() {
return this.currentPath;
}
@ -397,6 +416,18 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return this.currentKey;
}
public afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined {
this.afterHooks.push(hook);
return () => {
const index = this.afterHooks.indexOf(hook);
if (index !== -1) {
return this.afterHooks.splice(index, 1);
} else {
return undefined;
}
};
}
public push(path: string, flag?: any) {
const beforePath = this.currentPath;
if (path === beforePath) {

View File

@ -21,6 +21,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</div>
</FormSection>
<FormSection>
<template #label>Google Analytics</template>
<div class="_gaps_m">
<MkInput v-model="googleAnalyticsId">
<template #prefix><i class="ti ti-report-analytics"></i></template>
<template #label>Google Analytics ID</template>
</MkInput>
</div>
</FormSection>
</FormSuspense>
</MkSpacer>
<template #footer>
@ -49,17 +59,20 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
const googleAnalyticsId = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey;
deeplIsPro.value = meta.deeplIsPro;
googleAnalyticsId.value = meta.googleAnalyticsId;
}
function save() {
os.apiWithDialog('admin/update-meta', {
deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value,
googleAnalyticsId: googleAnalyticsId.value,
}).then(() => {
fetchInstance(true);
});

View File

@ -36,7 +36,7 @@
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary gradate @click="read(announcement)">
<i :class="!announcement.needEnrollmentTutorialToRead ? 'ti ti-check' : 'ti ti-presentation'"/>
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialTutorial.launchTutorial }}
</MkButton>
</div>
</div>

View File

@ -0,0 +1,220 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkWindow
ref="windowEl"
:initialWidth="400"
:initialHeight="500"
:canResize="true"
@close="windowEl?.close()"
@closed="emit('closed')"
>
<template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
<template v-else #header>New decoration</template>
<div style="display: flex; flex-direction: column; min-height: 100%;">
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
<div class="_gaps_m">
<div :class="$style.preview">
<div :class="[$style.previewItem, $style.light]">
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
</div>
<div :class="[$style.previewItem, $style.dark]">
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
</div>
</div>
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkInput v-model="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkTextarea v-model="description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<MkFolder>
<template #label>{{ i18n.ts.availableRoles }}</template>
<template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
<div class="_gaps">
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
</div>
</div>
</MkFolder>
<MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</MkSpacer>
<div :class="$style.footer">
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
</div>
</div>
</MkWindow>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { signinRequired } from '@/account.js';
const $i = signinRequired();
const props = defineProps<{
avatarDecoration?: any,
}>();
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
watch(roleIdsThatCanBeUsedThisDecoration, async () => {
rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
async function addRole() {
const roles = await misskeyApi('admin/roles/list');
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
const { canceled, result: role } = await os.select({
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
});
if (canceled || role == null) return;
rolesThatCanBeUsedThisDecoration.value.push(role);
}
async function removeRole(role, ev) {
rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
}
async function done() {
const params = {
url: url.value,
name: name.value,
description: description.value,
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
};
if (props.avatarDecoration) {
await os.apiWithDialog('admin/avatar-decorations/update', {
id: props.avatarDecoration.id,
...params,
});
emit('done', {
updated: {
id: props.avatarDecoration.id,
...params,
},
});
windowEl.value?.close();
} else {
const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
emit('done', {
created: created,
});
windowEl.value?.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: name.value }),
});
if (canceled) return;
misskeyApi('admin/avatar-decorations/delete', {
id: props.avatarDecoration.id,
}).then(() => {
emit('done', {
deleted: true,
});
windowEl.value?.close();
});
}
</script>
<style lang="scss" module>
.preview {
display: grid;
place-items: center;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
gap: var(--margin);
}
.previewItem {
width: 100%;
height: 100%;
min-height: 160px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
&.light {
background: #eee;
}
&.dark {
background: #222;
}
}
.roleItem {
display: flex;
}
.role {
flex: 1;
}
.roleUnassign {
width: 32px;
height: 32px;
margin-left: 8px;
align-self: center;
}
.footer {
position: sticky;
z-index: 10000;
bottom: 0;
left: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
background: var(--acrylicBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View File

@ -5,78 +5,39 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div class="_gaps">
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
<template #label>{{ avatarDecoration.name }}</template>
<template #caption>{{ avatarDecoration.description }}</template>
<div class="_gaps_m">
<MkInput v-model="avatarDecoration.name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkTextarea v-model="avatarDecoration.description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<MkInput v-model="avatarDecoration.url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<div class="buttons _buttons">
<MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
<div :class="$style.decorations">
<div
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
v-panel
:class="$style.decoration"
@click="edit(avatarDecoration)"
>
<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
</div>
</MkFolder>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { signinRequired } from '@/account.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
const $i = signinRequired();
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
function add() {
avatarDecorations.value.unshift({
_id: Math.random().toString(36),
id: null,
name: '',
description: '',
url: '',
});
}
function del(avatarDecoration) {
os.confirm({
type: 'warning',
text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
}).then(({ canceled }) => {
if (canceled) return;
avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
});
}
async function save(avatarDecoration) {
if (avatarDecoration.id == null) {
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
load();
} else {
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
}
}
function load() {
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;
@ -85,6 +46,37 @@ function load() {
load();
async function add(ev: MouseEvent) {
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
}, {
done: result => {
if (result.created) {
avatarDecorations.value.unshift(result.created);
}
},
closed: () => dispose(),
});
}
function edit(avatarDecoration) {
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
avatarDecoration: avatarDecoration,
}, {
done: result => {
if (result.updated) {
const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
avatarDecorations.value[index] = {
...avatarDecorations.value[index],
...result.updated,
};
} else if (result.deleted) {
avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
}
},
closed: () => dispose(),
});
}
const headerActions = computed(() => [{
asFullButton: true,
icon: 'ti ti-plus',
@ -99,3 +91,28 @@ definePageMetadata(() => ({
icon: 'ti ti-sparkles',
}));
</script>
<style lang="scss" module>
.decorations {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
grid-gap: 12px;
}
.decoration {
cursor: pointer;
padding: 16px 16px 28px 16px;
border-radius: 8px;
text-align: center;
font-size: 90%;
overflow: clip;
contain: content;
}
.decorationName {
position: relative;
z-index: 10;
font-weight: bold;
margin-bottom: 20px;
}
</style>

View File

@ -5,136 +5,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkSpacer :contentMax="1200">
<MkTab v-model="origin" style="margin-bottom: var(--margin);">
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkTab>
<div v-if="origin === 'local'">
<template v-if="tag == null">
<MkFoldableSection class="_margin" persistKey="explore-pinned-users">
<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
<MkUserList :pagination="pinnedUsers"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-popular-users">
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
<MkUserList :pagination="popularUsers"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-recently-updated-users">
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
<MkUserList :pagination="recentlyUpdatedUsers"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-recently-registered-users">
<template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
<MkUserList :pagination="recentlyRegisteredUsers"/>
</MkFoldableSection>
</template>
</div>
<div v-else>
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
<div>
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA>
</div>
</MkFoldableSection>
<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
<MkUserList :pagination="tagUsers"/>
</MkFoldableSection>
<template v-if="tag == null">
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
<MkUserList :pagination="popularUsersF"/>
</MkFoldableSection>
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
<MkUserList :pagination="recentlyUpdatedUsersF"/>
</MkFoldableSection>
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
<MkUserList :pagination="recentlyRegisteredUsersF"/>
</MkFoldableSection>
</template>
</div>
<MkFoldableSection class="_margin" persistKey="explore-pinned-users">
<template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
<MkUserList :pagination="pinnedUsers"/>
</MkFoldableSection>
<MkFoldableSection class="_margin" persistKey="explore-popular-users">
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
<MkUserList :pagination="popularUsers"/>
</MkFoldableSection>
</MkSpacer>
</template>
<script lang="ts" setup>
import { watch, ref, shallowRef, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkUserList from '@/components/MkUserList.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkTab from '@/components/MkTab.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
tag?: string;
}>();
const origin = ref('local');
const tagsEl = shallowRef<InstanceType<typeof MkFoldableSection>>();
const tagsLocal = ref<Misskey.entities.Hashtag[]>([]);
const tagsRemote = ref<Misskey.entities.Hashtag[]>([]);
watch(() => props.tag, () => {
if (tagsEl.value) tagsEl.value.toggleContent(props.tag == null);
});
const tagUsers = computed(() => ({
endpoint: 'hashtags/users' as const,
limit: 30,
params: {
tag: props.tag,
origin: 'combined',
sort: '+follower',
},
}));
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
const popularUsers = { endpoint: 'users', limit: 30, noPaging: true, params: {
state: 'alive',
origin: 'local',
sort: '+pv',
} };
const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'local',
sort: '+updatedAt',
} };
const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'local',
state: 'alive',
sort: '+createdAt',
} };
const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive',
origin: 'remote',
sort: '+follower',
} };
const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'combined',
sort: '+updatedAt',
} };
const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: {
origin: 'combined',
sort: '+createdAt',
} };
misskeyApi('hashtags/list', {
sort: '+attachedLocalUsers',
attachedToLocalUserOnly: true,
limit: 30,
}).then(tags => {
tagsLocal.value = tags;
});
misskeyApi('hashtags/list', {
sort: '+attachedRemoteUsers',
attachedToRemoteUserOnly: true,
limit: 30,
}).then(tags => {
tagsRemote.value = tags;
});
</script>

View File

@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
<i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { computed } from 'vue';
import { signinRequired } from '@/account.js';
const $i = signinRequired();
@ -37,6 +37,8 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'click'): void;
}>();
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
</script>
<style lang="scss" module>
@ -67,5 +69,6 @@ const emit = defineEmits<{
position: absolute;
bottom: 12px;
right: 12px;
color: var(--warn);
}
</style>

View File

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.footer" class="_buttonsCenter">
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
<MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
</div>
</div>
</MkModalWindow>
@ -61,6 +61,7 @@ const props = defineProps<{
id: string;
url: string;
name: string;
roleIdsThatCanBeUsedThisDecoration: string[];
};
}>();
@ -83,6 +84,7 @@ const emit = defineEmits<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);

View File

@ -34,12 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkButton v-if="periodChanged" primary class="save" @click="saveRemovalCondition"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkSwitch v-model="noPiningNotes" @update:modelValue="saveRemovalCondition()">
<MkSwitch v-model="noPiningNotes" @update:modelValue="saveRemovalCondition">
<template #label>{{ i18n.ts._autoRemoval.noPiningNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noPiningNotesDescription }}</template>
</MkSwitch>
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition()">
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition">
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
</MkSwitch>
@ -131,6 +131,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</div>
</FormSection>
<FormSection v-if="iAmModerator">
<template #label><i class="ti ti-aperture"></i> EZPZ User Normalization Menu</template>
<template #description>{{ i18n.ts.normalizeDescription }}</template>
<div class="_gaps_m">
<MkInfo warn rounded>
{{ i18n.ts.thisIsExperimentalFeature }}
</MkInfo>
<MkSwitch v-model="mapleDirectorMode">
{{ i18n.ts.useNormalization }}
</MkSwitch>
</div>
</FormSection>
</div>
</template>
@ -141,6 +156,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { iAmModerator, signinRequired } from '@/account.js';
@ -148,11 +165,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/MkInput.vue";
const $i = signinRequired();
const isVacation = ref<boolean | undefined>($i.isVacation !== null ? $i.isVacation : undefined);
const isVacation = ref<boolean>($i.isVacation ?? false);
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
@ -166,6 +181,7 @@ const hideDirectMessages = computed(defaultStore.makeGetterSetter('hideDirectMes
const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileList'));
const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog'));
const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList'));
const mapleDirectorMode = computed(defaultStore.makeGetterSetter('mapleDirectorMode'));
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
@ -204,6 +220,7 @@ watch([
hideDriveFileList,
hideRoleList,
hideModerationLog,
mapleDirectorMode,
], async () => {
await reloadAsk();
});

View File

@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserCardMini :user="item.mutee"/>
</MkA>
<button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
<button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button>
<button class="_button" :class="$style.remove" @click="editMute(item, $event)"><i class="ti ti-dots"></i></button>
</div>
<div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
@ -216,8 +216,20 @@ async function unrenoteMute(user, ev) {
}], ev.currentTarget ?? ev.target);
}
async function unmute(user, ev) {
os.popupMenu([{
async function editMute(muting, ev) {
os.popupMenu([...(muting.withNotification === false ? [{
icon: 'ti ti-bell',
text: i18n.ts.muteNotification,
action: async () => {
await os.apiWithDialog('mute/edit', { userId: muting.mutee.id, withNotification: true });
},
}] : [{
icon: 'ti ti-bell-off',
text: i18n.ts.unmuteNotification,
action: async () => {
await os.apiWithDialog('mute/edit', { userId: muting.mutee.id, withNotification: false });
},
}]), {
text: i18n.ts.unmute,
icon: 'ti ti-x',
action: async () => {

View File

@ -1,4 +1,4 @@
<!--
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<div class="_gaps_m">
<MkSwitch v-model="rememberNoteVisibility">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch>
<MkFolder v-if="!rememberNoteVisibility">
<template #label>{{ i18n.ts.defaultNoteVisibility }}</template>
<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
@ -67,20 +67,49 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="keepCw">{{ i18n.ts.keepCw }}</MkSwitch>
</div>
</FormSection>
<FormSection v-if="instance.googleAnalyticsId">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-lock-square"></i></template>
<template #label>{{ i18n.ts.gtagConsentCustomize }}</template>
<div class="_gaps_s">
<MkInfo>{{ i18n.tsx.gtagConsentCustomizeDescription({ host: instance.name ?? host }) }}</MkInfo>
<MkSwitch v-model="gtagConsentAnalytics">
{{ i18n.ts.gtagConsentAnalytics }}
<template #caption>{{ i18n.ts.gtagConsentAnalyticsDescription }}</template>
</MkSwitch>
<MkSwitch v-model="gtagConsentFunctionality">
{{ i18n.ts.gtagConsentFunctionality }}
<template #caption>{{ i18n.ts.gtagConsentFunctionalityDescription }}</template>
</MkSwitch>
<MkSwitch v-model="gtagConsentPersonalization">
{{ i18n.ts.gtagConsentPersonalization }}
<template #caption>{{ i18n.ts.gtagConsentPersonalizationDescription }}</template>
</MkSwitch>
</div>
</MkFolder>
</FormSection>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { miLocalStorage } from '@/local-storage.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { GtagConsent, GtagConsentParams } from 'vue-gtag';
const $i = signinRequired();
@ -99,6 +128,81 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
const gaConsentInternal = ref(miLocalStorage.getItem('gaConsent') === 'true');
const gaConsent = computed({
get: () => gaConsentInternal.value,
set: (value: boolean) => {
miLocalStorage.setItem('gaConsent', value ? 'true' : 'false');
gaConsentInternal.value = value;
},
});
const gtagConsentInternal = ref(
(miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams) ?? {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'granted',
},
);
const gtagConsent = computed({
get: () => gtagConsentInternal.value,
set: (value: GtagConsentParams) => {
miLocalStorage.setItemAsJson('gtagConsent', value);
gtagConsentInternal.value = value;
},
});
const gtagConsentAnalytics = computed({
get: () => gtagConsent.value.analytics_storage === 'granted',
set: (value: boolean) => {
gtagConsent.value = {
...gtagConsent.value,
ad_storage: value ? 'granted' : 'denied',
ad_user_data: value ? 'granted' : 'denied',
analytics_storage: value ? 'granted' : 'denied',
};
},
});
const gtagConsentFunctionality = computed({
get: () => gtagConsent.value.functionality_storage === 'granted',
set: (value: boolean) => {
gtagConsent.value = {
...gtagConsent.value,
functionality_storage: value ? 'granted' : 'denied',
};
},
});
const gtagConsentPersonalization = computed({
get: () => gtagConsent.value.personalization_storage === 'granted',
set: (value: boolean) => {
gtagConsent.value = {
...gtagConsent.value,
ad_personalization: value ? 'granted' : 'denied',
personalization_storage: value ? 'granted' : 'denied',
};
},
});
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
watch(gaConsent, async () => {
await reloadAsk();
});
watch(gtagConsent, async () => {
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent.value);
});
function save() {
misskeyApi('i/update', {
isLocked: !!isLocked.value,

View File

@ -1,100 +1,88 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :contentMax="800">
<div v-if="$i && !loading">
<div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
<div :class="$style.buttons">
<MkButton @click="onCancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton primary @click="onAccept">{{ i18n.ts.accept }}</MkButton>
<div>
<MkAnimBg style="position: fixed; top: 0;"/>
<div :class="$style.formContainer">
<div :class="$style.form">
<MkAuthConfirm
ref="authRoot"
:name="name"
@accept="onAccept"
@deny="onDeny"
/>
</div>
</div>
<div v-else-if="$i && loading">
<div>{{ i18n.ts._auth.callback }}</div>
<MkLoading class="loading"/>
<div style="display: none">
<form ref="postBindingForm" method="post" :action="actionUrl" autocomplete="off">
<input v-for="(value, key) in actionContext" :key="key" :name="key" :value="value" type="hidden"/>
</form>
</div>
</div>
<div v-else>
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
<MkSignin @login="onLogin"/>
</div>
</MkSpacer>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue';
import { $i, login } from '@/account.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
import { nextTick, onMounted, useTemplateRef } from "vue";
import { $i } from "@/account.js";
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:transaction-id"]');
if (transactionIdMeta) {
transactionIdMeta.remove();
}
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
const prompt = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:prompt"]')?.content;
const loading = ref(false);
const postBindingForm = ref<HTMLFormElement | null>(null);
const actionUrl = ref<string | undefined>(undefined);
const actionContext = ref<Record<string, string> | null>(null);
const authRoot = useTemplateRef('authRoot');
function onLogin(res): void {
login(res.i);
}
function onCancel(): void {
if (history.length > 1) history.back();
else location.href = '/';
}
function onAccept(): void {
loading.value = true;
os.promiseDialog(authorize());
}
async function authorize(): Promise<void> {
const res = await fetch(`/sso/${kind}/authorize`, {
async function onAccept(token: string) {
const result = await fetch(`/sso/${kind}/authorize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
transaction_id: transactionIdMeta?.content,
login_token: $i!.token,
login_token: token,
}),
}).then(res => res.json()).catch(() => {
authRoot.value?.showUI('failed');
});
const json = await res.json();
if (json.binding === 'post') {
actionUrl.value = json.action;
actionContext.value = json.context;
if (!result) return;
authRoot.value?.showUI('success');
if (result.binding === 'post') {
const form = document.createElement('form');
form.style.display = 'none';
form.action = result.action;
form.method = 'post';
form.acceptCharset = 'utf-8';
for (const [name, value] of Object.entries(result.context)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value as string;
form.appendChild(input);
}
nextTick(() => {
postBindingForm.value?.submit();
document.body.appendChild(form);
form.submit();
});
} else {
location.href = json.action;
location.href = result.action;
}
}
function onDeny(token: string) {
authRoot.value?.showUI('denied');
}
onMounted(() => {
if ($i && prompt === 'none') {
onAccept();
}
nextTick(() => {
if ($i && prompt === 'none') {
onAccept($i.token);
}
});
});
definePageMetadata(() => ({
@ -104,15 +92,24 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
.buttons {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
.formContainer {
min-height: 100svh;
padding: 32px 32px calc(env(safe-area-inset-bottom, 0px) + 32px) 32px;
box-sizing: border-box;
display: grid;
place-content: center;
}
.loginMessage {
text-align: center;
margin: 8px 0 24px;
.form {
position: relative;
z-index: 10;
border-radius: var(--radius);
background-color: var(--panel);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: clip;
max-width: 500px;
width: calc(100vw - 64px);
height: min(65svh, calc(100svh - calc(env(safe-area-inset-bottom, 0px) + 64px)));
overflow-y: scroll;
}
</style>

View File

@ -5,7 +5,7 @@
import { ShallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
import { AfterNavigationHook, IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
function getMainRouter(): IRouter {
const router = mainRouterHolder;
@ -40,6 +40,10 @@ class MainRouterProxy implements IRouter {
this.supplier = supplier;
}
get options(): { [key: string]: any } {
return {};
}
get current(): Resolved {
return this.supplier().current;
}
@ -60,6 +64,10 @@ class MainRouterProxy implements IRouter {
this.supplier().navHook = value;
}
isReady(): Promise<boolean> {
return this.supplier().isReady();
}
getCurrentKey(): string {
return this.supplier().getCurrentKey();
}
@ -68,6 +76,10 @@ class MainRouterProxy implements IRouter {
return this.supplier().getCurrentPath();
}
afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined {
return this.supplier().afterEach(hook);
}
push(path: string, flag?: any): void {
this.supplier().push(path, flag);
}

View File

@ -59,7 +59,7 @@ export function createAiScriptEnv(opts) {
}
const actualToken: string|null = token?.value ?? opts.token ?? null;
if (!rateLimiter.hit(ep.value)) return values.ERROR('rate_limited', values.NULL);
return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
return misskeyApi(ep.value, utils.valToJs(param), actualToken, undefined, 'aiscript').then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));

View File

@ -116,7 +116,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
}
async function userInfoUpdate() {
os.apiWithDialog('federation/update-remote-user', {
await os.apiWithDialog('federation/update-remote-user', {
userId: user.id,
});
}
async function immediateUserNormalization() {
if (!await getConfirmed(i18n.ts.normalizeConfirm)) return;
await os.apiWithDialog('admin/normalization', {
userId: user.id,
});
}
@ -344,6 +352,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
}]);
}
if ($i && iAmModerator && defaultStore.state.mapleDirectorMode) {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ti ti-aperture',
text: i18n.ts.normalize,
action: immediateUserNormalization,
}]);
}
if (user.host !== null) {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ti ti-refresh',

View File

@ -7,8 +7,39 @@ import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
export const pendingApiRequestsCount = ref(0);
let id: string | null = miLocalStorage.getItem('id');
export function generateClientTransactionId(initiator: string) {
if (id === null) {
id = crypto.randomUUID();
miLocalStorage.setItem('id', id);
}
return `${id}-${initiator}-${crypto.randomUUID()}`;
}
function handleResponse<_ResT>(
resolve: (value: (_ResT | PromiseLike<_ResT>)) => void,
reject: (reason?: any) => void,
): ((value: Response) => (void | PromiseLike<void>)) {
return async (res) => {
if (res.ok && res.status !== 204) {
const body = await res.json();
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
} else {
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
const body = await res
.json()
.catch(() => ({ statusCode: res.status, message: res.statusText }));
reject(typeof body.error === 'object' ? body.error : body);
}
};
}
// Implements Misskey.api.ApiClient.request
export function misskeyApi<
ResT = void,
@ -20,6 +51,7 @@ export function misskeyApi<
data: P = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
initiator: string = 'misskey',
): Promise<_ResT> {
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
@ -41,20 +73,10 @@ export function misskeyApi<
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
},
signal,
}).then(async (res) => {
if (res.ok && res.status !== 204) {
const body = await res.json();
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
} else {
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
const body = await res.json().catch(() => ({ statusCode: res.status, message: res.statusText }));
reject(typeof body.error === 'object' ? body.error : body);
}
}).catch(reject);
}).then(handleResponse(resolve, reject)).catch(reject);
});
promise.then(onFinally, onFinally);
@ -71,6 +93,7 @@ export function misskeyApiGet<
>(
endpoint: E,
data: P = {} as any,
initiator: string = 'misskey',
): Promise<_ResT> {
pendingApiRequestsCount.value++;
@ -86,17 +109,10 @@ export function misskeyApiGet<
method: 'GET',
credentials: 'omit',
cache: 'default',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve(undefined as _ResT); // void -> undefined
} else {
reject(body.error);
}
}).catch(reject);
headers: {
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
},
}).then(handleResponse(resolve, reject)).catch(reject);
});
promise.then(onFinally, onFinally);

View File

@ -0,0 +1,61 @@
/* eslint-disable id-denylist */
// buffering usage report data for 1 minute, then sending it to the server
// POST /api/usage [ { t: number, e: string, i: string, a: string } ]
// t: timestamp
// e: event type
// i: event initiator
// a: action
import { generateClientTransactionId } from '@/scripts/misskey-api.js';
import { miLocalStorage } from '@/local-storage.js';
import { GtagConsentParams } from 'vue-gtag';
import { instance } from '@/instance.js';
export interface UsageReport {
t: number;
e: string;
i: string;
a: string;
}
let disableUsageReport = !instance.googleAnalyticsId;
const usageReportBuffer: UsageReport[] = [];
let usageReportBufferTimer: number | null = null;
export function usageReport(data: UsageReport) {
if (disableUsageReport) return;
if (usageReportBuffer.length > 0) {
const last = usageReportBuffer[usageReportBuffer.length - 1];
if (last.t === data.t && last.e === data.e && last.a === data.a) return;
}
usageReportBuffer.push(data);
if (usageReportBufferTimer === null) {
usageReportBufferTimer = window.setTimeout(() => {
sendUsageReport();
}, 60 * 1000);
}
}
export function sendUsageReport() {
if (usageReportBuffer.length === 0) return;
const data = usageReportBuffer.splice(0, usageReportBuffer.length);
usageReportBufferTimer = null;
if ((miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams)?.ad_user_data !== 'granted') {
console.log('Usage report is not sent because the user has not consented to sharing data about ad interactions.');
disableUsageReport = true;
return;
}
window.fetch('/api/usage', {
method: 'POST',
body: JSON.stringify(data),
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
},
});
}

View File

@ -216,7 +216,7 @@ export const defaultStore = markRaw(new Storage('base', {
filter: {
withReplies: true,
withRenotes: true,
withSensitive: true,
withSensitive: false,
onlyFiles: false,
},
},
@ -252,7 +252,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
animatedMfm: {
where: 'device',
default: true,
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
where: 'device',
@ -288,7 +288,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native
default: 'fluentEmoji', // twemoji / fluentEmoji / native
},
disableDrawer: {
where: 'device',
@ -436,7 +436,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableCondensedLineForAcct: {
where: 'device',
default: false,
default: true,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
@ -448,7 +448,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
hideMutedNotes: {
where: 'device',
default: false,
default: true,
},
defaultWithReplies: {
where: 'account',
@ -473,7 +473,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableSeasonalScreenEffect: {
where: 'device',
default: false,
default: true,
},
dropAndFusion: {
where: 'device',
@ -488,7 +488,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableHorizontalSwipe: {
where: 'device',
default: true,
default: false,
},
trustedExternalWebsites: {
where: 'device',
@ -504,7 +504,11 @@ export const defaultStore = markRaw(new Storage('base', {
},
alwaysConfirmFollow: {
where: 'device',
default: true,
default: false,
},
mapleDirectorMode: {
where: 'deviceAccount',
default: false,
},
sound_masterVolume: {

View File

@ -1,6 +1,6 @@
/* eslint-disable */
declare module "*.vue" {
import { defineComponent } from "vue";
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}

View File

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

View File

@ -5,6 +5,7 @@
```ts
import { EventEmitter } from 'eventemitter3';
import _ReconnectingWebsocket from 'reconnecting-websocket';
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
//
@ -118,6 +119,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat
// @public (undocumented)
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
@ -253,6 +257,9 @@ type AdminInviteListResponse = operations['admin___invite___list']['responses'][
// @public (undocumented)
type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminNormalizationRequest = operations['admin___normalization']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
@ -1244,6 +1251,7 @@ declare namespace entities {
AdminAbuseReportResolverDeleteRequest,
AdminAbuseReportResolverUpdateRequest,
AdminAvatarDecorationsCreateRequest,
AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
@ -1291,6 +1299,7 @@ declare namespace entities {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
AdminNormalizationRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
@ -1614,6 +1623,7 @@ declare namespace entities {
MiauthGenTokenResponse,
MuteCreateRequest,
MuteDeleteRequest,
MuteEditRequest,
MuteListRequest,
MuteListResponse,
RenoteMuteCreateRequest,
@ -2556,6 +2566,9 @@ type MuteDeleteRequest = operations['mute___delete']['requestBody']['content']['
// @public (undocumented)
export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
// @public (undocumented)
type MuteEditRequest = operations['mute___edit']['requestBody']['content']['application/json'];
// @public (undocumented)
type MuteListRequest = operations['mute___list']['requestBody']['content']['application/json'];
@ -3024,7 +3037,7 @@ export class Stream extends EventEmitter<StreamEvents> {
constructor(origin: string, user: {
token: string;
} | null, options?: {
WebSocket?: any;
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
});
// (undocumented)
close(): void;

View File

@ -9,14 +9,14 @@
"devDependencies": {
"@misskey-dev/eslint-plugin": "^1.0.0",
"@readme/openapi-parser": "2.6.0",
"@types/node": "22.7.8",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.1",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.6",
"ts-case-convert": "2.1.0",
"tsx": "4.19.1",
"tsx": "4.19.2",
"typescript": "5.6.3"
},
"files": [

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.5.0-oscar.17a",
"version": "2024.5.0-oscar.18",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
@ -35,22 +35,22 @@
"url": "git+https://github.com/misskey-dev/misskey.js.git"
},
"devDependencies": {
"@microsoft/api-extractor": "7.47.5",
"@microsoft/api-extractor": "7.47.11",
"@misskey-dev/eslint-plugin": "1.0.0",
"@swc/jest": "0.2.36",
"@types/jest": "29.5.12",
"@types/node": "22.2.0",
"@swc/jest": "0.2.37",
"@types/jest": "29.5.14",
"@types/node": "22.9.0",
"@typescript-eslint/eslint-plugin": "7.10.0",
"@typescript-eslint/parser": "7.10.0",
"eslint": "8.57.0",
"eslint": "8.57.1",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
"mock-socket": "9.3.1",
"ncp": "2.0.0",
"nodemon": "3.1.4",
"tsd": "0.31.1",
"typescript": "5.5.4"
"nodemon": "3.1.7",
"tsd": "0.31.2",
"typescript": "5.6.3"
},
"files": [
"built",
@ -58,8 +58,8 @@
"built/dts"
],
"dependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.5.7",
"@swc/cli": "0.5.0",
"@swc/core": "1.8.0",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}

View File

@ -644,6 +644,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
request<E extends 'admin/normalization', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@ -3056,6 +3067,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
request<E extends 'mute/edit', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*

View File

@ -31,6 +31,7 @@ import type {
AdminAbuseReportResolverDeleteRequest,
AdminAbuseReportResolverUpdateRequest,
AdminAvatarDecorationsCreateRequest,
AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
@ -78,6 +79,7 @@ import type {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
AdminNormalizationRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
@ -401,6 +403,7 @@ import type {
MiauthGenTokenResponse,
MuteCreateRequest,
MuteDeleteRequest,
MuteEditRequest,
MuteListRequest,
MuteListResponse,
RenoteMuteCreateRequest,
@ -610,7 +613,7 @@ export type Endpoints = {
'admin/abuse-report-resolver/list': { req: AdminAbuseReportResolverListRequest; res: AdminAbuseReportResolverListResponse };
'admin/abuse-report-resolver/delete': { req: AdminAbuseReportResolverDeleteRequest; res: EmptyResponse };
'admin/abuse-report-resolver/update': { req: AdminAbuseReportResolverUpdateRequest; res: EmptyResponse };
'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse };
'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
@ -649,6 +652,7 @@ export type Endpoints = {
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };
'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse };
'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse };
'admin/normalization': { req: AdminNormalizationRequest; res: EmptyResponse };
'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse };
'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse };
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
@ -865,6 +869,7 @@ export type Endpoints = {
'miauth/gen-token': { req: MiauthGenTokenRequest; res: MiauthGenTokenResponse };
'mute/create': { req: MuteCreateRequest; res: EmptyResponse };
'mute/delete': { req: MuteDeleteRequest; res: EmptyResponse };
'mute/edit': { req: MuteEditRequest; res: EmptyResponse };
'mute/list': { req: MuteListRequest; res: MuteListResponse };
'renote-mute/create': { req: RenoteMuteCreateRequest; res: EmptyResponse };
'renote-mute/delete': { req: RenoteMuteDeleteRequest; res: EmptyResponse };

View File

@ -34,6 +34,7 @@ export type AdminAbuseReportResolverListResponse = operations['admin___abuse-rep
export type AdminAbuseReportResolverDeleteRequest = operations['admin___abuse-report-resolver___delete']['requestBody']['content']['application/json'];
export type AdminAbuseReportResolverUpdateRequest = operations['admin___abuse-report-resolver___update']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
@ -81,6 +82,7 @@ export type AdminInviteCreateRequest = operations['admin___invite___create']['re
export type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json'];
export type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json'];
export type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json'];
export type AdminNormalizationRequest = operations['admin___normalization']['requestBody']['content']['application/json'];
export type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
@ -404,6 +406,7 @@ export type MiauthGenTokenRequest = operations['miauth___gen-token']['requestBod
export type MiauthGenTokenResponse = operations['miauth___gen-token']['responses']['200']['content']['application/json'];
export type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
export type MuteDeleteRequest = operations['mute___delete']['requestBody']['content']['application/json'];
export type MuteEditRequest = operations['mute___edit']['requestBody']['content']['application/json'];
export type MuteListRequest = operations['mute___list']['requestBody']['content']['application/json'];
export type MuteListResponse = operations['mute___list']['responses']['200']['content']['application/json'];
export type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];

View File

@ -537,6 +537,15 @@ export type paths = {
*/
post: operations['admin___invite___list'];
};
'/admin/normalization': {
/**
* admin/normalization
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
post: operations['admin___normalization'];
};
'/admin/promo/create': {
/**
* admin/promo/create
@ -2643,6 +2652,15 @@ export type paths = {
*/
post: operations['mute___delete'];
};
'/mute/edit': {
/**
* mute/edit
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
post: operations['mute___edit'];
};
'/mute/list': {
/**
* mute/list
@ -4587,6 +4605,7 @@ export type components = {
/** Format: id */
muteeId: string;
mutee: components['schemas']['UserDetailedNotMe'];
withNotification: boolean;
};
RenoteMuting: {
/**
@ -5152,6 +5171,7 @@ export type components = {
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
googleAnalyticsId: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
@ -5299,6 +5319,7 @@ export type operations = {
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
googleAnalyticsId: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string | null;
@ -6629,9 +6650,22 @@ export type operations = {
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
/** @description OK (with results) */
200: {
content: {
'application/json': {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string | null;
name: string;
description: string;
url: string;
roleIdsThatCanBeUsedThisDecoration: string[];
};
};
};
/** @description Client error */
400: {
@ -8828,6 +8862,58 @@ export type operations = {
};
};
};
/**
* admin/normalization
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
*/
admin___normalization: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/promo/create
* @description No description provided.
@ -10203,6 +10289,7 @@ export type operations = {
enableTurnstile?: boolean;
turnstileSiteKey?: string | null;
turnstileSecretKey?: string | null;
googleAnalyticsId?: string | null;
/** @enum {string} */
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
/** @enum {string} */
@ -22837,6 +22924,8 @@ export type operations = {
userId: string;
/** @description A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute. */
expiresAt?: number | null;
/** @default true */
withNotification?: boolean;
};
};
};
@ -22935,6 +23024,59 @@ export type operations = {
};
};
};
/**
* mute/edit
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:mutes*
*/
mute___edit: {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
withNotification: boolean;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* mute/list
* @description No description provided.

View File

@ -34,7 +34,7 @@ export default class Stream extends EventEmitter<StreamEvents> {
private idCounter = 0;
constructor(origin: string, user: { token: string; } | null, options?: {
WebSocket?: any;
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
}) {
super();

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../packages/shared/.eslintrc.js',
],
};

View File

@ -1,3 +0,0 @@
node_modules
coverage
.idea

View File

@ -1,24 +0,0 @@
{
"name": "changelog-checker",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"run": "vite-node src/index.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/mdast": "4.0.3",
"@types/node": "20.10.7",
"@vitest/coverage-v8": "1.1.3",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.6.3",
"unified": "11.0.4",
"vite": "5.0.12",
"vite-node": "1.1.3",
"vitest": "1.1.3"
}
}

Some files were not shown because too many files have changed in this diff Show More