Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-07-16 14:14:48 +08:00 committed by GitHub
commit ada2d8dcb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 3068 additions and 1093 deletions

View File

@ -104,6 +104,7 @@ redis:
# apiKey: ''
# ssl: true
# index: ''
# scope: local
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────

View File

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

View File

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

View File

@ -49,6 +49,7 @@ delete: "Löschen"
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."
addToList: "Zu Liste hinzufügen"
addToAntenna: "Zu Antenne hinzufügen"
sendMessage: "Nachricht senden"
copyRSS: "RSS kopieren"
copyUsername: "Benutzernamen kopieren"
@ -56,6 +57,7 @@ copyUserId: "Benutzer-ID kopieren"
copyNoteId: "Notiz-ID kopieren"
copyFileId: "Datei-ID kopieren"
copyFolderId: "Ordner-ID kopieren"
copyProfileUrl: "Profil-URL kopieren"
searchUser: "Nach einem Benutzer suchen"
reply: "Antworten"
loadMore: "Mehr laden"
@ -154,6 +156,8 @@ addEmoji: "Emoji hinzufügen"
settingGuide: "Empfohlene Einstellung"
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."
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"
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 Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln."
flagAsCat: "Als Katze markieren"
@ -1070,6 +1074,23 @@ branding: "Branding"
enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen"
enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren"
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:
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
@ -1380,6 +1401,9 @@ _role:
ltlAvailable: "Kann auf die lokale Chronik zugreifen"
canPublicNote: "Kann öffentliche Notizen erstellen"
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"
driveCapacity: "Drive-Kapazität"
alwaysMarkNsfw: "Dateien immer als NSFW markieren"

View File

@ -49,6 +49,7 @@ delete: "Delete"
deleteAndEdit: "Delete and edit"
deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it."
addToList: "Add to list"
addToAntenna: "Add to antenna"
sendMessage: "Send a message"
copyRSS: "Copy RSS"
copyUsername: "Copy username"
@ -56,6 +57,7 @@ copyUserId: "Copy user ID"
copyNoteId: "Copy note ID"
copyFileId: "Copy file ID"
copyFolderId: "Copy folder ID"
copyProfileUrl: "Copy profile URL"
searchUser: "Search for a user"
reply: "Reply"
loadMore: "Load more"
@ -154,6 +156,8 @@ addEmoji: "Add an emoji"
settingGuide: "Recommended settings"
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."
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"
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 Misskey's internal systems to treat this account as a bot."
flagAsCat: "Mark this account as a cat"
@ -1070,6 +1074,23 @@ branding: "Branding"
enableServerMachineStats: "Publish server hardware stats"
enableIdenticonGeneration: "Enable user identicon generation"
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"
_initialAccountSetting:
accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."
@ -1380,6 +1401,9 @@ _role:
ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes"
canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis"
driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW"

View File

@ -54,6 +54,8 @@ copyRSS: "Copiar RSS"
copyUsername: "Copiar nombre de usuario"
copyUserId: "Copiar ID del usuario"
copyNoteId: "Copiar ID de la nota"
copyFileId: "Copiar un archivo ID"
copyFolderId: "Copiar carpeta ID"
searchUser: "Buscar un usuario"
reply: "Responder"
loadMore: "Ver más"
@ -313,6 +315,7 @@ copyUrl: "Copiar URL"
rename: "Renombrar"
avatar: "Avatar"
banner: "Banner"
displayOfSensitiveMedia: "Mostrar contenido sensible"
whenServerDisconnected: "Cuando se pierda la conexión con el servidor"
disconnectedFromServer: "Desconectado del servidor"
reload: "Recargar"
@ -847,7 +850,7 @@ manageAccounts: "Administrar cuenta"
makeReactionsPublic: "Hacer el historial de reacciones público"
makeReactionsPublicDescription: "Todas las reacciones que hayas hecho serán públicamente visibles."
classic: "Clásico"
muteThread: "Ocultar hilo"
muteThread: "Silenciar hilo"
unmuteThread: "Mostrar hilo"
ffVisibility: "Visibilidad de seguidores y seguidos"
ffVisibilityDescription: "Puedes configurar quien puede ver a quienes sigues y quienes te siguen"
@ -994,6 +997,7 @@ reactionAcceptance: "Aceptación de reacciones"
likeOnly: "Sólo 'me gusta'"
likeOnlyForRemote: "Sólo reacciones de instancias remotas"
nonSensitiveOnly: "Solo no sensible"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Sólo no contenido sensible (sólo me gusta en remote)"
rolesAssignedToMe: "Roles asignados a mí"
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
sensitiveWords: "Palabras sensibles"
@ -1029,9 +1033,14 @@ continue: "Continuar"
preservedUsernames: "Nombre de usuario reservado"
archive: "Archivo"
channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
displayOfNote: "Mostrar notas"
initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio"
youFollowing: "Siguiendo"
options: "Opción"
update: "Actualizar"
later: "Ahora no"
goToMisskey: "ir a Misskey"
additionalEmojiDictionary: "Diccionario adicional de Emoji"
installed: "Instalado"
branding: "Marca"
enableServerMachineStats: "Publicar estadísticas de hardware del servidor"

View File

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

22
locales/index.d.ts vendored
View File

