Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2023-07-17 17:28:23 +09:00
commit 26a2604134
91 changed files with 3186 additions and 1034 deletions

View file

@ -30,6 +30,10 @@ url: https://example.tld/
# The port that your CherryPick server should listen on. # The port that your CherryPick server should listen on.
port: 3000 port: 3000
# You can also use UNIX domain socket.
# socket: /path/to/misskey.sock
# chmodSocket: '777'
# ┌──────────────────────────┐ # ┌──────────────────────────┐
#───┘ PostgreSQL configuration └──────────────────────────────── #───┘ PostgreSQL configuration └────────────────────────────────
@ -104,6 +108,7 @@ redis:
# apiKey: '' # apiKey: ''
# ssl: true # ssl: true
# index: '' # index: ''
# scope: local
# ┌───────────────┐ # ┌───────────────┐
#───┘ ID generation └─────────────────────────────────────────── #───┘ ID generation └───────────────────────────────────────────

View file

@ -17,6 +17,10 @@
### General ### General
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました - identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
- 招待機能を改善しました
* 過去に発行した招待コードを確認できるようになりました
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
### Client ### Client
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように - deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
@ -32,6 +36,9 @@
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
- オフライン時の画面にリロードボタンを追加 - オフライン時の画面にリロードボタンを追加
- Deckで非ルートページにアクセスした際に簡易UIで表示しない設定を追加
- ロール設定画面でロールIDを確認できるように
- コンテキストメニュー表示時のパフォーマンスを改善
- Fix: サーバーメトリクスが90度傾いている - Fix: サーバーメトリクスが90度傾いている
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
@ -40,13 +47,21 @@
- Fix: フォルダーのページネーションが機能しない #11180 - Fix: フォルダーのページネーションが機能しない #11180
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正 - Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
- Fix: システムフォント設定が正しく反映されない問題を修正 - Fix: システムフォント設定が正しく反映されない問題を修正
- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正
- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更
### Server ### Server
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように - nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
- 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用 - 連合の配送ジョブのパフォーマンスを向上ロック機構の見直し、Redisキャッシュの活用
- 全体的なDBクエリのパフォーマンスを向上
- featuredートのsignedGet回数を減らしました - featuredートのsignedGet回数を減らしました
- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加
- MeilisearchにIndexするートの範囲を設定できるように
- Export notes with file detail
- Add unix socket support
- Fix: Remove Meilisearch index when notes are deleted
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正
## 13.13.2 ## 13.13.2

View file

@ -41,19 +41,23 @@ unfavorite: "إزالة من المفضلة"
favorited: "أُضيف إلى المفضلة." favorited: "أُضيف إلى المفضلة."
alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة." alreadyFavorited: "تمت إضافته بالفعل إلى المفضلة."
cantFavorite: "تعذرت الإضافة إلى المفضلة." cantFavorite: "تعذرت الإضافة إلى المفضلة."
pin: "دبّسها على الصفحة الشخصية" pin: "ثبتها على الصفحة الشخصية"
unpin: "ألغ تدبيسها من ملفك الشخصي" unpin: "فكها من ملفك الشخصي"
copyContent: "انسخ المحتوى" copyContent: "انسخ المحتوى"
copyLink: "انسخ الرابط" copyLink: "انسخ الرابط"
delete: "حذف" delete: "حذف"
deleteAndEdit: "إزالة وإعادة الصياغة" deleteAndEdit: "إزالة وإعادة الصياغة"
deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها." deleteAndEditConfirm: "أمتأكد من حذف الملاحظة؟ ستفقد كل مشاركاتها، والتفاعلات، والردود عليها."
addToList: "أضفه إلى قائمة" addToList: "أضفه إلى قائمة"
addToAntenna: "أضف إلى هوائي"
sendMessage: "أرسل رسالة" sendMessage: "أرسل رسالة"
copyRSS: "انسخ رابط RSS" copyRSS: "انسخ رابط RSS"
copyUsername: "انسخ اسم المستخدم" copyUsername: "انسخ اسم المستخدم"
copyUserId: "انسخ معرف المستخدم" copyUserId: "انسخ معرف المستخدم"
copyNoteId: "انسخ معرف الملاحظة" copyNoteId: "انسخ معرف الملاحظة"
copyFileId: "انسخ معرّف الملف"
copyFolderId: "انسخ معرّف المجلد"
copyProfileUrl: "انسخ رابط الملف الشخصي"
searchUser: "ابحث عن مستخدمين" searchUser: "ابحث عن مستخدمين"
reply: "رد" reply: "رد"
loadMore: "عرض المزيد" loadMore: "عرض المزيد"
@ -108,8 +112,8 @@ cantReRenote: "لا يمكنك إعادة نشر ملاحظة معاد نشره
quote: "اقتبس" quote: "اقتبس"
inChannelRenote: "إعادة نشر في قناة" inChannelRenote: "إعادة نشر في قناة"
inChannelQuote: "اقتباس في قناة" inChannelQuote: "اقتباس في قناة"
pinnedNote: "ملاحظة مدبسة" pinnedNote: "ملاحظة مثبتة"
pinned: "دبّسها على الصفحة الشخصية" pinned: "ثبتها على الصفحة الشخصية"
you: "أنت" you: "أنت"
clickToShow: "اضغط للعرض" clickToShow: "اضغط للعرض"
sensitive: "محتوى حساس" sensitive: "محتوى حساس"
@ -136,8 +140,10 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
suspendConfirm: "أمتأكد من تعليق الحساب؟" suspendConfirm: "أمتأكد من تعليق الحساب؟"
unsuspendConfirm: "أمتأكد من إلغاء تعليق؟" unsuspendConfirm: "أمتأكد من إلغاء تعليق؟"
selectList: "اختر قائمة" selectList: "اختر قائمة"
editList: "عدّل القائمة"
selectChannel: "اختر قناة" selectChannel: "اختر قناة"
selectAntenna: "اختر هوائيًا" selectAntenna: "اختر هوائيًا"
editAntenna: "عدّل الهوائي"
selectWidget: "اختر ودجة" selectWidget: "اختر ودجة"
editWidgets: "عدّل الودجات" editWidgets: "عدّل الودجات"
editWidgetsExit: "تم" editWidgetsExit: "تم"
@ -208,7 +214,7 @@ blockedUsers: "الحسابات المحجوبة"
noUsers: "ليس هناك مستخدمون" noUsers: "ليس هناك مستخدمون"
editProfile: "تعديل الملف التعريفي" editProfile: "تعديل الملف التعريفي"
noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟" noteDeleteConfirm: "هل تريد حذف هذه الملاحظة؟"
pinLimitExceeded: "لا يمكنك تدبيس الملاحظات بعد الآن." pinLimitExceeded: "لا يمكنك تثبيت الملاحظات بعد الآن."
intro: "لقد انتهت عملية تنصيب CherryPick. الرجاء إنشاء حساب إداري." intro: "لقد انتهت عملية تنصيب CherryPick. الرجاء إنشاء حساب إداري."
done: "تمّ" done: "تمّ"
processing: "المعالجة جارية" processing: "المعالجة جارية"
@ -307,6 +313,7 @@ copyUrl: "انسخ الرابط"
rename: "إعادة التسمية" rename: "إعادة التسمية"
avatar: "الصورة الرمزية" avatar: "الصورة الرمزية"
banner: "الصورة الرأسية" banner: "الصورة الرأسية"
displayOfSensitiveMedia: "عرض المحتوى الحساس"
whenServerDisconnected: "عند فقدان الاتصال بالخادم" whenServerDisconnected: "عند فقدان الاتصال بالخادم"
disconnectedFromServer: "قُطِع الإتصال بالخادم" disconnectedFromServer: "قُطِع الإتصال بالخادم"
reload: "انعش" reload: "انعش"
@ -345,12 +352,12 @@ iconUrl: "رابط الأيقونة"
bannerUrl: "رابط صورة اللافتة" bannerUrl: "رابط صورة اللافتة"
backgroundImageUrl: "رابط صورة الخلفية" backgroundImageUrl: "رابط صورة الخلفية"
basicInfo: "المعلومات الأساسية " basicInfo: "المعلومات الأساسية "
pinnedUsers: "المستخدمون المدبسون" pinnedUsers: "المستخدمون المثبتون"
pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
pinnedPages: "الصفحات المدبسة" pinnedPages: "الصفحات المثبتة"
pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تدبيسها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
pinnedClipId: "معرّف المشبك المدبس" pinnedClipId: "معرّف المشبك المثبت"
pinnedNotes: "ملاحظة مدبسة" pinnedNotes: "ملاحظة مثبتة"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "فعّل hCaptcha" enableHcaptcha: "فعّل hCaptcha"
hcaptchaSiteKey: "مفتاح الموقع" hcaptchaSiteKey: "مفتاح الموقع"
@ -741,7 +748,7 @@ unlikeConfirm: "أتريد إلغاء إعجابك؟"
fullView: "ملء الشاشة" fullView: "ملء الشاشة"
quitFullView: "اخرج من وضع ملء للشاشة" quitFullView: "اخرج من وضع ملء للشاشة"
addDescription: "أضف وصفًا" addDescription: "أضف وصفًا"
userPagePinTip: "لعرض ملاحظة هنا اختر \"دبسها على الصفحة الشخصية\" من قائمة تلك الملاحظة." userPagePinTip: "لعرض ملاحظة هنا اختر \"ثبتها على الصفحة الشخصية\" من قائمة تلك الملاحظة."
notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها." notSpecifiedMentionWarning: "في الملاحظة ذكر لمستخدمين لن يستلموها."
info: "عن" info: "عن"
userInfo: "معلومات المستخدم" userInfo: "معلومات المستخدم"
@ -849,6 +856,9 @@ oneDay: "يوم"
oneWeek: "أسبوع" oneWeek: "أسبوع"
oneMonth: "شهر" oneMonth: "شهر"
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب" failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
cropImage: "اقتصاص الصورة"
cropImageAsk: "أتريد اقتصاص هذه الصورة"
cropYes: "اقتص"
cropNo: "استخدمها كما هي" cropNo: "استخدمها كما هي"
file: "الملفات" file: "الملفات"
recentNHours: "آخر {n} ساعة" recentNHours: "آخر {n} ساعة"
@ -859,10 +869,12 @@ recommended: "مقترح"
driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم" driveCapOverrideLabel: "غيّر حجم قرص التخزين لهذا المستخدم"
driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل." driveCapOverrideCaption: "أعد الحجم إلى القيمة الافتراضية بإدخال 0 أو أقل."
requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير." requireAdminForView: "لاستعراض هذه الصفحة وجب عليك الولوج كمدير."
isSystemAccount: "حساب أنشأه النظام ويُدار من قِبله."
typeToConfirm: "أدخل {x} للتأكيد" typeToConfirm: "أدخل {x} للتأكيد"
deleteAccount: "احذف الحساب" deleteAccount: "احذف الحساب"
document: "التوثيق" document: "التوثيق"
numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا" numberOfPageCache: "عدد الصفحات المخزنة مؤقتًا"
numberOfPageCacheDescription: "رفع الرقم سيسحن تجربة المستخدم لكن سيرفع استهلاك الذاكرة."
logoutConfirm: "أتريد الخروج؟" logoutConfirm: "أتريد الخروج؟"
lastActiveDate: "آخر استخدام" lastActiveDate: "آخر استخدام"
statusbar: "شريط الحالة" statusbar: "شريط الحالة"
@ -921,6 +933,7 @@ color: "اللون"
manageCustomEmojis: "إدارة الإيموجي المخصصة" manageCustomEmojis: "إدارة الإيموجي المخصصة"
youCannotCreateAnymore: "وصلت لسقف الإنشاء." youCannotCreateAnymore: "وصلت لسقف الإنشاء."
cannotPerformTemporary: "غير متاح مؤقتاً" cannotPerformTemporary: "غير متاح مؤقتاً"
invalidParamError: "معاملات غير صالحة"
permissionDeniedError: "رُفضة العملية" permissionDeniedError: "رُفضة العملية"
preset: "إعدادات مسبقة" preset: "إعدادات مسبقة"
selectFromPresets: "اختر من الإعدادات المسبقة" selectFromPresets: "اختر من الإعدادات المسبقة"
@ -994,6 +1007,10 @@ _initialAccountSetting:
profileSetting: "إعدادات الملف الشخصي" profileSetting: "إعدادات الملف الشخصي"
privacySetting: "إعدادات الخصوصية" privacySetting: "إعدادات الخصوصية"
theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا." theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا."
skipAreYouSure: "أتريد تخطي إعداد الملف الشخصي؟"
laterAreYouSure: "أتريد إعداد الملف الشخصي لاحقًا؟"
_serverRules:
description: "مجموعة من القواعد لعرضها عند التسجيل، من المستحسن كتابة ملخصٍ للشروط الخدمة."
_accountMigration: _accountMigration:
moveFrom: "انقل حسابًا آخر لهذا الحساب" moveFrom: "انقل حسابًا آخر لهذا الحساب"
moveFromLabel: "الحساب الأصلي #{n}" moveFromLabel: "الحساب الأصلي #{n}"
@ -1077,6 +1094,7 @@ _role:
high: "عالية" high: "عالية"
_options: _options:
canManageCustomEmojis: "إدارة الإيموجي المخصصة" canManageCustomEmojis: "إدارة الإيموجي المخصصة"
pinMax: "حد عدد الملاحظات المثبتة"
_condition: _condition:
isLocal: "مستخدم محلي" isLocal: "مستخدم محلي"
isRemote: "مستخدم بعيد" isRemote: "مستخدم بعيد"
@ -1476,7 +1494,7 @@ _pages:
url: "رابط الصفحة" url: "رابط الصفحة"
summary: "ملخص الصفحة" summary: "ملخص الصفحة"
alignCenter: "توسيط العناصر" alignCenter: "توسيط العناصر"
hideTitleWhenPinned: "اخف عنوان الصفحة عند تدبيسها في ملف الشخصي" hideTitleWhenPinned: "اخف عنوان الصفحة عند تثبيتها في ملف الشخصي"
font: "الخط" font: "الخط"
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"

View file

@ -49,6 +49,7 @@ delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten" deleteAndEdit: "Löschen und Bearbeiten"
deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen."
addToList: "Zu Liste hinzufügen" addToList: "Zu Liste hinzufügen"
addToAntenna: "Zu Antenne hinzufügen"
sendMessage: "Nachricht senden" sendMessage: "Nachricht senden"
copyRSS: "RSS kopieren" copyRSS: "RSS kopieren"
copyUsername: "Benutzernamen kopieren" copyUsername: "Benutzernamen kopieren"
@ -56,6 +57,7 @@ copyUserId: "Benutzer-ID kopieren"
copyNoteId: "Notiz-ID kopieren" copyNoteId: "Notiz-ID kopieren"
copyFileId: "Datei-ID kopieren" copyFileId: "Datei-ID kopieren"
copyFolderId: "Ordner-ID kopieren" copyFolderId: "Ordner-ID kopieren"
copyProfileUrl: "Profil-URL kopieren"
searchUser: "Nach einem Benutzer suchen" searchUser: "Nach einem Benutzer suchen"
reply: "Antworten" reply: "Antworten"
loadMore: "Mehr laden" loadMore: "Mehr laden"
@ -154,6 +156,8 @@ addEmoji: "Emoji hinzufügen"
settingGuide: "Empfohlene Einstellung" settingGuide: "Empfohlene Einstellung"
cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern" cacheRemoteFiles: "Dateien von fremden Instanzen im Cache speichern"
cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet." cacheRemoteFilesDescription: "Ist diese Einstellung deaktiviert, so werden Dateien fremder Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz auf diesem Server gespart, aber durch fehlende Generierung von Vorschaubildern mehr Bandbreite verwendet."
cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern"
cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen."
flagAsBot: "Als Bot markieren" flagAsBot: "Als Bot markieren"
flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt CherryPicks interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt CherryPicks interne Systeme dieses Benutzerkonto als Bot behandeln."
flagAsCat: "Als Katze markieren" flagAsCat: "Als Katze markieren"
@ -1084,6 +1088,23 @@ branding: "Branding"
enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen" enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen"
enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren" enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren"
turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen." turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen."
createInviteCode: "Einladung erstellen"
createWithOptions: "Einladung mit Optionen erstellen"
createCount: "Einladungsanzahl"
inviteCodeCreated: "Einladung erstellt"
inviteLimitExceeded: "Du hast das Maximum an erstellbaren Einladungen erreicht."
createLimitRemaining: "Erstellbare Einladungen: Noch {limit}"
inviteLimitResetCycle: "Am {time} wird dies auf {limit} zurückgesetzt."
expirationDate: "Ablaufdatum"
noExpirationDate: "Keins"
inviteCodeUsedAt: "Einladung verwendet am"
registeredUserUsingInviteCode: "Einladung verwendet von"
waitingForMailAuth: "Bestätigungsemail ausstehend"
inviteCodeCreator: "Einladung erstellt von"
usedAt: "Benutzt am"
unused: "Unbenutzt"
used: "Benutzt"
expired: "Abgelaufen"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Dein Konto wurde erfolgreich erstellt!" accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten." letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@ -1394,6 +1415,9 @@ _role:
ltlAvailable: "Kann auf die lokale Chronik zugreifen" ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen" canPublicNote: "Kann öffentliche Notizen erstellen"
canInvite: "Erstellung von Einladungscodes für diese Instanz" canInvite: "Erstellung von Einladungscodes für diese Instanz"
inviteLimit: "Maximalanzahl an Einladungen"
inviteLimitCycle: "Zyklus des Einladungslimits"
inviteExpirationTime: "Gültigkeitsdauer von Einladungen"
canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten" canManageCustomEmojis: "Benutzerdefinierte Emojis verwalten"
driveCapacity: "Drive-Kapazität" driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren" alwaysMarkNsfw: "Dateien immer als NSFW markieren"

View file

@ -81,6 +81,7 @@ deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? Yo
copyAndEdit: "Copy and edit" copyAndEdit: "Copy and edit"
copyAndEditConfirm: "Are you sure you want to copy this note and edit it? The media included in the notes are also copied." copyAndEditConfirm: "Are you sure you want to copy this note and edit it? The media included in the notes are also copied."
addToList: "Add to list" addToList: "Add to list"
addToAntenna: "Add to antenna"
sendMessage: "Send a message" sendMessage: "Send a message"
copyRSS: "Copy RSS" copyRSS: "Copy RSS"
copyUsername: "Copy username" copyUsername: "Copy username"
@ -88,6 +89,7 @@ copyUserId: "Copy user ID"
copyNoteId: "Copy note ID" copyNoteId: "Copy note ID"
copyFileId: "Copy file ID" copyFileId: "Copy file ID"
copyFolderId: "Copy folder ID" copyFolderId: "Copy folder ID"
copyProfileUrl: "Copy profile URL"
searchUser: "Search for a user" searchUser: "Search for a user"
reply: "Reply" reply: "Reply"
loadMore: "Load more" loadMore: "Load more"
@ -186,6 +188,8 @@ addEmoji: "Add an emoji"
settingGuide: "Recommended settings" settingGuide: "Recommended settings"
cacheRemoteFiles: "Cache remote files" cacheRemoteFiles: "Cache remote files"
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated."
cacheRemoteSensitiveFiles: "Cache sensitive remote files"
cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching."
flagAsBot: "Mark this account as a bot" flagAsBot: "Mark this account as a bot"
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust CherryPick's internal systems to treat this account as a bot." flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust CherryPick's internal systems to treat this account as a bot."
flagAsCat: "Mark this account as a cat" flagAsCat: "Mark this account as a cat"
@ -1125,6 +1129,23 @@ branding: "Branding"
enableServerMachineStats: "Publish server hardware stats" enableServerMachineStats: "Publish server hardware stats"
enableIdenticonGeneration: "Enable user identicon generation" enableIdenticonGeneration: "Enable user identicon generation"
turnOffToImprovePerformance: "Turning this off can increase performance." turnOffToImprovePerformance: "Turning this off can increase performance."
createInviteCode: "Generate invite"
createWithOptions: "Generate with options"
createCount: "Invite count"
inviteCodeCreated: "Invite generated"
inviteLimitExceeded: "You've exceeded the limit of invites you can generate."
createLimitRemaining: "Invite limit: {limit} remaining"
inviteLimitResetCycle: "This limit will reset to {limit} at {time}."
expirationDate: "Expiration date"
noExpirationDate: "No expiration"
inviteCodeUsedAt: "Invite code used at"
registeredUserUsingInviteCode: "Invite used by"
waitingForMailAuth: "Email verification pending"
inviteCodeCreator: "Invite created by"
usedAt: "Used at"
unused: "Unused"
used: "Used"
expired: "Expired"
additionalPermissionsForFlash: "Allow to add permission to Play" additionalPermissionsForFlash: "Allow to add permission to Play"
thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions" thisFlashRequiresTheFollowingPermissions: "This Play requires the following permissions"
doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?" doYouWantToAllowThisPlayToAccessYourAccount: "Do you want to allow this Play to access your account?"
@ -1499,6 +1520,9 @@ _role:
ctlAvailable: "Can view the cat timeline" ctlAvailable: "Can view the cat timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canInvite: "Can create instance invite codes" canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis" canManageCustomEmojis: "Can manage custom emojis"
driveCapacity: "Drive capacity" driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW" alwaysMarkNsfw: "Always mark files as NSFW"

View file

