Merge branch 'misskey-dev:develop' into dev

This commit is contained in:
MomentQYC 2024-10-13 08:48:29 +08:00 committed by GitHub
commit 7b88e742b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
369 changed files with 2366 additions and 1684 deletions

View File

@ -75,7 +75,7 @@ jobs:
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Restore eslint cache
uses: actions/cache@v4.0.2
uses: actions/cache@v4.1.0
with:
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}

View File

@ -1,7 +1,25 @@
## 2024.10.1
### Note
- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、
7日間活動していない場合は自動的に招待制へと移行コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。
詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
### Client
- Enhance: l10nの更新
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
### Server
- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
### Server
- Fix: キューのエラーログを簡略化するように
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
## 2024.10.0
### Note
- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
- ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
- なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能ですUI上で初期パスワードの入力欄を空欄にすると続行できます
- ユーザーデータを読み込む際の型が一部変更されました。

View File

@ -578,18 +578,18 @@ ESMではディレクトリインポートは廃止されているのと、デ
### Lighten CSS vars
``` css
color: hsl(from var(--accent) h s calc(l + 10));
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
```
### Darken CSS vars
``` css
color: hsl(from var(--accent) h s calc(l - 10));
color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
```
### Add alpha to CSS vars
``` css
color: color(from var(--accent) srgb r g b / 0.5);
color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
```

View File

@ -34,7 +34,7 @@ defineProps<{
width: 100%;
height: 100%;
cursor: not-allowed;
--color: color(from var(--error) srgb r g b / 0.25);
--color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
}

View File

@ -1252,7 +1252,6 @@ _theme:
buttonBg: "خلفية الأزرار"
buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)"
inputBorder: "حواف حقل الإدخال"
listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)"
driveFolderBg: "خلفية مجلد قرص التخزين"
messageBg: "خلفية المحادثة"
_sfx:

View File

@ -1017,7 +1017,6 @@ _theme:
buttonBg: "বাটনের পটভূমি"
buttonHoverBg: "বাটনের পটভূমি (হভার)"
inputBorder: "ইনপুট ফিল্ডের বর্ডার"
listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)"
driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
wallpaperOverlay: "ওয়ালপেপার ওভারলে"
badge: "ব্যাজ"

View File

@ -453,6 +453,7 @@ totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'
moderator: "Moderador/a"
moderation: "Moderació"
moderationNote: "Nota de moderació "
moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
addModerationNote: "Afegir una nota de moderació "
moderationLogs: "Registre de moderació "
nUsersMentioned: "{n} usuaris mencionats"
@ -1284,6 +1285,14 @@ unknownWebAuthnKey: "Passkey desconeguda"
passkeyVerificationFailed: "La verificació a fallat"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya."
messageToFollower: "Missatge als meus seguidors"
target: "Assumpte "
_abuseUserReport:
forward: "Reenviar "
forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima."
resolve: "Solució "
accept: "Acceptar "
reject: "Rebutjar"
resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament."
_delivery:
status: "Estat d'entrega "
stop: "Suspés"
@ -1974,7 +1983,6 @@ _theme:
buttonBg: "Fons botó "
buttonHoverBg: "Fons botó (en passar-hi per sobre)"
inputBorder: "Contorn del cap d'introducció "
listItemHoverBg: "Fons dels elements d'una llista"
driveFolderBg: "Fons de la carpeta Disc"
wallpaperOverlay: "Superposició del fons de pantalla "
badge: "Insígnia "
@ -2520,6 +2528,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "Fitxer marcat com a sensible"
unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer"
resolveAbuseReport: "Informe resolt"
forwardAbuseReport: "Informe reenviat"
updateAbuseReportNote: "Nota de moderació d'un informe actualitzat"
createInvitation: "Crear codi d'invitació "
createAd: "Anunci creat"
deleteAd: "Anunci esborrat"

View File

@ -1629,7 +1629,6 @@ _theme:
buttonBg: "Pozadí tlačítka"
buttonHoverBg: "Pozadí tlačítka (Hover)"
inputBorder: "Ohraničení vstupního pole"
listItemHoverBg: "Pozadí položky seznamu (Hover)"
driveFolderBg: "Pozadí složky disku"
wallpaperOverlay: "Překrytí tapety"
badge: "Odznak"

View File

@ -1784,7 +1784,6 @@ _theme:
buttonBg: "Hintergrund von Schaltflächen"
buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)"
inputBorder: "Rahmen von Eingabefeldern"
listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)"
driveFolderBg: "Hintergrund von Drive-Ordnern"
wallpaperOverlay: "Hintergrundbild-Overlay"
badge: "Wappen"

View File

@ -112,7 +112,7 @@ enterEmoji: "Enter an emoji"
renote: "Renote"
unrenote: "Remove renote"
renoted: "Renoted."
renotedToX: "Renote to {name}."
renotedToX: "Renoted to {name}."
cantRenote: "This post can't be renoted."
cantReRenote: "A renote can't be renoted."
quote: "Quote"
@ -454,6 +454,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords"
moderator: "Moderator"
moderation: "Moderation"
moderationNote: "Moderation note"
moderationNoteDescription: "You can fill in notes that will be shared only among moderators."
addModerationNote: "Add moderation note"
moderationLogs: "Moderation logs"
nUsersMentioned: "Mentioned by {n} users"
@ -921,6 +922,7 @@ followersVisibility: "Visibility of followers"
continueThread: "View thread continuation"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password."
incorrectTotp: "The one-time password is incorrect or has expired."
voteConfirm: "Confirm your vote for \"{choice}\"?"
hide: "Hide"
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
@ -1284,6 +1286,14 @@ 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"
_abuseUserReport:
forward: "Forward"
forwardDescription: "Forward the report to a remote server as an anonymous system account."
resolve: "Resolve"
accept: "Accept"
reject: "Reject"
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
_delivery:
status: "Delivery status"
stop: "Suspended"
@ -1737,7 +1747,7 @@ _role:
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW"
canUpdateBioMedia: "Allow to edit an icon or a banner image"
canUpdateBioMedia: "Can edit an icon or a banner image"
pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas"
wordMuteMax: "Maximum number of characters allowed in word mutes"
@ -1974,7 +1984,6 @@ _theme:
buttonBg: "Button background"
buttonHoverBg: "Button background (Hover)"
inputBorder: "Input field border"
listItemHoverBg: "List item background (Hover)"
driveFolderBg: "Drive folder background"
wallpaperOverlay: "Wallpaper overlay"
badge: "Badge"
@ -2473,22 +2482,22 @@ _webhookSettings:
reaction: "When receiving a reaction"
mention: "When being mentioned"
_systemEvents:
abuseReport: "When received a new abuse report"
abuseReportResolved: "When resolved abuse report"
abuseReport: "When received a new report"
abuseReportResolved: "When resolved report"
userCreated: "When user is created"
deleteConfirm: "Are you sure you want to delete the Webhook?"
testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
_abuseReport:
_notificationRecipient:
createRecipient: "Add a recipient for abuse reports"
modifyRecipient: "Edit a recipient for abuse reports"
createRecipient: "Add a recipient for reports"
modifyRecipient: "Edit a recipient for reports"
recipientType: "Notification type"
_recipientType:
mail: "Email"
webhook: "Webhook"
_captions:
mail: "Send the email to moderators' email addresses when you receive abuse."
webhook: "Send a notification to SystemWebhook when you receive or resolve abuse."
mail: "Send the email to moderators' email addresses when you receive reports."
webhook: "Send a notification to System Webhook when you receive or resolve reports."
keywords: "Keywords"
notifiedUser: "Users to notify"
notifiedWebhook: "Webhook to use"
@ -2521,6 +2530,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive"
resolveAbuseReport: "Report resolved"
forwardAbuseReport: "Report forwarded"
updateAbuseReportNote: "Moderation note of a report updated"
createInvitation: "Invite generated"
createAd: "Ad created"
deleteAd: "Ad deleted"
@ -2528,18 +2539,18 @@ _moderationLogTypes:
createAvatarDecoration: "Avatar decoration created"
updateAvatarDecoration: "Avatar decoration updated"
deleteAvatarDecoration: "Avatar decoration deleted"
unsetUserAvatar: "Unset this user's avatar"
unsetUserBanner: "Unset this user's banner"
createSystemWebhook: "Create SystemWebhook"
updateSystemWebhook: "Update SystemWebhook"
deleteSystemWebhook: "Delete SystemWebhook"
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
deleteAccount: "Delete the account"
deletePage: "Delete the page"
deleteFlash: "Delete Play"
deleteGalleryPost: "Delete the gallery post"
unsetUserAvatar: "User avatar unset"
unsetUserBanner: "User banner unset"
createSystemWebhook: "System Webhook created"
updateSystemWebhook: "System Webhook updated"
deleteSystemWebhook: "System Webhook deleted"
createAbuseReportNotificationRecipient: "Recipient for reports created"
updateAbuseReportNotificationRecipient: "Recipient for reports updated"
deleteAbuseReportNotificationRecipient: "Recipient for reports deleted"
deleteAccount: "Account deleted"
deletePage: "Page deleted"
deleteFlash: "Play deleted"
deleteGalleryPost: "Gallery post deleted"
_fileViewer:
title: "File details"
type: "File type"