@ -159,6 +159,8 @@ export interface Locale {
"settingGuide": string;
"cacheRemoteFiles": string;
"cacheRemoteFilesDescription": string;
"cacheRemoteSensitiveFiles": string;
"cacheRemoteSensitiveFilesDescription": string;
"flagAsBot": string;
"flagAsBotDescription": string;
"flagAsCat": string;
@ -1075,6 +1077,23 @@ export interface Locale {
"enableServerMachineStats": string;
"enableIdenticonGeneration": 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;
"_initialAccountSetting": {
"accountCreated": string;
"letsStartAccountSetup": string;
@ -1465,6 +1484,9 @@ export interface Locale {
"ltlAvailable": string;
"canPublicNote": string;
"canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;
"inviteExpirationTime": string;
"canManageCustomEmojis": string;
"driveCapacity": string;
"alwaysMarkNsfw": string;

View File

@ -49,11 +49,15 @@ delete: "Elimina"
deleteAndEdit: "Elimina e modifica"
deleteAndEditConfirm: "Vuoi davvero cancellare questa nota e scriverla di nuovo? Verranno eliminate anche tutte le reazioni, rinote e risposte collegate."
addToList: "Aggiungi alla lista"
addToAntenna: "Aggiungi all'antenna"
sendMessage: "Invia messaggio"
copyRSS: "Copia RSS"
copyUsername: "Copia nome utente"
copyUserId: "Copia ID del profilo"
copyNoteId: "Copia ID della Nota"
copyFileId: "Copia ID del file"
copyFolderId: "Copia ID della cartella"
copyProfileUrl: "Copia URL del profilo"
searchUser: "Cerca profilo"
reply: "Rispondi"
loadMore: "Mostra di più"
@ -136,8 +140,10 @@ unblockConfirm: "Vuoi davvero sbloccare il profilo?"
suspendConfirm: "Vuoi sospendere questo profilo?"
unsuspendConfirm: "Vuoi revocare la sospensione si questo profilo?"
selectList: "Seleziona una lista"
editList: "Modifica Lista"
selectChannel: "Seleziona canale"
selectAntenna: "Scegli un'antenna"
editAntenna: "Modifica Antenna"
selectWidget: "Seleziona il riquadro"
editWidgets: "Modifica i riquadri"
editWidgetsExit: "Conferma le modifiche"
@ -311,6 +317,7 @@ copyUrl: "Copia URL"
rename: "Modifica nome"
avatar: "Foto del profilo"
banner: "Intestazione"
displayOfSensitiveMedia: "Visibilità dei media sensibili"
whenServerDisconnected: "Quando la connessione col server è persa"
disconnectedFromServer: "Il server si è disconnesso"
reload: "Ricarica"
@ -1064,6 +1071,7 @@ installed: "Installazione avvenuta"
branding: "Branding"
enableServerMachineStats: "Pubblicare le informazioni sul server"
enableIdenticonGeneration: "Generazione automatica delle Identicon"
turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni"
_initialAccountSetting:
accountCreated: "Il tuo profilo è stato creato!"
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
@ -1436,6 +1444,7 @@ _ad:
back: "Indietro"
reduceFrequencyOfThisAd: "Visualizza questa pubblicità meno spesso"
hide: "Nascondi"
timezoneinfo: "Il giorno della settimana è determinato in base al fuso orario del server."
_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."
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
@ -1487,6 +1496,10 @@ _aboutMisskey:
donate: "Sostieni Misskey"
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
patrons: "Sostenitori"
_displayOfSensitiveMedia:
respect: "Nascondere i media sensibili"
ignore: "Non nascondere i media sensibili"
force: "Nascondi tutti i media"
_instanceTicker:
none: "Nascondi"
remote: "Mostra solo per i profili remoti"

View File

@ -156,6 +156,8 @@ addEmoji: "絵文字を追加"
settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
@ -1072,6 +1074,23 @@ branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
createWithOptions: "オプションを指定して作成"
createCount: "作成数"
inviteCodeCreated: "招待コードを作成しました"
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
expirationDate: "有効期限"
noExpirationDate: "有効期限を設けない"
inviteCodeUsedAt: "招待コードが使用された日時"
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
waitingForMailAuth: "メール認証待ち"
inviteCodeCreator: "招待コードを作成したユーザー"
usedAt: "使用日時"
unused: "未使用"
used: "使用済み"
expired: "期限切れ"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
@ -1387,6 +1406,9 @@ _role:
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
inviteExpirationTime: "招待コードの有効期限"
canManageCustomEmojis: "カスタム絵文字の管理"
driveCapacity: "ドライブ容量"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"

View File

@ -1066,6 +1066,9 @@ installed: "インストール済み"
branding: "あ"
enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。"
unused: "つこてへん"
used: "もうつこてる"
_initialAccountSetting:
accountCreated: "アカウント作り終わったで。"
letsStartAccountSetup: "アカウントの初期設定をしよか。"
@ -1489,6 +1492,10 @@ _aboutMisskey:
donate: "Misskeyに寄付"
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
patrons: "支援者"
_displayOfSensitiveMedia:
respect: "きわどいのは見とうない"
ignore: "きわどいのも見たい"
force: "常にメディアを隠すで"
_instanceTicker:
none: "表示せん"
remote: "リモートユーザーに表示"

View File

@ -20,6 +20,7 @@ noNotes: "Geen notities"
noNotifications: "Geen meldingen"
instance: "Server"
settings: "Instellingen"
notificationSettings: "Notificatie instellingen"
basicSettings: "Basisinstellingen"
otherSettings: "Overige instellingen"
openInWindow: "In een venster openen"
@ -48,8 +49,15 @@ delete: "Verwijderen"
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."
addToList: "Aan lijst toevoegen"
addToAntenna: "Voeg toe aan antenne"
sendMessage: "Verstuur bericht"
copyRSS: "Kopieer RSS"
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"
reply: "Antwoord"
loadMore: "Laad meer"

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
---
_lang_: "Türkçe"
headlineMisskey: "Notlarla bağlanmış bir ağ"
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Misskey'e hoş geldiniz.\nMisskey, 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>Misskey</b>Dünya'nın en sunucularında biri。"
monthAndDay: "{month}Ay {day}Gün"
@ -11,7 +12,9 @@ forgotPassword: "şifremi unuttum"
ok: "TAMAM"
gotIt: "Anladım"
cancel: "İptal"
noThankYou: "Hayır, teşekkürler"
enterUsername: "Kullanıcı adınızı giriniz"
renotedBy: "{user} tarafından Renotelandı"
noNotes: "Notlar mevcut değil."
noNotifications: "Bildirim bulunmuyor"
instance: "Sunucu"
@ -45,15 +48,35 @@ delete: "Sil"
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."
addToList: "Listeye ekle"
addToAntenna: "Antene ekle"
sendMessage: "Mesaj Gönder"
copyRSS: "RSSKopyala"
copyUsername: "Kullanıcı Adını Kopyala"
copyUserId: "Kullanıcıyı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"
reply: "yanıt"
loadMore: "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"
noLists: "Liste yok"
note: "not"
@ -64,6 +87,16 @@ followsYou: "seni takip ediyor"
createList: "Liste oluştur"
manageLists: "Yönetici Listeleri"
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"
followRequest: "Takip isteği"
followRequests: "Takip istekleri"
@ -76,9 +109,24 @@ renoted: "yeniden adlandırılmış"
cantRenote: "Ayrılamama"
cantReRenote: "not alabilirmiyim"
quote: "alıntı"
inChannelRenote: "Kanal içi Renote"
inChannelQuote: "Kanal içi Alıntı"
pinnedNote: "Sabitlenen"
pinned: "Sabitlenmiş"
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ç"
renoteMute: "sesi kapat"
renoteUnmute: "sesi açmayı iptal et"
@ -88,46 +136,280 @@ suspend: "askıya al"
unsuspend: "askıya alma"
blockConfirm: "Onayı engelle"
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ç"
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"
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 Misskey'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"
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: "Misskey 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"
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"
manageAntennas: "Anten ayarları"
userList: "Listeler"
resetPassword: "Şifre sıfırlama"
noMessagesYet: "Şimdilik mesaj yok"
details: "Detaylar"
deck: "Güverte"
smtpHost: "Sağlayıcı"
smtpUser: "Kullanıcı Adı"
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ı"
global: "Küresel"
squareAvatars: "Kare avatarlar"
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:
color: "Renk"
keys:
mention: "Bahset"
renote: "vazgeçme"
_sfx:
note: "notlar"
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:
profile: "Profil"
instanceInfo: "Sunucu Bilgisi"
notifications: "Bildirim"
timeline: "Zaman çizelgesi"
calendar: "Takvim"
clock: "Saat"
activity: "Etkinlik"
federation: "Federasyon"
jobQueue: "İşlem sırası"
_userList:
chooseList: "Bir liste seç"
_cw:
show: "Devamını yükle"
_poll:
vote: "Oy kullan"
_visibility:
publicDescription: "Herkese açık"
home: "Ana sayfa"
followers: "takipçi"
_profile:
username: "Kullanıcı Adı"
_exportOrImport:
followingList: "takipçi"
muteList: "Gizle"
blockingList: "engelle"
userLists: "Listeler"
_charts:
federation: "Federasyon"
_timelines:
home: "Ana sayfa"
global: "Küresel"
_pages:
blocks:
image: "Görseller"
_notification:
youWereFollowed: "seni takip etti"
unreadAntennaNote: "{name} anteni"
_types:
follow: "takipçi"
mention: "Bahset"
renote: "vazgeçme"
quote: "alıntı"
reaction: "Tepkiler"
receiveFollowRequest: "Takip isteği alındı"
followRequestAccepted: "Takip isteği kabul edildi"
_actions:
reply: "yanıt"
renote: "vazgeçme"
_deck:
configureColumn: "Sütun seçenekleri"
_columns:
notifications: "Bildirim"
tl: "Zaman çizelgesi"
list: "Listeler"
mentions: "Bahsetmeler"

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.14.0-beta.2",
"version": "13.14.0-beta.4",
"codename": "nasubi",
"repository": {
"type": "git",
@ -59,8 +59,8 @@
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"cross-env": "7.0.3",
"cypress": "12.17.0",
"eslint": "8.44.0",
"cypress": "12.17.1",
"eslint": "8.45.0",
"start-server-and-test": "2.0.0"
},
"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

@ -74,7 +74,7 @@
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.3.0",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.68",
"@swc/core": "1.3.69",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "5.3.1",
@ -82,11 +82,11 @@
"autwh": "0.1.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"bullmq": "4.2.0",
"bullmq": "4.3.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.0",
"chalk": "5.2.0",
"chalk-template": "0.4.0",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "3.5.3",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
@ -141,14 +141,14 @@
"rxjs": "7.8.1",
"s-age": "1.1.2",
"sanitize-html": "2.11.0",
"semver": "7.5.3",
"sharp": "0.32.1",
"semver": "7.5.4",
"sharp": "0.32.3",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.9",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.18.6",
"systeminformation": "5.18.7",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.7",
@ -158,7 +158,6 @@
"typescript": "5.1.6",
"ulid": "2.3.0",
"unzipper": "0.10.14",
"uuid": "9.0.0",
"vary": "1.1.2",
"web-push": "3.6.3",
"ws": "8.13.0",
@ -175,14 +174,14 @@
"@types/content-disposition": "0.5.5",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.21",
"@types/jest": "29.5.2",
"@types/jest": "29.5.3",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "21.1.1",
"@types/jsonld": "1.5.9",
"@types/jsrsasign": "10.5.8",
"@types/mime-types": "2.1.1",
"@types/ms": "^0.7.31",
"@types/node": "20.4.0",
"@types/node": "20.4.2",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.8",
"@types/oauth": "0.9.1",
@ -201,7 +200,6 @@
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/unzipper": "0.10.6",
"@types/uuid": "9.0.2",
"@types/vary": "1.1.0",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
@ -210,7 +208,7 @@
"@typescript-eslint/parser": "5.61.0",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.44.0",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"execa": "7.1.1",
"jest": "29.6.1",

View File

@ -31,7 +31,7 @@ function greet() {
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |'));
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length)));
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length)));
//#endregion
console.log(' Misskey is an open-source decentralized microblogging platform.');