@ -49,11 +49,15 @@ delete: "Borrar"
deleteAndEdit: "Borrar y editar" deleteAndEdit: "Borrar y editar"
deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas." deleteAndEditConfirm: "¿Estás seguro de que quieres borrar esta nota y editarla? Perderás todas las reacciones, renotas y respuestas."
addToList: "Agregar a lista" addToList: "Agregar a lista"
addToAntenna: "Añadir a la antena"
sendMessage: "Enviar un mensaje" sendMessage: "Enviar un mensaje"
copyRSS: "Copiar RSS" copyRSS: "Copiar RSS"
copyUsername: "Copiar nombre de usuario" copyUsername: "Copiar nombre de usuario"
copyUserId: "Copiar ID del usuario" copyUserId: "Copiar ID del usuario"
copyNoteId: "Copiar ID de la nota" copyNoteId: "Copiar ID de la nota"
copyFileId: "Copiar un archivo ID"
copyFolderId: "Copiar carpeta ID"
copyProfileUrl: "Copiar la URL del perfil"
searchUser: "Buscar un usuario" searchUser: "Buscar un usuario"
reply: "Responder" reply: "Responder"
loadMore: "Ver más" loadMore: "Ver más"
@ -152,6 +156,8 @@ addEmoji: "Agregar emoji"
settingGuide: "Configuración sugerida" settingGuide: "Configuración sugerida"
cacheRemoteFiles: "Mantener en cache los archivos remotos" cacheRemoteFiles: "Mantener en cache los archivos remotos"
cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas." cacheRemoteFilesDescription: "Si desactiva esta configuración, Los archivos remotos se cargarán desde el link directo sin usar la caché. Con eso se puede ahorrar almacenamiento del servidor, pero eso aumentará el tráfico al no crear miniaturas."
cacheRemoteSensitiveFiles: "Cachear archivos remotos sensibles"
cacheRemoteSensitiveFilesDescription: "Cuando esta opción está desactivada, los archivos remotos sensibles son cargador directamente de la instancia origen sin ser cacheados."
flagAsBot: "Esta cuenta es un bot" flagAsBot: "Esta cuenta es un bot"
flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de CherryPick para que trate a esta cuenta como un bot." flagAsBotDescription: "En caso de que esta cuenta fuera usada por un programa, active esta opción. Al hacerlo, esta opción servirá para otros desarrolladores para evitar cadenas infinitas de reacciones, y ajustará los sistemas internos de CherryPick para que trate a esta cuenta como un bot."
flagAsCat: "Esta cuenta es un gato" flagAsCat: "Esta cuenta es un gato"
@ -313,6 +319,7 @@ copyUrl: "Copiar URL"
rename: "Renombrar" rename: "Renombrar"
avatar: "Avatar" avatar: "Avatar"
banner: "Banner" banner: "Banner"
displayOfSensitiveMedia: "Mostrar contenido sensible"
whenServerDisconnected: "Cuando se pierda la conexión con el servidor" whenServerDisconnected: "Cuando se pierda la conexión con el servidor"
disconnectedFromServer: "Desconectado del servidor" disconnectedFromServer: "Desconectado del servidor"
reload: "Recargar" reload: "Recargar"
@ -859,7 +866,7 @@ manageAccounts: "Administrar cuenta"
makeReactionsPublic: "Hacer el historial de reacciones público" makeReactionsPublic: "Hacer el historial de reacciones público"
makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles." makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles."
classic: "Clásico" classic: "Clásico"
muteThread: "Ocultar hilo" muteThread: "Silenciar hilo"
unmuteThread: "Mostrar hilo" unmuteThread: "Mostrar hilo"
ffVisibility: "Visibilidad de seguidores y seguidos" ffVisibility: "Visibilidad de seguidores y seguidos"
ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen" ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen"
@ -1008,10 +1015,12 @@ reactionAcceptance: "Aceptación de reacciones"
likeOnly: "Sólo 'me gusta'" likeOnly: "Sólo 'me gusta'"
likeOnlyForRemote: "Sólo reacciones de instancias remotas" likeOnlyForRemote: "Sólo reacciones de instancias remotas"
nonSensitiveOnly: "Solo no sensible" nonSensitiveOnly: "Solo no sensible"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Sólo no contenido sensible (sólo me gusta en remote)"
rolesAssignedToMe: "Roles asignados a mí" rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?" resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles" sensitiveWords: "Palabras sensibles"
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea" sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
sensitiveWordsDescription2: "Si se usan espacios se crearán expresiones AND y las palabras subsecuentes con barras inclinadas se convertirán en expresiones regulares."
notesSearchNotAvailable: "No se puede buscar una nota" notesSearchNotAvailable: "No se puede buscar una nota"
license: "Licencia" license: "Licencia"
unfavoriteConfirm: "¿Desea quitar de favoritos?" unfavoriteConfirm: "¿Desea quitar de favoritos?"
@ -1031,25 +1040,71 @@ dataSaver: "Ahorro de datos"
accountMigration: "Migración de cuenta" accountMigration: "Migración de cuenta"
accountMoved: "Este usuario se movió a una nueva cuenta:" accountMoved: "Este usuario se movió a una nueva cuenta:"
accountMovedShort: "Esta cuenta ha sido migrada." accountMovedShort: "Esta cuenta ha sido migrada."
operationForbidden: "Operación prohibida"
forceShowAds: "Siempre mostrar anuncios"
addMemo: "Añadir nota" addMemo: "Añadir nota"
editMemo: "Editar nota" editMemo: "Editar nota"
reactionsList: "Lista de reacciones" reactionsList: "Lista de reacciones"
renotesList: "Renotas" renotesList: "Renotas"
notificationDisplay: "Notificaciones"
leftTop: "Arriba a la izquierda"
rightTop: "Arriba a la derecha"
leftBottom: "Abajo a la izquierda"
rightBottom: "Abajo a la derecha"
stackAxis: "Dirección de apilado" stackAxis: "Dirección de apilado"
vertical: "Vertical"
horizontal: "Horizontal" horizontal: "Horizontal"
position: "Posición" position: "Posición"
serverRules: "Reglas del servidor" serverRules: "Reglas del servidor"
pleaseConfirmBelowBeforeSignup: "Por favor confirma antes de continuar el registro"
pleaseAgreeAllToContinue: "Tienes que estar de acuerdo con los campos anteriores para contnuar."
continue: "Continuar" continue: "Continuar"
preservedUsernames: "Nombre de usuario reservado" preservedUsernames: "Nombre de usuario reservado"
preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de línea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas."
createNoteFromTheFile: "Componer una nota desde éste archivo"
archive: "Archivo" archive: "Archivo"
channelArchiveConfirmTitle: "¿Seguro de archivar {name}?" channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas."
thisChannelArchived: "El canal ha sido archivado."
displayOfNote: "Mostrar notas"
initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio"
youFollowing: "Siguiendo" youFollowing: "Siguiendo"
preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)"
preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada."
options: "Opción" options: "Opción"
specifyUser: "Especificar usuario"
failedToPreviewUrl: "No se pudo generar la vista previa"
update: "Actualizar" update: "Actualizar"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles que pueden usar este emoji como reacción"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Si no se especifican roles, cualquiera podrá usar éste emoji como reacción."
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Éstos roles deben ser públicos."
cancelReactionConfirm: "¿Realmente quieres eliminar la reacción?"
changeReactionConfirm: "¿Realmente quieres cambiar la reacción?"
later: "Ahora no"
goToMisskey: "ir a CherryPick"
additionalEmojiDictionary: "Diccionario adicional de Emoji"
installed: "Instalado" installed: "Instalado"
branding: "Marca" branding: "Marca"
enableServerMachineStats: "Publicar estadísticas de hardware del servidor" enableServerMachineStats: "Publicar estadísticas de hardware del servidor"
enableIdenticonGeneration: "Activar generación de identicon por usuario" enableIdenticonGeneration: "Activar generación de identicon por usuario"
turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento."
createInviteCode: "Generar invitación"
createWithOptions: "Generar con opciones"
createCount: "Conteo de invitaciones"
inviteCodeCreated: "Invitación generada"
inviteLimitExceeded: "Has excedido el límite de invitaciones que puedes generar."
createLimitRemaining: "Límite de invitaciones: quedan {limit}"
inviteLimitResetCycle: "El límite ha sido reiniciado a {limit} por {time}."
expirationDate: "Fecha de caducidad"
noExpirationDate: "Sin caducidad"
inviteCodeUsedAt: "Código de invitación usado el"
registeredUserUsingInviteCode: "Invitación usada por"
waitingForMailAuth: "Verificación de correo pendiente"
inviteCodeCreator: "Invitación creada por"
usedAt: "Usada el"
unused: "Sin usar"
used: "Usada"
expired: "Caducada"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "¡La cuenta ha sido creada!" accountCreated: "¡La cuenta ha sido creada!"
letsStartAccountSetup: "Para empezar, creemos tu perfil." letsStartAccountSetup: "Para empezar, creemos tu perfil."
@ -1059,14 +1114,28 @@ _initialAccountSetting:
theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde." theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde."
youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después." youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después."
followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo." followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo."
pushNotificationDescription: "Habilitar las notificaciones push te permitirá recibir notificaciones de {name} directamente en tu dispositivo."
initialAccountSettingCompleted: "¡Configuración del perfil completada!"
haveFun: "¡Disfruta de {name}!"
ifYouNeedLearnMore: "Si quieres aprender cómo usar {name} (CherryPick), por favor, visita {link}."
skipAreYouSure: "¿Realmente quieres saltarte la configuración del perfil?"
laterAreYouSure: "¿Realmente quieres configurar tu perfil después?"
_serverRules:
description: "Un conjunto de reglas que serán mostradas antes del registro. Configurar un sumario de términos de servicio es recomendado."
_accountMigration: _accountMigration:
moveFrom: "Trasladar de otra cuenta a ésta" moveFrom: "Trasladar de otra cuenta a ésta"
moveFromSub: "Crear un alias para otra cuenta."
moveFromLabel: "Cuenta desde la que se realiza el traslado:" moveFromLabel: "Cuenta desde la que se realiza el traslado:"
moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com" moveFromDescription: "Si quieres transferir seguidores de otra cuenta a esta cuenta y trasladarlos, tendrás que crear un alias aquí. Asegúrate de crearlo antes de realizar el traslado. Introduce la cuenta desde la que estás moviendo los seguidores así: @person@instance.com"
moveTo: "Mover esta cuenta a una nueva" moveTo: "Mover esta cuenta a una nueva"
moveToLabel: "Cuenta destino:" moveToLabel: "Cuenta destino:"
moveCannotBeUndone: "La migración de la cuenta no puede ser revertida."
moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com" moveAccountDescription: "Esta operación no puede deshacerse. En primer lugar, asegúrese de haber creado un alias para esta cuenta en la cuenta a la que se va a trasladar. Después de crear el alias, introduzca la cuenta a la que se está trasladando de la siguiente manera: @person@instance.com"
moveAccountHowTo: "Para migrar, primero crea un alias para ésta cuenta en la cuenta a donde te moverás.\nDespués de crear el alias, ingresa la cuenta a mover de la siguiente forma:\n@usuario@servidor.ejempo.com"
startMigration: "Migrar"
migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado." migrationConfirm: "¿Estás seguro de que quieres mover esta cuenta a {account}? Una vez trasladada, no podrás deshacer el traslado y no podrás volver a utilizar la cuenta original.\n\nAdemás, compruebe que ha configurado un alias en el destino del traslado."
movedAndCannotBeUndone: "\nLa migración decuenta ha sido completada.\nNo se puede revertir éste proceso."
postMigrationNote: "Ésta cuenta dejará de seguir a todas las cuentas en las siguientes 24 horas después de que finalice la migración.\nEl número de seguidos y seguidores serán 0. Para evitar que Para evitar que tus seguidores dejen de ver las publicaciones, todas serán marcadas como \"sólo seguidores\"."
movedTo: "Cuenta destino:" movedTo: "Cuenta destino:"
_achievements: _achievements:
earnedAt: "Desbloqueado el" earnedAt: "Desbloqueado el"
@ -1241,6 +1310,7 @@ _achievements:
description: "30 minutos dedicados a CherryPick" description: "30 minutos dedicados a CherryPick"
_client60min: _client60min:
title: "Viendo mucho CherryPick." title: "Viendo mucho CherryPick."
description: "Dejar abierto CherryPick por al menos 60 minutos"
_noteDeletedWithin1min: _noteDeletedWithin1min:
title: "Ah... Mejor no..." title: "Ah... Mejor no..."
description: "Borrar una nota antes que de pase 1 minuto" description: "Borrar una nota antes que de pase 1 minuto"
@ -1329,6 +1399,8 @@ _role:
iconUrl: "URL del ícono" iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema" asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo." descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
isExplorable: "Hacer el rol explorable"
descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.."
displayOrder: "Posición" displayOrder: "Posición"
descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz." descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros" canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
@ -1343,8 +1415,12 @@ _role:
ltlAvailable: "Explorar la línea de tiempo local" ltlAvailable: "Explorar la línea de tiempo local"
canPublicNote: "Permitir la publicación" canPublicNote: "Permitir la publicación"
canInvite: "Puede crear códigos de invitación" canInvite: "Puede crear códigos de invitación"
inviteLimit: "Límite de invitaciones"
inviteLimitCycle: "Enfriamiento del límite de invitaciones"
inviteExpirationTime: "Intervalo de caducidad de invitaciones"
canManageCustomEmojis: "Administrar emojis personalizados" canManageCustomEmojis: "Administrar emojis personalizados"
driveCapacity: "Capacidad del drive" driveCapacity: "Capacidad del drive"
alwaysMarkNsfw: "Siempre marcar archivos como NSFW"
pinMax: "Máximo de notas fijadas" pinMax: "Máximo de notas fijadas"
antennaMax: "Máximo de antenas" antennaMax: "Máximo de antenas"
wordMuteMax: "Máximo de caracteres en palabras silenciadas" wordMuteMax: "Máximo de caracteres en palabras silenciadas"
@ -1404,6 +1480,7 @@ _ad:
back: "Deseleccionar" back: "Deseleccionar"
reduceFrequencyOfThisAd: "Mostrar menos este anuncio." reduceFrequencyOfThisAd: "Mostrar menos este anuncio."
hide: "No mostrar" hide: "No mostrar"
timezoneinfo: "El día de la semana está determidado por la zona horaria del servidor."
_forgotPassword: _forgotPassword:
enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña." enterEmail: "Ingrese el correo usado para registrar la cuenta. Se enviará un link para resetear la contraseña."
ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador." ifNoEmail: "Si no utilizó un correo para crear la cuenta, contáctese con el administrador."
@ -1455,6 +1532,10 @@ _aboutMisskey:
donate: "Donar a Misskey" donate: "Donar a Misskey"
morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰" morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰"
patrons: "Patrocinadores" patrons: "Patrocinadores"
_displayOfSensitiveMedia:
respect: "Esconder medios marcados como sensibles"
ignore: "Mostrar medios marcados como sensibles"
force: "Esconder todala multimedia"
_mfm: _mfm:
cheatSheet: "Hoja de referencia de MFM" cheatSheet: "Hoja de referencia de MFM"
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM." intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM."
@ -1539,6 +1620,8 @@ _channel:
following: "Siguiendo" following: "Siguiendo"
usersCount: "{n} participantes" usersCount: "{n} participantes"
notesCount: "{n} notas" notesCount: "{n} notas"
nameAndDescription: "Nombre y descripción"
nameOnly: "Sólo nombre"
_menuDisplay: _menuDisplay:
sideFull: "Horizontal" sideFull: "Horizontal"
sideIcon: "Horizontal (ícono)" sideIcon: "Horizontal (ícono)"
@ -1657,6 +1740,13 @@ _time:
hour: "Horas" hour: "Horas"
day: "Días" day: "Días"
_timelineTutorial: _timelineTutorial:
title: "Cómo usar CherryPick"
step1_1: "Ésta es la \"línea de tiempo\". Todas las \"notas\" que sean publicadas en {name} serán mostradas cronológicamente aquí."
step1_2: "Hay varias líneas de tiempo. Por ejemplo, la línea temporal \"Inicio\" contiene las notas de otros usuarios que sigues, y la línea \"Local\" contandrá las notas de todos los usuarios de {name}."
step2_1: "Ahora probemos publicar una nota. Puedes hacerlo presionando el botón que tiene un ícono de lápiz."
step2_2: "¿Qué tal si escribimos una introducción? o sólo un \"¡Hola {name}!\" ¿No te apetece?"
step3_1: "¿Terminaste de publicar tu primera nota?"
step3_2: "Tu primera nota ahora se mostrará en tu línea de tiempo."
step4_1: "También puedes añadir \"Reacciones\" a notas." step4_1: "También puedes añadir \"Reacciones\" a notas."
step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar." step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar."
_2fa: _2fa:
@ -1992,6 +2082,7 @@ _deck:
introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!" introduction: "¡Crea la interfaz perfecta para tí organizando las columnas libremente!"
introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras."
widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget."
useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raíz con la interfaz simple"
_columns: _columns:
main: "Principal" main: "Principal"
widgets: "Widgets" widgets: "Widgets"
@ -2002,6 +2093,7 @@ _deck:
channel: "Canal" channel: "Canal"
mentions: "Menciones" mentions: "Menciones"
direct: "Notas directas" direct: "Notas directas"
roleTimeline: "Linea de tiempo del rol"
_dialog: _dialog:
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}."
charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."

View file

@ -52,6 +52,11 @@ addToList: "Ajouter à une liste"
sendMessage: "Envoyer un message" sendMessage: "Envoyer un message"
copyRSS: "Copier le RSS" copyRSS: "Copier le RSS"
copyUsername: "Copier le nom dutilisateur·rice" copyUsername: "Copier le nom dutilisateur·rice"
copyUserId: "Copier l'identifiant de l'utilisateur"
copyNoteId: "Copier l'identifiant de la note"
copyFileId: "Copier l'identifiant du fichier"
copyFolderId: "Copier l'identifiant du dossier"
copyProfileUrl: "Copier l'URL du profil"
searchUser: "Chercher un·e utilisateur·rice" searchUser: "Chercher un·e utilisateur·rice"
reply: "Répondre" reply: "Répondre"
loadMore: "Afficher plus …" loadMore: "Afficher plus …"
@ -134,6 +139,7 @@ unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce com
selectList: "Sélectionner une liste" selectList: "Sélectionner une liste"
selectChannel: "Sélectionner un canal" selectChannel: "Sélectionner un canal"
selectAntenna: "Sélectionner une antenne" selectAntenna: "Sélectionner une antenne"
editAntenna: "Modifier l'antenne"
selectWidget: "Sélectionner un widget" selectWidget: "Sélectionner un widget"
editWidgets: "Modifier les widgets" editWidgets: "Modifier les widgets"
editWidgetsExit: "Valider les modifications" editWidgetsExit: "Valider les modifications"
@ -146,6 +152,8 @@ addEmoji: "Ajouter un émoji"
settingGuide: "Configuration proposée" settingGuide: "Configuration proposée"
cacheRemoteFiles: "Mise en cache des fichiers distants" cacheRemoteFiles: "Mise en cache des fichiers distants"
cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées." cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis linstance distante. La désactiver diminuera certes lutilisation de lespace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées."
cacheRemoteSensitiveFiles: "Mettre en cache les fichiers distants sensibles"
cacheRemoteSensitiveFilesDescription: "Si vous désactivez ce paramètre, les fichiers sensibles distants ne seront pas mis en cache et un lien direct sera utilisé à la place"
flagAsBot: "Ce compte est un robot" flagAsBot: "Ce compte est un robot"
flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de CherryPick pour traiter ce compte comme un robot." flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de CherryPick pour traiter ce compte comme un robot."
flagAsCat: "Ce compte est un chat" flagAsCat: "Ce compte est un chat"
@ -154,6 +162,7 @@ flagShowTimelineReplies: "Afficher les réponses dans le fil"
flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée." flagShowTimelineRepliesDescription: "Affiche les réponses des utilisateurs aux notes des autres utilisateurs dans la timeline si cette option est activée."
autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez" autoAcceptFollowed: "Accepter automatiquement les demandes dabonnement venant dutilisateur·rice·s que vous suivez"
addAccount: "Ajouter un compte" addAccount: "Ajouter un compte"
reloadAccountsList: "Rafraichir la liste des comptes"
loginFailed: "Échec de la connexion" loginFailed: "Échec de la connexion"
showOnRemote: "Voir sur linstance distante" showOnRemote: "Voir sur linstance distante"
general: "Général" general: "Général"
@ -260,6 +269,8 @@ noMoreHistory: "Il ny a plus dhistorique"
startMessaging: "Commencer à discuter" startMessaging: "Commencer à discuter"
nUsersRead: "Lu par {n} personnes" nUsersRead: "Lu par {n} personnes"
agreeTo: "Jaccepte {0}" agreeTo: "Jaccepte {0}"
agree: "Accepter"
termsOfService: "Conditions d'utilisation"
start: "Commencer" start: "Commencer"
home: "Principal" home: "Principal"
remoteUserCaution: "Les informations de ce compte risqueraient dêtre incomplètes du fait que lutilisateur·rice provient dune instance distante." remoteUserCaution: "Les informations de ce compte risqueraient dêtre incomplètes du fait que lutilisateur·rice provient dune instance distante."
@ -302,6 +313,7 @@ copyUrl: "Copier lURL"
rename: "Renommer" rename: "Renommer"
avatar: "Avatar" avatar: "Avatar"
banner: "Bannière" banner: "Bannière"
displayOfSensitiveMedia: "Afficher les médias sensibles"
whenServerDisconnected: "Lorsque la connexion au serveur est perdue" whenServerDisconnected: "Lorsque la connexion au serveur est perdue"
disconnectedFromServer: "Déconnecté·e du serveur" disconnectedFromServer: "Déconnecté·e du serveur"
reload: "Rafraîchir" reload: "Rafraîchir"
@ -391,11 +403,15 @@ about: "Informations"
aboutMisskey: "À propos de CherryPick" aboutMisskey: "À propos de CherryPick"
administrator: "Administrateur" administrator: "Administrateur"
token: "Jeton" token: "Jeton"
2fa: "Authentification à deux facteurs"
totp: "Application d'authentification"
totpDescription: "Entrez un mot de passe à usage unique à l'aide d'une application d'authentification"
moderator: "Modérateur·rice·s" moderator: "Modérateur·rice·s"
moderation: "Modérations" moderation: "Modérations"
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s" nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
securityKey: "Clé de sécurité" securityKey: "Clé de sécurité"
lastUsed: "Dernier utilisé" lastUsed: "Dernier utilisé"
lastUsedAt: "Dernière utilisation : {t}"
unregister: "Se désinscrire" unregister: "Se désinscrire"
passwordLessLogin: "Se connecter sans mot de passe" passwordLessLogin: "Se connecter sans mot de passe"
resetPassword: "Réinitialiser le mot de passe" resetPassword: "Réinitialiser le mot de passe"
@ -545,9 +561,14 @@ userSuspended: "Cet·te utilisateur·rice a été suspendu·e."
userSilenced: "Cette utilisateur·trice a été mis·e en sourdine." userSilenced: "Cette utilisateur·trice a été mis·e en sourdine."
yourAccountSuspendedTitle: "Ce compte est suspendu" yourAccountSuspendedTitle: "Ce compte est suspendu"
yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte." yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte."
tokenRevoked: "Ce jeton est invalide."
tokenRevokedDescription: "Votre jeton de connexion a expiré. Veuillez vous reconnecter."
accountDeleted: "Compte supprimé"
accountDeletedDescription: "Ce compte a été supprimé."
menu: "Menu" menu: "Menu"
divider: "Séparateur" divider: "Séparateur"
addItem: "Ajouter un élément" addItem: "Ajouter un élément"
rearrange: "Trier par"
relays: "Relais" relays: "Relais"
addRelay: "Ajouter un relais" addRelay: "Ajouter un relais"
inboxUrl: "Inbox URL" inboxUrl: "Inbox URL"
@ -689,6 +710,8 @@ contact: "Contact"
useSystemFont: "Utiliser la police par défaut du système" useSystemFont: "Utiliser la police par défaut du système"
clips: "Clips" clips: "Clips"
experimentalFeatures: "Fonctionnalités expérimentales" experimentalFeatures: "Fonctionnalités expérimentales"
experimental: "Expérimental"
thisIsExperimentalFeature: "Ceci est une fonctionnalité expérimentale. Il y a une possibilité que les spécifications changent ou qu'elle ne fonctionne pas correctement."
developer: "Développeur" developer: "Développeur"
makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"." makeExplorable: "Rendre le compte visible sur la page \"Découvrir\"."
makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"." makeExplorableDescription: "Si vous désactivez cette option, votre compte n'apparaîtra pas sur la page \"Découvrir\"."
@ -773,6 +796,7 @@ noMaintainerInformationWarning: "Informations administrateur non configurées."
noBotProtectionWarning: "La protection contre les bots n'est pas configurée." noBotProtectionWarning: "La protection contre les bots n'est pas configurée."
configure: "Configurer" configure: "Configurer"
postToGallery: "Publier dans la galerie" postToGallery: "Publier dans la galerie"
postToHashtag: "Publier avec ce hashtag"
gallery: "Galerie" gallery: "Galerie"
recentPosts: "Les plus récentes" recentPosts: "Les plus récentes"
popularPosts: "Les plus consultées" popularPosts: "Les plus consultées"
@ -811,6 +835,7 @@ lastCommunication: "Dernière communication"
resolved: "Résolu" resolved: "Résolu"
unresolved: "En attente" unresolved: "En attente"
breakFollow: "Ne plus suivre" breakFollow: "Ne plus suivre"
breakFollowConfirm: "Êtes-vous sûr de vouloir vous désabonner?"
itsOn: "Activé" itsOn: "Activé"
itsOff: "Désactivé" itsOff: "Désactivé"
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"

View file