View File

@ -1915,7 +1915,6 @@ _theme:
buttonBg: "Fondo de botón"
buttonHoverBg: "Fondo de botón (hover)"
inputBorder: "Borde de los campos de entrada"
listItemHoverBg: "Fondo de elemento de listas (hover)"
driveFolderBg: "Fondo de capeta del drive"
wallpaperOverlay: "Transparencia del fondo de pantalla"
badge: "Medalla"

View File

@ -1701,7 +1701,6 @@ _theme:
buttonBg: "Arrière-plan du bouton"
buttonHoverBg: "Arrière-plan du bouton (survolé)"
inputBorder: "Cadre de la zone de texte"
listItemHoverBg: "Arrière-plan d'item de liste (survolé)"
driveFolderBg: "Arrière-plan du dossier de disque"
wallpaperOverlay: "Superposition de fond d'écran"
badge: "Badge"

View File

@ -1924,7 +1924,6 @@ _theme:
buttonBg: "Latar belakang tombol"
buttonHoverBg: "Latar belakang tombol (Mengambang)"
inputBorder: "Batas bidang masukan"
listItemHoverBg: "Latar belakang daftar item (Mengambang)"
driveFolderBg: "Latar belakang folder drive"
wallpaperOverlay: "Lapisan wallpaper"
badge: "Lencana"

12
locales/index.d.ts vendored
View File

@ -5174,6 +5174,10 @@ export interface Locale extends ILocale {
*
*/
"target": string;
/**
* CAPTCHAのテストを目的とした機能です<strong>使</strong>
*/
"testCaptchaWarning": string;
"_abuseUserReport": {
/**
*
@ -5704,6 +5708,10 @@ export interface Locale extends ILocale {
* URLやWebページのURLを指定します
*/
"inquiryUrlDescription": string;
/**
*
*/
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
};
"_accountMigration": {
/**
@ -7725,10 +7733,6 @@ export interface Locale extends ILocale {
*
*/
"inputBorder": string;
/**
* ()
*/
"listItemHoverBg": string;
/**
*
*/

View File

@ -1975,7 +1975,6 @@ _theme:
buttonBg: "Sfondo del pulsante"
buttonHoverBg: "Sfondo del pulsante (sorvolato)"
inputBorder: "Inquadra casella di testo"
listItemHoverBg: "Sfondo della voce di elenco (sorvolato)"
driveFolderBg: "Sfondo della cartella di disco"
wallpaperOverlay: "Sovrapposizione dello sfondo"
badge: "Distintivo"

View File

@ -1289,6 +1289,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
_abuseUserReport:
forward: "転送"
@ -1442,6 +1443,7 @@ _serverSettings:
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
@ -2023,7 +2025,6 @@ _theme:
buttonBg: "ボタンの背景"
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
listItemHoverBg: "リスト項目の背景 (ホバー)"
driveFolderBg: "ドライブフォルダーの背景"
wallpaperOverlay: "壁紙のオーバーレイ"
badge: "バッジ"

View File

@ -1943,7 +1943,6 @@ _theme:
buttonBg: "ボタンの背景"
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
listItemHoverBg: "リスト項目の背景 (ホバー)"
driveFolderBg: "ドライブフォルダーの背景"
wallpaperOverlay: "壁紙のオーバーレイ"
badge: "バッジ"

View File

@ -1984,7 +1984,6 @@ _theme:
buttonBg: "버튼 배경"
buttonHoverBg: "버튼 배경 (호버)"
inputBorder: "입력 필드 테두리"
listItemHoverBg: "리스트 항목 배경 (호버)"
driveFolderBg: "드라이브 폴더 배경"
wallpaperOverlay: "배경화면 오버레이"
badge: "배지"

View File

@ -1205,7 +1205,6 @@ _theme:
buttonBg: "Tło przycisku"
buttonHoverBg: "Tło przycisku (po najechaniu)"
inputBorder: "Obramowanie pola wejścia"
listItemHoverBg: "Tło elementu listy (po najechaniu)"
driveFolderBg: "Tło folderu na dysku"
wallpaperOverlay: "Nakładka tapety"
badge: "Odznaka"

View File

@ -1,5 +1,5 @@
---
_lang_: "日本語"
_lang_: "Português"
headlineMisskey: "Uma rede ligada por notas"
introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀"
poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>."
@ -25,7 +25,7 @@ basicSettings: "Configurações básicas"
otherSettings: "Outras configurações"
openInWindow: "Abrir em um janela"
profile: "Perfil"
timeline: "Cronologia"
timeline: "Linha do tempo"
noAccountDescription: "Este usuário não tem uma descrição."
login: "Iniciar sessão"
loggingIn: "Iniciando sessão…"
@ -1058,7 +1058,7 @@ resetPasswordConfirm: "Deseja realmente mudar a sua senha?"
sensitiveWords: "Palavras sensíveis"
sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha."
sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
prohibitedWords: "Palavras proibídas"
prohibitedWords: "Palavras proibidas"
prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha."
prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
hiddenTags: "Hashtags escondidas"
@ -1416,7 +1416,7 @@ _achievements:
_types:
_notes1:
title: "Configurando o meu misskey"
description: "Post uma nota pela primeira vez"
description: "Poste uma nota pela primeira vez"
flavor: "Divirta-se com o Misskey!"
_notes10:
title: "Algumas notas"
@ -1944,7 +1944,6 @@ _theme:
buttonBg: "Plano de fundo de botão"
buttonHoverBg: "Plano de fundo de botão (Selecionado)"
inputBorder: "Borda de campo digitável"
listItemHoverBg: "Plano de fundo do item de uma lista (Selecionado)"
driveFolderBg: "Plano de fundo da pasta no Drive"
wallpaperOverlay: "Sobreposição do papel de parede."
badge: "Emblema"

View File

@ -1694,7 +1694,6 @@ _theme:
buttonBg: "Фон кнопки"
buttonHoverBg: "Текст кнопки"
inputBorder: "Рамка поля ввода"
listItemHoverBg: "Фон пункта списка (под указателем)"
driveFolderBg: "Фон папки «Диска»"
wallpaperOverlay: "Слой обоев"
badge: "Значок"

View File

@ -1108,7 +1108,6 @@ _theme:
buttonBg: "Pozadie tlačidla"
buttonHoverBg: "Pozadie tlačidla (pod kurzorom)"
inputBorder: "Okraj vstupného poľa"
listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)"
driveFolderBg: "Pozadie priečinu disku"
wallpaperOverlay: "Vrstvenie pozadia"
badge: "Odznak"

View File

@ -1943,7 +1943,6 @@ _theme:
buttonBg: "ปุ่มพื้นหลัง"
buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)"
driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
badge: "ตรา"