View File

@ -63,6 +63,7 @@ export type Source = {
apiKey: string;
ssl?: boolean;
index: string;
scope?: 'local' | 'global' | string[];
};
proxy?: string;

View File

@ -81,6 +81,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js';
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
@ -205,6 +206,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
@ -329,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService,
HashtagEntityService,
InstanceEntityService,
InviteCodeEntityService,
ModerationLogEntityService,
MutingEntityService,
RenoteMutingEntityService,
@ -448,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService,
$HashtagEntityService,
$InstanceEntityService,
$InviteCodeEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$RenoteMutingEntityService,
@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService,
HashtagEntityService,
InstanceEntityService,
InviteCodeEntityService,
ModerationLogEntityService,
MutingEntityService,
RenoteMutingEntityService,
@ -685,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService,
$HashtagEntityService,
$InstanceEntityService,
$InviteCodeEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$RenoteMutingEntityService,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,9 @@ export type RolePolicies = {
ltlAvailable: boolean;
canPublicNote: boolean;
canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
inviteExpirationTime: number;
canManageCustomEmojis: boolean;
canSearchNotes: boolean;
canHideAds: boolean;
@ -42,6 +45,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
ltlAvailable: true,
canPublicNote: true,
canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
inviteExpirationTime: 0,
canManageCustomEmojis: false,
canSearchNotes: false,
canHideAds: false,
@ -277,6 +283,9 @@ export class RoleService implements OnApplicationShutdown {
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', 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)),
canSearchNotes: calc('canSearchNotes', 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()
export class SearchService {
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
private meilisearchNoteIndex: Index | null = null;
constructor(
@ -92,6 +93,10 @@ export class SearchService {
},
});
}
if (config.meilisearch?.scope) {
this.meilisearchIndexScope = config.meilisearch.scope;
}
}
@bindThis
@ -100,7 +105,22 @@ export class SearchService {
if (!['home', 'public'].includes(note.visibility)) return;
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,
createdAt: note.createdAt.getTime(),
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 { In } from 'typeorm';
import { v4 as uuid } from 'uuid';
import * as mfm from 'mfm-js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
@ -613,7 +612,7 @@ export class ApRendererService {
@bindThis
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
if (typeof x === 'object' && x.id == null) {
x.id = `${this.config.url}/${uuid()}`;
x.id = `${this.config.url}/${randomUUID()}`;
}
return Object.assign({

View File

@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import { MetaService } from '@/core/MetaService.js';
@ -20,9 +19,6 @@ export class ApImageService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@ -47,7 +43,7 @@ export class ApImageService {
const image = await this.apResolverService.createResolver().resolve(value);
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') {
@ -62,12 +58,17 @@ export class ApImageService {
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({
url: image.url,
user: actor,
uri: image.url,
sensitive: image.sensitive,
isLink: !instance.cacheRemoteFiles,
isLink: !shouldBeCached,
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
});
if (!file.isLink || file.url === image.url) return file;

View File

@ -254,7 +254,7 @@ export default abstract class Chart<T extends Schema> {
private convertRawRecord(x: RawRecord<T>): KVs<T> {
const kvs = {} as Record<string, number>;
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
}
return kvs as KVs<T>;
}
@ -627,7 +627,7 @@ export default abstract class Chart<T extends Schema> {
}
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
// (隙間埋めできないため)
const outdatedLog = await repository.findOne({

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

@ -4,7 +4,7 @@ export type Acct = {
};
export function parse(acct: string): Acct {
if (acct.startsWith('@')) acct = acct.substr(1);
if (acct.startsWith('@')) acct = acct.substring(1);
const split = acct.split('@', 2);
return { username: split[0], host: split[1] ?? null };
}

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

@ -19,6 +19,7 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.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 { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js';
@ -52,6 +53,7 @@ export const refs = {
RenoteMuting: packedRenoteMutingSchema,
Blocking: packedBlockingSchema,
Hashtag: packedHashtagSchema,
InviteCode: packedInviteCodeSchema,
Page: packedPageSchema,
Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema,

View File

@ -67,8 +67,9 @@ export function maximum(xs: number[]): number {
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
groups[groups.length - 1].push(x);
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}

View File

@ -1,7 +1,6 @@
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
import type { Clip } from './Clip.js';
@Entity()
export class Meta {
@ -126,6 +125,11 @@ export class Meta {
})
public cacheRemoteFiles: boolean;
@Column('boolean', {
default: true,
})
public cacheRemoteSensitiveFiles: boolean;
@Column({
...id(),
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 { User } from './User.js';
@Entity()
export class RegistrationTicket {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone')
public createdAt: Date;
@Index({ unique: true })
@Column('varchar', {
length: 64,
})
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

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull, MoreThan, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository } from '@/models/index.js';
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService {
this.logger.info('Deleting cached remote files...');
let deletedCount = 0;
let cursor: any = null;
let cursor: DriveFile['id'] | null = null;
while (true) {
const files = await this.driveFilesRepository.find({
@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService {
break;
}
cursor = files[files.length - 1].id;
cursor = files.at(-1)?.id ?? null;
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));

View File

@ -70,7 +70,7 @@ export class DeleteAccountProcessorService {
break;
}
cursor = notes[notes.length - 1].id;
cursor = notes.at(-1)?.id ?? null;
await this.notesRepository.delete(notes.map(note => note.id));
@ -101,7 +101,7 @@ export class DeleteAccountProcessorService {
break;
}
cursor = files[files.length - 1].id;
cursor = files.at(-1)?.id ?? null;
for (const file of files) {
await this.driveService.deleteFileSync(file);

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService {
}
let deletedCount = 0;
let cursor: any = null;
let cursor: DriveFile['id'] | null = null;
while (true) {
const files = await this.driveFilesRepository.find({
@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService {
break;
}
cursor = files[files.length - 1].id;
cursor = files.at(-1)?.id ?? null;
for (const file of files) {
await this.driveService.deleteFileSync(file);

View File

@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
@ -53,7 +53,7 @@ export class ExportBlockingProcessorService {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
let cursor: any = null;
let cursor: Blocking['id'] | null = null;
while (true) {
const blockings = await this.blockingsRepository.find({
@ -72,7 +72,7 @@ export class ExportBlockingProcessorService {
break;
}
cursor = blockings[blockings.length - 1].id;
cursor = blockings.at(-1)?.id ?? null;
for (const block of blockings) {
const u = await this.usersRepository.findOneBy({ id: block.blockeeId });

View File

@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService {
break;
}
cursor = favorites[favorites.length - 1].id;
cursor = favorites.at(-1)?.id ?? null;
for (const favorite of favorites) {
let poll: Poll | undefined;

View File

@ -79,7 +79,7 @@ export class ExportFollowingProcessorService {
break;
}
cursor = followings[followings.length - 1].id;
cursor = followings.at(-1)?.id ?? null;
for (const following of followings) {
const u = await this.usersRepository.findOneBy({ id: following.followeeId });

View File

@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull, MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js';
import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
@ -56,7 +56,7 @@ export class ExportMutingProcessorService {
const stream = fs.createWriteStream(path, { flags: 'a' });
let exportedCount = 0;
let cursor: any = null;
let cursor: Muting['id'] | null = null;
while (true) {
const mutes = await this.mutingsRepository.find({
@ -76,7 +76,7 @@ export class ExportMutingProcessorService {
break;
}
cursor = mutes[mutes.length - 1].id;
cursor = mutes.at(-1)?.id ?? null;
for (const mute of mutes) {
const u = await this.usersRepository.findOneBy({ id: mute.muteeId });

View File

@ -11,6 +11,8 @@ import { createTemp } from '@/misc/create-temp.js';
import type { Poll } from '@/models/entities/Poll.js';
import type { Note } from '@/models/entities/Note.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 type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@ -34,6 +36,8 @@ export class ExportNotesProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private driveFileEntityService: DriveFileEntityService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
}
@ -90,14 +94,15 @@ export class ExportNotesProcessorService {
break;
}
cursor = notes[notes.length - 1].id;
cursor = notes.at(-1)?.id ?? null;
for (const note of notes) {
let poll: Poll | undefined;
if (note.hasPoll) {
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;
await write(isFirst ? content : ',\n' + content);
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 {
id: note.id,
text: note.text,
createdAt: note.createdAt,
fileIds: note.fileIds,
files: files,
replyId: note.replyId,
renoteId: note.renoteId,
poll: poll,

View File

@ -181,7 +181,7 @@ export class ActivityPubServerService {
undefined,
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: followings[followings.length - 1].id,
cursor: followings.at(-1)!.id,
})}` : undefined,
);
@ -273,7 +273,7 @@ export class ActivityPubServerService {
undefined,
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: followings[followings.length - 1].id,
cursor: followings.at(-1)!.id,
})}` : undefined,
);
@ -398,7 +398,7 @@ export class ActivityPubServerService {
})}` : undefined,
notes.length ? `${partOf}?${url.query({
page: 'true',
until_id: notes[notes.length - 1].id,
until_id: notes.at(-1)!.id,
})}` : undefined,
);

View File

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

View File

@ -38,7 +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_getTableStats from './endpoints/admin/get-table-stats.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___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_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -230,6 +231,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_update from './endpoints/i/webhooks/update.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___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___emoji from './endpoints/emoji.js';
@ -378,7 +383,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_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 $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 $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_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 };
@ -570,6 +576,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_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 $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 $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
@ -722,7 +732,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getIndexStats,
$admin_getTableStats,
$admin_getUserIps,
$invite,
$admin_invite_create,
$admin_invite_list,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@ -914,6 +925,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$invite_create,
$invite_delete,
$invite_list,
$invite_limit,
$meta,
$emojis,
$emoji,
@ -1060,7 +1075,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getIndexStats,
$admin_getTableStats,
$admin_getUserIps,
$invite,
$admin_invite_create,
$admin_invite_list,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@ -1252,6 +1268,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$invite_create,
$invite_delete,
$invite_list,
$invite_limit,
$meta,
$emojis,
$emoji,

View File

@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm';
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 { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
@ -109,13 +109,15 @@ export class SignupApiService {
}
}
let ticket: RegistrationTicket | null = null;
if (instance.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
}
const ticket = await this.registrationTicketsRepository.findOneBy({
ticket = await this.registrationTicketsRepository.findOneBy({
code: invitationCode,
});
@ -124,7 +126,15 @@ export class SignupApiService {
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) {
@ -148,14 +158,14 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
await this.userPendingsRepository.insert({
const pendingUser = await this.userPendingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
code,
email: emailAddress!,
username: username,
password: hash,
});
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
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: ${link}`);
if (ticket) {
await this.registrationTicketsRepository.update(ticket.id, {
usedAt: new Date(),
pendingUserId: pendingUser.id,
});
}
reply.code(204);
return;
} else {
@ -176,6 +193,14 @@ export class SignupApiService {
includeSecrets: true,
});
if (ticket) {
await this.registrationTicketsRepository.update(ticket.id, {
usedAt: new Date(),
usedBy: account,
usedById: account.id,
});
}
return {
...res,
token: secret,
@ -212,6 +237,15 @@ export class SignupApiService {
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);
} catch (err) {
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());

View File

@ -38,7 +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_getTableStats from './endpoints/admin/get-table-stats.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___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_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@ -230,6 +231,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_update from './endpoints/i/webhooks/update.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___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___emoji from './endpoints/emoji.js';
@ -376,7 +381,8 @@ const eps = [
['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps],
['invite', ep___invite],
['admin/invite/create', ep___admin_invite_create],
['admin/invite/list', ep___admin_invite_list],
['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
@ -568,6 +574,10 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['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],
['meta', ep___meta],
['emojis', ep___emojis],
['emoji', ep___emoji],

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 { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
@ -20,6 +19,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
@ -332,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,

View File

@ -43,6 +43,7 @@ export const paramDef = {
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
enableHcaptcha: { type: 'boolean' },
hcaptchaSiteKey: { type: 'string', nullable: true },
@ -193,6 +194,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.cacheRemoteFiles = ps.cacheRemoteFiles;
}
if (ps.cacheRemoteSensitiveFiles !== undefined) {
set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles;
}
if (ps.emailRequiredForSignup !== undefined) {
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 { Endpoint } from '@/server/api/endpoint-base.js';
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Generate token
const token = uuid();
const token = randomUUID();
// Create session token document
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 { MoreThan } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.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 { secureRndstr } from '@/misc/secure-rndstr.js';
export const meta = {
tags: ['meta'],
@ -15,12 +15,9 @@ export const meta = {
type: 'object',
optional: false, nullable: false,
properties: {
code: {
type: 'string',
optional: false, nullable: false,
example: '2ERUA5VR',
maxLength: 8,
minLength: 8,
remaining: {
type: 'integer',
optional: false, nullable: true,
},
},
},
@ -39,21 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.registrationTicketsRepository)
private registrationTicketsRepository: RegistrationTicketsRepository,
private idService: IdService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const code = secureRndstr(8, {
chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns)
});
const policies = await this.roleService.getUserPolicies(me.id);
await this.registrationTicketsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
code,
});
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
createdById: me.id,
}) : null;
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',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
@ -329,6 +333,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: (await this.usersRepository.countBy({
host: IsNull(),
})) === 0,

View File

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

View File

@ -35,7 +35,7 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- 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}`)
if !config.clientManifestExists

View File

@ -4,6 +4,7 @@ import * as assert from 'assert';
import { Test } from '@nestjs/testing';
import { jest } from '@jest/globals';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
@ -11,9 +12,12 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, ICollection, IPost } from '@/core/activitypub/type.js';
import { Note } from '@/models/index.js';
import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
import { Meta, Note } from '@/models/index.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';
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', () => {
let imageService: ApImageService;
let noteService: ApNoteService;
let personService: ApPersonService;
let rendererService: ApRendererService;
let resolver: MockResolver;
const metaInitial = {
cacheRemoteFiles: true,
cacheRemoteSensitiveFiles: true,
blockedHosts: [] as string[],
sensitiveWords: [] as string[],
} as Meta;
let meta = metaInitial;
beforeAll(async () => {
const app = await Test.createTestingModule({
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();
await app.init();
@ -80,6 +115,7 @@ describe('ActivityPub', () => {
noteService = app.get<ApNoteService>(ApNoteService);
personService = app.get<ApPersonService>(ApPersonService);
rendererService = app.get<ApRendererService>(ApRendererService);
imageService = app.get<ApImageService>(ApImageService);
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
// 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);
});
});
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

@ -447,12 +447,12 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
for (const limit of [1, 5, 10, 100, undefined]) {
// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
if (ordering === 'desc') {
const end = expected[expected.length - 1];
const end = expected.at(-1)!;
let last = await fetchEntities(rangeToParam({ limit, since: end }));
const actual: Entity[] = [];
while (last.length !== 0) {
actual.push(...last);
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
last = await fetchEntities(rangeToParam({ limit, until: last.at(-1), since: end }));
}
actual.push(end);
assert.deepStrictEqual(
@ -467,7 +467,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
const actual: Entity[] = [];
while (last.length !== 0) {
actual.push(...last);
last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
last = await fetchEntities(rangeToParam({ limit, since: last.at(-1) }));
}
assert.deepStrictEqual(
actual.map(({ id, createdAt }) => id + ':' + createdAt),
@ -480,7 +480,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
const actual: Entity[] = [];
while (last.length !== 0) {
actual.push(...last);
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
last = await fetchEntities(rangeToParam({ limit, until: last.at(-1) }));
}
assert.deepStrictEqual(
actual.map(({ id, createdAt }) => id + ':' + createdAt),

View File

@ -115,3 +115,27 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
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

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

View File

@ -20,9 +20,9 @@
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.3",
"@tabler/icons-webfont": "2.24.0",
"@tabler/icons-webfont": "2.25.0",
"@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",
"astring": "1.8.6",
"autosize": "6.0.1",
@ -70,30 +70,30 @@
"typescript": "5.1.6",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.4.1",
"vite": "4.4.4",
"vue": "3.3.4",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.0.26",
"@storybook/addon-essentials": "7.0.26",
"@storybook/addon-interactions": "7.0.26",
"@storybook/addon-links": "7.0.26",
"@storybook/addon-storysource": "7.0.26",
"@storybook/addons": "7.0.26",
"@storybook/blocks": "7.0.26",
"@storybook/core-events": "7.0.26",
"@storybook/addon-actions": "7.0.27",
"@storybook/addon-essentials": "7.0.27",
"@storybook/addon-interactions": "7.0.27",
"@storybook/addon-links": "7.0.27",
"@storybook/addon-storysource": "7.0.27",
"@storybook/addons": "7.0.27",
"@storybook/blocks": "7.0.27",
"@storybook/core-events": "7.0.27",
"@storybook/jest": "0.1.0",
"@storybook/manager-api": "7.0.26",
"@storybook/preview-api": "7.0.26",
"@storybook/react": "7.0.26",
"@storybook/react-vite": "7.0.26",
"@storybook/manager-api": "7.0.27",
"@storybook/preview-api": "7.0.27",
"@storybook/react": "7.0.27",
"@storybook/react-vite": "7.0.27",
"@storybook/testing-library": "0.2.0",
"@storybook/theming": "7.0.26",
"@storybook/types": "7.0.26",
"@storybook/vue3": "7.0.26",
"@storybook/vue3-vite": "7.0.26",
"@storybook/theming": "7.0.27",
"@storybook/types": "7.0.27",
"@storybook/vue3": "7.0.27",
"@storybook/vue3-vite": "7.0.27",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
@ -102,10 +102,10 @@
"@types/gulp-rename": "2.0.2",
"@types/matter-js": "0.18.5",
"@types/micromatch": "4.0.2",
"@types/node": "20.4.0",
"@types/node": "20.4.2",
"@types/punycode": "2.1.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/tinycolor2": "1.4.3",
"@types/uuid": "9.0.2",
@ -118,8 +118,8 @@
"acorn": "8.10.0",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3",
"cypress": "12.17.0",
"eslint": "8.44.0",
"cypress": "12.17.1",
"eslint": "8.45.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.15.1",
"fast-glob": "3.3.0",
@ -131,13 +131,13 @@
"react": "18.2.0",
"react-dom": "18.2.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",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2",
"vitest": "0.33.0",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.1",
"vue-tsc": "1.8.4"
"vue-tsc": "1.8.5"
}
}

View File

@ -25,8 +25,8 @@
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
<p :class="$style.name">
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substring(0, file.name.lastIndexOf('.')) : file.name }}</span>
<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substring(file.name.lastIndexOf('.')) }}</span>
</p>
</div>
</div>

View File

@ -568,7 +568,7 @@ function fetchMoreFolders() {
os.api('drive/folders', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: folders.value[folders.value.length - 1].id,
untilId: folders.value.at(-1)?.id,
limit: max + 1,
}).then(folders => {
if (folders.length === max + 1) {
@ -591,7 +591,7 @@ function fetchMoreFiles() {
os.api('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: files.value[files.value.length - 1].id,
untilId: files.value.at(-1)?.id,
limit: max + 1,
}).then(files => {
if (files.length === max + 1) {

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,123 @@
<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()">{{ i18n.ts.copy }}</MkButton>
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()">{{ 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;
}
.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

@ -1,6 +1,6 @@
<template>
<component
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target"
:title="url"
>
<slot></slot>

View File

@ -59,8 +59,8 @@ function draw(): void {
polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
headX = _polylinePoints[_polylinePoints.length - 1][0];
headY = _polylinePoints[_polylinePoints.length - 1][1];
headX = _polylinePoints.at(-1)![0];
headY = _polylinePoints.at(-1)![1];
}
watch(() => props.src, draw, { immediate: true });

View File

@ -120,7 +120,7 @@ const contextmenu = $computed(() => ([{
function back() {
history.pop();
router.replace(history[history.length - 1].path, history[history.length - 1].key);
router.replace(history.at(-1)!.path, history.at(-1)!.key);
}
function reload() {

View File

@ -233,7 +233,7 @@ const fetchMore = async (): Promise<void> => {
...(props.pagination.offsetMode ? {
offset: offset.value,
} : {
untilId: Array.from(items.value.keys())[items.value.size - 1],
untilId: Array.from(items.value.keys()).at(-1),
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
@ -297,7 +297,7 @@ const fetchMoreAhead = async (): Promise<void> => {
...(props.pagination.offsetMode ? {
offset: offset.value,
} : {
sinceId: Array.from(items.value.keys())[items.value.size - 1],
sinceId: Array.from(items.value.keys()).at(-1),
}),
}).then(res => {
if (res.length === 0) {

View File

@ -564,7 +564,7 @@ async function onPaste(ev: ClipboardEvent) {
return;
}
quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
});
}
}

View File

@ -6,7 +6,7 @@
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]"
@click="toggleReaction()"
>
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
<span :class="$style.count">{{ count }}</span>
</button>
</template>

View File

@ -32,7 +32,7 @@
</div>
</template>
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div>
<article :class="$style.body">

View File

@ -88,7 +88,7 @@ onMounted(() => {
user = props.q;
} else {
const query = props.q.startsWith('@') ?
Acct.parse(props.q.substr(1)) :
Acct.parse(props.q.substring(1)) :
{ userId: props.q };
os.api('users/show', query).then(res => {

View File

@ -18,7 +18,7 @@ const props = defineProps<{
useOriginalSize?: boolean;
}>();
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', ''));
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
const rawUrl = computed(() => {

View File

@ -256,7 +256,7 @@ export default function(props: {
case 'mention': {
return [h(MkMention, {
key: Math.random(),
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
username: token.props.username,
})];
}

View File

@ -11,13 +11,13 @@ export default function(props: { src: string; tag?: string; textTag?: string; },
parsed.push(str);
break;
} else {
if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen));
parsed.push({
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
});
}
str = str.substr(nextBracketClose + 1);
str = str.substring(nextBracketClose + 1);
}
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));

View File

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

View File

@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'ti ti-user-plus',
text: i18n.ts.invite,
text: i18n.ts.createInviteCode,
action: invite,
}] : [])],
}, {
@ -95,6 +95,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.users,
to: '/admin/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',
text: i18n.ts.roles,
@ -240,10 +245,10 @@ provideMetadataReceiver((info) => {
});
const invite = () => {
os.api('invite').then(x => {
os.api('admin/invite/create').then(x => {
os.alert({
type: 'info',
text: x.code,
text: x?.[0].code,
});
}).catch(err => {
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

@ -85,7 +85,7 @@ onMounted(() => {
connection.on('stats', onStats);
connection.on('statsLog', onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
id: Math.random().toString().substring(2, 10),
length: 100,
});
});

View File

@ -156,7 +156,7 @@ onMounted(async () => {
nextTick(() => {
queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
id: Math.random().toString().substring(2, 10),
length: 100,
});
});

View File

@ -106,7 +106,7 @@ onMounted(() => {
connection.on('stats', onStats);
connection.on('statsLog', onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
id: Math.random().toString().substring(2, 10),
length: 200,
});
});

View File

@ -171,6 +171,65 @@
</div>
</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'])">
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
<template #suffix>

View File

@ -51,6 +51,29 @@
</MkSwitch>
</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'])">
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</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 #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</MkSwitch>
<template v-if="cacheRemoteFiles">
<MkSwitch v-model="cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</FormSection>
@ -104,7 +111,6 @@ import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
let name: string | null = $ref(null);
let description: string | null = $ref(null);
@ -112,13 +118,14 @@ let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
let cacheRemoteSensitiveFiles: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
async function init() {
async function init(): Promise<void> {
const meta = await os.api('admin/meta');
name = meta.name;
description = meta.description;
@ -126,6 +133,7 @@ async function init() {
maintainerEmail = meta.maintainerEmail;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
@ -133,7 +141,7 @@ async function init() {
deeplIsPro = meta.deeplIsPro;
}
function save() {
function save(): void {
os.apiWithDialog('admin/update-meta', {
name,
description,
@ -141,6 +149,7 @@ function save() {
maintainerEmail,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
cacheRemoteSensitiveFiles,
enableServiceWorker,
swPublicKey,
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

@ -14,7 +14,7 @@
<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 }`">
<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"/>
</MkA>
</div>
@ -32,6 +32,7 @@ import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { userListsCache } from '@/cache';
import { infoImageUrl } from '@/instance';
import { $i } from '@/account';
const items = $computed(() => userListsCache.value.value ?? []);
@ -66,10 +67,6 @@ const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.manageLists,
icon: 'ti ti-list',
action: {
icon: 'ti ti-plus',
handler: create,
},
});
onActivated(() => {
@ -90,4 +87,9 @@ onActivated(() => {
text-decoration: none;
}
}
.nUsers {
font-size: .9em;
opacity: .7;
}
</style>

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