@ -51,11 +51,7 @@ export default function generateDTS() {
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags, ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
), ),
), ),
ts.factory.createExportAssignment( ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
undefined,
true,
ts.factory.createIdentifier('locales'),
),
]; ];
const printed = ts.createPrinter({ const printed = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed, newLine: ts.NewLineKind.LineFeed,

25
locales/index.d.ts vendored
View file

@ -191,6 +191,8 @@ export interface Locale {
"settingGuide": string; "settingGuide": string;
"cacheRemoteFiles": string; "cacheRemoteFiles": string;
"cacheRemoteFilesDescription": string; "cacheRemoteFilesDescription": string;
"cacheRemoteSensitiveFiles": string;
"cacheRemoteSensitiveFilesDescription": string;
"flagAsBot": string; "flagAsBot": string;
"flagAsBotDescription": string; "flagAsBotDescription": string;
"flagAsCat": string; "flagAsCat": string;
@ -1130,6 +1132,23 @@ export interface Locale {
"enableServerMachineStats": string; "enableServerMachineStats": string;
"enableIdenticonGeneration": string; "enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string; "turnOffToImprovePerformance": string;
"createInviteCode": string;
"createWithOptions": string;
"createCount": string;
"inviteCodeCreated": string;
"inviteLimitExceeded": string;
"createLimitRemaining": string;
"inviteLimitResetCycle": string;
"expirationDate": string;
"noExpirationDate": string;
"inviteCodeUsedAt": string;
"registeredUserUsingInviteCode": string;
"waitingForMailAuth": string;
"inviteCodeCreator": string;
"usedAt": string;
"unused": string;
"used": string;
"expired": string;
"additionalPermissionsForFlash": string; "additionalPermissionsForFlash": string;
"thisFlashRequiresTheFollowingPermissions": string; "thisFlashRequiresTheFollowingPermissions": string;
"doYouWantToAllowThisPlayToAccessYourAccount": string; "doYouWantToAllowThisPlayToAccessYourAccount": string;
@ -1589,6 +1608,9 @@ export interface Locale {
"ctlAvailable": string; "ctlAvailable": string;
"canPublicNote": string; "canPublicNote": string;
"canInvite": string; "canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;
"inviteExpirationTime": string;
"canManageCustomEmojis": string; "canManageCustomEmojis": string;
"driveCapacity": string; "driveCapacity": string;
"alwaysMarkNsfw": string; "alwaysMarkNsfw": string;
@ -2338,6 +2360,7 @@ export interface Locale {
"introduction": string; "introduction": string;
"introduction2": string; "introduction2": string;
"widgetsIntroduction": string; "widgetsIntroduction": string;
"useSimpleUiForNonRootPages": string;
"_columns": { "_columns": {
"main": string; "main": string;
"widgets": string; "widgets": string;
@ -2383,4 +2406,4 @@ export interface Locale {
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;
}; };
export = locales; export default locales;

View file

@ -49,11 +49,15 @@ delete: "Elimina"
deleteAndEdit: "Elimina e modifica" deleteAndEdit: "Elimina e modifica"
deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate." deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate."
addToList: "Aggiungi alla lista" addToList: "Aggiungi alla lista"
addToAntenna: "Aggiungi all'antenna"
sendMessage: "Invia messaggio" sendMessage: "Invia messaggio"
copyRSS: "Copia RSS" copyRSS: "Copia RSS"
copyUsername: "Copia nome utente" copyUsername: "Copia nome utente"
copyUserId: "Copia ID del profilo" copyUserId: "Copia ID del profilo"
copyNoteId: "Copia ID della Nota" copyNoteId: "Copia ID della Nota"
copyFileId: "Copia ID del file"
copyFolderId: "Copia ID della cartella"
copyProfileUrl: "Copia URL del profilo"
searchUser: "Cerca profilo" searchUser: "Cerca profilo"
reply: "Rispondi" reply: "Rispondi"
loadMore: "Mostra di più" loadMore: "Mostra di più"
@ -136,8 +140,10 @@ unblockConfirm: "Vuoi davvero sbloccare il profilo?"
suspendConfirm: "Vuoi sospendere questo profilo?" suspendConfirm: "Vuoi sospendere questo profilo?"
unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?" unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?"
selectList: "Seleziona una lista" selectList: "Seleziona una lista"
editList: "Modifica Lista"
selectChannel: "Seleziona canale" selectChannel: "Seleziona canale"
selectAntenna: "Scegli un'antenna" selectAntenna: "Scegli un'antenna"
editAntenna: "Modifica Antenna"
selectWidget: "Seleziona il riquadro" selectWidget: "Seleziona il riquadro"
editWidgets: "Modifica i riquadri" editWidgets: "Modifica i riquadri"
editWidgetsExit: "Conferma le modifiche" editWidgetsExit: "Conferma le modifiche"
@ -150,6 +156,8 @@ addEmoji: "Aggiungi un emoji"
settingGuide: "Configurazione suggerita" settingGuide: "Configurazione suggerita"
cacheRemoteFiles: "Memorizza i file remoti nella cache" cacheRemoteFiles: "Memorizza i file remoti nella cache"
cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime." cacheRemoteFilesDescription: "Disabilitando questa opzione, i file remoti verranno linkati direttamente senza essere memorizzati nella cache. Sarà possibile risparmiare spazio di archiviazione sul server, ma il traffico aumenterà in quanto non verranno generate anteprime."
cacheRemoteSensitiveFiles: "Memorizza nella cache i file sensibili remoti"
cacheRemoteSensitiveFilesDescription: "Disattivando questa opzione, i file sensibili verranno caricati direttamente dall'istanza remota senza essere salvati dal server."
flagAsBot: "Io sono un robot" flagAsBot: "Io sono un robot"
flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di CherryPick si adegueranno al fine di trattare questo profilo come bot." flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente operazioni automatiche. L'attivazione segnala agli altri sviluppatori come comportarsi per evitare catene dinterazione infinite con altri bot. I sistemi interni di CherryPick si adegueranno al fine di trattare questo profilo come bot."
flagAsCat: "Sono un gatto" flagAsCat: "Sono un gatto"
@ -311,6 +319,7 @@ copyUrl: "Copia URL"
rename: "Modifica nome" rename: "Modifica nome"
avatar: "Foto del profilo" avatar: "Foto del profilo"
banner: "Intestazione" banner: "Intestazione"
displayOfSensitiveMedia: "Visibilità dei media sensibili"
whenServerDisconnected: "Quando la connessione col server è persa" whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Il server si è disconnesso" disconnectedFromServer: "Il server si è disconnesso"
reload: "Ricarica" reload: "Ricarica"
@ -1078,6 +1087,24 @@ installed: "Installazione avvenuta"
branding: "Branding" branding: "Branding"
enableServerMachineStats: "Pubblicare le informazioni sul server" enableServerMachineStats: "Pubblicare le informazioni sul server"
enableIdenticonGeneration: "Generazione automatica delle Identicon" enableIdenticonGeneration: "Generazione automatica delle Identicon"
turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni"
createInviteCode: "Genera codice di invito"
createWithOptions: "Genera con opzioni"
createCount: "Conteggio inviti"
inviteCodeCreated: "Inviti generati"
inviteLimitExceeded: "Hai raggiunto il numero massimo di codici invito generabili."
createLimitRemaining: "Inviti generabili: {limit} rimanenti"
inviteLimitResetCycle: "Alle {time}, il limite verrà ripristinato a {limit}"
expirationDate: "Scadenza"
noExpirationDate: "Perpetuo"
inviteCodeUsedAt: "Codice di invito usato alle"
registeredUserUsingInviteCode: "Codice di invito usato da"
waitingForMailAuth: "In attesa della verifica email"
inviteCodeCreator: "Codice di invito creato da"
usedAt: "Usato alle"
unused: "Inutilizzato"
used: "Utilizzato"
expired: "Scaduto"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "Il tuo profilo è stato creato!" accountCreated: "Il tuo profilo è stato creato!"
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@ -1388,6 +1415,9 @@ _role:
ltlAvailable: "Disponibilità della Timeline Locale" ltlAvailable: "Disponibilità della Timeline Locale"
canPublicNote: "Può scrivere Note con Visibilità Pubblica" canPublicNote: "Può scrivere Note con Visibilità Pubblica"
canInvite: "Genera codici di invito all'istanza" canInvite: "Genera codici di invito all'istanza"
inviteLimit: "Limite di codici invito"
inviteLimitCycle: "Intervallo di emissione del codice di invito"
inviteExpirationTime: "Scadenza del codice di invito"
canManageCustomEmojis: "Gestire le emoji personalizzate" canManageCustomEmojis: "Gestire le emoji personalizzate"
driveCapacity: "Capienza del Drive" driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Imposta sempre come NSFW" alwaysMarkNsfw: "Imposta sempre come NSFW"
@ -1450,6 +1480,7 @@ _ad:
back: "Indietro" back: "Indietro"
reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso" reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
hide: "Nascondi" hide: "Nascondi"
timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server."
_forgotPassword: _forgotPassword:
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
@ -1501,6 +1532,10 @@ _aboutMisskey:
donate: "Sostieni Misskey" donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰" morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori" patrons: "Sostenitori"
_displayOfSensitiveMedia:
respect: "Nascondere i media sensibili"
ignore: "Non nascondere i media sensibili"
force: "Nascondi tutti i media"
_mfm: _mfm:
cheatSheet: "Bigliettino MFM" cheatSheet: "Bigliettino MFM"
intro: "MFM è un linguaggio Markdown particolare che si può usare in diverse parti di Misskey. Qui puoi visualizzare a colpo d'occhio tutta la sintassi MFM utile." intro: "MFM è un linguaggio Markdown particolare che si può usare in diverse parti di Misskey. Qui puoi visualizzare a colpo d'occhio tutta la sintassi MFM utile."

View file

@ -188,6 +188,8 @@ addEmoji: "絵文字を追加"
settingGuide: "おすすめ設定" settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。" cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
flagAsBot: "Botとして設定" flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、CherryPickのシステム上での扱いがBotに合ったものになります。" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、CherryPickのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!" flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
@ -1127,6 +1129,23 @@ branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する" enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。" turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
createWithOptions: "オプションを指定して作成"
createCount: "作成数"
inviteCodeCreated: "招待コードを作成しました"
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
expirationDate: "有効期限"
noExpirationDate: "有効期限を設けない"
inviteCodeUsedAt: "招待コードが使用された日時"
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
waitingForMailAuth: "メール認証待ち"
inviteCodeCreator: "招待コードを作成したユーザー"
usedAt: "使用日時"
unused: "未使用"
used: "使用済み"
expired: "期限切れ"
additionalPermissionsForFlash: "Playへの追加許可" additionalPermissionsForFlash: "Playへの追加許可"
thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています" thisFlashRequiresTheFollowingPermissions: "このPlayは以下の権限を要求しています"
doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか" doYouWantToAllowThisPlayToAccessYourAccount: "このPlayによるアカウントへのアクセスを許可しますか"
@ -1511,6 +1530,9 @@ _role:
ctlAvailable: "キャットタイムラインの閲覧" ctlAvailable: "キャットタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canInvite: "サーバー招待コードの発行" canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
inviteExpirationTime: "招待コードの有効期限"
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
@ -2251,6 +2273,7 @@ _deck:
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください" widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
_columns: _columns:
main: "メイン" main: "メイン"

View file

@ -1080,6 +1080,9 @@ installed: "インストール済み"
branding: "あ" branding: "あ"
enableServerMachineStats: "サーバーのマシン情報見せびらかすで" enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする" enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。"
unused: "つこてへん"
used: "もうつこてる"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "アカウント作り終わったで。" accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。" letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1503,6 +1506,10 @@ _aboutMisskey:
donate: "Misskeyに寄付" donate: "Misskeyに寄付"
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰" morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
patrons: "支援者" patrons: "支援者"
_displayOfSensitiveMedia:
respect: "きわどいのは見とうない"
ignore: "きわどいのも見たい"
force: "常にメディアを隠すで"
_mfm: _mfm:
cheatSheet: "MFMチートシート" cheatSheet: "MFMチートシート"
intro: "MFMは、Misskey内の色んな所で使える専用のマークアップ言語やで。このページでMFMで使える構文一覧が確認できるで。" intro: "MFMは、Misskey内の色んな所で使える専用のマークアップ言語やで。このページでMFMで使える構文一覧が確認できるで。"

View file

@ -20,6 +20,7 @@ noNotes: "Geen notities"
noNotifications: "Geen meldingen" noNotifications: "Geen meldingen"
instance: "Server" instance: "Server"
settings: "Instellingen" settings: "Instellingen"
notificationSettings: "Notificatie instellingen"
basicSettings: "Basisinstellingen" basicSettings: "Basisinstellingen"
otherSettings: "Overige instellingen" otherSettings: "Overige instellingen"
openInWindow: "In een venster openen" openInWindow: "In een venster openen"
@ -48,8 +49,15 @@ delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken" deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop." deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."
addToList: "Aan lijst toevoegen" addToList: "Aan lijst toevoegen"
addToAntenna: "Voeg toe aan antenne"
sendMessage: "Verstuur bericht" sendMessage: "Verstuur bericht"
copyRSS: "Kopieer RSS"
copyUsername: "Kopiëren gebruikersnaam " copyUsername: "Kopiëren gebruikersnaam "
copyUserId: "Kopieer gebruiker ID"
copyNoteId: "Kopieer notitie ID"
copyFileId: "Kopieer veld ID"
copyFolderId: "Kopieer folder ID"
copyProfileUrl: "Kopieer profiel URL"
searchUser: "Zoeken een gebruiker" searchUser: "Zoeken een gebruiker"
reply: "Antwoord" reply: "Antwoord"
loadMore: "Laad meer" loadMore: "Laad meer"

View file

@ -54,6 +54,9 @@ copyRSS: "Скопировать RSS"
copyUsername: "Скопировать имя пользователя" copyUsername: "Скопировать имя пользователя"
copyUserId: "Скопировать ID пользователя" copyUserId: "Скопировать ID пользователя"
copyNoteId: "Скопировать ID заметки" copyNoteId: "Скопировать ID заметки"
copyFileId: "Скопировать ID файла"
copyFolderId: "Скопировать ID папки"
copyProfileUrl: "Скопировать URL профиля "
searchUser: "Поиск людей" searchUser: "Поиск людей"
reply: "Ответить" reply: "Ответить"
loadMore: "Показать еще" loadMore: "Показать еще"
@ -136,8 +139,10 @@ unblockConfirm: "Разблокировать этот аккаунт?"
suspendConfirm: "Заморозить этот аккаунт?" suspendConfirm: "Заморозить этот аккаунт?"
unsuspendConfirm: "Разморозить этот аккаунт?" unsuspendConfirm: "Разморозить этот аккаунт?"
selectList: "Выберите список" selectList: "Выберите список"
editList: "Редактировать список"
selectChannel: "Выберите канал" selectChannel: "Выберите канал"
selectAntenna: "Выберите антенну" selectAntenna: "Выберите антенну"
editAntenna: "Редактировать антенну"
selectWidget: "Выберите виджет" selectWidget: "Выберите виджет"
editWidgets: "Редактировать виджеты" editWidgets: "Редактировать виджеты"
editWidgetsExit: "Готово" editWidgetsExit: "Готово"

View file

@ -20,6 +20,7 @@ noNotes: "Inga noteringar"
noNotifications: "Inga notifikationer" noNotifications: "Inga notifikationer"
instance: "Instanser" instance: "Instanser"
settings: "Inställningar" settings: "Inställningar"
notificationSettings: "Notifieringsinställningar"
basicSettings: "Basinställningar" basicSettings: "Basinställningar"
otherSettings: "Andra inställningar" otherSettings: "Andra inställningar"
openInWindow: "Öppna i ett fönster" openInWindow: "Öppna i ett fönster"
@ -53,6 +54,8 @@ copyRSS: "Kopiera RSS"
copyUsername: "Kopiera användarnamn" copyUsername: "Kopiera användarnamn"
copyUserId: "Kopiera användar-ID" copyUserId: "Kopiera användar-ID"
copyNoteId: "Kopiera noter-ID" copyNoteId: "Kopiera noter-ID"
copyFileId: "Kopiera Fil-ID"
copyFolderId: "Kopiera mapp-ID"
searchUser: "Sök användare" searchUser: "Sök användare"
reply: "Svara" reply: "Svara"
loadMore: "Ladda mer" loadMore: "Ladda mer"
@ -106,6 +109,7 @@ cantRenote: "Inlägget kunde inte bli omnoterat."
cantReRenote: "En omnotering kan inte bli omnoterad." cantReRenote: "En omnotering kan inte bli omnoterad."
quote: "Citat" quote: "Citat"
inChannelRenote: "Omnotera inom kanalen" inChannelRenote: "Omnotera inom kanalen"
inChannelQuote: "I kanal citat"
pinnedNote: "Fästad not" pinnedNote: "Fästad not"
pinned: "Fäst till profil" pinned: "Fäst till profil"
you: "Du" you: "Du"
@ -309,6 +313,7 @@ banner: "Banner"
reload: "Ladda om" reload: "Ladda om"
doNothing: "Ignorera" doNothing: "Ignorera"
reloadConfirm: "Vill du ladda om tidslinjen?" reloadConfirm: "Vill du ladda om tidslinjen?"
watch: "Titta"
accept: "Tillåt" accept: "Tillåt"
reject: "Neka" reject: "Neka"
normal: "Normal" normal: "Normal"
@ -334,13 +339,22 @@ invite: "Inbjudan"
inMb: "I megabyte" inMb: "I megabyte"
iconUrl: "URL till profilbilden" iconUrl: "URL till profilbilden"
bannerUrl: "URL till banner-bilden" bannerUrl: "URL till banner-bilden"
basicInfo: "Grundläggande info"
pinnedUsers: "Fästa användare"
pinnedPages: "Fästa sidor"
pinnedNotes: "Fästad not" pinnedNotes: "Fästad not"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Aktivera hCaptcha" enableHcaptcha: "Aktivera hCaptcha"
hcaptchaSiteKey: "Webbplatsnyckel" hcaptchaSiteKey: "Webbplatsnyckel"
hcaptchaSecretKey: "Hemlig nyckel"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Aktivera reCAPTCHA" enableRecaptcha: "Aktivera reCAPTCHA"
recaptchaSiteKey: "Webbplatsnyckel"
recaptchaSecretKey: "Hemlig nyckel"
turnstile: "Turnstile"
enableTurnstile: "Aktivera Turnstile" enableTurnstile: "Aktivera Turnstile"
turnstileSiteKey: "Webbplatsnyckel"
turnstileSecretKey: "Hemlig nyckel"
antennas: "Antenner" antennas: "Antenner"
manageAntennas: "Hantera Antenner" manageAntennas: "Hantera Antenner"
name: "Namn" name: "Namn"
@ -352,6 +366,7 @@ notifyAntenna: "Notifiera om nya noter"
withFileAntenna: "Endast noter med filer" withFileAntenna: "Endast noter med filer"
enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" enableServiceworker: "Aktivera pushnotiser i denna webbläsaren"
antennaUsersDescription: "Ange ett användarnamn per linje" antennaUsersDescription: "Ange ett användarnamn per linje"
withReplies: "Med svar"
notesAndReplies: "Inlägg och svar" notesAndReplies: "Inlägg och svar"
silence: "Tystnad" silence: "Tystnad"
recentlyUpdatedUsers: "Nyligen aktiva användare" recentlyUpdatedUsers: "Nyligen aktiva användare"
@ -362,6 +377,9 @@ userList: "Listor"
about: "Om" about: "Om"
aboutMisskey: "Om CherryPick" aboutMisskey: "Om CherryPick"
administrator: "Administratör" administrator: "Administratör"
2fa: "Tvåfaktorsautentisering"
totp: "Autentiseringsapp"
moderator: "Moderator"
passwordLessLogin: "Lösenordsfri inloggning" passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey."
resetPassword: "Återställ Lösenord" resetPassword: "Återställ Lösenord"

View file

@ -3,7 +3,7 @@ _lang_: "ภาษาไทย"
headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต" headlineMisskey: "เชื่อมต่อเครือข่ายโดยโน้ต"
introMisskey: "ยินดีต้อนรับจ้าาา! CherryPick เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀" introMisskey: "ยินดีต้อนรับจ้าาา! CherryPick เป็นบริการไมโครบล็อกโอเพ่นซอร์ส แบบการกระจายอำนาจ\nสร้าง \"โน้ต\" เพื่อแบ่งปันความคิดของคุณกับทุกคนรอบตัวคุณกันเถอะ 📡\nด้วยการ \"รีแอคชั่นผู้คน\" คุณยังสามารถแสดงความรู้สึกของคุณเกี่ยวกับบันทึกของทุกคนได้อย่างรวดเร็ว 👍\n\nแล้วมาท่องสำรวจโลกใบใหม่กันเถอะ! 🚀"
poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>CherryPick</b> (เรียกว่า \"อินสแตนซ์ CherryPick\")" poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริการที่ถูกขับเคลื่อนโดยแพลตฟอร์มโอเพ่นซอร์ส <b>CherryPick</b> (เรียกว่า \"อินสแตนซ์ CherryPick\")"
monthAndDay: "{เดือน}/{วัน}" monthAndDay: "{month}/{day}"
search: "ค้นหา" search: "ค้นหา"
notifications: "การเเจ้งเตือน" notifications: "การเเจ้งเตือน"
username: "ชื่อผู้ใช้" username: "ชื่อผู้ใช้"
@ -15,7 +15,7 @@ gotIt: "เข้าใจแล้ว !"
cancel: "ยกเลิก" cancel: "ยกเลิก"
noThankYou: "ไม่เป็นไร" noThankYou: "ไม่เป็นไร"
enterUsername: "ใส่ชื่อผู้ใช้" enterUsername: "ใส่ชื่อผู้ใช้"
renotedBy: "รีโน้ตโดย {ผู้ใช้}" renotedBy: "รีโน้ตโดย {user}"
noNotes: "ไม่มีโน้ต" noNotes: "ไม่มีโน้ต"
noNotifications: "ไม่มีการแจ้งเตือน" noNotifications: "ไม่มีการแจ้งเตือน"
instance: "อินสแตนซ์" instance: "อินสแตนซ์"
@ -49,11 +49,15 @@ delete: "ลบ"
deleteAndEdit: "ลบและแก้ไข" deleteAndEdit: "ลบและแก้ไข"
deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ" deleteAndEditConfirm: "นายแน่ใจแล้วเหรอ? ว่าต้องการลบโน้ตนี้และแก้ไข คุณอาจจะสูญเสียการโต้ตอบ, โน้ต, และการตอบกลับทั้งหมดได้นะ"
addToList: "เพิ่มในลิสต์" addToList: "เพิ่มในลิสต์"
addToAntenna: "เพิ่มไปยังเสาอากาศ"
sendMessage: "ส่งข้อความ" sendMessage: "ส่งข้อความ"
copyRSS: "คัดลอก RSS" copyRSS: "คัดลอก RSS"
copyUsername: "คัดลอกชื่อผู้ใช้" copyUsername: "คัดลอกชื่อผู้ใช้"
copyUserId: "คัดลอก ID ผู้ใช้" copyUserId: "คัดลอก ID ผู้ใช้"
copyNoteId: "คัดลอก ID โน้ต " copyNoteId: "คัดลอก ID โน้ต "
copyFileId: "คัดลอกไฟล์ ID"
copyFolderId: "คัดลอกโฟลเดอร์ ID"
copyProfileUrl: "คัดลอกโปรไฟล์ URL"
searchUser: "ค้นหาผู้ใช้งาน" searchUser: "ค้นหาผู้ใช้งาน"
reply: "ตอบกลับ" reply: "ตอบกลับ"
loadMore: "โหลดเพิ่มเติม" loadMore: "โหลดเพิ่มเติม"
@ -152,6 +156,8 @@ addEmoji: "แทรกอีโมจิ"
settingGuide: "การตั้งค่าที่แนะนำ" settingGuide: "การตั้งค่าที่แนะนำ"
cacheRemoteFiles: "แคชไฟล์ระยะไกล" cacheRemoteFiles: "แคชไฟล์ระยะไกล"
cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ" cacheRemoteFilesDescription: "เมื่อปิดใช้งานการตั้งค่านี้ ไฟล์ระยะไกลนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกล แต่กรณีการปิดใช้งานนี้จะช่วยลดปริมาณการใช้พื้นที่จัดเก็บข้อมูล แต่เพิ่มปริมาณการใช้งาน เพราะเนื่องจากจะไม่มีการสร้างภาพขนาดย่อ"
cacheRemoteSensitiveFiles: "ไฟล์ระยะไกลที่มีความละเอียดอ่อนแคช"
cacheRemoteSensitiveFilesDescription: "เมื่อปิดการใช้งานแล้วการตั้งค่านี้ ไฟล์รีโมตที่มีความละเอียดอ่อนนั้นจะถูกโหลดโดยตรงจากอินสแตนซ์ระยะไกลโดยที่ไม่มีการแคช"
flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท" flagAsBot: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นบอท"
flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ CherryPick เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท" flagAsBotDescription: "การเปิดใช้งานตัวเลือกนี้หากบัญชีนี้ถูกควบคุมโดยนักเขียนโปรแกรม หรือ ถ้าหากเปิดใช้งาน มันจะทำหน้าที่เป็นแฟล็กสำหรับนักพัฒนารายอื่นๆ และเพื่อป้องกันการโต้ตอบแบบไม่มีที่สิ้นสุดกับบอทตัวอื่นๆ และยังสามารถปรับเปลี่ยนระบบภายในของ CherryPick เพื่อปฏิบัติต่อบัญชีนี้เป็นบอท"
flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว" flagAsCat: "ทำเครื่องหมายบอกว่าบัญชีนี้เป็นแมว"
@ -313,6 +319,7 @@ copyUrl: "คัดลอก URL"
rename: "เปลี่ยนชื่อ" rename: "เปลี่ยนชื่อ"
avatar: "ไอคอน" avatar: "ไอคอน"
banner: "แบนเนอร์" banner: "แบนเนอร์"
displayOfSensitiveMedia: "แสดงผลสื่อละเอียดอ่อน"
whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์" whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์"
disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์" disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์"
reload: "รีโหลด" reload: "รีโหลด"
@ -331,9 +338,9 @@ tosUrl: "เงื่อนไขการให้บริการ URL"
thisYear: "ปีนี้" thisYear: "ปีนี้"
thisMonth: "เดือนนี้" thisMonth: "เดือนนี้"
today: "วันนี้" today: "วันนี้"
dayX: "{วัน}" dayX: "{day}"
monthX: "{เดือน}" monthX: "{เดือน}"
yearX: "{ปี}" yearX: "{year}"
pages: "หน้า" pages: "หน้า"
integration: "รวบรวม" integration: "รวบรวม"
connectService: "เชื่อมต่อ" connectService: "เชื่อมต่อ"
@ -1057,16 +1064,19 @@ preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ท
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
archive: "เก็บถาวร" archive: "เก็บถาวร"
channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?" channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?"
channelArchiveConfirmDescription: "ช่องที่ถูกเก็บถาวรแล้วนั้นจะไม่ปรากฏในรายการช่องหรือผลการค้นหานั้นอีกต่อไปไม่สามารถเพิ่มโพสต์ใหม่ได้อีกต่อไปนะ"
thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ" thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
displayOfNote: "การแสดงโน้ต" displayOfNote: "การแสดงโน้ต"
initialAccountSetting: "ตั้งค่าโปรไฟล์" initialAccountSetting: "ตั้งค่าโปรไฟล์"
youFollowing: "ติดตามแล้ว" youFollowing: "ติดตามแล้ว"
preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)" preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)"
preventAiLearningDescription: "การส่งคำร้องขอโปรแกรมรวบรวมข้อมูลไม่ให้ใช้ข้อความที่โพสต์หรือรูปภาพ ฯลฯ ในชุดข้อมูลแมชชีนเลิร์นนิง (Predictive / Generative AI) สิ่งนี้นั้นทำได้โดยการเพิ่มแฟล็กการตอบสนอง \"noai\" HTML ให้กับเนื้อหาที่เกี่ยวข้อง แต่อย่างไรก็ตามแล้ว การป้องกันโดยสมบูรณ์นั้นไม่สามารถทำได้ผ่านแฟล็กนี้เนื่องจากอาจจะทำให้ถูกเพิกเฉยได้"
options: "ตัวเลือกบทบาท" options: "ตัวเลือกบทบาท"
specifyUser: "ผู้ใช้เฉพาะ" specifyUser: "ผู้ใช้เฉพาะ"
failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้" failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
update: "อัปเดต" update: "อัปเดต"
rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้" rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้"
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ถ้าหากไม่ได้ระบุบทบาท ทุกคนนั้นก็สามารถใช้อิโมจินี้เป็นการแสดงความรู้สึกได้นะ"
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ" rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ"
cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?" cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?"
changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?" changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?"
@ -1078,6 +1088,23 @@ branding: "แบรนดิ้ง"
enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์" enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์"
enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ" enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ"
turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้" turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้"
createInviteCode: "สร้างคำเชิญ"
createWithOptions: "สร้างด้วยตัวเลือก"
createCount: "จำนวนการเชิญ"
inviteCodeCreated: "สร้างคำเชิญแล้ว"
inviteLimitExceeded: "คุณสร้างคำเชิญเกินถึงขีดจำกัดแล้วนะ"
createLimitRemaining: "ขีดจำกัดการเชิญ: {limit} ที่เหลืออยู่"
inviteLimitResetCycle: "ขีดจำกัดนี้จะถูกรีเซ็ตเป็น {limit} ที่ {time}."
expirationDate: "วันที่หมดอายุ"
noExpirationDate: "ไม่มีหมดอายุ"
inviteCodeUsedAt: "รหัสคำเชิญใช้แล้วที่"
registeredUserUsingInviteCode: "ใช้คำเชิญแล้วโดย"
waitingForMailAuth: "กำลังรอการยืนยันอีเมล"
inviteCodeCreator: "สร้างการเชิญแล้วโดย"
usedAt: "ใช้แล้วที่"
unused: "ไม่ใช้แล้ว"
used: "ใช้แล้ว"
expired: "หมดอายุแล้ว"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!"
letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ"
@ -1085,8 +1112,12 @@ _initialAccountSetting:
profileSetting: "ตั้งค่าโปรไฟล์" profileSetting: "ตั้งค่าโปรไฟล์"
privacySetting: "ตั้งค่าความเป็นส่วนตัว" privacySetting: "ตั้งค่าความเป็นส่วนตัว"
theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ" theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
youCanEditMoreSettingsInSettingsPageLater: "ยังมีการตั้งค่าอื่นๆ อีกมากมายที่คุณนั้นสามารถกำหนดค่าได้จาก \"การตั้งค่า\" เพื่อให้แน่ใจว่าได้เยี่ยมชมมันได้ภายหลังนะ"
followUsers: "ลองติดตามผู้ใช้บางคนที่คุณอาจจะสนใจเพื่อสร้างไทม์ไลน์ของคุณสิ !"
pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
haveFun: "สนุกกับ {name}!" haveFun: "สนุกกับ {name}!"
ifYouNeedLearnMore: "ถ้าหากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับวิธีใช้ {ชื่อ} (CherryPick) กรุณาไปที่ {link}"
skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?" skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?"
laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?" laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?"
_serverRules: _serverRules:
@ -1384,6 +1415,9 @@ _role:
ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น" ltlAvailable: "การดูไทม์ไลน์ในท้องถิ่น"
canPublicNote: "สามารถส่งโน้ตสาธารณะ" canPublicNote: "สามารถส่งโน้ตสาธารณะ"
canInvite: "สร้างรหัสเชิญอินสแตนซ์" canInvite: "สร้างรหัสเชิญอินสแตนซ์"
inviteLimit: "จำกัดการเชิญ"
inviteLimitCycle: "จำกัดการเชิญไว้คูลดาวน์"
inviteExpirationTime: "วันหมดอายุของรหัสการเชิญ"
canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง" canManageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
driveCapacity: "ความจุของไดรฟ์" driveCapacity: "ความจุของไดรฟ์"
alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ" alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
@ -1498,6 +1532,10 @@ _aboutMisskey:
donate: "บริจาคให้กับ Misskey" donate: "บริจาคให้กับ Misskey"
morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰" morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰"
patrons: "สมาชิกพันธมิตร" patrons: "สมาชิกพันธมิตร"
_displayOfSensitiveMedia:
respect: "ซ่อนสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน"
ignore: "แสดงผลสื่อทำเครื่องหมายบอกว่าละเอียดอ่อน"
force: "ซ่อนสื่อทั้งหมด"
_mfm: _mfm:
cheatSheet: "โค้ด MFM Cheat Sheet" cheatSheet: "โค้ด MFM Cheat Sheet"
intro: "MFM เป็นภาษามาร์กอัปพิเศษเฉพาะของ Misskey ที่สามารถใช้ได้ในหลายที่ คุณยังสามารถดูรายการไวยากรณ์ MFM ที่มีอยู่ทั้งหมดได้ที่นี่นะ" intro: "MFM เป็นภาษามาร์กอัปพิเศษเฉพาะของ Misskey ที่สามารถใช้ได้ในหลายที่ คุณยังสามารถดูรายการไวยากรณ์ MFM ที่มีอยู่ทั้งหมดได้ที่นี่นะ"
@ -1703,6 +1741,8 @@ _time:
day: "วัน" day: "วัน"
_timelineTutorial: _timelineTutorial:
title: "วิธีใช้งาน CherryPick" title: "วิธีใช้งาน CherryPick"
step1_1: "นี่คือ \"ไทม์ไลน์\" \"โน้ต\" ทั้งหมดที่ส่งใน {name} จะแสดงรายการตามลำดับเวลาที่นี่นะ"
step1_2: "อาจจะมีไทม์ไลน์ที่แตกต่างกันเล็กน้อยยกตัวอย่างเช่น \"ไทม์ไลน์หน้าแรก\" จะมีโน้ตของผู้ใช้ที่คุณติดตามและ \"ไทม์ไลน์ท้องถิ่น\" จะมีโน้ตจากผู้ใช้ทั้งหมดของ {name}"
step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ" step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ"
step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?" step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?"
step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?" step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?"

View file

@ -1,5 +1,6 @@
--- ---
_lang_: "Türkçe" _lang_: "Türkçe"
headlineMisskey: "Notlarla bağlanmış bir ağ"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan CherryPick'e hoş geldiniz.\nCherryPick, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀." introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan CherryPick'e hoş geldiniz.\nCherryPick, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>CherryPick</b>Dünya'nın en sunucularında biri。" poweredByMisskeyDescription: "name}Açık kaynak bir platform\n<b>CherryPick</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün" monthAndDay: "{month}Ay {day}Gün"
@ -11,7 +12,9 @@ forgotPassword: "şifremi unuttum"
ok: "TAMAM" ok: "TAMAM"
gotIt: "Anladım" gotIt: "Anladım"
cancel: "İptal" cancel: "İptal"
noThankYou: "Hayır, teşekkürler"
enterUsername: "Kullanıcı adınızı giriniz" enterUsername: "Kullanıcı adınızı giriniz"
renotedBy: "{user} tarafından Renotelandı"
noNotes: "Notlar mevcut değil." noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor" noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu" instance: "Sunucu"
@ -45,15 +48,35 @@ delete: "Sil"
deleteAndEdit: "Sil ve yeniden düzenle" deleteAndEdit: "Sil ve yeniden düzenle"
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir." deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
addToList: "Listeye ekle" addToList: "Listeye ekle"
addToAntenna: "Antene ekle"
sendMessage: "Mesaj Gönder" sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala" copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala" copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "KullanıcıyıKopyala" copyUserId: "KullanıcıyıKopyala"
copyNoteId: "Kimlik notunu kopyala" copyNoteId: "Kimlik notunu kopyala"
copyFileId: "Dosya ID'sini kopyala"
copyFolderId: "Klasör ID'sini kopyala"
copyProfileUrl: "Profil URL'sini kopyala"
searchUser: "Kullanıcıları ara" searchUser: "Kullanıcıları ara"
reply: "yanıt" reply: "yanıt"
loadMore: "Devamını yükle" loadMore: "Devamını yükle"
showMore: "Devamını yükle" showMore: "Devamını yükle"
showLess: "Kapat"
youGotNewFollower: "seni takip etti"
receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi"
mention: "Bahset"
mentions: "Bahsetmeler"
directNotes: "Kişisel mesajlar"
importAndExport: "İçeri/Dışarı aktar"
import: "İçeri aktar"
export: "Dışa aktar"
files: "Dosyalar"
download: "İndir"
driveFileDeleteConfirm: "\"{name}\" dosyası silinsin mi? Dosya kullanıldığı tüm notlardan kaybolacaktır."
unfollowConfirm: "{name} takipten çıkarılsın mı?"
exportRequested: "Dışa aktarım talep ettiniz. Bu biraz zaman alabilir. İşlem bitince Sürücünüze eklenecektir."
importRequested: "Dışa aktarım talep ettiniz. Bu işlem biraz zaman alabilir."
lists: "Listeler" lists: "Listeler"
noLists: "Liste yok" noLists: "Liste yok"
note: "not" note: "not"
@ -64,6 +87,16 @@ followsYou: "seni takip ediyor"
createList: "Liste oluştur" createList: "Liste oluştur"
manageLists: "Yönetici Listeleri" manageLists: "Yönetici Listeleri"
error: "hata" error: "hata"
somethingHappened: "Bir hata oluştu"
retry: "Tekrar dene"
pageLoadError: "Sayfa yüklenemedi."
pageLoadErrorDescription: "Bu genelde ağ veya tarayıcı ön belleği hatalarından olur. Lütfen ön belleği temizlemeyi veya birkaç dakika beklemeyi ve sayfayı yenilemeyi deneyin."
serverIsDead: "Sunucu yanıt vermiyor. Birkaç dakika sonra tekrar deneyin."
youShouldUpgradeClient: "Sayfayı görüntülemek için yenileyin."
enterListName: "Liste ismi"
privacy: "Gizlilik"
makeFollowManuallyApprove: "Takip istekleri elle onaylansın"
defaultNoteVisibility: "Varsayılan görünürlük"
follow: "takipçi" follow: "takipçi"
followRequest: "Takip isteği" followRequest: "Takip isteği"
followRequests: "Takip istekleri" followRequests: "Takip istekleri"
@ -76,9 +109,24 @@ renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama" cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim" cantReRenote: "not alabilirmiyim"
quote: "alıntı" quote: "alıntı"
inChannelRenote: "Kanal içi Renote"
inChannelQuote: "Kanal içi Alıntı"
pinnedNote: "Sabitlenen" pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş" pinned: "Sabitlenmiş"
you: "sen" you: "sen"
clickToShow: "Görüntülemek için tıkla"
sensitive: "Hassas içerik"
add: "Ekle"
reaction: "Tepkiler"
reactions: "Tepkiler"
reactionSetting: "Palette görünecek tepkiler"
reactionSettingDescription2: "Sıralamak için sürükleyin, silmek için tıklayın, eklemek için \"+\" tuşuna tıklayın."
rememberNoteVisibility: "Görünürlük ayarlarını hatırla"
attachCancel: "Eki sil"
markAsSensitive: "Hassas içerik olarak işaretle"
unmarkAsSensitive: "Hassas içerik işaretini kaldır"
enterFileName: "Dosya ismini gir"
mute: "Gizle"
unmute: "sesi aç" unmute: "sesi aç"
renoteMute: "sesi kapat" renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et" renoteUnmute: "sesi açmayı iptal et"
@ -88,46 +136,280 @@ suspend: "askıya al"
unsuspend: "askıya alma" unsuspend: "askıya alma"
blockConfirm: "Onayı engelle" blockConfirm: "Onayı engelle"
unblockConfirm: "engellemeyi kaldır onayla" unblockConfirm: "engellemeyi kaldır onayla"
suspendConfirm: "Hesap askıya alınsın mı?"
unsuspendConfirm: "Hesap askıdan kaldırılsın mı"
selectList: "Bir liste seç"
editList: "Listeyi düzenle"
selectChannel: "Kanal seç" selectChannel: "Kanal seç"
selectAntenna: "Bir anten seç"
editAntenna: "Anteni düzenle"
selectWidget: "Araç seç"
editWidgets: "Araçları düzenle"
editWidgetsExit: "Tamam"
customEmojis: "Özel Emoji"
emoji: "Emoji"
emojis: "Emoji"
emojiName: "Emoji adı"
emojiUrl: "Emoji URL'si"
addEmoji: "Emoji ekle"
settingGuide: "Önerilen ayarlar"
cacheRemoteFiles: "Uzak dosyalar ön belleğe alınsın"
cacheRemoteFilesDescription: "Bu ayar açık olduğunda diğer sitelerin dosyaları doğrudan uzak sunucudan yüklenecektir. Bu ayarı kapatmak depolama kullanımını azaltacak ama küçük resimler oluşturulmadığından trafiği arttıracaktır."
cacheRemoteSensitiveFiles: "Hassas uzak dosyalar ön belleğe alınsın"
cacheRemoteSensitiveFilesDescription: "Bu ayar kapalı olduğunda hassas uzak dosyalar ön belleğe alınmadan doğrudan uzak sunucudan yüklenecektir."
flagAsBot: "Bot olarak işaretle" flagAsBot: "Bot olarak işaretle"
flagAsBotDescription: "Bu seçeneği hesap bir program tarafından kontrol ediliyorsa işaretleyin. Bu, diğer geliştiricilerin sonsuz etkileşim zincirleri oluşturmasını engellemeye yardımcı olur ve CherryPick'in iç sisteminin hesaba bir bot gibi davranmasını sağlar."
flagAsCat: "Kedi hesabı"
flagAsCatDescription: "Kedi hesabı"
flagShowTimelineReplies: "Zaman akışında notlara gelen cevapları göster"
flagShowTimelineRepliesDescription: "Açık olduğu durumda, zaman akışında kullanıcıların başkalarına verdiği cevaplar gözükür."
autoAcceptFollowed: "Takip edilen hesapların takip isteklerini kabul et"
addAccount: "Hesap ekle"
reloadAccountsList: "Hesap listesini güncelle"
loginFailed: "Giriş başarısız oldu"
showOnRemote: "Uzak sunucuda görüntüle"
general: "Genel"
wallpaper: "Duvar kağıdı"
setWallpaper: "Duvar kağıdını ayarla"
removeWallpaper: "Duvar kağıdını sil"
searchWith: "Arama: {q}"
youHaveNoLists: "Hiç listeniz yok"
followConfirm: "{name} takip edilsin mi?"
proxyAccount: "Vekil hesabı"
proxyAccountDescription: "Proxy hesabı, belirli koşullar altında kullanıcılar için uzaktan takipçi işlevi gören bir hesaptır. Örneğin, bir kullanıcı listeye bir uzak kullanıcı eklediğinde, o kullanıcıyı takip eden yerel bir kullanıcı yoksa uzak kullanıcının etkinliği örneğe teslim edilmeyecektir, dolayısıyla bunun yerine proxy hesabı takip edilecektir."
host: "Sağlayıcı"
selectUser: "Kullanıcı seç"
recipient: "Kime"
annotation: "Açıklamalar"
federation: "Federasyon"
instances: "Sunucu" instances: "Sunucu"
registeredAt: "Katılma tarihi"
latestRequestReceivedAt: "Alınan son talep"
latestStatus: "En son durum"
storageUsage: "Depolama kullanımı"
charts: "Çizelgeler"
perHour: "Saatlik"
perDay: "Günlük"
stopActivityDelivery: "Durum güncellemelerini gönderme"
blockThisInstance: "Bu sunucuyu engelle"
operations: "İşlemler"
software: "Yazılımlar"
version: "Sürüm"
metadata: "Meta Verileri"
withNFiles: "{n} tane dosya"
monitor: "Monitör"
jobQueue: "İşlem sırası"
cpuAndMemory: "İşlemci ve Hafıza"
network: "Ağ"
disk: "Disk"
instanceInfo: "Sunucu Bilgisi"
statistics: "İstatistikler"
clearQueue: "Sırayı temizle"
clearQueueConfirmTitle: "Sıra silinsin mi?"
clearQueueConfirmText: "Sırada kalan hiçbir şey iletilmeyecek. Genelde bu işlem gerekli değildir."
clearCachedFiles: "Ön belleği temizle"
clearCachedFilesConfirm: "Ön belleğe alınmış tüm uzak sunucu dosyaları silinsin mi?"
blockedInstances: "Engellenen sunucular"
blockedInstancesDescription: "Engellemek istediğiniz sunucuların alan adlarını satır sonlarıyla ayırarak yazın. Yazılan sunucular bu sunucuyla iletişime geçemeyecek."
muteAndBlock: "Susturma ve Engelleme"
mutedUsers: "Susturulan kullanıcılar"
blockedUsers: "Engellenen kullanıcılar"
noUsers: "Kullanıcı yok"
editProfile: "Profili düzenle"
noteDeleteConfirm: "Bu notu silmek istediğinizden emin misiniz?"
pinLimitExceeded: "Daha fazla not sabitlenemez"
intro: "CherryPick yüklemesi tamamlandı! Lütfen yönetici hesabını oluşturun."
done: "Tamamlandı"
preview: "Önizleme"
default: "Varsayılan"
defaultValueIs: "Varsayılan: {value}"
noCustomEmojis: "Emoji bulunamadı"
noJobs: "Hiç işlem yok"
federating: "Federe ediliyor"
blocked: "Engellenmiş"
suspended: "Askıya alınmış"
all: "Tümü"
subscribing: "Abonelik"
publishing: "Paylaşım"
notResponding: "Cevap yok"
instanceFollowing: "Sunucuda takip edenler"
instanceFollowers: "Sunucu takipçileri"
instanceUsers: "Sunucu kullanıcıları"
changePassword: "Şifreyi değiştir"
security: "Güvenlik"
retypedNotMatch: "Girişler uyuşmuyor."
currentPassword: "Geçerli şifre"
newPassword: "Yeni şifre"
newPasswordRetype: "Yeni şifre (tekrar)"
attachFile: "Dosya ekle"
more: "Daha!"
featured: "Öne Çıkan"
usernameOrUserId: "Kullanıcı adı veya ID'si"
noSuchUser: "Kullanıcı bulunamadı"
lookup: "Sorgu"
announcements: "Duyurular"
imageUrl: "Görsel URL'si"
remove: "Sil" remove: "Sil"
removed: "Silindi"
removeAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?"
deleteAreYouSure: "\"{x}\" silmek istediğinizden emin misiniz?"
resetAreYouSure: "Sıfırlansın mı?"
saved: "Kaydedildi"
messaging: "Mesajlar"
upload: "Yükle"
keepOriginalUploading: "Orijinal görseli koru"
keepOriginalUploadingDescription: "Orijinal olarak yüklenen görüntüyü olduğu gibi kaydeder. Kapatılırsa, yükleme sırasında web'de görüntülenecek bir sürüm oluşturulur."
fromUrl: "Bağlantıdan"
uploadFromUrl: "Bağlantıdan yükle"
uploadFromUrlDescription: "Yüklemek istediğiniz dosyanın bağlantısı"
uploadFromUrlRequested: "Yükleme talep edildi"
uploadFromUrlMayTakeTime: "Yüklemenin tamamlanması biraz süre alabilir."
explore: "Keşfet"
messageRead: "Okundu"
noMoreHistory: "Bundan öncesi yok"
startMessaging: "Yeni bir sohbet başlat"
nUsersRead: "{n} kişi okudu"
agreeTo: "Kabul Ediyorum: {0}"
agree: "Kabul Et"
agreeBelow: "Aşağıdakileri kabul ederim"
basicNotesBeforeCreateAccount: "Önemli notlar"
termsOfService: "Şartlar ve Koşullar"
start: "Başla"
home: "Ana sayfa"
remoteUserCaution: "Bu kullanıcı bir uzak sunucudan olduğu için alınan bilgiler tam olmayabilir."
activity: "Etkinlik"
images: "Görseller"
image: "Görseller"
birthday: "Doğum günü"
yearsOld: "{age} yaşında"
registeredDate: "Kayıt tarihi"
location: "Konum"
theme: "Temalar"
themeForLightMode: "Aydınlık Tema"
themeForDarkMode: "Karanlık Tema"
light: "Aydınlık"
dark: "Karanlık"
lightThemes: "Aydınlık Temalar"
darkThemes: "Karanlık Temalar"
syncDeviceDarkMode: "Sistem Koyu Modu ile senkronize et"
drive: "Sürücü"
fileName: "Dosya adı"
selectFile: "Dosya seç"
selectFiles: "Dosya seç"
selectFolder: "Klasör seç"
selectFolders: "Klasör seç"
renameFile: "Dosyayı yeniden adlandır"
folderName: "Klasör adı"
createFolder: "Klasör oluştur"
renameFolder: "Klasörü Yeniden Adlandır"
deleteFolder: "Klasörü sil"
addFile: "Dosya ekle"
emptyDrive: "Sürücü boş"
hasChildFilesOrFolders: "Klasör boş olmadığından silinemiyor"
doNothing: "Bir şey yapma"
reloadConfirm: "Zaman akışı yenilensin mi?"
maintainerName: "Yönetici ismi"
monthX: "{month} ay"
enableRegistration: "Kayıtlara izin ver"
pinnedNotes: "Sabitlenen" pinnedNotes: "Sabitlenen"
manageAntennas: "Anten ayarları"
userList: "Listeler" userList: "Listeler"
resetPassword: "Şifre sıfırlama"
noMessagesYet: "Şimdilik mesaj yok"
details: "Detaylar"
deck: "Güverte"
smtpHost: "Sağlayıcı"
smtpUser: "Kullanıcı Adı" smtpUser: "Kullanıcı Adı"
smtpPass: "Şifre" smtpPass: "Şifre"
notificationSetting: "Bildirim ayarları"
noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et."
clearCache: "Ön belleği temizle"
onlineUsersCount: "{n} kullanıcı çevrim içi"
user: "Kullanıcı" user: "Kullanıcı"
global: "Küresel"
squareAvatars: "Kare avatarlar"
searchByGoogle: "Arama" searchByGoogle: "Arama"
file: "Dosyalar"
pushNotification: "Push bildirimleri"
subscribePushNotification: "Push bildirimlerini etkinleştir"
unsubscribePushNotification: "Push bildirimlerini kapat"
pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık"
pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor"
noRole: "Rol bulunamadı"
color: "Renk"
addMemo: "Kısa not ekle"
_accountDelete:
started: "Silme işlemi başlatıldı"
_email:
_follow:
title: "seni takip etti"
_theme: _theme:
color: "Renk"
keys: keys:
mention: "Bahset"
renote: "vazgeçme" renote: "vazgeçme"
_sfx: _sfx:
note: "notlar" note: "notlar"
notification: "Bildirim" notification: "Bildirim"
chat: "Mesajlar"
_2fa:
renewTOTPCancel: "Hayır, teşekkürler"
_permissions:
"read:blocks": "Engellenen hesapları gör"
"write:blocks": "Engellenen hesap listesini düzenle"
_widgets: _widgets:
profile: "Profil" profile: "Profil"
instanceInfo: "Sunucu Bilgisi"
notifications: "Bildirim" notifications: "Bildirim"
timeline: "Zaman çizelgesi" timeline: "Zaman çizelgesi"
calendar: "Takvim"
clock: "Saat"
activity: "Etkinlik"
federation: "Federasyon"
jobQueue: "İşlem sırası"
_userList:
chooseList: "Bir liste seç"
_cw: _cw:
show: "Devamını yükle" show: "Devamını yükle"
_poll:
vote: "Oy kullan"
_visibility: _visibility:
publicDescription: "Herkese açık"
home: "Ana sayfa"
followers: "takipçi" followers: "takipçi"
_profile: _profile:
username: "Kullanıcı Adı" username: "Kullanıcı Adı"
_exportOrImport: _exportOrImport:
followingList: "takipçi" followingList: "takipçi"
muteList: "Gizle"
blockingList: "engelle" blockingList: "engelle"
userLists: "Listeler" userLists: "Listeler"
_charts:
federation: "Federasyon"
_timelines:
home: "Ana sayfa"
global: "Küresel"
_pages:
blocks:
image: "Görseller"
_notification: _notification:
youWereFollowed: "seni takip etti"
unreadAntennaNote: "{name} anteni"
_types: _types:
follow: "takipçi" follow: "takipçi"
mention: "Bahset"
renote: "vazgeçme" renote: "vazgeçme"
quote: "alıntı" quote: "alıntı"
reaction: "Tepkiler"
receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi"
_actions: _actions:
reply: "yanıt" reply: "yanıt"
renote: "vazgeçme" renote: "vazgeçme"
_deck: _deck:
configureColumn: "Sütun seçenekleri"
_columns: _columns:
notifications: "Bildirim" notifications: "Bildirim"
tl: "Zaman çizelgesi" tl: "Zaman çizelgesi"
list: "Listeler" list: "Listeler"
mentions: "Bahsetmeler"

View file

@ -49,11 +49,15 @@ delete: "删除"
deleteAndEdit: "删除并编辑" deleteAndEdit: "删除并编辑"
deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。"
addToList: "添加至列表" addToList: "添加至列表"
addToAntenna: "添加到天线"
sendMessage: "发送" sendMessage: "发送"
copyRSS: "复制RSS" copyRSS: "复制RSS"
copyUsername: "复制用户名" copyUsername: "复制用户名"
copyUserId: "复制用户 ID" copyUserId: "复制用户 ID"
copyNoteId: "复制帖子 ID" copyNoteId: "复制帖子 ID"
copyFileId: "复制文件ID"
copyFolderId: "复制文件夹ID"
copyProfileUrl: "复制配置文件URL"
searchUser: "搜索用户" searchUser: "搜索用户"
reply: "回复" reply: "回复"
loadMore: "查看更多" loadMore: "查看更多"
@ -313,6 +317,7 @@ copyUrl: "复制链接"
rename: "重命名" rename: "重命名"
avatar: "头像" avatar: "头像"
banner: "横幅" banner: "横幅"
displayOfSensitiveMedia: "显示敏感媒体"
whenServerDisconnected: "与服务器连接中断时" whenServerDisconnected: "与服务器连接中断时"
disconnectedFromServer: "已和服务器断开连接" disconnectedFromServer: "已和服务器断开连接"
reload: "重新加载" reload: "重新加载"
@ -1080,6 +1085,11 @@ installed: "已安装"
branding: "品牌" branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息" enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon" enableIdenticonGeneration: "启用生成用户 Identicon"
turnOffToImprovePerformance: "关闭该选项可以提高性能。"
inviteCodeCreated: "已创建邀请码"
unused: "未使用"
used: "已使用"
expired: "已过期"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "账户创建完成了!" accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。" letsStartAccountSetup: "来进行帐户的初始设置吧。"
@ -1167,53 +1177,53 @@ _achievements:
flavor: "真的有那么多可以写的东西吗?" flavor: "真的有那么多可以写的东西吗?"
_login3: _login3:
title: "初学者 I" title: "初学者 I"
description: "连续登录 3 天" description: "累计登录 3 天"
flavor: "今天开始我就是 Misskist" flavor: "今天开始我就是 Misskist"
_login7: _login7:
title: "初学者 II" title: "初学者 II"
description: "连续登录 7 天" description: "累计登录 7 天"
flavor: "您开始习惯了吗?" flavor: "您开始习惯了吗?"
_login15: _login15:
title: "初学者 III" title: "初学者 III"
description: "连续登录 15 天" description: "累计登录 15 天"
_login30: _login30:
title: "Misskist " title: "Misskist "
description: "连续登录 30 天" description: "累计登录 30 天"
_login60: _login60:
title: "Misskist Ⅱ" title: "Misskist Ⅱ"
description: "连续登录 60 天" description: "累计登录 60 天"
_login100: _login100:
title: "Misskist Ⅲ" title: "Misskist Ⅲ"
description: "登入 100 天" description: "累计登入 100 天"
flavor: "那个用户,是 Misskist 喔" flavor: "那个用户,是 Misskist 喔"
_login200: _login200:
title: "定期联系Ⅰ" title: "定期联系Ⅰ"
description: "总登录天数 200 天" description: "累计登录 200 天"
_login300: _login300:
title: "定期联系Ⅱ" title: "定期联系Ⅱ"
description: "总登录天数 300 天" description: "累计登录 300 天"
_login400: _login400:
title: "定期联系Ⅲ" title: "定期联系Ⅲ"
description: "总登录天数 400 天" description: "累计登录 400 天"
_login500: _login500:
title: "老熟人Ⅰ" title: "老熟人Ⅰ"
description: "总登录天数 500 天" description: "累计登录 500 天"
flavor: "诸君,我喜欢贴文" flavor: "诸君,我喜欢贴文"
_login600: _login600:
title: "老熟人Ⅱ" title: "老熟人Ⅱ"
description: "总登录天数 600 天" description: "累计登录 600 天"
_login700: _login700:
title: "老熟人Ⅲ" title: "老熟人Ⅲ"
description: "总登录天数 700 天" description: "累计登录 700 天"
_login800: _login800:
title: "帖子大师 " title: "帖子大师 "
description: "总登录天数 800 天" description: "累计登录 800 天"
_login900: _login900:
title: "帖子大师 Ⅱ" title: "帖子大师 Ⅱ"
description: "总登录天数 900 天" description: "累计登录 900 天"
_login1000: _login1000:
title: "帖子大师 Ⅲ" title: "帖子大师 Ⅲ"
description: "总登录天数 1000 天" description: "累计登录 1000 天"
flavor: "感谢您使用 CherryPick" flavor: "感谢您使用 CherryPick"
_noteClipped1: _noteClipped1:
title: "忍不住要收藏到便签" title: "忍不住要收藏到便签"
@ -1452,6 +1462,7 @@ _ad:
back: "返回" back: "返回"
reduceFrequencyOfThisAd: "减少此广告的频率" reduceFrequencyOfThisAd: "减少此广告的频率"
hide: "不显示" hide: "不显示"
timezoneinfo: "星期几是由服务器的时区所指定的。"
_forgotPassword: _forgotPassword:
enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。" enterEmail: "请输入您设置的电子邮箱地址,密码重置链接将发送至该邮箱上。"
ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
@ -1503,6 +1514,10 @@ _aboutMisskey:
donate: "赞助 Misskey" donate: "赞助 Misskey"
morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰" morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰"
patrons: "支持者" patrons: "支持者"
_displayOfSensitiveMedia:
respect: "隐藏敏感媒体"
ignore: "显示敏感媒体"
force: "隐藏所有内容"
_mfm: _mfm:
cheatSheet: "MFM代码速查表" cheatSheet: "MFM代码速查表"
intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。" intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。"

View file

@ -1,7 +1,7 @@
--- ---
_lang_: "繁體中文" _lang_: "繁體中文"
headlineMisskey: "貼文連繫網" headlineMisskey: "貼文連繫網"
introMisskey: "歡迎! CherryPick是一個開放原始碼且去中心化的社群網路。\n透過「貼文」分享周邊新鮮事並告訴其他人您的想法📡\n透過「反應」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧🚀" introMisskey: "歡迎! CherryPick是一個開源且去中心化的社群網絡。\n通過「貼文」分享周邊新鮮事並告訴其他人您的想法📡\n透過「情感」功能,對大家的貼文表達情感!👍\n一起來探索這個新的世界吧🚀"
poweredByMisskeyDescription: "{name}是使用開放原始碼平台<b>CherryPick</b>的服務之一(稱為 CherryPick 伺服器)。\n" poweredByMisskeyDescription: "{name}是使用開放原始碼平台<b>CherryPick</b>的服務之一(稱為 CherryPick 伺服器)。\n"
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "搜尋" search: "搜尋"
@ -49,6 +49,7 @@ delete: "刪除"
deleteAndEdit: "刪除並編輯" deleteAndEdit: "刪除並編輯"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。" deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
addToList: "加入至清單" addToList: "加入至清單"
addToAntenna: "新增至天線"
sendMessage: "發送訊息" sendMessage: "發送訊息"
copyRSS: "複製RSS" copyRSS: "複製RSS"
copyUsername: "複製使用者名稱" copyUsername: "複製使用者名稱"
@ -56,6 +57,7 @@ copyUserId: "複製使用者ID"
copyNoteId: "複製貼文 ID" copyNoteId: "複製貼文 ID"
copyFileId: "複製檔案ID" copyFileId: "複製檔案ID"
copyFolderId: "複製資料夾ID" copyFolderId: "複製資料夾ID"
copyProfileUrl: "複製個人資料網址"
searchUser: "搜尋使用者" searchUser: "搜尋使用者"
reply: "回覆" reply: "回覆"
loadMore: "載入更多" loadMore: "載入更多"
@ -74,8 +76,8 @@ files: "檔案"
download: "下載" download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n" driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n"
unfollowConfirm: "確定要取消追隨{name}嗎?" unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" exportRequested: "已請求匯出。這可能會花一點時間。匯出的檔案將會被放到雲端裡。"
importRequested: "已請求匯入。這可能會花一點時間" importRequested: "已請求匯入。這可能會花一點時間"
lists: "清單" lists: "清單"
noLists: "你沒有任何清單" noLists: "你沒有任何清單"
note: "貼文" note: "貼文"
@ -89,9 +91,9 @@ error: "錯誤"
somethingHappened: "發生錯誤" somethingHappened: "發生錯誤"
retry: "重試" retry: "重試"
pageLoadError: "載入頁面失敗" pageLoadError: "載入頁面失敗"
pageLoadErrorDescription: "這通常是因為網路錯誤或瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試" pageLoadErrorDescription: "這通常是網路錯誤或瀏覽器快取殘留而引起的。請先清除瀏覽器快取,稍後再重試"
serverIsDead: "伺服器沒有回應。請稍等片刻再試。" serverIsDead: "伺服器沒有回應。請稍等片刻再試。"
youShouldUpgradeClient: "請重新載入以使用新版本的客戶端顯示此頁面" youShouldUpgradeClient: "請重新載入以使用新版客戶端顯示此頁面"
enterListName: "輸入清單名稱" enterListName: "輸入清單名稱"
privacy: "隱私" privacy: "隱私"
makeFollowManuallyApprove: "手動審核追隨請求" makeFollowManuallyApprove: "手動審核追隨請求"
@ -113,13 +115,13 @@ inChannelQuote: "在頻道內引用"
pinnedNote: "已置頂的貼文" pinnedNote: "已置頂的貼文"
pinned: "置頂" pinned: "置頂"
you: "您" you: "您"
clickToShow: "按一下以顯示" clickToShow: "點擊查看"
sensitive: "敏感內容" sensitive: "敏感內容"
add: "新增" add: "新增"
reaction: "反應" reaction: "反應"
reactions: "反應" reactions: "反應"
reactionSetting: "在選擇器中顯示反應" reactionSetting: "在選擇器中顯示反應"
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" reactionSettingDescription2: "拖動以交換,點擊以刪除,按下「+」以新增。"
rememberNoteVisibility: "記住貼文可見性" rememberNoteVisibility: "記住貼文可見性"
attachCancel: "移除附件" attachCancel: "移除附件"
markAsSensitive: "標記為敏感內容" markAsSensitive: "標記為敏感內容"
@ -154,8 +156,10 @@ addEmoji: "加入表情符號"
settingGuide: "推薦設定" settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案" cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。" cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。"
cacheRemoteSensitiveFiles: "快取遠端的敏感檔案"
cacheRemoteSensitiveFilesDescription: "若停用這個設定,則不會快取遠端的敏感檔案,而是直接連結。"
flagAsBot: "此使用者是機器人" flagAsBot: "此使用者是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整CherryPick內部系統將本帳戶識別為機器人" flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整CherryPick內部系統將本帳戶識別為機器人"
flagAsCat: "喵~~~~~~~~~~~~~~!!!!!!!!!!!!" flagAsCat: "喵~~~~~~~~~~~~~~!!!!!!!!!!!!"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" flagShowTimelineReplies: "在時間軸上顯示貼文的回覆"
@ -350,7 +354,7 @@ invite: "邀請"
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小" driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小" driveCapacityPerRemoteAccount: "每個非本地用戶的雲端空間大小"
inMb: "以Mbps為單位" inMb: "以Mbps為單位"
iconUrl: "圖標URL" iconUrl: "圖標URL (例如 favicon)"
bannerUrl: "橫幅圖片URL" bannerUrl: "橫幅圖片URL"
backgroundImageUrl: "背景圖片的來源網址 " backgroundImageUrl: "背景圖片的來源網址 "
basicInfo: "基本資訊" basicInfo: "基本資訊"
@ -410,7 +414,7 @@ totp: "驗證應用程式"
totpDescription: "以驗證應用程式輸入一次性密碼" totpDescription: "以驗證應用程式輸入一次性密碼"
moderator: "審查員" moderator: "審查員"
moderation: "審查" moderation: "審查"
nUsersMentioned: "提到了{n}" nUsersMentioned: "被提及到 {n} 次"
securityKeyAndPasskey: "安全金鑰・Passkey" securityKeyAndPasskey: "安全金鑰・Passkey"
securityKey: "安全金鑰" securityKey: "安全金鑰"
lastUsed: "上次使用" lastUsed: "上次使用"
@ -515,7 +519,7 @@ showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
objectStorage: "Object Storage (物件儲存)" objectStorage: "Object Storage (物件儲存)"
useObjectStorage: "使用Object Storage" useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”" objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理請指定其URL例如S3'https://<bucket>.s3.amazonaws.com'、GCS'https://storage.googleapis.com/<bucket>'。"
objectStorageBucket: "儲存空間Bucket" objectStorageBucket: "儲存空間Bucket"
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 "
objectStoragePrefix: "前綴" objectStoragePrefix: "前綴"
@ -604,7 +608,7 @@ plugins: "外掛"
preferencesBackups: "備份設定檔" preferencesBackups: "備份設定檔"
deck: "多欄模式" deck: "多欄模式"
undeck: "取消多欄模式" undeck: "取消多欄模式"
useBlurEffectForModal: "在模態框使用模糊效果" useBlurEffectForModal: "在對話框使用模糊效果"
useFullReactionPicker: "使用全尺寸的反應選擇器" useFullReactionPicker: "使用全尺寸的反應選擇器"
width: "寬度" width: "寬度"
height: "高度" height: "高度"
@ -679,7 +683,7 @@ instanceTicker: "貼文的實例來源"
waitingFor: "等待{x}" waitingFor: "等待{x}"
random: "隨機" random: "隨機"
system: "系統" system: "系統"
switchUi: "切換面" switchUi: "切換面"
desktop: "桌面" desktop: "桌面"
clip: "摘錄" clip: "摘錄"
createNew: "新建" createNew: "新建"
@ -825,7 +829,7 @@ emailNotConfiguredWarning: "沒有設定電子郵件地址"
ratio: "%" ratio: "%"
previewNoteText: "預覽文本" previewNoteText: "預覽文本"
customCss: "自定義 CSS" customCss: "自定義 CSS"
customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能致客戶端無法正常使用。" customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能致客戶端無法正常使用。"
global: "全域" global: "全域"
squareAvatars: "頭像以方形顯示" squareAvatars: "頭像以方形顯示"
sent: "發送" sent: "發送"
@ -1048,8 +1052,8 @@ rightTop: "右上"
leftBottom: "左下" leftBottom: "左下"
rightBottom: "右下" rightBottom: "右下"
stackAxis: "堆疊方向" stackAxis: "堆疊方向"
vertical: "向" vertical: "向"
horizontal: "向" horizontal: "向"
position: "位置" position: "位置"
serverRules: "伺服器規則" serverRules: "伺服器規則"
pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,請確認下列事項。" pleaseConfirmBelowBeforeSignup: "在本伺服器註冊之前,請確認下列事項。"
@ -1084,6 +1088,23 @@ branding: "品牌宣傳"
enableServerMachineStats: "公布伺服器的機器資訊" enableServerMachineStats: "公布伺服器的機器資訊"
enableIdenticonGeneration: "啟用每個使用者的Identicon" enableIdenticonGeneration: "啟用每個使用者的Identicon"
turnOffToImprovePerformance: "關閉時會提高性能。" turnOffToImprovePerformance: "關閉時會提高性能。"
createInviteCode: "建立邀請碼"
createWithOptions: "使用選項建立"
createCount: "建立數"
inviteCodeCreated: "已建立邀請碼"
inviteLimitExceeded: "可建立的邀請碼已達上限。"
createLimitRemaining: "可建立的邀請碼:剩餘 {limit} 個"
inviteLimitResetCycle: "可以在 {time} 內建立最多 {limit} 個邀請碼。"
expirationDate: "有效日期"
noExpirationDate: "不設有效日期"
inviteCodeUsedAt: "使用邀請碼的日期和時間"
registeredUserUsingInviteCode: "用了邀請碼的使用者"
waitingForMailAuth: "等待電子郵件認證"
inviteCodeCreator: "建立了邀請碼的使用者"
usedAt: "使用的日期和時間"
unused: "未使用"
used: "已使用"
expired: "過期"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "帳戶已建立完成!" accountCreated: "帳戶已建立完成!"
letsStartAccountSetup: "來進行帳戶的初始設定吧。" letsStartAccountSetup: "來進行帳戶的初始設定吧。"
@ -1394,6 +1415,9 @@ _role:
ltlAvailable: "瀏覽本地時間軸" ltlAvailable: "瀏覽本地時間軸"
canPublicNote: "允許公開貼文" canPublicNote: "允許公開貼文"
canInvite: "發行實例邀請碼" canInvite: "發行實例邀請碼"
inviteLimit: "可建立邀請碼的數量"
inviteLimitCycle: "邀請碼的發放間隔"
inviteExpirationTime: "邀請碼的有效日期"
canManageCustomEmojis: "管理自訂表情符號" canManageCustomEmojis: "管理自訂表情符號"
driveCapacity: "雲端硬碟容量" driveCapacity: "雲端硬碟容量"
alwaysMarkNsfw: "總是將檔案標記為NSFW" alwaysMarkNsfw: "總是將檔案標記為NSFW"
@ -1599,8 +1623,8 @@ _channel:
nameAndDescription: "名稱與說明" nameAndDescription: "名稱與說明"
nameOnly: "僅名稱" nameOnly: "僅名稱"
_menuDisplay: _menuDisplay:
sideFull: "向" sideFull: "向"
sideIcon: "向(圖示)" sideIcon: "向(圖示)"
top: "頂部" top: "頂部"
hide: "隱藏" hide: "隱藏"
_wordMute: _wordMute:
@ -1717,7 +1741,7 @@ _time:
day: "日" day: "日"
_timelineTutorial: _timelineTutorial:
title: "CherryPick的使用方法" title: "CherryPick的使用方法"
step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。" step1_1: "這個畫面是「時間軸」。發布到{name}的「貼文」按照時間順序顯示。"
step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。" step1_2: "時間軸有多種類型,例如在「首頁時間軸」中流動的是您追蹤的人的貼文;而在「本地時間軸」流動的是{name}全體的貼文。"
step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。" step2_1: "試試看,發布個貼文吧!按畫面上鉛筆圖示的按鈕開啟表格。"
step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。" step2_2: "初次貼文的內容,建議包括自我介紹以及「開始使用{name}」。"

View file

@ -1,6 +1,6 @@
{ {
"name": "cherrypick", "name": "cherrypick",
"version": "13.14.0-beta.2", "version": "13.14.0-beta.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -59,8 +59,8 @@
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.61.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.17.0", "cypress": "12.17.1",
"eslint": "8.44.0", "eslint": "8.45.0",
"start-server-and-test": "2.0.0" "start-server-and-test": "2.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View file

@ -0,0 +1,25 @@
export class RefactorInviteSystem1688720440658 {
name = 'RefactorInviteSystem1688720440658'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "pendingUserId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdById" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedById" character varying(32)`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189" UNIQUE ("usedById")`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_beba993576db0261a15364ea96e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189" FOREIGN KEY ("usedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_beba993576db0261a15364ea96e"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedById"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdById"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "pendingUserId"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "expiresAt"`);
}
}

View file

@ -0,0 +1,13 @@
export class AddIndexToRelations1688880985544 {
name = 'AddIndexToRelations1688880985544'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_beba993576db0261a15364ea96" ON "registration_ticket" ("createdById") `);
await queryRunner.query(`CREATE INDEX "IDX_b6f93f2f30bdbb9a5ebdc7c718" ON "registration_ticket" ("usedById") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_b6f93f2f30bdbb9a5ebdc7c718"`);
await queryRunner.query(`DROP INDEX "public"."IDX_beba993576db0261a15364ea96"`);
}
}

View file

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

View file

@ -76,7 +76,7 @@
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0", "@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.68", "@swc/core": "1.3.69",
"@vitalets/google-translate-api": "9.2.0", "@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.12.0", "ajv": "8.12.0",
@ -85,11 +85,11 @@
"autwh": "0.1.0", "autwh": "0.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bullmq": "4.2.0", "bullmq": "4.3.0",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.0", "cbor": "9.0.0",
"chalk": "5.2.0", "chalk": "5.3.0",
"chalk-template": "0.4.0", "chalk-template": "1.1.0",
"cherrypick-js": "workspace:*", "cherrypick-js": "workspace:*",
"cherrypick-mfm-js": "github:kokonect-link/mfm.js", "cherrypick-mfm-js": "github:kokonect-link/mfm.js",
"chokidar": "3.5.3", "chokidar": "3.5.3",
@ -144,15 +144,15 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"semver": "7.5.3", "semver": "7.5.4",
"sharp": "0.32.1", "sharp": "0.32.3",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp", "sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.9", "slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.6", "systeminformation": "5.18.7",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.7",
@ -162,7 +162,6 @@
"typescript": "5.1.6", "typescript": "5.1.6",
"ulid": "2.3.0", "ulid": "2.3.0",
"unzipper": "0.10.14", "unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.3", "web-push": "3.6.3",
"ws": "8.13.0", "ws": "8.13.0",
@ -179,14 +178,14 @@
"@types/content-disposition": "0.5.5", "@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21", "@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.2", "@types/jest": "29.5.3",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1", "@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.9", "@types/jsonld": "1.5.9",
"@types/jsrsasign": "10.5.8", "@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1", "@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31", "@types/ms": "^0.7.31",
"@types/node": "20.4.0", "@types/node": "20.4.2",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8", "@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -205,7 +204,6 @@
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6", "@types/unzipper": "0.10.6",
"@types/uuid": "9.0.2",
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
@ -214,7 +212,7 @@
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.61.0",
"aws-sdk-client-mock": "3.0.0", "aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.44.0", "eslint": "8.45.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"execa": "7.1.1", "execa": "7.1.1",
"jest": "29.6.1", "jest": "29.6.1",

View file

@ -80,7 +80,7 @@ export async function masterMain() {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimit);
} }
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
} }
function showEnvironment(): void { function showEnvironment(): void {

View file

@ -14,7 +14,9 @@ export type Source = {
repository_url?: string; repository_url?: string;
feedback_url?: string; feedback_url?: string;
url: string; url: string;
port: number; port?: number;
socket?: string;
chmodSocket?: string;
disableHsts?: boolean; disableHsts?: boolean;
db: { db: {
host: string; host: string;
@ -63,6 +65,7 @@ export type Source = {
apiKey: string; apiKey: string;
ssl?: boolean; ssl?: boolean;
index: string; index: string;
scope?: 'local' | 'global' | string[];
}; };
proxy?: string; proxy?: string;

View file

@ -82,6 +82,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
import { HashtagEntityService } from './entities/HashtagEntityService.js'; import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js'; import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js'; import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js'; import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js'; import { MutingEntityService } from './entities/MutingEntityService.js';
@ -211,6 +212,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService }; const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService }; const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService }; const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useExisting: MessagingMessageEntityService }; const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useExisting: MessagingMessageEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService }; const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService }; const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
@ -340,6 +342,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
GalleryPostEntityService, GalleryPostEntityService,
HashtagEntityService, HashtagEntityService,
InstanceEntityService, InstanceEntityService,
InviteCodeEntityService,
MessagingMessageEntityService, MessagingMessageEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
@ -464,6 +467,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$GalleryPostEntityService, $GalleryPostEntityService,
$HashtagEntityService, $HashtagEntityService,
$InstanceEntityService, $InstanceEntityService,
$InviteCodeEntityService,
$MessagingMessageEntityService, $MessagingMessageEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,
@ -588,6 +592,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
GalleryPostEntityService, GalleryPostEntityService,
HashtagEntityService, HashtagEntityService,
InstanceEntityService, InstanceEntityService,
InviteCodeEntityService,
MessagingMessageEntityService, MessagingMessageEntityService,
ModerationLogEntityService, ModerationLogEntityService,
MutingEntityService, MutingEntityService,
@ -711,6 +716,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv
$GalleryPostEntityService, $GalleryPostEntityService,
$HashtagEntityService, $HashtagEntityService,
$InstanceEntityService, $InstanceEntityService,
$InviteCodeEntityService,
$MessagingMessageEntityService, $MessagingMessageEntityService,
$ModerationLogEntityService, $ModerationLogEntityService,
$MutingEntityService, $MutingEntityService,

View file

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { v4 as uuid } from 'uuid';
import { IsNull, DataSource } from 'typeorm'; import { IsNull, DataSource } from 'typeorm';
import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
@ -24,7 +24,7 @@ export class CreateSystemUserService {
@bindThis @bindThis
public async createSystemUser(username: string): Promise<User> { public async createSystemUser(username: string): Promise<User> {
const password = uuid(); const password = randomUUID();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);

View file

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import sharp from 'sharp'; import sharp from 'sharp';
import { sharpBmp } from 'sharp-read-bmp'; import { sharpBmp } from 'sharp-read-bmp';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
@ -162,7 +162,7 @@ export class DriveService {
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
// for original // for original
const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`; const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
const url = `${ baseUrl }/${ key }`; const url = `${ baseUrl }/${ key }`;
// for alts // for alts
@ -179,7 +179,7 @@ export class DriveService {
]; ];
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`; webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
@ -187,7 +187,7 @@ export class DriveService {
} }
if (alts.thumbnail) { if (alts.thumbnail) {
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`; thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
@ -212,9 +212,9 @@ export class DriveService {
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
} else { // use internal storage } else { // use internal storage
const accessKey = uuid(); const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + uuid(); const thumbnailAccessKey = 'thumbnail-' + randomUUID();
const webpublicAccessKey = 'webpublic-' + uuid(); const webpublicAccessKey = 'webpublic-' + randomUUID();
const url = this.internalStorageService.saveFromPath(accessKey, path); const url = this.internalStorageService.saveFromPath(accessKey, path);
@ -584,9 +584,9 @@ export class DriveService {
if (isLink) { if (isLink) {
file.url = url; file.url = url;
// ローカルプロキシ用 // ローカルプロキシ用
file.accessKey = uuid(); file.accessKey = randomUUID();
file.thumbnailAccessKey = 'thumbnail-' + uuid(); file.thumbnailAccessKey = 'thumbnail-' + randomUUID();
file.webpublicAccessKey = 'webpublic-' + uuid(); file.webpublicAccessKey = 'webpublic-' + randomUUID();
} }
} }
@ -713,9 +713,9 @@ export class DriveService {
webpublicUrl: null, webpublicUrl: null,
storedInternal: false, storedInternal: false,
// ローカルプロキシ用 // ローカルプロキシ用
accessKey: uuid(), accessKey: randomUUID(),
thumbnailAccessKey: 'thumbnail-' + uuid(), thumbnailAccessKey: 'thumbnail-' + randomUUID(),
webpublicAccessKey: 'webpublic-' + uuid(), webpublicAccessKey: 'webpublic-' + randomUUID(),
}); });
} else { } else {
this.driveFilesRepository.delete(file.id); this.driveFilesRepository.delete(file.id);

View file

@ -1,5 +1,6 @@
import * as http from 'node:http'; import * as http from 'node:http';
import * as https from 'node:https'; import * as https from 'node:https';
import * as net from 'node:net';
import CacheableLookup from 'cacheable-lookup'; import CacheableLookup from 'cacheable-lookup';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@ -46,14 +47,14 @@ export class HttpRequestService {
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup as unknown as net.LookupFunction,
} as http.AgentOptions); });
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: cache.lookup as unknown as net.LookupFunction,
} as https.AgentOptions); });
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
@ -144,7 +145,7 @@ export class HttpRequestService {
method: args.method ?? 'GET', method: args.method ?? 'GET',
headers: { headers: {
'User-Agent': this.config.userAgent, 'User-Agent': this.config.userAgent,
...(args.headers ?? {}) ...(args.headers ?? {}),
}, },
body: args.body, body: args.body,
size: args.size ?? 10 * 1024 * 1024, size: args.size ?? 10 * 1024 * 1024,

View file

@ -1,5 +1,5 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
@ -416,7 +416,7 @@ export class QueueService {
to: webhook.url, to: webhook.url,
secret: webhook.secret, secret: webhook.secret,
createdAt: Date.now(), createdAt: Date.now(),
eventId: uuid(), eventId: randomUUID(),
}; };
return this.webhookDeliverQueue.add(webhook.id, data, { return this.webhookDeliverQueue.add(webhook.id, data, {

View file

@ -71,7 +71,7 @@ export class RemoteUserResolveService {
return await this.apPersonService.createPerson(self.href); return await this.apPersonService.createPerson(self.href);
} }
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す // ユーザー情報が古い場合は、WebFingerからやりなおして返す
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {

View file

@ -23,6 +23,9 @@ export type RolePolicies = {
ctlAvailable: boolean; ctlAvailable: boolean;
canPublicNote: boolean; canPublicNote: boolean;
canInvite: boolean; canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
inviteExpirationTime: number;
canManageCustomEmojis: boolean; canManageCustomEmojis: boolean;
canSearchNotes: boolean; canSearchNotes: boolean;
canHideAds: boolean; canHideAds: boolean;
@ -46,6 +49,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
ctlAvailable: true, ctlAvailable: true,
canPublicNote: true, canPublicNote: true,
canInvite: false, canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
inviteExpirationTime: 0,
canManageCustomEmojis: false, canManageCustomEmojis: false,
canSearchNotes: false, canSearchNotes: false,
canHideAds: false, canHideAds: false,
@ -283,6 +289,9 @@ export class RoleService implements OnApplicationShutdown {
ctlAvailable: calc('ctlAvailable', vs => vs.some(v => v === true)), ctlAvailable: calc('ctlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),

View file

@ -52,6 +52,7 @@ function compileQuery(q: Q): string {
@Injectable() @Injectable()
export class SearchService { export class SearchService {
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
private meilisearchNoteIndex: Index | null = null; private meilisearchNoteIndex: Index | null = null;
constructor( constructor(
@ -92,6 +93,10 @@ export class SearchService {
}, },
}); });
} }
if (config.meilisearch?.scope) {
this.meilisearchIndexScope = config.meilisearch.scope;
}
} }
@bindThis @bindThis
@ -100,7 +105,22 @@ export class SearchService {
if (!['home', 'public'].includes(note.visibility)) return; if (!['home', 'public'].includes(note.visibility)) return;
if (this.meilisearch) { if (this.meilisearch) {
this.meilisearchNoteIndex!.addDocuments([{ switch (this.meilisearchIndexScope) {
case 'global':
break;
case 'local':
if (note.userHost == null) break;
return;
default: {
if (note.userHost == null) break;
if (this.meilisearchIndexScope.includes(note.userHost)) break;
return;
}
}
await this.meilisearchNoteIndex?.addDocuments([{
id: note.id, id: note.id,
createdAt: note.createdAt.getTime(), createdAt: note.createdAt.getTime(),
userId: note.userId, userId: note.userId,

View file

@ -1,7 +1,6 @@
import { createPublicKey } from 'node:crypto'; import { createPublicKey, randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { v4 as uuid } from 'uuid';
import * as mfm from 'cherrypick-mfm-js'; import * as mfm from 'cherrypick-mfm-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -644,7 +643,7 @@ export class ApRendererService {
@bindThis @bindThis
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } { public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
if (typeof x === 'object' && x.id == null) { if (typeof x === 'object' && x.id == null) {
x.id = `${this.config.url}/${uuid()}`; x.id = `${this.config.url}/${randomUUID()}`;
} }
return Object.assign({ return Object.assign({

View file

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/index.js'; import type { DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js'; import type { RemoteUser } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -20,9 +19,6 @@ export class ApImageService {
private logger: Logger; private logger: Logger;
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.driveFilesRepository) @Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository, private driveFilesRepository: DriveFilesRepository,
@ -47,7 +43,7 @@ export class ApImageService {
const image = await this.apResolverService.createResolver().resolve(value); const image = await this.apResolverService.createResolver().resolve(value);
if (image.url == null) { if (image.url == null) {
throw new Error('invalid image: url not privided'); throw new Error('invalid image: url not provided');
} }
if (typeof image.url !== 'string') { if (typeof image.url !== 'string') {
@ -62,12 +58,17 @@ export class ApImageService {
const instance = await this.metaService.fetch(); const instance = await this.metaService.fetch();
// Cache if remote file cache is on AND either
// 1. remote sensitive file is also on
// 2. or the image is not sensitive
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
const file = await this.driveService.uploadFromUrl({ const file = await this.driveService.uploadFromUrl({
url: image.url, url: image.url,
user: actor, user: actor,
uri: image.url, uri: image.url,
sensitive: image.sensitive, sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles, isLink: !shouldBeCached,
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
}); });
if (!file.isLink || file.url === image.url) return file; if (!file.isLink || file.url === image.url) return file;

View file

@ -0,0 +1,52 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js';
import type { User } from '@/models/entities/User.js';
import type { RegistrationTicket } from '@/models/entities/RegistrationTicket.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
export class InviteCodeEntityService {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private userEntityService: UserEntityService,
) {
}
@bindThis
public async pack(
src: RegistrationTicket['id'] | RegistrationTicket,
me?: { id: User['id'] } | null | undefined,
): Promise<Packed<'InviteCode'>> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: {
id: src,
},
relations: ['createdBy', 'usedBy'],
});
return await awaitAll({
id: target.id,
code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: target.createdAt.toISOString(),
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt,
});
}
@bindThis
public packMany(
targets: any[],
me: { id: User['id'] },
) {
return Promise.all(targets.map(x => this.pack(x, me)));
}
}

View file

@ -0,0 +1,20 @@
import { secureRndstr } from './secure-rndstr.js';
const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns)
export function generateInviteCode(): string {
const code = secureRndstr(8, {
chars: CHARS,
});
const uniqueId = [];
let n = Math.floor(Date.now() / 1000 / 60);
while (true) {
uniqueId.push(CHARS[n % CHARS.length]);
const t = Math.floor(n / CHARS.length);
if (!t) break;
n = t;
}
return code + uniqueId.reverse().join('');
}

View file

@ -20,6 +20,7 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'
import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageSchema } from '@/models/json-schema/page.js'; import { packedPageSchema } from '@/models/json-schema/page.js';
import { packedUserGroupSchema } from '@/models/json-schema/user-group.js'; import { packedUserGroupSchema } from '@/models/json-schema/user-group.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
@ -56,6 +57,7 @@ export const refs = {
RenoteMuting: packedRenoteMutingSchema, RenoteMuting: packedRenoteMutingSchema,
Blocking: packedBlockingSchema, Blocking: packedBlockingSchema,
Hashtag: packedHashtagSchema, Hashtag: packedHashtagSchema,
InviteCode: packedInviteCodeSchema,
Page: packedPageSchema, Page: packedPageSchema,
Channel: packedChannelSchema, Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema, QueueCount: packedQueueCountSchema,

View file

@ -1,7 +1,6 @@
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id.js'; import { id } from '../id.js';
import { User } from './User.js'; import { User } from './User.js';
import type { Clip } from './Clip.js';
@Entity() @Entity()
export class Meta { export class Meta {
@ -126,6 +125,11 @@ export class Meta {
}) })
public cacheRemoteFiles: boolean; public cacheRemoteFiles: boolean;
@Column('boolean', {
default: true,
})
public cacheRemoteSensitiveFiles: boolean;
@Column({ @Column({
...id(), ...id(),
nullable: true, nullable: true,

View file

@ -1,17 +1,60 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
import { id } from '../id.js'; import { id } from '../id.js';
import { User } from './User.js';
@Entity() @Entity()
export class RegistrationTicket { export class RegistrationTicket {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index({ unique: true }) @Index({ unique: true })
@Column('varchar', { @Column('varchar', {
length: 64, length: 64,
}) })
public code: string; public code: string;
@Column('timestamp with time zone', {
nullable: true,
})
public expiresAt: Date | null;
@Column('timestamp with time zone')
public createdAt: Date;
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public createdBy: User | null;
@Index()
@Column({
...id(),
nullable: true,
})
public createdById: User['id'] | null;
@OneToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public usedBy: User | null;
@Index()
@Column({
...id(),
nullable: true,
})
public usedById: User['id'] | null;
@Column('timestamp with time zone', {
nullable: true,
})
public usedAt: Date | null;
@Column('varchar', {
length: 32,
nullable: true,
})
public pendingUserId: string | null;
} }

View file

@ -0,0 +1,45 @@
export const packedInviteCodeSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
code: {
type: 'string',
optional: false, nullable: false,
example: 'GR6S02ERUA5VR',
},
expiresAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
createdBy: {
type: 'object',
optional: false, nullable: true,
ref: 'UserLite',
},
usedBy: {
type: 'object',
optional: false, nullable: true,
ref: 'UserLite',
},
usedAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
used: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View file

@ -11,6 +11,8 @@ import { createTemp } from '@/misc/create-temp.js';
import type { Poll } from '@/models/entities/Poll.js'; import type { Poll } from '@/models/entities/Poll.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js'; import type { DbJobDataWithUser } from '../types.js';
@ -34,6 +36,8 @@ export class ExportNotesProcessorService {
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private driveFileEntityService: DriveFileEntityService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
} }
@ -97,7 +101,8 @@ export class ExportNotesProcessorService {
if (note.hasPoll) { if (note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id }); poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
} }
const content = JSON.stringify(serialize(note, poll)); const files = await this.driveFileEntityService.packManyByIds(note.fileIds);
const content = JSON.stringify(serialize(note, poll, files));
const isFirst = exportedNotesCount === 0; const isFirst = exportedNotesCount === 0;
await write(isFirst ? content : ',\n' + content); await write(isFirst ? content : ',\n' + content);
exportedNotesCount++; exportedNotesCount++;
@ -125,12 +130,13 @@ export class ExportNotesProcessorService {
} }
} }
function serialize(note: Note, poll: Poll | null = null): Record<string, unknown> { function serialize(note: Note, poll: Poll | null = null, files: Packed<'DriveFile'>[]): Record<string, unknown> {
return { return {
id: note.id, id: note.id,
text: note.text, text: note.text,
createdAt: note.createdAt, createdAt: note.createdAt,
fileIds: note.fileIds, fileIds: note.fileIds,
files: files,
replyId: note.replyId, replyId: note.replyId,
renoteId: note.renoteId, renoteId: note.renoteId,
poll: poll, poll: poll,

View file

@ -224,7 +224,18 @@ export class ServerService implements OnApplicationShutdown {
} }
}); });
if (this.config.socket) {
if (fs.existsSync(this.config.socket)) {
fs.unlinkSync(this.config.socket);
}
fastify.listen({ path: this.config.socket }, (err, address) => {
if (this.config.chmodSocket) {
fs.chmodSync(this.config.socket!, this.config.chmodSocket);
}
});
} else {
fastify.listen({ port: this.config.port, host: '0.0.0.0' }); fastify.listen({ port: this.config.port, host: '0.0.0.0' });
}
await fastify.ready(); await fastify.ready();
} }

View file

@ -1,8 +1,8 @@
import { randomUUID } from 'node:crypto';
import { pipeline } from 'node:stream'; import { pipeline } from 'node:stream';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js'; import { getIpHash } from '@/misc/get-ip-hash.js';
import type { LocalUser, User } from '@/models/entities/User.js'; import type { LocalUser, User } from '@/models/entities/User.js';
@ -372,7 +372,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (err instanceof ApiError || err instanceof AuthenticationError) { if (err instanceof ApiError || err instanceof AuthenticationError) {
throw err; throw err;
} else { } else {
const errId = uuid(); const errId = randomUUID();
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, { this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
ep: ep.name, ep: ep.name,
ps: data, ps: data,

View file

@ -38,8 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___invite from './endpoints/invite.js'; import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
import * as ep___inviteRevoke from './endpoints/invite-revoke.js'; import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -233,6 +233,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
import * as ep___invite_limit from './endpoints/invite/limit.js';
import * as ep___messaging_history from './endpoints/messaging/history.js'; import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js'; import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
@ -403,8 +407,8 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default }; const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default }; const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
const $inviteRevoke: Provider = { provide: 'ep:invite-revoke', useClass: ep___inviteRevoke.default }; const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default }; const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
@ -598,6 +602,10 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default }; const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default };
const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default }; const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default };
const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default }; const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default };
@ -772,8 +780,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getIndexStats, $admin_getIndexStats,
$admin_getTableStats, $admin_getTableStats,
$admin_getUserIps, $admin_getUserIps,
$invite, $admin_invite_create,
$inviteRevoke, $admin_invite_list,
$admin_promo_create, $admin_promo_create,
$admin_queue_clear, $admin_queue_clear,
$admin_queue_deliverDelayed, $admin_queue_deliverDelayed,
@ -967,6 +975,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_webhooks_show, $i_webhooks_show,
$i_webhooks_update, $i_webhooks_update,
$i_webhooks_delete, $i_webhooks_delete,
$invite_create,
$invite_delete,
$invite_list,
$invite_limit,
$messaging_history, $messaging_history,
$messaging_messages, $messaging_messages,
$messaging_messages_create, $messaging_messages_create,
@ -1135,8 +1147,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getIndexStats, $admin_getIndexStats,
$admin_getTableStats, $admin_getTableStats,
$admin_getUserIps, $admin_getUserIps,
$invite, $admin_invite_create,
$inviteRevoke, $admin_invite_list,
$admin_promo_create, $admin_promo_create,
$admin_queue_clear, $admin_queue_clear,
$admin_queue_deliverDelayed, $admin_queue_deliverDelayed,
@ -1329,6 +1341,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_webhooks_show, $i_webhooks_show,
$i_webhooks_update, $i_webhooks_update,
$i_webhooks_delete, $i_webhooks_delete,
$invite_create,
$invite_delete,
$invite_list,
$invite_limit,
$messaging_history, $messaging_history,
$messaging_messages, $messaging_messages,
$messaging_messages_create, $messaging_messages_create,

View file

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js'; import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, RegistrationTicket } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js'; import { CaptchaService } from '@/core/CaptchaService.js';
@ -109,13 +109,15 @@ export class SignupApiService {
} }
} }
let ticket: RegistrationTicket | null = null;
if (instance.disableRegistration) { if (instance.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') { if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400); reply.code(400);
return; return;
} }
const ticket = await this.registrationTicketsRepository.findOneBy({ ticket = await this.registrationTicketsRepository.findOneBy({
code: invitationCode, code: invitationCode,
}); });
@ -124,7 +126,15 @@ export class SignupApiService {
return; return;
} }
this.registrationTicketsRepository.delete(ticket.id); if (ticket.expiresAt && ticket.expiresAt < new Date()) {
reply.code(400);
return;
}
if (ticket.usedAt) {
reply.code(400);
return;
}
} }
if (instance.emailRequiredForSignup) { if (instance.emailRequiredForSignup) {
@ -148,14 +158,14 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8); const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt); const hash = await bcrypt.hash(password, salt);
await this.userPendingsRepository.insert({ const pendingUser = await this.userPendingsRepository.insert({
id: this.idService.genId(), id: this.idService.genId(),
createdAt: new Date(), createdAt: new Date(),
code, code,
email: emailAddress!, email: emailAddress!,
username: username, username: username,
password: hash, password: hash,
}); }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
const link = `${this.config.url}/signup-complete/${code}`; const link = `${this.config.url}/signup-complete/${code}`;
@ -163,6 +173,13 @@ export class SignupApiService {
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`, `To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
`To complete signup, please click this link: ${link}`); `To complete signup, please click this link: ${link}`);
if (ticket) {
await this.registrationTicketsRepository.update(ticket.id, {
usedAt: new Date(),
pendingUserId: pendingUser.id,
});
}
reply.code(204); reply.code(204);
return; return;
} else { } else {
@ -176,6 +193,14 @@ export class SignupApiService {
includeSecrets: true, includeSecrets: true,
}); });
if (ticket) {
await this.registrationTicketsRepository.update(ticket.id, {
usedAt: new Date(),
usedBy: account,
usedById: account.id,
});
}
return { return {
...res, ...res,
token: secret, token: secret,
@ -212,6 +237,15 @@ export class SignupApiService {
emailVerifyCode: null, emailVerifyCode: null,
}); });
const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id });
if (ticket) {
await this.registrationTicketsRepository.update(ticket.id, {
usedBy: account,
usedById: account.id,
pendingUserId: null,
});
}
return this.signinService.signin(request, reply, account as LocalUser); return this.signinService.signin(request, reply, account as LocalUser);
} catch (err) { } catch (err) {
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString()); throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());

View file

@ -38,8 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___invite from './endpoints/invite.js'; import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
import * as ep___inviteRevoke from './endpoints/invite-revoke.js'; import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -233,6 +233,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
import * as ep___invite_limit from './endpoints/invite/limit.js';
import * as ep___messaging_history from './endpoints/messaging/history.js'; import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js'; import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
@ -401,8 +405,8 @@ const eps = [
['admin/get-index-stats', ep___admin_getIndexStats], ['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats], ['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps], ['admin/get-user-ips', ep___admin_getUserIps],
['invite', ep___invite], ['admin/invite/create', ep___admin_invite_create],
['invite-revoke', ep___inviteRevoke], ['admin/invite/list', ep___admin_invite_list],
['admin/promo/create', ep___admin_promo_create], ['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
@ -596,6 +600,10 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete], ['i/webhooks/delete', ep___i_webhooks_delete],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
['invite/limit', ep___invite_limit],
['messaging/history', ep___messaging_history], ['messaging/history', ep___messaging_history],
['messaging/messages', ep___messaging_messages], ['messaging/messages', ep___messaging_messages],
['messaging/messages/create', ep___messaging_messages_create], ['messaging/messages/create', ep___messaging_messages_create],

View file

@ -0,0 +1,80 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
import { generateInviteCode } from '@/misc/generate-invite-code.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
errors: {
invalidDateTime: {
message: 'Invalid date-time format',
code: 'INVALID_DATE_TIME',
id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49',
},
},
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
code: {
type: 'string',
optional: false, nullable: false,
example: 'GR6S02ERUA5VR',
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
count: { type: 'integer', minimum: 1, maximum: 100, default: 1 },
expiresAt: { type: 'string', nullable: true },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private inviteCodeEntityService: InviteCodeEntityService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
throw new ApiError(meta.errors.invalidDateTime);
}
const ticketsPromises = [];
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
}
const tickets = await Promise.all(ticketsPromises);
return await this.inviteCodeEntityService.packMany(tickets, me);
});
}
}

View file

@ -0,0 +1,70 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
offset: { type: 'integer', default: 0 },
type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' },
sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private inviteCodeEntityService: InviteCodeEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registrationTicketsRepository.createQueryBuilder('ticket')
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
switch (ps.type) {
case 'unused': query.andWhere('ticket.usedBy IS NULL'); break;
case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break;
case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break;
}
switch (ps.sort) {
case '+createdAt': query.orderBy('ticket.createdAt', 'DESC'); break;
case '-createdAt': query.orderBy('ticket.createdAt', 'ASC'); break;
case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break;
case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break;
default: query.orderBy('ticket.id', 'DESC'); break;
}
query.limit(ps.limit);
query.skip(ps.offset);
const tickets = await query.getMany();
return await this.inviteCodeEntityService.packMany(tickets, me);
});
}
}

View file

@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
@ -20,6 +19,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: { emailRequiredForSignup: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -340,6 +343,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// pinnedPages: instance.pinnedPages, // pinnedPages: instance.pinnedPages,
// pinnedClipId: instance.pinnedClipId, // pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
pinnedUsers: instance.pinnedUsers, pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,

View file

@ -43,6 +43,7 @@ export const paramDef = {
defaultLightTheme: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true },
cacheRemoteFiles: { type: 'boolean' }, cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' },
enableHcaptcha: { type: 'boolean' }, enableHcaptcha: { type: 'boolean' },
hcaptchaSiteKey: { type: 'string', nullable: true }, hcaptchaSiteKey: { type: 'string', nullable: true },
@ -199,6 +200,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.cacheRemoteFiles = ps.cacheRemoteFiles; set.cacheRemoteFiles = ps.cacheRemoteFiles;
} }
if (ps.cacheRemoteSensitiveFiles !== undefined) {
set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles;
}
if (ps.emailRequiredForSignup !== undefined) { if (ps.emailRequiredForSignup !== undefined) {
set.emailRequiredForSignup = ps.emailRequiredForSignup; set.emailRequiredForSignup = ps.emailRequiredForSignup;
} }

View file

@ -1,4 +1,4 @@
import { v4 as uuid } from 'uuid'; import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js'; import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
// Generate token // Generate token
const token = uuid(); const token = randomUUID();
// Create session token document // Create session token document
const doc = await this.authSessionsRepository.insert({ const doc = await this.authSessionsRepository.insert({

View file

@ -0,0 +1,82 @@
import { MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
import { IdService } from '@/core/IdService.js';
import { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js';
import { generateInviteCode } from '@/misc/generate-invite-code.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
requireRolePolicy: 'canInvite',
errors: {
exceededCreateLimit: {
message: 'You have exceeded the limit for creating an invitation code.',
code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE',
id: '8b165dd3-6f37-4557-8db1-73175d63c641',
},
},
res: {
type: 'object',
optional: false, nullable: false,
properties: {
code: {
type: 'string',
optional: false, nullable: false,
example: 'GR6S02ERUA5VR',
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private inviteCodeEntityService: InviteCodeEntityService,
private idService: IdService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me.id);
if (policies.inviteLimit) {
const count = await this.registrationTicketsRepository.countBy({
createdAt: MoreThan(new Date(Date.now() - (policies.inviteLimitCycle * 1000 * 60))),
createdById: me.id,
});
if (count >= policies.inviteLimit) {
throw new ApiError(meta.errors.exceededCreateLimit);
}
}
const ticket = await this.registrationTicketsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
createdBy: me,
createdById: me.id,
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
return await this.inviteCodeEntityService.pack(ticket, me);
});
}
}

View file

@ -0,0 +1,71 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
requireRolePolicy: 'canInvite',
errors: {
noSuchCode: {
message: 'No such invite code.',
code: 'NO_SUCH_INVITE_CODE',
id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634',
},
cantDelete: {
message: 'You can\'t delete this invite code.',
code: 'CAN_NOT_DELETE_INVITE_CODE',
id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '5eb8d909-2540-4970-90b8-dd6f86088121',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
inviteId: { type: 'string', format: 'misskey:id' },
},
required: ['inviteId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId });
const isModerator = await this.roleService.isModerator(me);
if (ticket == null) {
throw new ApiError(meta.errors.noSuchCode);
}
if (ticket.createdById !== me.id && !isModerator) {
throw new ApiError(meta.errors.accessDenied);
}
if (ticket.usedAt && !isModerator) {
throw new ApiError(meta.errors.cantDelete);
}
await this.registrationTicketsRepository.delete(ticket.id);
});
}
}

View file

@ -1,9 +1,9 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js'; import type { RegistrationTicketsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js'; import { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@ -15,12 +15,9 @@ export const meta = {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
properties: { properties: {
code: { remaining: {
type: 'string', type: 'integer',
optional: false, nullable: false, optional: false, nullable: true,
example: '2ERUA5VR',
maxLength: 8,
minLength: 8,
}, },
}, },
}, },
@ -39,21 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.registrationTicketsRepository) @Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository, private registrationTicketsRepository: RegistrationTicketsRepository,
private idService: IdService, private roleService: RoleService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const code = secureRndstr(8, { const policies = await this.roleService.getUserPolicies(me.id);
chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns)
});
await this.registrationTicketsRepository.insert({ const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
id: this.idService.genId(), createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
createdAt: new Date(), createdById: me.id,
code, }) : null;
});
return { return {
code, remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null,
}; };
}); });
} }

View file

@ -0,0 +1,58 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistrationTicketsRepository } from '@/models/index.js';
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
requireRolePolicy: 'canInvite',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private inviteCodeEntityService: InviteCodeEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId)
.andWhere('ticket.createdById = :meId', { meId: me.id })
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
const tickets = await query
.limit(ps.limit)
.getMany();
return await this.inviteCodeEntityService.packMany(tickets, me);
});
}
}

View file

@ -83,6 +83,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: { emailRequiredForSignup: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
@ -330,6 +334,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...(ps.detail ? { ...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: (await this.usersRepository.countBy({ requireSetup: (await this.usersRepository.countBy({
host: IsNull(), host: IsNull(),
})) === 0, })) === 0,

View file

@ -1,7 +1,7 @@
import { randomUUID } from 'node:crypto';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
import { createBullBoard } from '@bull-board/api'; import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { BullAdapter } from '@bull-board/api/bullAdapter.js';
import { FastifyAdapter } from '@bull-board/fastify'; import { FastifyAdapter } from '@bull-board/fastify';
@ -676,7 +676,7 @@ export class ClientServerService {
}); });
fastify.setErrorHandler(async (error, request, reply) => { fastify.setErrorHandler(async (error, request, reply) => {
const errId = uuid(); const errId = randomUUID();
this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, { this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, {
path: request.routerPath, path: request.routerPath,
params: request.params, params: request.params,

View file

@ -35,7 +35,7 @@ html
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842 //- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.24.0') link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.25.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists if !config.clientManifestExists

View file

@ -4,6 +4,7 @@ import * as assert from 'assert';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@ -11,9 +12,12 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, ICollection, IPost } from '@/core/activitypub/type.js'; import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
import { Note } from '@/models/index.js'; import { Meta, Note } from '@/models/index.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
import { MetaService } from '@/core/MetaService.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { MockResolver } from '../misc/mock-resolver.js'; import { MockResolver } from '../misc/mock-resolver.js';
const host = 'https://host1.test'; const host = 'https://host1.test';
@ -63,15 +67,46 @@ function createRandomFeaturedCollection(actor: NonTransientIActor, length: numbe
}; };
} }
async function createRandomRemoteUser(
resolver: MockResolver,
personService: ApPersonService,
): Promise<RemoteUser> {
const actor = createRandomActor();
resolver.register(actor.id, actor);
return await personService.createPerson(actor.id, resolver);
}
describe('ActivityPub', () => { describe('ActivityPub', () => {
let imageService: ApImageService;
let noteService: ApNoteService; let noteService: ApNoteService;
let personService: ApPersonService; let personService: ApPersonService;
let rendererService: ApRendererService; let rendererService: ApRendererService;
let resolver: MockResolver; let resolver: MockResolver;
const metaInitial = {
cacheRemoteFiles: true,
cacheRemoteSensitiveFiles: true,
blockedHosts: [] as string[],
sensitiveWords: [] as string[],
} as Meta;
let meta = metaInitial;
beforeAll(async () => { beforeAll(async () => {
const app = await Test.createTestingModule({ const app = await Test.createTestingModule({
imports: [GlobalModule, CoreModule], imports: [GlobalModule, CoreModule],
})
.overrideProvider(DownloadService).useValue({
async downloadUrl(): Promise<{ filename: string }> {
return {
filename: 'dummy.tmp',
};
},
})
.overrideProvider(MetaService).useValue({
async fetch(): Promise<Meta> {
return meta;
},
}).compile(); }).compile();
await app.init(); await app.init();
@ -80,6 +115,7 @@ describe('ActivityPub', () => {
noteService = app.get<ApNoteService>(ApNoteService); noteService = app.get<ApNoteService>(ApNoteService);
personService = app.get<ApPersonService>(ApPersonService); personService = app.get<ApPersonService>(ApPersonService);
rendererService = app.get<ApRendererService>(ApRendererService); rendererService = app.get<ApRendererService>(ApRendererService);
imageService = app.get<ApImageService>(ApImageService);
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService)); resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
@ -219,4 +255,91 @@ describe('ActivityPub', () => {
assert.strictEqual(note.uri, actor2Note.id); assert.strictEqual(note.uri, actor2Note.id);
}); });
}); });
describe('Images', () => {
test('Create images', async () => {
const imageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
imageObject,
);
assert.ok(!driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
assert.ok(!sensitiveDriveFile.isLink);
});
test('cacheRemoteFiles=false disables caching', async () => {
meta = { ...metaInitial, cacheRemoteFiles: false };
const imageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
imageObject,
);
assert.ok(driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
assert.ok(sensitiveDriveFile.isLink);
});
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
meta = { ...metaInitial, cacheRemoteSensitiveFiles: false };
const imageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/foo.png',
name: '',
};
const driveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
imageObject,
);
assert.ok(!driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
mediaType: 'image/png',
url: 'http://host1.test/bar.png',
name: '',
sensitive: true,
};
const sensitiveDriveFile = await imageService.createImage(
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
assert.ok(sensitiveDriveFile.isLink);
});
});
}); });

View file

@ -285,6 +285,7 @@ type DetailedInstanceMetadata = LiteInstanceMetadata & {
pinnedPages: string[]; pinnedPages: string[];
pinnedClipId: string | null; pinnedClipId: string | null;
cacheRemoteFiles: boolean; cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
requireSetup: boolean; requireSetup: boolean;
proxyAccountName: string | null; proxyAccountName: string | null;
features: Record<string, any>; features: Record<string, any>;
@ -341,6 +342,10 @@ export type Endpoints = {
req: TODO; req: TODO;
res: TODO; res: TODO;
}; };
'admin/meta': {
req: TODO;
res: TODO;
};
'admin/reset-password': { 'admin/reset-password': {
req: TODO; req: TODO;
res: TODO; res: TODO;
@ -495,6 +500,14 @@ export type Endpoints = {
req: TODO; req: TODO;
res: TODO; res: TODO;
}; };
'admin/invite/create': {
req: TODO;
res: TODO;
};
'admin/invite/list': {
req: TODO;
res: TODO;
};
'admin/moderators/add': { 'admin/moderators/add': {
req: TODO; req: TODO;
res: TODO; res: TODO;
@ -1561,6 +1574,28 @@ export type Endpoints = {
req: TODO; req: TODO;
res: TODO; res: TODO;
}; };
'invite/create': {
req: NoParams;
res: Invite;
};
'invite/delete': {
req: {
inviteId: Invite['id'];
};
res: null;
};
'invite/list': {
req: {
limit?: number;
sinceId?: Invite['id'];
untilId?: Invite['id'];
};
res: Invite[];
};
'invite/limit': {
req: NoParams;
res: InviteLimit;
};
'messaging/history': { 'messaging/history': {
req: { req: {
limit?: number; limit?: number;
@ -2246,6 +2281,8 @@ declare namespace entities {
Blocking, Blocking,
Instance, Instance,
Signin, Signin,
Invite,
InviteLimit,
UserSorting, UserSorting,
OriginType OriginType
} }
@ -2346,6 +2383,23 @@ type Instance = {
// @public (undocumented) // @public (undocumented)
type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
// @public (undocumented)
type Invite = {
id: ID;
code: string;
expiresAt: DateString | null;
createdAt: DateString;
createdBy: UserLite | null;
usedBy: UserLite | null;
usedAt: DateString | null;
used: boolean;
};
// @public (undocumented)
type InviteLimit = {
remaining: number;
};
// @public (undocumented) // @public (undocumented)
function isAPIError(reason: any): reason is APIError; function isAPIError(reason: any): reason is APIError;
@ -2800,7 +2854,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// //
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:631:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:629:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View file

@ -20,13 +20,13 @@
"url": "git+https://github.com/misskey-dev/misskey.js.git" "url": "git+https://github.com/misskey-dev/misskey.js.git"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "7.36.1", "@microsoft/api-extractor": "7.36.2",
"@swc/jest": "0.2.26", "@swc/jest": "0.2.26",
"@types/jest": "29.5.2", "@types/jest": "29.5.3",
"@types/node": "20.4.0", "@types/node": "20.4.2",
"@typescript-eslint/eslint-plugin": "5.61.0", "@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.61.0",
"eslint": "8.44.0", "eslint": "8.45.0",
"jest": "29.6.1", "jest": "29.6.1",
"jest-fetch-mock": "3.0.3", "jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.4.0", "jest-websocket-mock": "2.4.0",
@ -39,7 +39,7 @@
], ],
"dependencies": { "dependencies": {
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.68", "@swc/core": "1.3.69",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0" "reconnecting-websocket": "4.4.0"
} }

View file

@ -2,7 +2,7 @@ import type {
Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance, Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance,
LiteInstanceMetadata, LiteInstanceMetadata,
MeDetailed, MeDetailed,
Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, MeSignup, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, MeSignup, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, Invite, InviteLimit,
} from './entities.js'; } from './entities.js';
type TODO = Record<string, any> | null; type TODO = Record<string, any> | null;
@ -20,6 +20,7 @@ export type Endpoints = {
'admin/get-table-stats': { req: TODO; res: TODO; }; 'admin/get-table-stats': { req: TODO; res: TODO; };
'admin/invite': { req: TODO; res: TODO; }; 'admin/invite': { req: TODO; res: TODO; };
'admin/logs': { req: TODO; res: TODO; }; 'admin/logs': { req: TODO; res: TODO; };
'admin/meta': { req: TODO; res: TODO; };
'admin/reset-password': { req: TODO; res: TODO; }; 'admin/reset-password': { req: TODO; res: TODO; };
'admin/resolve-abuse-user-report': { req: TODO; res: TODO; }; 'admin/resolve-abuse-user-report': { req: TODO; res: TODO; };
'admin/resync-chart': { req: TODO; res: TODO; }; 'admin/resync-chart': { req: TODO; res: TODO; };
@ -57,6 +58,8 @@ export type Endpoints = {
'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; }; 'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; };
'admin/federation/remove-all-following': { req: TODO; res: TODO; }; 'admin/federation/remove-all-following': { req: TODO; res: TODO; };
'admin/federation/update-instance': { req: TODO; res: TODO; }; 'admin/federation/update-instance': { req: TODO; res: TODO; };
'admin/invite/create': { req: TODO; res: TODO; };
'admin/invite/list': { req: TODO; res: TODO; };
'admin/moderators/add': { req: TODO; res: TODO; }; 'admin/moderators/add': { req: TODO; res: TODO; };
'admin/moderators/remove': { req: TODO; res: TODO; }; 'admin/moderators/remove': { req: TODO; res: TODO; };
'admin/promo/create': { req: TODO; res: TODO; }; 'admin/promo/create': { req: TODO; res: TODO; };
@ -442,6 +445,13 @@ export type Endpoints = {
// flash // flash
'flash/gen-token': { req: TODO; res: TODO; }; 'flash/gen-token': { req: TODO; res: TODO; };
// invite
'invite/create': { req: NoParams; res: Invite; };
'invite/delete': { req: { inviteId: Invite['id']; }; res: null; };
'invite/list': { req: { limit?: number; sinceId?: Invite['id']; untilId?: Invite['id'] }; res: Invite[]; };
'invite/limit': { req: NoParams; res: InviteLimit; };
// messaging // messaging
'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; }; 'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; };
'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; }; 'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; };

View file

@ -346,6 +346,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
pinnedPages: string[]; pinnedPages: string[];
pinnedClipId: string | null; pinnedClipId: string | null;
cacheRemoteFiles: boolean; cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
requireSetup: boolean; requireSetup: boolean;
proxyAccountName: string | null; proxyAccountName: string | null;
features: Record<string, any>; features: Record<string, any>;
@ -524,6 +525,21 @@ export type Signin = {
success: boolean; success: boolean;
}; };
export type Invite = {
id: ID;
code: string;
expiresAt: DateString | null;
createdAt: DateString;
createdBy: UserLite | null;
usedBy: UserLite | null;
usedAt: DateString | null;
used: boolean;
}
export type InviteLimit = {
remaining: number;
}
export type UserSorting = export type UserSorting =
| '+follower' | '+follower'
| '-follower' | '-follower'

View file

@ -115,3 +115,27 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
url: null, url: null,
}; };
} }
export function inviteCode(isUsed = false, hasExpiration = false, isExpired = false, isCreatedBySystem = false) {
const date = new Date();
const createdAt = new Date();
createdAt.setDate(date.getDate() - 1)
const expiresAt = new Date();
if (isExpired) {
expiresAt.setHours(date.getHours() - 1)
} else {
expiresAt.setHours(date.getHours() + 1)
}
return {
id: "9gyqzizw77",
code: "SLF3JKF7UV2H9",
expiresAt: hasExpiration ? expiresAt.toISOString() : null,
createdAt: createdAt.toISOString(),
createdBy: isCreatedBySystem ? null : userDetailed('8i3rvznx32'),
usedBy: isUsed ? userDetailed('3i3r2znx1v') : null,
usedAt: isUsed ? date.toISOString() : null,
used: isUsed,
}
}

View file

@ -404,6 +404,7 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.vue'),
glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInviteCode.vue'),
glob('src/pages/user/home.vue'), glob('src/pages/user/home.vue'),
]); ]);
const components = globs.flat(); const components = globs.flat();

View file

@ -20,9 +20,9 @@
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.3", "@syuilo/aiscript": "0.13.3",
"@tabler/icons-webfont": "2.24.0", "@tabler/icons-webfont": "2.25.0",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.11", "@vue-macros/reactivity-transform": "0.3.14",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"astring": "1.8.6", "astring": "1.8.6",
"autosize": "6.0.1", "autosize": "6.0.1",
@ -72,30 +72,30 @@
"typescript": "5.1.6", "typescript": "5.1.6",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.8.0", "vanilla-tilt": "1.8.0",
"vite": "4.4.1", "vite": "4.4.4",
"vue": "3.3.4", "vue": "3.3.4",
"vue-prism-editor": "2.0.0-alpha.2", "vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "7.0.26", "@storybook/addon-actions": "7.0.27",
"@storybook/addon-essentials": "7.0.26", "@storybook/addon-essentials": "7.0.27",
"@storybook/addon-interactions": "7.0.26", "@storybook/addon-interactions": "7.0.27",
"@storybook/addon-links": "7.0.26", "@storybook/addon-links": "7.0.27",
"@storybook/addon-storysource": "7.0.26", "@storybook/addon-storysource": "7.0.27",
"@storybook/addons": "7.0.26", "@storybook/addons": "7.0.27",
"@storybook/blocks": "7.0.26", "@storybook/blocks": "7.0.27",
"@storybook/core-events": "7.0.26", "@storybook/core-events": "7.0.27",
"@storybook/jest": "0.1.0", "@storybook/jest": "0.1.0",
"@storybook/manager-api": "7.0.26", "@storybook/manager-api": "7.0.27",
"@storybook/preview-api": "7.0.26", "@storybook/preview-api": "7.0.27",
"@storybook/react": "7.0.26", "@storybook/react": "7.0.27",
"@storybook/react-vite": "7.0.26", "@storybook/react-vite": "7.0.27",
"@storybook/testing-library": "0.2.0", "@storybook/testing-library": "0.2.0",
"@storybook/theming": "7.0.26", "@storybook/theming": "7.0.27",
"@storybook/types": "7.0.26", "@storybook/types": "7.0.27",
"@storybook/vue3": "7.0.26", "@storybook/vue3": "7.0.27",
"@storybook/vue3-vite": "7.0.26", "@storybook/vue3-vite": "7.0.27",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/vue": "7.0.0", "@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
@ -104,10 +104,10 @@
"@types/gulp-rename": "2.0.2", "@types/gulp-rename": "2.0.2",
"@types/matter-js": "0.18.5", "@types/matter-js": "0.18.5",
"@types/micromatch": "4.0.2", "@types/micromatch": "4.0.2",
"@types/node": "20.4.0", "@types/node": "20.4.2",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/testing-library__jest-dom": "5.14.7", "@types/testing-library__jest-dom": "5.14.8",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.2", "@types/uuid": "9.0.2",
@ -120,8 +120,8 @@
"acorn": "8.10.0", "acorn": "8.10.0",
"chokidar-cli": "3.0.0", "chokidar-cli": "3.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.17.0", "cypress": "12.17.1",
"eslint": "8.44.0", "eslint": "8.45.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.15.1", "eslint-plugin-vue": "9.15.1",
"fast-glob": "3.3.0", "fast-glob": "3.3.0",
@ -133,13 +133,13 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.0",
"storybook": "7.0.26", "storybook": "7.0.27",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly", "summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2", "vite-plugin-turbosnap": "1.0.2",
"vitest": "0.33.0", "vitest": "0.33.0",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1", "vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.4" "vue-tsc": "1.8.5"
} }
} }

View file

@ -13,11 +13,12 @@ import { miLocalStorage } from '@/local-storage';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
import { mainRouter } from '@/router'; import { mainRouter } from '@/router';
import { initializeSw } from '@/scripts/initialize-sw'; import { initializeSw } from '@/scripts/initialize-sw';
import { deckStore } from '@/ui/deck/deck-store';
import { userName } from '@/filters/user'; import { userName } from '@/filters/user';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : new URLSearchParams(window.location.search).has('zen') || (ui === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :

View file

@ -356,9 +356,7 @@ onMounted(() => {
props.textarea.addEventListener('keydown', onKeydown); props.textarea.addEventListener('keydown', onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) { document.body.addEventListener('mousedown', onMousedown);
el.addEventListener('mousedown', onMousedown);
}
nextTick(() => { nextTick(() => {
exec(); exec();
@ -374,9 +372,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
props.textarea.removeEventListener('keydown', onKeydown); props.textarea.removeEventListener('keydown', onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) { document.body.removeEventListener('mousedown', onMousedown);
el.removeEventListener('mousedown', onMousedown);
}
}); });
</script> </script>

View file

@ -61,15 +61,11 @@ onMounted(() => {
rootEl.style.top = `${top}px`; rootEl.style.top = `${top}px`;
rootEl.style.left = `${left}px`; rootEl.style.left = `${left}px`;
for (const el of Array.from(document.querySelectorAll('body *'))) { document.body.addEventListener('mousedown', onMousedown);
el.addEventListener('mousedown', onMousedown);
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
for (const el of Array.from(document.querySelectorAll('body *'))) { document.body.removeEventListener('mousedown', onMousedown);
el.removeEventListener('mousedown', onMousedown);
}
}); });
function onMousedown(evt: Event) { function onMousedown(evt: Event) {

View file

@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { userDetailed, inviteCode } from '../../.storybook/fakes';
import { commonHandlers } from '../../.storybook/mocks';
import MkInviteCode from './MkInviteCode.vue';
export const Default = {
render(args) {
return {
components: {
MkInviteCode,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkInviteCode v-bind="props" />',
};
},
args: {
invite: inviteCode() as any,
},
parameters: {
layout: 'centered',
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json(userDetailed(req.params.userId as string)));
}),
],
},
},
decorators: [() => ({
template: '<div style="width:100cqmin"><story/></div>',
})],
} satisfies StoryObj<typeof MkInviteCode>;
export const Used = {
...Default,
args: {
invite: inviteCode(true) as any
},
} satisfies StoryObj<typeof MkInviteCode>;
export const Expired = {
...Default,
args: {
invite: inviteCode(false, true, true) as any
},
} satisfies StoryObj<typeof MkInviteCode>;

View file

@ -0,0 +1,124 @@
<template>
<MkFolder>
<template #label>{{ invite.code }}</template>
<template #suffix>
<span v-if="invite.used">{{ i18n.ts.used }}</span>
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
</template>
<div class="_gaps_s" :class="$style.root">
<div :class="$style.items">
<div>
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
<div>{{ invite.code }}</div>
</div>
<div v-if="moderator">
<div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div>
<div v-if="invite.createdBy" :class="$style.user">
<MkAvatar :user="invite.createdBy" :class="$style.avatar" link preview/>
<MkUserName :user="invite.createdBy" :nowrap="false"/>
<div v-if="moderator">({{ invite.createdBy.id }})</div>
</div>
<div v-else>system</div>
</div>
<div v-if="invite.used">
<div :class="$style.label">{{ i18n.ts.registeredUserUsingInviteCode }}</div>
<div v-if="invite.usedBy" :class="$style.user">
<MkAvatar :user="invite.usedBy" :class="$style.avatar" link preview/>
<MkUserName :user="invite.usedBy" :nowrap="false"/>
<div v-if="moderator">({{ invite.usedBy.id }})</div>
</div>
<div v-else>{{ i18n.ts.unknown }} ({{ i18n.ts.waitingForMailAuth }})</div>
</div>
<div v-if="invite.expiresAt && !invite.used">
<div :class="$style.label">{{ i18n.ts.expirationDate }}</div>
<div><MkTime :time="invite.expiresAt" mode="absolute"/></div>
</div>
<div v-if="invite.usedAt">
<div :class="$style.label">{{ i18n.ts.inviteCodeUsedAt }}</div>
<div><MkTime :time="invite.usedAt" mode="absolute"/></div>
</div>
<div v-if="moderator">
<div :class="$style.label">{{ i18n.ts.createdAt }}</div>
<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
</div>
</div>
<div :class="$style.buttons">
<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import * as misskey from 'misskey-js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { i18n } from '@/i18n';
import * as os from '@/os';
const props = defineProps<{
invite: misskey.entities.Invite;
moderator?: boolean;
}>();
const emits = defineEmits<{
(event: 'deleted', value: string): void;
}>();
const isExpired = computed(() => {
return props.invite.expiresAt && new Date(props.invite.expiresAt) < new Date();
});
function deleteCode() {
os.apiWithDialog('invite/delete', {
inviteId: props.invite.id,
});
emits('deleted', props.invite.id);
}
function copyInviteCode() {
copyToClipboard(props.invite.code);
os.success();
}
</script>
<style lang="scss" module>
.root {
text-align: left;
}
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-gap: 12px;
}
.label {
font-size: 0.85em;
padding: 0 0 8px 0;
user-select: none;
opacity: 0.7;
}
.user {
display: flex;
align-items: center;
gap: 8px;
}
.avatar {
--height: 24px;
width: var(--height);
height: var(--height);
}
.buttons {
display: flex;
gap: 8px;
}
</style>

View file

@ -59,6 +59,9 @@ export const ROLE_POLICIES = [
'ctlAvailable', 'ctlAvailable',
'canPublicNote', 'canPublicNote',
'canInvite', 'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis', 'canManageCustomEmojis',
'canSearchNotes', 'canSearchNotes',
'canHideAds', 'canHideAds',

View file

@ -263,6 +263,18 @@ const patronsWithIconWithMisskey = [{
}, { }, {
name: 'Nagi8410', name: 'Nagi8410',
icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg', icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
}, {
name: '山岡士郎',
icon: 'https://misskey-hub.net/patrons/84b9056341684266bb1eda3e680d094d.jpg',
}, {
name: 'よもやまたろう',
icon: 'https://misskey-hub.net/patrons/4273c9cce50d445f8f7d0f16113d6d7f.jpg',
}, {
name: '花咲ももか',
icon: 'https://misskey-hub.net/patrons/8c9b2b9128cb4fee99f04bb4f86f2efa.jpg',
}, {
name: 'カガミ',
icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
}]; }];
const patronsWithCherryPick = [ const patronsWithCherryPick = [
@ -363,6 +375,7 @@ const patronsWithMisskey = [
'渡志郎', '渡志郎',
'ぷーざ', 'ぷーざ',
'越貝鯛丸', '越貝鯛丸',
'Nick / pprmint.',
]; ];
let isKokonect = false; let isKokonect = false;

View file

@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
}, ...(instance.disableRegistration ? [{ }, ...(instance.disableRegistration ? [{
type: 'button', type: 'button',
icon: 'ti ti-user-plus', icon: 'ti ti-user-plus',
text: i18n.ts.invite, text: i18n.ts.createInviteCode,
action: invite, action: invite,
}, { }, {
type: 'button', type: 'button',
@ -100,6 +100,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.users, text: i18n.ts.users,
to: '/admin/users', to: '/admin/users',
active: currentPage?.route.name === 'users', active: currentPage?.route.name === 'users',
}, {
icon: 'ti ti-user-plus',
text: i18n.ts.invite,
to: '/admin/invites',
active: currentPage?.route.name === 'invites',
}, { }, {
icon: 'ti ti-badges', icon: 'ti ti-badges',
text: i18n.ts.roles, text: i18n.ts.roles,
@ -245,10 +250,10 @@ provideMetadataReceiver((info) => {
}); });
const invite = () => { const invite = () => {
os.api('invite').then(x => { os.api('admin/invite/create').then(x => {
os.alert({ os.alert({
type: 'info', type: 'info',
text: x.code, text: x?.[0].code,
}); });
}).catch(err => { }).catch(err => {
os.alert({ os.alert({

View file

@ -0,0 +1,126 @@
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
<div class="_gaps_m">
<MkFolder :expanded="false">
<template #icon><i class="ti ti-plus"></i></template>
<template #label>{{ i18n.ts.createInviteCode }}</template>
<div class="_gaps_m">
<MkSwitch v-model="noExpirationDate">
<template #label>{{ i18n.ts.noExpirationDate }}</template>
</MkSwitch>
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expirationDate }}</template>
</MkInput>
<MkInput v-model="createCount" type="number">
<template #label>{{ i18n.ts.createCount }}</template>
</MkInput>
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
</div>
</MkFolder>
<div :class="$style.inputs">
<MkSelect v-model="type" :class="$style.input">
<template #label>{{ i18n.ts.state }}</template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="unused">{{ i18n.ts.unused }}</option>
<option value="used">{{ i18n.ts.used }}</option>
<option value="expired">{{ i18n.ts.expired }}</option>
</MkSelect>
<MkSelect v-model="sort" :class="$style.input">
<template #label>{{ i18n.ts.sort }}</template>
<option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option>
<option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option>
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
</MkSelect>
</div>
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #default="{ items }">
<div class="_gaps_s">
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
</div>
</template>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef } from 'vue';
import XHeader from './_header_.vue';
import { i18n } from '@/i18n';
import * as os from '@/os';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import MkInviteCode from '@/components/MkInviteCode.vue';
import { definePageMetadata } from '@/scripts/page-metadata';
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
let type = ref('all');
let sort = ref('+createdAt');
const pagination: Paging = {
endpoint: 'admin/invite/list' as const,
limit: 10,
params: computed(() => ({
type: type.value,
sort: sort.value,
})),
offsetMode: true,
};
const expiresAt = ref('');
const noExpirationDate = ref(true);
const createCount = ref(1);
async function createWithOptions() {
const options = {
expiresAt: noExpirationDate.value ? null : expiresAt.value,
count: createCount.value,
};
const tickets = await os.api('admin/invite/create', options);
os.alert({
type: 'success',
title: i18n.ts.inviteCodeCreated,
text: tickets?.map(x => x.code).join('\n'),
});
tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket));
}
function deleted(id: string) {
if (pagingComponent.value) {
pagingComponent.value.items.delete(id);
}
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.invite,
icon: 'ti ti-user-plus',
});
</script>
<style lang="scss" module>
.inputs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.input {
flex: 1;
}
</style>

View file

@ -1,5 +1,9 @@
<template> <template>
<div class="_gaps"> <div class="_gaps">
<MkInput v-if="readonly" :modelValue="role.id" :readonly="true">
<template #label>ID</template>
</MkInput>
<MkInput v-model="role.name" :readonly="readonly"> <MkInput v-model="role.name" :readonly="readonly">
<template #label>{{ i18n.ts._role.name }}</template> <template #label>{{ i18n.ts._role.name }}</template>
</MkInput> </MkInput>
@ -211,6 +215,65 @@
</div> </div>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
<template #suffix>
<span v-if="role.policies.inviteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.inviteLimit.value }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimit)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.inviteLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.inviteLimit.value" :disabled="role.policies.inviteLimit.useDefault" type="number" :readonly="readonly">
</MkInput>
<MkRange v-model="role.policies.inviteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
<template #suffix>
<span v-if="role.policies.inviteLimitCycle.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.inviteLimitCycle.value + i18n.ts._time.minute }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimitCycle)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.inviteLimitCycle.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.inviteLimitCycle.value" :disabled="role.policies.inviteLimitCycle.useDefault" type="number" :readonly="readonly">
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
<MkRange v-model="role.policies.inviteLimitCycle.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
<template #suffix>
<span v-if="role.policies.inviteExpirationTime.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.inviteExpirationTime.value + i18n.ts._time.minute }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteExpirationTime)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.inviteExpirationTime.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.inviteExpirationTime.value" :disabled="role.policies.inviteExpirationTime.useDefault" type="number" :readonly="readonly">
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
<MkRange v-model="role.policies.inviteExpirationTime.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
<template #suffix> <template #suffix>

View file

@ -67,6 +67,29 @@
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
<template #suffix>{{ policies.inviteLimit }}</template>
<MkInput v-model="policies.inviteLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>
<MkInput v-model="policies.inviteLimitCycle" type="number">
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
<template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template>
<MkInput v-model="policies.inviteExpirationTime" type="number">
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>

View file

@ -37,6 +37,13 @@
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template> <template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</MkSwitch> </MkSwitch>
<template v-if="cacheRemoteFiles">
<MkSwitch v-model="cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div> </div>
</FormSection> </FormSection>
@ -146,7 +153,6 @@ import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
let name: string | null = $ref(null); let name: string | null = $ref(null);
let description: string | null = $ref(null); let description: string | null = $ref(null);
@ -154,6 +160,7 @@ let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null); let maintainerEmail: string | null = $ref(null);
let pinnedUsers: string = $ref(''); let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false); let cacheRemoteFiles: boolean = $ref(false);
let cacheRemoteSensitiveFiles: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false); let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null); let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null); let swPrivateKey: any = $ref(null);
@ -166,7 +173,7 @@ let ctav3Location: string = $ref('');
let ctav3Model: string = $ref(''); let ctav3Model: string = $ref('');
let ctav3Glossary: string = $ref(''); let ctav3Glossary: string = $ref('');
async function init() { async function init(): Promise<void> {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
name = meta.name; name = meta.name;
description = meta.description; description = meta.description;
@ -174,6 +181,7 @@ async function init() {
maintainerEmail = meta.maintainerEmail; maintainerEmail = meta.maintainerEmail;
pinnedUsers = meta.pinnedUsers.join('\n'); pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles; cacheRemoteFiles = meta.cacheRemoteFiles;
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
enableServiceWorker = meta.enableServiceWorker; enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey; swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey; swPrivateKey = meta.swPrivateKey;
@ -187,7 +195,7 @@ async function init() {
ctav3Glossary = meta.ctav3Glossary; ctav3Glossary = meta.ctav3Glossary;
} }
function save() { function save(): void {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
name, name,
description, description,
@ -195,6 +203,7 @@ function save() {
maintainerEmail, maintainerEmail,
pinnedUsers: pinnedUsers.split('\n'), pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles, cacheRemoteFiles,
cacheRemoteSensitiveFiles,
enableServiceWorker, enableServiceWorker,
swPublicKey, swPublicKey,
swPrivateKey, swPrivateKey,

View file

@ -0,0 +1,114 @@
<template>
<MkStickyContainer>
<template #header>
<MkPageHeader/>
</template>
<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
<div :class="$style.root">
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
<div :class="$style.text">
<i class="ti ti-alert-triangle"></i>
{{ i18n.ts.nothing }}
</div>
</div>
</MKSpacer>
<MkSpacer v-else :contentMax="800">
<div class="_gaps_m" style="text-align: center;">
<div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
<div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #default="{ items }">
<div class="_gaps_s">
<MkInviteCode v-for="item in (items as Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
</div>
</template>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, ref, shallowRef } from 'vue';
import type { Invite } from 'misskey-js/built/entities';
import { i18n } from '@/i18n';
import * as os from '@/os';
import MkButton from '@/components/MkButton.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import MkInviteCode from '@/components/MkInviteCode.vue';
import { definePageMetadata } from '@/scripts/page-metadata';
import { serverErrorImageUrl, instance } from '@/instance';
import { $i } from '@/account';
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
const currentInviteLimit = ref<null | number>(null);
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
const pagination: Paging = {
endpoint: 'invite/list' as const,
limit: 10,
};
const resetCycle = computed<null | string>(() => {
if (!inviteLimitCycle) return null;
const minutes = inviteLimitCycle;
if (minutes < 60) return minutes + i18n.ts._time.minute;
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + i18n.ts._time.hour;
return Math.floor(hours / 24) + i18n.ts._time.day;
});
async function create() {
const ticket = await os.api('invite/create');
os.alert({
type: 'success',
title: i18n.ts.inviteCodeCreated,
text: ticket.code,
});
pagingComponent.value?.prepend(ticket);
update();
}
function deleted(id: string) {
if (pagingComponent.value) {
pagingComponent.value.items.delete(id);
}
update();
}
async function update() {
currentInviteLimit.value = (await os.api('invite/limit')).remaining;
}
update();
definePageMetadata({
title: i18n.ts.invite,
icon: 'ti ti-user-plus',
});
</script>
<style lang="scss" module>
.root {
padding: 32px;
text-align: center;
align-items: center;
}
.text {
margin: 0 0 8px 0;
}
.img {
vertical-align: bottom;
width: 128px;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
</style>

View file

@ -12,7 +12,7 @@
<div v-if="items.length > 0" class="_gaps"> <div v-if="items.length > 0" class="_gaps">
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
<div style="margin-bottom: 4px;">{{ list.name }}</div> <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div>
<MkAvatars :userIds="list.userIds" :limit="10"/> <MkAvatars :userIds="list.userIds" :limit="10"/>
</MkA> </MkA>
</div> </div>
@ -29,6 +29,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { userListsCache } from '@/cache'; import { userListsCache } from '@/cache';
import { infoImageUrl } from '@/instance'; import { infoImageUrl } from '@/instance';
import { $i } from '@/account';
const items = $computed(() => userListsCache.value.value ?? []); const items = $computed(() => userListsCache.value.value ?? []);
@ -67,10 +68,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.manageLists, title: i18n.ts.manageLists,
icon: 'ti ti-list', icon: 'ti ti-list',
action: {
icon: 'ti ti-plus',
handler: create,
},
}); });
onActivated(() => { onActivated(() => {
@ -91,4 +88,9 @@ onActivated(() => {
text-decoration: none; text-decoration: none;
} }
} }
.nUsers {
font-size: .9em;
opacity: .7;
}
</style> </style>

View file

@ -20,6 +20,7 @@
<MkFolder defaultOpen> <MkFolder defaultOpen>
<template #label>{{ i18n.ts.members }}</template> <template #label>{{ i18n.ts.members }}</template>
<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton> <MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
@ -29,6 +30,10 @@
</MkA> </MkA>
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button> <button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
</div> </div>
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-if="fetching" class="loading"/>
</div> </div>
</MkFolder> </MkFolder>
</div> </div>
@ -49,34 +54,57 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache'; import { userListsCache } from '@/cache';
import { UserList, UserLite } from 'misskey-js/built/entities';
import { $i } from '@/account';
import { defaultStore } from '@/store';
const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
const props = defineProps<{ const props = defineProps<{
listId: string; listId: string;
}>(); }>();
let list = $ref(null); const FETCH_USERS_LIMIT = 20;
let users = $ref([]);
let list = $ref<UserList | null>(null);
let users = $ref<UserLite[]>([]);
let queueUserIds = $ref<string[]>([]);
let fetching = $ref(true);
const isPublic = ref(false); const isPublic = ref(false);
const name = ref(''); const name = ref('');
function fetchList() { function fetchList() {
fetching = true;
os.api('users/lists/show', { os.api('users/lists/show', {
listId: props.listId, listId: props.listId,
}).then(_list => { }).then(_list => {
list = _list; list = _list;
name.value = list.name; name.value = list.name;
isPublic.value = list.isPublic; isPublic.value = list.isPublic;
queueUserIds = list.userIds;
os.api('users/show', { return fetchMoreUsers();
userIds: list.userIds,
}).then(_users => {
users = _users;
}); });
}
function fetchMoreUsers() {
if (!list) return;
if (fetching && users.length !== 0) return; // fetchingtrueusers
fetching = true;
os.api('users/show', {
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
}).then(_users => {
users = users.concat(_users);
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
}).finally(() => {
fetching = false;
}); });
} }
function addUser() { function addUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
if (!list) return;
os.apiWithDialog('users/lists/push', { os.apiWithDialog('users/lists/push', {
listId: list.id, listId: list.id,
userId: user.id, userId: user.id,
@ -92,6 +120,7 @@ async function removeUser(user, ev) {
icon: 'ti ti-x', icon: 'ti ti-x',
danger: true, danger: true,
action: async () => { action: async () => {
if (!list) return;
os.api('users/lists/pull', { os.api('users/lists/pull', {
listId: list.id, listId: list.id,
userId: user.id, userId: user.id,
@ -103,6 +132,7 @@ async function removeUser(user, ev) {
} }
async function deleteList() { async function deleteList() {
if (!list) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.t('removeAreYouSure', { x: list.name }), text: i18n.t('removeAreYouSure', { x: list.name }),
@ -117,6 +147,7 @@ async function deleteList() {
} }
async function updateSettings() { async function updateSettings() {
if (!list) return;
await os.apiWithDialog('users/lists/update', { await os.apiWithDialog('users/lists/update', {
listId: list.id, listId: list.id,
name: name.value, name: name.value,
@ -166,6 +197,11 @@ definePageMetadata(computed(() => list ? {
align-self: center; align-self: center;
} }
.more {
margin-left: auto;
margin-right: auto;
}
.footer { .footer {
-webkit-backdrop-filter: var(--blur, blur(15px)); -webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px));

View file

@ -1,5 +1,7 @@
<template> <template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkSwitch v-model="useSimpleUiForNonRootPages">{{ i18n.ts._deck.useSimpleUiForNonRootPages }}</MkSwitch>
<MkSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</MkSwitch> <MkSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</MkSwitch>
<MkSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</MkSwitch> <MkSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</MkSwitch>
@ -21,6 +23,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow')); const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign')); const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));

View file

@ -209,6 +209,10 @@ export const routes = [{
}, { }, {
path: '/about-misskey', path: '/about-misskey',
component: page(() => import('./pages/about-misskey.vue')), component: page(() => import('./pages/about-misskey.vue')),
}, {
path: '/invite',
name: 'invite',
component: page(() => import('./pages/invite.vue')),
}, { }, {
path: '/ads', path: '/ads',
component: page(() => import('./pages/ads.vue')), component: page(() => import('./pages/ads.vue')),
@ -440,6 +444,10 @@ export const routes = [{
path: '/server-rules', path: '/server-rules',
name: 'server-rules', name: 'server-rules',
component: page(() => import('./pages/admin/server-rules.vue')), component: page(() => import('./pages/admin/server-rules.vue')),
}, {
path: '/invites',
name: 'invites',
component: page(() => import('./pages/admin/invites.vue')),
}, { }, {
path: '/', path: '/',
component: page(() => import('./pages/_empty_.vue')), component: page(() => import('./pages/_empty_.vue')),

View file

@ -132,9 +132,7 @@ export function play(type: 'noteMy' | 'note' | 'chat' | 'chatBg' | 'antenna' | '
} }
export function playFile(file: string, volume: number) { export function playFile(file: string, volume: number) {
const masterVolume = soundConfigStore.state.sound_masterVolume;
if (masterVolume === 0) return;
const audio = setVolume(getAudio(file), volume); const audio = setVolume(getAudio(file), volume);
if (audio.volume === 0) return;
audio.play(); audio.play();
} }

View file

@ -33,7 +33,12 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.ads, text: i18n.ts.ads,
icon: 'ti ti-ad', icon: 'ti ti-ad',
to: '/ads', to: '/ads',
}, { }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
type: 'link',
to: '/invite',
text: i18n.ts.invite,
icon: 'ti ti-user-plus',
} : undefined, {
type: 'parent', type: 'parent',
text: i18n.ts.tools, text: i18n.ts.tools,
icon: 'ti ti-tool', icon: 'ti ti-tool',
@ -52,23 +57,7 @@ export function openInstanceMenu(ev: MouseEvent) {
to: '/clicker', to: '/clicker',
text: '🍪👈', text: '🍪👈',
icon: 'ti ti-cookie', icon: 'ti ti-cookie',
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
text: i18n.ts.invite,
icon: 'ti ti-user-plus',
action: () => {
os.api('invite').then(x => {
os.alert({
type: 'info',
text: x.code,
});
}).catch(err => {
os.alert({
type: 'error',
text: err,
});
});
},
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
type: 'link', type: 'link',
to: '/custom-emojis-manager', to: '/custom-emojis-manager',
text: i18n.ts.manageCustomEmojis, text: i18n.ts.manageCustomEmojis,

View file

@ -52,6 +52,10 @@ export const deckStore = markRaw(new Storage('deck', {
where: 'deviceAccount', where: 'deviceAccount',
default: true, default: true,
}, },
useSimpleUiForNonRootPages: {
where: 'deviceAccount',
default: true,
},
})); }));
export const loadDeck = async () => { export const loadDeck = async () => {

View file

@ -16,7 +16,7 @@
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "5.61.0", "@typescript-eslint/parser": "5.61.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
"eslint": "8.44.0", "eslint": "8.45.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },

View file

@ -216,6 +216,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
tag: `achievement:${data.body.achievement}`, tag: `achievement:${data.body.achievement}`,
}]; }];
case 'pollEnded':
return [t('_notification.pollEnded'), {
body: data.body.note.text ?? '',
badge: iconUrl('chart-arrows'),
data,
}];
case 'app': case 'app':
return [data.body.header ?? data.body.body, { return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '', body: data.body.header ? data.body.body : '',

File diff suppressed because it is too large Load diff