View File

@ -1302,7 +1302,6 @@ _theme:
buttonBg: "Фон кнопки"
buttonHoverBg: "Фон кнопки (при наведенні)"
inputBorder: "Край поля вводу"
listItemHoverBg: "Фон елементу в списку (при наведенні)"
driveFolderBg: "Фон папки на диску"
wallpaperOverlay: "Накладання шпалер"
badge: "Значок"

View File

@ -1546,7 +1546,6 @@ _theme:
buttonBg: "Nền nút"
buttonHoverBg: "Nền nút (Chạm)"
inputBorder: "Đường viền khung soạn thảo"
listItemHoverBg: "Nền mục liệt kê (Chạm)"
driveFolderBg: "Nền thư mục Ổ đĩa"
wallpaperOverlay: "Lớp phủ hình nền"
badge: "Huy hiệu"

View File

@ -1199,10 +1199,10 @@ followingOrFollower: "关注中或关注者"
fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复"
hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复"
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?"
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?"
showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?"
externalServices: "外部服务"
sourceCode: "源代码"
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
@ -1290,6 +1290,10 @@ target: "对象"
_abuseUserReport:
forward: "转发"
forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
resolve: "解决"
accept: "确认"
reject: "拒绝"
resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚选择「拒绝」将案件以否定的态度标记为已解决。"
_delivery:
status: "投递状态"
stop: "停止投递"
@ -1626,7 +1630,7 @@ _achievements:
_postedAt0min0sec:
title: "报时"
description: "在 0 点发布一篇帖子"
flavor: "报时信号最后一响,零点整"
flavor: "嘟 · 嘟 · 嘟 · 哔——"
_selfQuote:
title: "自我引用"
description: "引用了自己的帖子"
@ -1980,7 +1984,6 @@ _theme:
buttonBg: "按钮背景"
buttonHoverBg: "按钮背景(悬停)"
inputBorder: "输入框边框"
listItemHoverBg: "下拉列表项目背景(悬停)"
driveFolderBg: "网盘的文件夹背景"
wallpaperOverlay: "壁纸叠加层"
badge: "徽章"

View File

@ -1975,7 +1975,6 @@ _theme:
buttonBg: "按鈕背景"
buttonHoverBg: "按鈕背景 (漂浮)"
inputBorder: "輸入框邊框"
listItemHoverBg: "列表物品背景 (漂浮)"
driveFolderBg: "雲端硬碟文件夾背景"
wallpaperOverlay: "壁紙覆蓋層"
badge: "徽章"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.10.0-beta.6",
"version": "2024.10.1-beta.3",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class Testcaptcha1728550878802 {
name = 'Testcaptcha1728550878802'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
}
}

View File

@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return;
}
const moderatorIds = await this.roleService.getModeratorIds(true, true);
const moderatorIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});
for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) {
@ -370,7 +373,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
}
// モデレータ権限の有無で通知先設定を振り分ける
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
const authorizedUserIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) {

View File

@ -119,5 +119,18 @@ export class CaptchaService {
throw new Error(`turnstile-failed: ${errorCodes}`);
}
}
@bindThis
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
if (response == null) {
throw new Error('testcaptcha-failed: no response provided');
}
const success = response === 'testcaptcha-passed';
if (!success) {
throw new Error('testcaptcha-failed');
}
}
}

View File

@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
@bindThis
public async update(id: MiEmoji['id'], data: {
public async update(data: (
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
) & {
driveFile?: MiDriveFile;
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}, moderator?: MiUser): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
}, moderator?: MiUser): Promise<
null
| 'NO_SUCH_EMOJI'
| 'SAME_NAME_EMOJI_EXISTS'
> {
const emoji = data.id
? await this.getEmojiById(data.id)
: await this.getEmojiByName(data.name!);
if (emoji === null) return 'NO_SUCH_EMOJI';
const id = emoji.id;
// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
if (doNameUpdate) {
const isDuplicate = await this.checkDuplicate(data.name!);
if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
}
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
const packed = await this.emojiEntityService.packDetailed(emoji.id);
if (emoji.name === data.name) {
if (!doNameUpdate) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [packed],
});
@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
after: updated,
});
}
return null;
}
@bindThis

View File

@ -93,6 +93,13 @@ export class QueueService {
repeat: { pattern: '0 0 * * *' },
removeOnComplete: true,
});
this.systemQueue.add('checkModeratorsActivity', {
}, {
// 毎時30分に起動
repeat: { pattern: '30 * * * *' },
removeOnComplete: true,
});
}
@bindThis

View File

@ -103,6 +103,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
@ -138,6 +139,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@ -430,22 +432,35 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return check.isExplorable;
}
/**
* ID一覧を取得する.
*
* @param opts.includeAdmins (デフォルト: true)
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
* @param opts.excludeExpire (デフォルト: false)
*/
@bindThis
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
public async getModeratorIds(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser['id'][]> {
const includeAdmins = opts?.includeAdmins ?? true;
const includeRoot = opts?.includeRoot ?? false;
const excludeExpire = opts?.excludeExpire ?? false;
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator);
// TODO: isRootなアカウントも含める
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];
const now = Date.now();
const result = [
// Setを経由して重複を除去ユーザIDは重複する可能性があるので
...new Set(
const now = Date.now();
const resultSet = new Set(
assigns
.filter(it =>
(excludeExpire)
@ -453,19 +468,35 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
: true,
)
.map(a => a.userId),
),
];
);
return result.sort((x, y) => x.localeCompare(y));
if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
}
return [...resultSet].sort((x, y) => x.localeCompare(y));
}
@bindThis
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
const ids = await this.getModeratorIds(includeAdmins);
const users = ids.length > 0 ? await this.usersRepository.findBy({
public async getModerators(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser[]> {
const ids = await this.getModeratorIds(opts);
return ids.length > 0
? await this.usersRepository.findBy({
id: In(ids),
}) : [];
return users;
})
: [];
}
@bindThis

View File

@ -40,7 +40,7 @@ export class FlashEntityService {
// { schema: 'UserDetailed' } すると無限ループするので注意
const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
let isLiked = false;
let isLiked = undefined;
if (meId) {
isLiked = hint?.likedFlashIds
? hint.likedFlashIds.includes(flash.id)

View File

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

View File

@ -258,6 +258,11 @@ export class MiMeta {
})
public turnstileSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableTestcaptcha: boolean;
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
@Column('enum', {

View File

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

View File

@ -6,6 +6,7 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@ -80,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,
CheckExpiredMutingsProcessorService,
CheckModeratorsActivityProcessorService,
QueueProcessorService,
],
exports: [

View File

@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
@ -66,7 +67,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
// onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
const maxAttempts = job.opts ? job.opts.attempts : 0;
const maxAttempts = job.opts.attempts ?? 0;
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
}
@ -120,24 +121,35 @@ export class QueueProcessorService implements OnApplicationShutdown {
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
function renderError(e: Error): any {
if (e) { // 何故かeがundefinedで来ることがある
function renderError(e?: Error) {
// 何故かeがundefinedで来ることがある
if (!e) return '?';
if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
return `${e.name}: ${e.message}`;
}
return {
stack: e.stack,
message: e.message,
name: e.name,
};
} else {
return {
stack: '?',
message: '?',
name: '?',
};
}
function renderJob(job?: Bull.Job) {
if (!job) return '?';
return {
name: job.name || undefined,
info: getJobInfo(job),
failedReason: job.failedReason || undefined,
data: job.data,
};
}
//#region system
@ -150,6 +162,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
@ -172,15 +185,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err: Error) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, {
Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -229,15 +242,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, {
Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -269,15 +282,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Deliver: ${err.message}`, {
Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -309,15 +322,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Inbox: ${err.message}`, {
Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -349,15 +362,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, {
Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -389,15 +402,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`);
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, {
Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -436,15 +449,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, {
Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion
@ -477,15 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
.on('active', (job) => logger.debug(`active id=${job.id}`))
.on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => {
logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) });
logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) });
if (config.sentryForBackend) {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, {
Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, {
level: 'error',
extra: { job, err },
});
}
})
.on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) }))
.on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`));
}
//#endregion

View File

@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
// モデレーターが不在と判断する日付の閾値
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24;
@Injectable()
export class CheckModeratorsActivityProcessorService {
private logger: Logger;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
}
@bindThis
public async process(): Promise<void> {
this.logger.info('start.');
const meta = await this.metaService.fetch(false);
if (!meta.disableRegistration) {
await this.processImpl();
} else {
this.logger.info('is already invitation only.');
}
this.logger.succ('finish.');
}
@bindThis
private async processImpl() {
const { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays();
if (isModeratorsInactive) {
this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
await this.changeToInvitationOnly();
// TODO: モデレータに通知メールMisskey通知
// TODO: SystemWebhook通知
} else {
if (inactivityLimitCountdown <= 2) {
this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`);
// TODO: 警告メール
}
}
}
/**
* trueの場合はモデレーターが不在である
* isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に
* {@link MiUser.lastActiveDate}{@link MODERATOR_INACTIVITY_LIMIT_DAYS}
* {@link MiUser.lastActiveDate}nullの場合は
*
* -----
*
* ###
* - 実行日時: 2022-01-30 12:00:00
* - 判定基準: 2022-01-23 12:00:00{@link MODERATOR_INACTIVITY_LIMIT_DAYS}
*
* ####
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00
* - モデレータB: lastActiveDate = 2022-01-23 12:00:00 0
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 -1
* - モデレータD: lastActiveDate = null
*
* Bのアクティビティのみ判定基準日よりも古くないため
*
* ####
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00
* - モデレータB: lastActiveDate = 2022-01-22 12:00:00 -1
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 -1
* - モデレータD: lastActiveDate = null
*
* A, B, Cのアクティビティは判定基準日よりも古いため
*/
@bindThis
public async evaluateModeratorsInactiveDays() {
const today = new Date();
const inactivePeriod = new Date(today);
inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
const moderators = await this.fetchModerators()
.then(it => it.filter(it => it.lastActiveDate != null));
const inactiveModerators = moderators
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
const inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC);
return {
isModeratorsInactive: inactiveModerators.length === moderators.length,
inactiveModerators,
inactivityLimitCountdown,
};
}
@bindThis
private async changeToInvitationOnly() {
await this.metaService.update({ disableRegistration: true });
}
@bindThis
private async fetchModerators() {
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
return this.roleService.getModerators({
includeAdmins: true,
includeRoot: true,
excludeExpire: true,
});
}
}

View File

@ -119,6 +119,7 @@ export class ApiServerService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
'testcaptcha-response'?: string;
}
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
@ -132,6 +133,7 @@ export class ApiServerService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
'testcaptcha-response'?: string;
};
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));

View File

@ -71,6 +71,7 @@ export class SigninApiService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
'testcaptcha-response'?: string;
};
}>,
reply: FastifyReply,
@ -194,6 +195,12 @@ export class SigninApiService {
throw new FastifyReplyError(400, err);
});
}
if (this.meta.enableTestcaptcha) {
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
}
if (same) {

View File

@ -67,6 +67,7 @@ export class SignupApiService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
'testcaptcha-response'?: string;
}
}>,
reply: FastifyReply,
@ -99,6 +100,12 @@ export class SignupApiService {
throw new FastifyReplyError(400, err);
});
}
if (this.meta.enableTestcaptcha) {
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
}
const username = body['username'];

View File

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@ -78,25 +78,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
let emojiId;
if (ps.id) {
emojiId = ps.id;
const emoji = await this.customEmojiService.getEmojiById(ps.id);
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
if (ps.name && (ps.name !== emoji.name)) {
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
}
} else {
if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
const emoji = await this.customEmojiService.getEmojiByName(ps.name);
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
emojiId = emoji.id;
}
// JSON schemeのanyOfの型変換がうまくいっていないらしい
const required = { id: ps.id, name: ps.name } as
| { id: MiEmoji['id']; name?: string }
| { id?: MiEmoji['id']; name: string };
await this.customEmojiService.update(emojiId, {
const error = await this.customEmojiService.update({
...required,
driveFile,
name: ps.name,
category: ps.category,
aliases: ps.aliases,
license: ps.license,
@ -104,6 +93,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me);
switch (error) {
case null: return;
case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
const mustBeNever: never = error;
});
}
}

View File

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

View File

@ -71,13 +71,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
case 'moderator': {
const moderatorIds = await this.roleService.getModeratorIds(false);
const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
if (moderatorIds.length === 0) return [];
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
break;
}
case 'adminOrModerator': {
const adminOrModeratorIds = await this.roleService.getModeratorIds();
const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
if (adminOrModeratorIds.length === 0) return [];
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
break;

View File

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

View File

@ -98,7 +98,7 @@
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {

View File

@ -5,8 +5,8 @@
*/
html {
background-color: var(--bg);
color: var(--fg);
background-color: var(--MI_THEME-bg);
color: var(--MI_THEME-fg);
}
#splash {
@ -17,7 +17,7 @@ html {
width: 100vw;
height: 100vh;
cursor: wait;
background-color: var(--bg);
background-color: var(--MI_THEME-bg);
opacity: 1;
transition: opacity 0.5s ease;
}
@ -45,7 +45,7 @@ html {
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
color: var(--MI_THEME-accent);
}
#splashSpinner > .spinner {

View File

@ -5,8 +5,8 @@
*/
html {
background-color: var(--bg);
color: var(--fg);
background-color: var(--MI_THEME-bg);
color: var(--MI_THEME-fg);
}
html.embed {
@ -24,7 +24,7 @@ html.embed {
width: 100vw;
height: 100vh;
cursor: wait;
background-color: var(--bg);
background-color: var(--MI_THEME-bg);
opacity: 1;
transition: opacity 0.5s ease;
}
@ -33,7 +33,7 @@ html.embed #splash {
box-sizing: border-box;
min-height: 300px;
border-radius: var(--radius, 12px);
border: 1px solid var(--divider, #e8e8e8);
border: 1px solid var(--MI_THEME-divider, #e8e8e8);
}
html.embed.norounded #splash {
@ -67,7 +67,7 @@ html.embed.noborder #splash {
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
color: var(--MI_THEME-accent);
}
#splashSpinner > .spinner {

View File

@ -10,6 +10,8 @@ import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
@ -277,9 +277,9 @@ describe('RoleService', () => {
});
describe('getModeratorIds', () => {
test('includeAdmins = false, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@ -295,13 +295,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds(false, false);
const result = await roleService.getModeratorIds({
includeAdmins: false,
includeRoot: false,
excludeExpire: false,
});
expect(result).toEqual([modeUser1.id, modeUser2.id]);
});
test('includeAdmins = false, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@ -317,13 +321,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds(false, true);
const result = await roleService.getModeratorIds({
includeAdmins: false,
includeRoot: false,
excludeExpire: true,
});
expect(result).toEqual([modeUser1.id]);
});
test('includeAdmins = true, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@ -339,13 +347,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds(true, false);
const result = await roleService.getModeratorIds({
includeAdmins: true,
includeRoot: false,
excludeExpire: false,
});
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
});
test('includeAdmins = true, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@ -361,9 +373,111 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds(true, true);
const result = await roleService.getModeratorIds({
includeAdmins: true,
includeRoot: false,
excludeExpire: true,
});
expect(result).toEqual([adminUser1.id, modeUser1.id]);
});
test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
const role2 = await createRole({ name: 'moderator', isModerator: true });
const role3 = await createRole({ name: 'normal' });
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: modeUser1.id, roleId: role2.id }),
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
const result = await roleService.getModeratorIds({
includeAdmins: false,
includeRoot: true,
excludeExpire: false,
});
expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
});
test('root has moderator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
const role2 = await createRole({ name: 'moderator', isModerator: true });
const role3 = await createRole({ name: 'normal' });
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
assignRole({ userId: modeUser1.id, roleId: role2.id }),
assignRole({ userId: rootUser.id, roleId: role2.id }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
]);
const result = await roleService.getModeratorIds({
includeAdmins: false,
includeRoot: true,
excludeExpire: false,
});
expect(result).toEqual([modeUser1.id, rootUser.id]);
});
test('root has administrator role', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
const role2 = await createRole({ name: 'moderator', isModerator: true });
const role3 = await createRole({ name: 'normal' });
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
assignRole({ userId: rootUser.id, roleId: role1.id }),
assignRole({ userId: modeUser1.id, roleId: role2.id }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
]);
const result = await roleService.getModeratorIds({
includeAdmins: true,
includeRoot: true,
excludeExpire: false,
});
expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
});
test('root has moderator role(expire)', async () => {
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
const role2 = await createRole({ name: 'moderator', isModerator: true });
const role3 = await createRole({ name: 'normal' });
await Promise.all([
assignRole({ userId: adminUser1.id, roleId: role1.id }),
assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
assignRole({ userId: normalUser1.id, roleId: role3.id }),
]);
const result = await roleService.getModeratorIds({
includeAdmins: false,
includeRoot: true,
excludeExpire: true,
});
expect(result).toEqual([rootUser.id]);
});
});
describe('conditional role', () => {

View File

@ -0,0 +1,235 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { RoleService } from '@/core/RoleService.js';
import { GlobalModule } from '@/GlobalModule.js';
import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js';
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
describe('CheckModeratorsActivityProcessorService', () => {
let app: TestingModule;
let clock: lolex.InstalledClock;
let service: CheckModeratorsActivityProcessorService;
// --------------------------------------------------------------------------------------
let usersRepository: UsersRepository;
let userProfilesRepository: UserProfilesRepository;
let idService: IdService;
let roleService: jest.Mocked<RoleService>;
// --------------------------------------------------------------------------------------
async function createUser(data: Partial<MiUser> = {}) {
const id = idService.gen();
const user = await usersRepository
.insert({
id: id,
username: `user_${id}`,
usernameLower: `user_${id}`.toLowerCase(),
...data,
})
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
await userProfilesRepository.insert({
userId: user.id,
});
return user;
}
function mockModeratorRole(users: MiUser[]) {
roleService.getModerators.mockReset();
roleService.getModerators.mockResolvedValue(users);
}
// --------------------------------------------------------------------------------------
beforeAll(async () => {
app = await Test
.createTestingModule({
imports: [
GlobalModule,
],
providers: [
CheckModeratorsActivityProcessorService,
IdService,
{
provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
},
{
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
},
{
provide: QueueLoggerService, useFactory: () => ({
logger: ({
createSubLogger: () => ({
info: jest.fn(),
warn: jest.fn(),
succ: jest.fn(),
}),
}),
}),
},
],
})
.compile();
usersRepository = app.get(DI.usersRepository);
userProfilesRepository = app.get(DI.userProfilesRepository);
service = app.get(CheckModeratorsActivityProcessorService);
idService = app.get(IdService);
roleService = app.get(RoleService) as jest.Mocked<RoleService>;
app.enableShutdownHooks();
});
beforeEach(async () => {
clock = lolex.install({
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
});
afterEach(async () => {
clock.uninstall();
await usersRepository.delete({});
await userProfilesRepository.delete({});
roleService.getModerators.mockReset();
});
afterAll(async () => {
await app.close();
});
// --------------------------------------------------------------------------------------
describe('evaluateModeratorsInactiveDays', () => {
test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => {
const [user1, user2, user3, user4] = await Promise.all([
// 期限よりも1秒新しいタイミングでアクティブ化セーフ
createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
// 期限ちょうどにアクティブ化(セーフ)
createUser({ lastActiveDate: subDays(baseDate, 7) }),
// 期限よりも1秒古いタイミングでアクティブ化アウト
createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
// 対象外
createUser({ lastActiveDate: null }),
]);
mockModeratorRole([user1, user2, user3, user4]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user3]);
});
test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => {
const [user1, user2] = await Promise.all([
// 期限よりも1秒古いタイミングでアクティブ化アウト
createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
// 対象外
createUser({ lastActiveDate: null }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(true);
expect(result.inactiveModerators).toEqual([user1]);
});
test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
// 期限まで残り24時間->猶予1日として計算されるはずである
createUser({ lastActiveDate: subDays(baseDate, 6) }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
expect(result.inactivityLimitCountdown).toBe(1);
});
test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
// 期限まで残り25時間->猶予1日として計算されるはずである
createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
expect(result.inactivityLimitCountdown).toBe(1);
});
test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
// 期限まで残り23時間->猶予0日として計算されるはずである
createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
expect(result.inactivityLimitCountdown).toBe(0);
});
test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
// 期限ちょうど->猶予0日として計算されるはずである
createUser({ lastActiveDate: subDays(baseDate, 7) }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
expect(result.inactivityLimitCountdown).toBe(0);
});
test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
// 期限より1時間超過->猶予-1日として計算されるはずである
createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
]);
mockModeratorRole([user1, user2]);
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(true);
expect(result.inactiveModerators).toEqual([user1, user2]);
expect(result.inactivityLimitCountdown).toBe(-1);
});
});
});

View File

@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
--size: 38px;
&.colored {
color: var(--accent);
color: var(--MI_THEME-accent);
}
&.inline {

View File

@ -31,17 +31,17 @@ defineProps<{
display: flex;
align-items: center;
width: 100%;
padding: var(--margin);
padding: var(--MI-margin);
margin-top: 4px;
border: 1px solid var(--inputBorder);
border-radius: var(--radius);
background-color: var(--panel);
border: 1px solid var(--MI_THEME-inputBorder);
border-radius: var(--MI-radius);
background-color: var(--MI_THEME-panel);
transition: background-color .1s, border-color .1s;
&:hover {
text-decoration: none;
border-color: var(--inputBorderHover);
background-color: var(--buttonHoverBg);
border-color: var(--MI_THEME-inputBorderHover);
background-color: var(--MI_THEME-buttonHoverBg);
}
}

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
<i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
</div>
@ -94,8 +94,8 @@ async function onclick(ev: MouseEvent) {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
background-color: var(--MI_THEME-fg);
color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@ -114,19 +114,19 @@ async function onclick(ev: MouseEvent) {
.visible {
position: relative;
//box-shadow: 0 0 0 1px var(--divider) inset;
background: var(--bg);
//box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
background: var(--MI_THEME-bg);
background-size: 16px 16px;
}
html[data-color-scheme=dark] .visible {
--c: rgb(255 255 255 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
html[data-color-scheme=light] .visible {
--c: rgb(0 0 0 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
.imageContainer {
@ -150,10 +150,10 @@ html[data-color-scheme=light] .visible {
}
.indicator {
/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
color: var(--accentLighten);
color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;

View File

@ -29,9 +29,9 @@ defineProps<{
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
padding: var(--margin);
border: 1px solid var(--divider);
border-radius: var(--radius);
padding: var(--MI-margin);
border: 1px solid var(--MI_THEME-divider);
border-radius: var(--MI-radius);
background-color: #000;
&:hover {
@ -49,7 +49,7 @@ defineProps<{
}
.videoOverlayPlayButton {
background: var(--accent);
background: var(--MI_THEME-accent);
color: #fff;
padding: 1rem;
border-radius: 99rem;

View File

@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`;
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--mention'));
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
</script>
@ -37,7 +37,7 @@ const bgCss = bg.toRgbString();
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: 999px;
color: var(--mention);
color: var(--MI_THEME-mention);
}
.host {

View File

@ -26,8 +26,8 @@ const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
color: var(--MI_THEME-fg);
border-left: solid 3px var(--MI_THEME-fg);
opacity: 0.7;
`.split('\n').join(' ');
@ -251,7 +251,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'border': {
let color = validColor(token.props.args.color);
color = color ? `#${color}` : 'var(--accent)';
color = color ? `#${color}` : 'var(--MI_THEME-accent)';
let b_style = token.props.args.style;
if (
typeof b_style !== 'string' ||
@ -284,7 +284,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const child = token.children[0];
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
return h('span', {
style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
}, [
h('i', {
class: 'ti ti-clock',
@ -355,7 +355,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return [h(EmA, {
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
style: 'color:var(--MI_THEME-hashtag);',
}, `#${token.props.hashtag}`)];
}

View File

@ -189,8 +189,8 @@ const isDeleted = ref(false);
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: dashed 2px var(--focus);
border-radius: var(--radius);
border: dashed 2px var(--MI_THEME-focus);
border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@ -212,9 +212,9 @@ const isDeleted = ref(false);
right: 12px;
padding: 0 4px;
margin-bottom: 0 !important;
background: var(--popup);
background: var(--MI_THEME-popup);
border-radius: 8px;
box-shadow: 0px 4px 32px var(--shadow);
box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
}
.footerButton {
@ -259,7 +259,7 @@ const isDeleted = ref(false);
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
color: var(--MI_THEME-renote);
& + .article {
padding-top: 8px;
@ -356,7 +356,7 @@ const isDeleted = ref(false);
width: 58px;
height: 58px;
position: sticky !important;
top: calc(22px + var(--stickyTop, 0px));
top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
@ -377,12 +377,12 @@ const isDeleted = ref(false);
width: 100%;
margin-top: 14px;
position: sticky;
bottom: calc(var(--stickyBottom, 0px) + 14px);
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
background: var(--popup);
background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@ -403,16 +403,16 @@ const isDeleted = ref(false);
z-index: 2;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@ -424,13 +424,13 @@ const isDeleted = ref(false);
}
.replyIcon {
color: var(--accent);
color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@ -449,7 +449,7 @@ const isDeleted = ref(false);
.quoteNote {
padding: 16px;
border: dashed 1px var(--renote);
border: dashed 1px var(--MI_THEME-renote);
border-radius: 8px;
overflow: clip;
}
@ -473,7 +473,7 @@ const isDeleted = ref(false);
}
&:hover {
color: var(--fgHighlighted);
color: var(--MI_THEME-fgHighlighted);
}
}
@ -550,7 +550,7 @@ const isDeleted = ref(false);
margin: 0 10px 0 0;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
top: calc(14px + var(--MI-stickyTop, 0px));
}
}

View File

@ -195,7 +195,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
color: var(--MI_THEME-renote);
}
.renoteAvatar {
@ -281,7 +281,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
padding: 4px 6px;
font-size: 80%;
line-height: 1;
border: solid 0.5px var(--divider);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: 4px;
}
@ -323,14 +323,14 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
}
.noteReplyTarget {
color: var(--accent);
color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.rn {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
color: var(--MI_THEME-renote);
}
.reactionOmitted {
@ -350,7 +350,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
.quoteNote {
padding: 16px;
border: dashed 1px var(--renote);
border: dashed 1px var(--MI_THEME-renote);
border-radius: 8px;
overflow: clip;
}
@ -364,12 +364,12 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
width: 100%;
margin-top: 14px;
position: sticky;
bottom: calc(var(--stickyBottom, 0px) + 14px);
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
background: var(--popup);
background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@ -390,16 +390,16 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
z-index: 2;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
background: linear-gradient(0deg, var(--MI_THEME-panel), var(--MI_THEME-X15));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@ -422,7 +422,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
}
&:hover {
color: var(--fgHighlighted);
color: var(--MI_THEME-fgHighlighted);
}
}
@ -438,7 +438,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
opacity: 0.7;
&.reacted {
color: var(--accent);
color: var(--MI_THEME-accent);
}
}

View File

@ -72,7 +72,7 @@ defineProps<{
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
border: solid 0.5px var(--divider);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: 3px;
}

View File

@ -53,7 +53,7 @@ const showContent = ref(false);
height: 34px;
border-radius: 8px;
position: sticky !important;
top: calc(16px + var(--stickyTop, 0px));
top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
}

View File

@ -123,7 +123,7 @@ if (props.detail) {
}
.reply, .more {
border-left: solid 0.5px var(--divider);
border-left: solid 0.5px var(--MI_THEME-divider);
margin-top: 10px;
}
@ -144,7 +144,7 @@ if (props.detail) {
.muted {
text-align: center;
padding: 8px !important;
border: 1px solid var(--divider);
border: 1px solid var(--MI_THEME-divider);
margin: 8px 8px 0 8px;
border-radius: 8px;
}

View File

@ -43,10 +43,10 @@ defineExpose({
<style lang="scss" module>
.root {
background: var(--panel);
background: var(--MI_THEME-panel);
}
.note {
border-bottom: 0.5px solid var(--divider);
border-bottom: 0.5px solid var(--MI_THEME-divider);
}
</style>

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice">
<div :class="$style.bg" :style="{ 'width': `${choice.votes / total * 100}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
<EmMfm :text="choice.text" :plain="true"/>
<span style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
@ -52,8 +52,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
position: relative;
margin: 4px 0;
padding: 4px;
//border: solid 0.5px var(--divider);
background: var(--accentedBg);
//border: solid 0.5px var(--MI_THEME-divider);
background: var(--MI_THEME-accentedBg);
border-radius: 4px;
overflow: clip;
}
@ -63,8 +63,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
top: 0;
left: 0;
height: 100%;
background: var(--accent);
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
background: var(--MI_THEME-accent);
background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
transition: width 1s ease;
}
@ -72,11 +72,11 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
position: relative;
display: inline-block;
padding: 3px 5px;
background: var(--panel);
background: var(--MI_THEME-panel);
border-radius: 3px;
}
.info {
color: var(--fg);
color: var(--MI_THEME-fg);
}
</style>

View File

@ -38,7 +38,7 @@ const props = defineProps<{
justify-content: center;
&.canToggle {
background: var(--buttonBg);
background: var(--MI_THEME-buttonBg);
&:hover {
background: rgba(0, 0, 0, 0.1);
@ -72,12 +72,12 @@ const props = defineProps<{
}
&.reacted, &.reacted:hover {
background: var(--accentedBg);
color: var(--accent);
box-shadow: 0 0 0 1px var(--accent) inset;
background: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
> .count {
color: var(--accent);
color: var(--MI_THEME-accent);
}
> .icon {

View File

@ -65,11 +65,11 @@ const collapsed = ref(isLong);
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
background: var(--panel);
background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@ -78,7 +78,7 @@ const collapsed = ref(isLong);
&:hover {
> .fadeLabel {
background: var(--panelHighlight);
background: var(--MI_THEME-panelHighlight);
}
}
}
@ -87,25 +87,25 @@ const collapsed = ref(isLong);
.reply {
margin-right: 6px;
color: var(--accent);
color: var(--MI_THEME-accent);
}
.rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
color: var(--MI_THEME-renote);
}
.showLess {
width: 100%;
margin-top: 14px;
position: sticky;
bottom: calc(var(--stickyBottom, 0px) + 14px);
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
background: var(--popup);
background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;

View File

@ -98,10 +98,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
<style lang="scss" module>
.old1 {
color: var(--warn);
color: var(--MI_THEME-warn);
}
.old1.old2 {
color: var(--error);
color: var(--MI_THEME-error);
}
</style>

View File

@ -20,7 +20,7 @@ withDefaults(defineProps<{
<style module lang="scss">
.timelineRoot {
background-color: var(--panel);
background-color: var(--MI_THEME-panel);
height: 100%;
max-height: var(--embedMaxHeight, none);
display: flex;
@ -29,7 +29,7 @@ withDefaults(defineProps<{
.header {
flex-shrink: 0;
border-bottom: 1px solid var(--divider);
border-bottom: 1px solid var(--MI_THEME-divider);
}
.body {

View File

@ -100,7 +100,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
gap: var(--margin);
gap: var(--MI-margin);
overflow: hidden;
.headerClipIconRoot {
@ -110,8 +110,8 @@ function top(ev: MouseEvent) {
line-height: 32px;
font-size: 14px;
text-align: center;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
border-radius: 50%;
}

View File

@ -47,6 +47,6 @@ if (note.value?.url != null || note.value?.uri != null) {
<style lang="scss" module>
.noteEmbedRoot {
background-color: var(--panel);
background-color: var(--MI_THEME-panel);
}
</style>

View File

@ -83,7 +83,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
gap: var(--margin);
gap: var(--MI-margin);
overflow: hidden;
.headerClipIconRoot {
@ -93,8 +93,8 @@ function top(ev: MouseEvent) {
line-height: 32px;
font-size: 14px;
text-align: center;
background-color: var(--accentedBg);
color: var(--accent);
background-color: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
border-radius: 50%;
}

View File

@ -117,7 +117,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
gap: var(--margin);
gap: var(--MI-margin);
overflow: hidden;
.avatarLink {

View File

@ -7,18 +7,18 @@
*/
:root {
--radius: 12px;
--marginFull: 14px;
--marginHalf: 10px;
--MI-radius: 12px;
--MI-marginFull: 14px;
--MI-marginHalf: 10px;
--margin: var(--marginFull);
--MI-margin: var(--MI-marginFull);
}
html {
background-color: transparent;
color-scheme: light dark;
color: var(--fg);
accent-color: var(--accent);
color: var(--MI_THEME-fg);
accent-color: var(--MI_THEME-accent);
overflow: clip;
overflow-wrap: break-word;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@ -29,7 +29,7 @@ html {
-webkit-text-size-adjust: 100%;
&, * {
scrollbar-color: var(--scrollbarHandle) transparent;
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
@ -42,14 +42,14 @@ html {
}
&::-webkit-scrollbar-thumb {
background: var(--scrollbarHandle);
background: var(--MI_THEME-scrollbarHandle);
&:hover {
background: var(--scrollbarHandleHover);
background: var(--MI_THEME-scrollbarHandleHover);
}
&:active {
background: var(--accent);
background: var(--MI_THEME-accent);
}
}
}
@ -93,7 +93,7 @@ rt {
}
:focus-visible {
outline: var(--focus) solid 2px;
outline: var(--MI_THEME-focus) solid 2px;
outline-offset: -2px;
&:hover {
@ -151,38 +151,38 @@ rt {
._buttonGray {
@extend ._button;
background: var(--buttonBg);
background: var(--MI_THEME-buttonBg);
&:not(:disabled):hover {
background: var(--buttonHoverBg);
background: var(--MI_THEME-buttonHoverBg);
}
}
._buttonPrimary {
@extend ._button;
color: var(--fgOnAccent);
background: var(--accent);
color: var(--MI_THEME-fgOnAccent);
background: var(--MI_THEME-accent);
&:not(:disabled):hover {
background: hsl(from var(--accent) h s calc(l + 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
background: hsl(from var(--accent) h s calc(l - 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
}
}
._buttonGradate {
@extend ._buttonPrimary;
color: var(--fgOnAccent);
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--MI_THEME-fgOnAccent);
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
@ -199,13 +199,13 @@ rt {
}
._help {
color: var(--accent);
color: var(--MI_THEME-accent);
cursor: help;
}
._textButton {
@extend ._button;
color: var(--accent);
color: var(--MI_THEME-accent);
&:focus-visible {
outline-offset: 2px;
@ -217,13 +217,13 @@ rt {
}
._panel {
background: var(--panel);
border-radius: var(--radius);
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
overflow: clip;
}
._margin {
margin: var(--margin) 0;
margin: var(--MI-margin) 0;
}
._gaps_m {
@ -241,7 +241,7 @@ rt {
._gaps {
display: flex;
flex-direction: column;
gap: var(--margin);
gap: var(--MI-margin);
}
._buttons {
@ -263,24 +263,24 @@ rt {
padding: 10px;
box-sizing: border-box;
text-align: center;
border: solid 0.5px var(--divider);
border-radius: var(--radius);
border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--MI-radius);
&:active {
border-color: var(--accent);
border-color: var(--MI_THEME-accent);
}
}
._popup {
background: var(--popup);
border-radius: var(--radius);
background: var(--MI_THEME-popup);
border-radius: var(--MI-radius);
contain: content;
}
._acrylic {
background: var(--acrylicPanel);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
background: var(--MI_THEME-acrylicPanel);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
}
._fullinfo {
@ -296,7 +296,7 @@ rt {
}
._link {
color: var(--link);
color: var(--MI_THEME-link);
}
._caption {

View File

@ -61,7 +61,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
for (const [k, v] of Object.entries(props)) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照

View File

@ -88,14 +88,14 @@ onUnmounted(() => {
<style lang="scss" module>
.rootForEmbedPage {
box-sizing: border-box;
border: 1px solid var(--divider);
background-color: var(--bg);
border: 1px solid var(--MI_THEME-divider);
background-color: var(--MI_THEME-bg);
overflow: hidden;
position: relative;
height: auto;
&.rounded {
border-radius: var(--radius);
border-radius: var(--MI-radius);
}
&.noBorder {

View File

@ -30,7 +30,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
@ -67,7 +67,6 @@
switchOnFg: '@accent',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
badge: '#31b1ce',

View File

@ -30,7 +30,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
popup: ':lighten<3<@panel',
@ -67,7 +67,6 @@
switchOnFg: '@fgOnAccent',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
badge: '#31b1ce',

View File

@ -36,7 +36,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
@ -50,7 +50,6 @@
htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@ -55,7 +55,7 @@
codeBoolean: '#c59eff',
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
@ -69,7 +69,6 @@
buttonGradateB: ':hue<20<@accent',
htmlThemeColor: '@bg',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',

View File

@ -56,7 +56,7 @@
codeBoolean: '#c59eff',
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
@ -71,7 +71,6 @@
buttonGradateB: ':hue<20<@accent',
htmlThemeColor: '@bg',
panelHighlight: ':lighten<3<@panel',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: '#74747433',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',

View File

@ -39,7 +39,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
panelBorder: '" solid 1px var(--divider)',
panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
@ -52,7 +52,6 @@
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
panelHighlight: ':darken<3<@panel',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -43,7 +43,7 @@ async function main() {
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--${k}`, v.toString());
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {

View File

@ -226,7 +226,7 @@ export async function openAccountMenu(opts: {
function showSigninDialog() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => {
done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
addAccount(res.id, res.i);
success();
},
@ -236,9 +236,9 @@ export async function openAccountMenu(opts: {
function createAccount() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
switchAccountWithToken(res.i);
done: (res: Misskey.entities.SignupResponse) => {
addAccount(res.id, res.token);
switchAccountWithToken(res.token);
},
closed: () => dispose(),
});

View File

@ -182,24 +182,18 @@ export async function common(createVue: () => App<Element>) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
} else {
if (defaultStore.state.darkMode) {
applyTheme(darkTheme.value);
} else {
applyTheme(lightTheme.value);
}
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
document.documentElement.style.removeProperty('--MI-blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
document.documentElement.style.setProperty('--MI-blur', 'none');
}
}, { immediate: true });

View File

@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkFolder>
<template #icon>
<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
<i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i>
<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i>
<i v-else-if="report.resolved" class="ti ti-slash"></i>
<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
<i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i>
</template>
<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
<template #caption>{{ report.comment }}</template>
@ -17,11 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer>
<div class="_buttons">
<template v-if="!report.resolved">
<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
</template>
<template v-if="report.targetUser.host == null">
<template v-if="report.targetUser.host != null">
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
</template>

View File

@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
.root {
padding: 16px;
font-size: 90%;
background: var(--infoWarnBg);
color: var(--error);
border-radius: var(--radius);
background: var(--MI_THEME-infoWarnBg);
color: var(--MI_THEME-error);
border-radius: var(--MI-radius);
}
.link {

View File

@ -193,12 +193,12 @@ tick();
function calcColors() {
const computedStyle = getComputedStyle(document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
hHandColor.value = accent;
nowColor.value = accent;
}

View File

@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.header">
<span :class="$style.icon">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span :class="$style.title">{{ announcement.title }}</span>
</div>
@ -83,8 +83,8 @@ onMounted(() => {
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--panel);
border-radius: var(--radius);
background: var(--MI_THEME-panel);
border-radius: var(--MI-radius);
}
.header {

View File

@ -170,6 +170,6 @@ function addUser() {
.actions {
margin-top: 16px;
padding: 24px 0;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>

View File

@ -106,7 +106,7 @@ const containerStyle = computed(() => {
const border = isBordered ? {
borderWidth: c.borderWidth ?? '1px',
borderColor: c.borderColor ?? 'var(--divider)',
borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
borderStyle: c.borderStyle ?? 'solid',
} : undefined;
@ -165,7 +165,7 @@ function openPostForm() {
}
.postForm {
background: var(--bg);
background: var(--MI_THEME-bg);
border-radius: 8px;
}
</style>

View File

@ -407,16 +407,16 @@ onBeforeUnmount(() => {
text-overflow: ellipsis;
&:hover {
background: var(--X3);
background: var(--MI_THEME-X3);
}
&[data-selected='true'] {
background: var(--accent);
background: var(--MI_THEME-accent);
color: #fff !important;
}
&:active {
background: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
color: #fff !important;
}
}

View File

@ -129,7 +129,7 @@ function onMousedown(evt: MouseEvent): void {
font-size: 95%;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);
background: var(--MI_THEME-buttonBg);
border-radius: 5px;
overflow: clip;
box-sizing: border-box;
@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void {
}
&:not(:disabled):hover {
background: var(--buttonHoverBg);
background: var(--MI_THEME-buttonHoverBg);
}
&:not(:disabled):active {
background: var(--buttonHoverBg);
background: var(--MI_THEME-buttonHoverBg);
}
&.small {
@ -167,15 +167,15 @@ function onMousedown(evt: MouseEvent): void {
&.primary {
font-weight: bold;
color: var(--fgOnAccent) !important;
background: var(--accent);
color: var(--MI_THEME-fgOnAccent) !important;
background: var(--MI_THEME-accent);
&:not(:disabled):hover {
background: hsl(from var(--accent) h s calc(l + 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
background: hsl(from var(--accent) h s calc(l + 5));
background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
}
@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void {
&.gradate {
font-weight: bold;
color: var(--fgOnAccent) !important;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--MI_THEME-fgOnAccent) !important;
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}

View File

@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
<div ref="captchaEl"></div>
</div>
<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
<div v-if="testcaptchaPassed">
<div style="color: green;">Test captcha passed!</div>
</div>
<div v-else>
<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
</div>
</div>
<div v-else ref="captchaEl"></div>
</div>
</template>
@ -29,7 +40,7 @@ export type Captcha = {
getResponse(id: string): string;
};
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@ -54,12 +65,16 @@ const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false);
const variable = computed(() => {
switch (props.provider) {
case 'hcaptcha': return 'hcaptcha';
case 'recaptcha': return 'grecaptcha';
case 'turnstile': return 'turnstile';
case 'mcaptcha': return 'mcaptcha';
case 'testcaptcha': return 'testcaptcha';
}
});
@ -71,6 +86,7 @@ const src = computed(() => {
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
case 'mcaptcha': return null;
case 'testcaptcha': return null;
}
});
@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
if (loaded || props.provider === 'mcaptcha') {
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') {
function reset() {
if (captcha.value.reset) captcha.value.reset();
testcaptchaPassed.value = false;
testcaptchaInput.value = '';
}
async function requestRender() {
@ -127,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) {
}
}
function testcaptchaSubmit() {
testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
if (!testcaptchaPassed.value) testcaptchaInput.value = '';
}
onMounted(() => {
if (available.value) {
window.addEventListener('message', onReceivedMessage);

View File

@ -68,9 +68,9 @@ async function onClick() {
position: relative;
display: inline-block;
font-weight: bold;
color: var(--accent);
color: var(--MI_THEME-accent);
background: transparent;
border: solid 1px var(--accent);
border: solid 1px var(--MI_THEME-accent);
padding: 0;
height: 31px;
font-size: 16px;
@ -99,17 +99,17 @@ async function onClick() {
}
&.active {
color: var(--fgOnAccent);
background: var(--accent);
color: var(--MI_THEME-fgOnAccent);
background: var(--MI_THEME-accent);
&:hover {
background: var(--accentLighten);
border-color: var(--accentLighten);
background: var(--MI_THEME-accentLighten);
border-color: var(--MI_THEME-accentLighten);
}
&:active {
background: var(--accentDarken);
border-color: var(--accentDarken);
background: var(--MI_THEME-accentDarken);
border-color: var(--MI_THEME-accentDarken);
}
}

View File

@ -100,7 +100,7 @@ const bannerStyle = computed(() => {
height: 100%;
border-radius: inherit;
pointer-events: none;
box-shadow: inset 0 0 0 2px var(--focus);
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
}
> .name {
@ -148,7 +148,7 @@ const bannerStyle = computed(() => {
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
color: var(--warn);
color: var(--MI_THEME-warn);
border-radius: 6px;
font-weight: bold;
font-size: 1em;
@ -167,7 +167,7 @@ const bannerStyle = computed(() => {
> footer {
padding: 12px 16px;
border-top: solid 0.5px var(--divider);
border-top: solid 0.5px var(--MI_THEME-divider);
> span {
opacity: 0.7;
@ -213,8 +213,8 @@ const bannerStyle = computed(() => {
top: 0;
right: 0;
transform: translate(25%, -25%);
background-color: var(--accent);
border: solid var(--bg) 4px;
background-color: var(--MI_THEME-accent);
border: solid var(--MI_THEME-bg) 4px;
border-radius: 100%;
width: 1.5rem;
height: 1.5rem;

View File

@ -863,8 +863,8 @@ onMounted(() => {
left: 0;
width: 100%;
height: 100%;
-webkit-backdrop-filter: var(--blur, blur(12px));
backdrop-filter: var(--blur, blur(12px));
-webkit-backdrop-filter: var(--MI-blur, blur(12px));
backdrop-filter: var(--MI-blur, blur(12px));
display: flex;
justify-content: center;
align-items: center;

View File

@ -53,11 +53,11 @@ defineExpose({
> .item {
font-size: 85%;
padding: 4px 12px 4px 8px;
border: solid 1px var(--divider);
border: solid 1px var(--MI_THEME-divider);
border-radius: 999px;
&:hover {
border-color: var(--inputBorderHover);
border-color: var(--MI_THEME-inputBorderHover);
}
&.disabled {

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