forked from FoundKeyGang/FoundKey
Compare commits
16 commits
76b4e4ee08
...
3fc984ad2d
Author | SHA1 | Date | |
---|---|---|---|
Puniko | 3fc984ad2d | ||
Puniko | c3a6dcd6f0 | ||
Puniko | 3d1a991051 | ||
Chloe Kudryavtsev | 0ad0ab7589 | ||
7972540e3a | |||
Johann150 | ba77a81cc0 | ||
Johann150 | b04ef21b6e | ||
Johann150 | 965ee4b041 | ||
Johann150 | 34d55e2dda | ||
1d99657a45 | |||
bd598cd9ba | |||
Johann150 | b374a79eb1 | ||
Johann150 | c978a687e9 | ||
Johann150 | 4dc8822d05 | ||
Johann150 | ff4b6d932e | ||
Johann150 | a786fd99fc |
|
@ -12,7 +12,7 @@ password: "Kata sandi"
|
||||||
forgotPassword: "Lupa Kata Sandi"
|
forgotPassword: "Lupa Kata Sandi"
|
||||||
fetchingAsApObject: "Mengambil data dari Fediverse..."
|
fetchingAsApObject: "Mengambil data dari Fediverse..."
|
||||||
ok: "OK"
|
ok: "OK"
|
||||||
gotIt: "Saya mengerti"
|
gotIt: "Saya mengerti!"
|
||||||
cancel: "Batalkan"
|
cancel: "Batalkan"
|
||||||
renotedBy: "direnote oleh {user}"
|
renotedBy: "direnote oleh {user}"
|
||||||
noNotes: "Tidak ada catatan"
|
noNotes: "Tidak ada catatan"
|
||||||
|
@ -24,7 +24,7 @@ otherSettings: "Pengaturan lainnya"
|
||||||
openInWindow: "Buka di jendela"
|
openInWindow: "Buka di jendela"
|
||||||
profile: "Profil"
|
profile: "Profil"
|
||||||
timeline: "Linimasa"
|
timeline: "Linimasa"
|
||||||
noAccountDescription: "Pengguna ini belum menulis bio"
|
noAccountDescription: "Pengguna ini belum menulis biodata mereka."
|
||||||
login: "Masuk"
|
login: "Masuk"
|
||||||
loggingIn: "Sedang masuk"
|
loggingIn: "Sedang masuk"
|
||||||
logout: "Keluar"
|
logout: "Keluar"
|
||||||
|
@ -62,8 +62,8 @@ files: "Berkas"
|
||||||
download: "Unduh"
|
download: "Unduh"
|
||||||
driveFileDeleteConfirm: "Hapus {name}? Catatan dengan berkas terkait juga akan terhapus."
|
driveFileDeleteConfirm: "Hapus {name}? Catatan dengan berkas terkait juga akan terhapus."
|
||||||
unfollowConfirm: "Berhenti mengikuti {name}?"
|
unfollowConfirm: "Berhenti mengikuti {name}?"
|
||||||
exportRequested: "Kamu telah meminta ekspor. Ini akan memakan waktu sesaat. Setelah\
|
exportRequested: "Anda telah meminta ekspor. Ini mungkin memerlukan waktu beberapa\
|
||||||
\ ekspor selesai, berkas yang dihasilkan akan ditambahkan ke Drive"
|
\ saat. File ini akan ditambahkan ke Drive Anda setelah selesai."
|
||||||
importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat."
|
importRequested: "Kamu telah meminta impor. Ini akan memakan waktu sesaat."
|
||||||
lists: "Daftar"
|
lists: "Daftar"
|
||||||
note: "Catat"
|
note: "Catat"
|
||||||
|
@ -100,8 +100,8 @@ clickToShow: "Klik untuk melihat"
|
||||||
sensitive: "Konten sensitif"
|
sensitive: "Konten sensitif"
|
||||||
add: "Tambahkan"
|
add: "Tambahkan"
|
||||||
reaction: "Reaksi"
|
reaction: "Reaksi"
|
||||||
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus,\
|
reactionSettingDescription2: "Seret untuk menyusun ulang, klik untuk menghapus, tekan\
|
||||||
\ tekan \"+\" untuk menambahkan"
|
\ \"+\" untuk menambahkan."
|
||||||
attachCancel: "Hapus lampiran"
|
attachCancel: "Hapus lampiran"
|
||||||
markAsSensitive: "Tandai sebagai konten sensitif"
|
markAsSensitive: "Tandai sebagai konten sensitif"
|
||||||
unmarkAsSensitive: "Hapus tanda konten sensitif"
|
unmarkAsSensitive: "Hapus tanda konten sensitif"
|
||||||
|
@ -193,10 +193,10 @@ blockedUsers: "Pengguna yang diblokir"
|
||||||
noUsers: "Tidak ada pengguna"
|
noUsers: "Tidak ada pengguna"
|
||||||
editProfile: "Sunting profil"
|
editProfile: "Sunting profil"
|
||||||
noteDeleteConfirm: "Apakah kamu yakin ingin menghapus catatan ini?"
|
noteDeleteConfirm: "Apakah kamu yakin ingin menghapus catatan ini?"
|
||||||
pinLimitExceeded: "Kamu tidak dapat menyematkan catatan lagi"
|
pinLimitExceeded: "Anda tidak dapat menyematkan catatan lagi."
|
||||||
intro: "Instalasi FoundKey telah selesai! Mohon untuk membuat pengguna admin."
|
intro: "Instalasi FoundKey telah selesai! Mohon untuk membuat pengguna admin."
|
||||||
done: "Selesai"
|
done: "Selesai"
|
||||||
processing: "Memproses"
|
processing: "Pemrosesan..."
|
||||||
preview: "Pratinjau"
|
preview: "Pratinjau"
|
||||||
default: "Bawaan"
|
default: "Bawaan"
|
||||||
noCustomEmojis: "Tidak ada emoji kustom"
|
noCustomEmojis: "Tidak ada emoji kustom"
|
||||||
|
@ -210,7 +210,7 @@ publishing: "Sedang menyiarkan langsung"
|
||||||
notResponding: "Tidak ada respon"
|
notResponding: "Tidak ada respon"
|
||||||
changePassword: "Ubah kata sandi"
|
changePassword: "Ubah kata sandi"
|
||||||
security: "Keamanan"
|
security: "Keamanan"
|
||||||
retypedNotMatch: "Input tidak sama"
|
retypedNotMatch: "Input tidak cocok."
|
||||||
currentPassword: "Kata sandi saat ini"
|
currentPassword: "Kata sandi saat ini"
|
||||||
newPassword: "Kata sandi baru"
|
newPassword: "Kata sandi baru"
|
||||||
newPasswordRetype: "Ulangi kata sandi baru"
|
newPasswordRetype: "Ulangi kata sandi baru"
|
||||||
|
@ -237,7 +237,7 @@ fromUrl: "Dari URL"
|
||||||
uploadFromUrl: "Unggah dari URL"
|
uploadFromUrl: "Unggah dari URL"
|
||||||
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
|
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
|
||||||
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
||||||
uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesai"
|
uploadFromUrlMayTakeTime: "Mungkin diperlukan waktu hingga unggahan selesai."
|
||||||
explore: "Jelajahi"
|
explore: "Jelajahi"
|
||||||
messageRead: "Telah dibaca"
|
messageRead: "Telah dibaca"
|
||||||
noMoreHistory: "Tidak ada sejarah lagi"
|
noMoreHistory: "Tidak ada sejarah lagi"
|
||||||
|
@ -403,7 +403,7 @@ newMessageExists: "Kamu mendapatkan pesan baru"
|
||||||
onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan"
|
onlyOneFileCanBeAttached: "Kamu hanya dapat melampirkan satu berkas ke dalam pesan"
|
||||||
signinRequired: "Silahkan login"
|
signinRequired: "Silahkan login"
|
||||||
invitationCode: "Kode undangan"
|
invitationCode: "Kode undangan"
|
||||||
checking: "Memeriksa"
|
checking: "Memeriksa..."
|
||||||
available: "Tersedia"
|
available: "Tersedia"
|
||||||
unavailable: "Tidak tersedia"
|
unavailable: "Tidak tersedia"
|
||||||
usernameInvalidFormat: "Hanya dapat menerima karakter a-z, A-Z dan angka 0-9."
|
usernameInvalidFormat: "Hanya dapat menerima karakter a-z, A-Z dan angka 0-9."
|
||||||
|
@ -445,11 +445,10 @@ showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di linimasa"
|
||||||
objectStorage: "Object Storage"
|
objectStorage: "Object Storage"
|
||||||
useObjectStorage: "Gunakan object storage"
|
useObjectStorage: "Gunakan object storage"
|
||||||
objectStorageBaseUrl: "Base URL"
|
objectStorageBaseUrl: "Base URL"
|
||||||
objectStorageBaseUrlDesc: "Prefix URL digunakan untuk mengkonstruksi URL ke object\
|
objectStorageBaseUrlDesc: "URL yang digunakan sebagai referensi. Tentukan URL CDN\
|
||||||
\ (media) referencing. Tentukan URL jika kamu menggunakan CDN atau Proxy, jika tidak\
|
\ atau Proksi Anda jika Anda menggunakan keduanya.\nUntuk S3 gunakan 'https://<bucket>.s3.amazonaws.com'\
|
||||||
\ tentukan alamat yang dapat diakses secara publik sesuai dengan panduan dari layanan\
|
\ dan untuk GCS atau layanan yang setara gunakan 'https://storage.googleapis.com/<bucket>',\
|
||||||
\ yang akan kamu gunakan, contohnya. 'https://<bucket>.s3.amazonaws.com' untuk AWS\
|
\ dst."
|
||||||
\ S3, dan 'https://storage.googleapis.com/<bucket>' untuk GCS."
|
|
||||||
objectStorageBucket: "Bucket"
|
objectStorageBucket: "Bucket"
|
||||||
objectStorageBucketDesc: "Mohon tentukan nama bucket yang digunakan pada layanan yang\
|
objectStorageBucketDesc: "Mohon tentukan nama bucket yang digunakan pada layanan yang\
|
||||||
\ telah dikonfigurasi."
|
\ telah dikonfigurasi."
|
||||||
|
@ -647,8 +646,8 @@ contact: "Kontak"
|
||||||
useSystemFont: "Gunakan font bawaan sistem operasi"
|
useSystemFont: "Gunakan font bawaan sistem operasi"
|
||||||
clips: "Klip"
|
clips: "Klip"
|
||||||
makeExplorable: "Buat akun tampil di \"Jelajahi\""
|
makeExplorable: "Buat akun tampil di \"Jelajahi\""
|
||||||
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di\
|
makeExplorableDescription: "Jika Anda menonaktifkannya, akun Anda tidak akan muncul\
|
||||||
\ bagian \"Jelajahi:"
|
\ di bagian \"Jelajahi\"."
|
||||||
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada linimasa"
|
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada linimasa"
|
||||||
duplicate: "Duplikat"
|
duplicate: "Duplikat"
|
||||||
left: "Kiri"
|
left: "Kiri"
|
||||||
|
@ -759,7 +758,7 @@ ffVisibility: "Visibilitas Mengikuti/Pengikut"
|
||||||
ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu\
|
ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu\
|
||||||
\ ikuti."
|
\ ikuti."
|
||||||
continueThread: "Lihat lanjutan thread"
|
continueThread: "Lihat lanjutan thread"
|
||||||
deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?"
|
deleteAccountConfirm: "Ini akan menghapuskan akun {handle} secara permanen. Lanjutkan?"
|
||||||
incorrectPassword: "Kata sandi salah."
|
incorrectPassword: "Kata sandi salah."
|
||||||
voteConfirm: "Konfirmasi suara kamu untuk ({choice})?"
|
voteConfirm: "Konfirmasi suara kamu untuk ({choice})?"
|
||||||
hide: "Sembunyikan"
|
hide: "Sembunyikan"
|
||||||
|
@ -1079,7 +1078,7 @@ _auth:
|
||||||
shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?"
|
shareAccess: "Apakah kamu ingin mengijinkan \"{name}\" untuk mengakses akun ini?"
|
||||||
shareAccessAsk: "Apakah kamu ingin mengijinkan aplikasi ini untuk mengakses akun\
|
shareAccessAsk: "Apakah kamu ingin mengijinkan aplikasi ini untuk mengakses akun\
|
||||||
\ kamu?"
|
\ kamu?"
|
||||||
permissionAsk: "Aplikasi ini membutuhkan beberapa ijin, yaitu:"
|
permissionAsk: "Aplikasi ini meminta izin berikut ini"
|
||||||
pleaseGoBack: "Mohon kembali ke aplikasi kamu"
|
pleaseGoBack: "Mohon kembali ke aplikasi kamu"
|
||||||
callback: "Mengembalikan kamu ke aplikasi"
|
callback: "Mengembalikan kamu ke aplikasi"
|
||||||
denied: "Akses ditolak"
|
denied: "Akses ditolak"
|
||||||
|
@ -1263,7 +1262,7 @@ _notification:
|
||||||
youWereFollowed: "Mengikuti kamu"
|
youWereFollowed: "Mengikuti kamu"
|
||||||
youReceivedFollowRequest: "Kamu menerima permintaan mengikuti"
|
youReceivedFollowRequest: "Kamu menerima permintaan mengikuti"
|
||||||
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
||||||
youWereInvitedToGroup: "Telah diundang ke grup"
|
youWereInvitedToGroup: "{userName} mengundang Anda ke grup"
|
||||||
pollEnded: "Hasil Kuesioner telah keluar"
|
pollEnded: "Hasil Kuesioner telah keluar"
|
||||||
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
|
emptyPushNotificationMessage: "Pembaruan notifikasi dorong"
|
||||||
_types:
|
_types:
|
||||||
|
|
|
@ -1295,7 +1295,7 @@ _remoteInteract:
|
||||||
description: 今すぐにこのアクションを実行することはできません。あなたのインスタンス上で、またはログインして行う必要があるかもしれません。
|
description: 今すぐにこのアクションを実行することはできません。あなたのインスタンス上で、またはログインして行う必要があるかもしれません。
|
||||||
movedTo: このユーザーは {handle} に引っ越しました。
|
movedTo: このユーザーは {handle} に引っ越しました。
|
||||||
uploadFailedDescription: ファイルをアップロードできませんでした。
|
uploadFailedDescription: ファイルをアップロードできませんでした。
|
||||||
uploadFailedSize: このファイルは大きすぎるためアップロードできません。
|
uploadFailedSize: ファイルサイズが大きすぎるためアップロードできません。
|
||||||
uploadFailed: アップロード失敗
|
uploadFailed: アップロード失敗
|
||||||
showAttachedNotes: 添付ノートを表示
|
showAttachedNotes: 添付ノートを表示
|
||||||
attachedToNotes: このファイルが添付されたノート
|
attachedToNotes: このファイルが添付されたノート
|
||||||
|
|
|
@ -181,14 +181,15 @@ clearCachedFiles: "Очистить кэш"
|
||||||
clearCachedFilesConfirm: "Удалить все закэшированные файлы с других сайтов?"
|
clearCachedFilesConfirm: "Удалить все закэшированные файлы с других сайтов?"
|
||||||
blockedInstances: "Заблокированные инстансы"
|
blockedInstances: "Заблокированные инстансы"
|
||||||
blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать.\
|
blockedInstancesDescription: "Введите список инстансов, которые хотите заблокировать.\
|
||||||
\ Они больше не смогут обмениваться с вашим инстансом."
|
\ Они больше не смогут обмениваться с вашим инстансом. Не-ASCII доменные имена должны\
|
||||||
|
\ быть переведены в punycode. Субдомены тоже будут заблокированы"
|
||||||
muteAndBlock: "Скрытие и блокировка"
|
muteAndBlock: "Скрытие и блокировка"
|
||||||
mutedUsers: "Скрытые пользователи"
|
mutedUsers: "Скрытые пользователи"
|
||||||
blockedUsers: "Заблокированные пользователи"
|
blockedUsers: "Заблокированные пользователи"
|
||||||
noUsers: "Нет ни одного пользователя"
|
noUsers: "Нет ни одного пользователя"
|
||||||
editProfile: "Редактировать профиль"
|
editProfile: "Редактировать профиль"
|
||||||
noteDeleteConfirm: "Вы хотите удалить эту заметку?"
|
noteDeleteConfirm: "Вы хотите удалить эту заметку?"
|
||||||
pinLimitExceeded: "Нельзя закрепить ещё больше заметок"
|
pinLimitExceeded: "Нельзя закрепить ещё больше заметок."
|
||||||
intro: "Установка FoundKey завершена! А теперь создайте учетную запись администратора."
|
intro: "Установка FoundKey завершена! А теперь создайте учетную запись администратора."
|
||||||
done: "Готово"
|
done: "Готово"
|
||||||
processing: "Обработка"
|
processing: "Обработка"
|
||||||
|
@ -722,7 +723,7 @@ misskeyUpdated: "FoundKey обновился!"
|
||||||
whatIsNew: "Что новенького?"
|
whatIsNew: "Что новенького?"
|
||||||
translate: "Перевод"
|
translate: "Перевод"
|
||||||
translatedFrom: "Перевод. Язык оригинала — {x}"
|
translatedFrom: "Перевод. Язык оригинала — {x}"
|
||||||
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи."
|
||||||
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере.\
|
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере.\
|
||||||
\ Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания\
|
\ Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания\
|
||||||
\ (_). Имена пользователей не могут быть изменены позже."
|
\ (_). Имена пользователей не могут быть изменены позже."
|
||||||
|
@ -749,7 +750,7 @@ ffVisibility: "Видимость подписок и подписчиков"
|
||||||
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и\
|
ffVisibilityDescription: "Здесь можно настроить, кто будет видеть ваши подписки и\
|
||||||
\ подписчиков."
|
\ подписчиков."
|
||||||
continueThread: "Показать следующие ответы"
|
continueThread: "Показать следующие ответы"
|
||||||
deleteAccountConfirm: "Учётная запись будет безвозвратно удалена. Подтверждаете?"
|
deleteAccountConfirm: "Учётная запись {handle} будет безвозвратно удалена. Подтверждаете?"
|
||||||
incorrectPassword: "Пароль неверен."
|
incorrectPassword: "Пароль неверен."
|
||||||
voteConfirm: "Отдать голос за «{choice}»?"
|
voteConfirm: "Отдать голос за «{choice}»?"
|
||||||
hide: "Спрятать"
|
hide: "Спрятать"
|
||||||
|
@ -1268,3 +1269,81 @@ _deck:
|
||||||
mentions: "Упоминания"
|
mentions: "Упоминания"
|
||||||
direct: "Личное"
|
direct: "Личное"
|
||||||
_services: {}
|
_services: {}
|
||||||
|
botFollowRequiresApproval: Запросы на подписку от аккаунтов помеченных как бот требуют
|
||||||
|
подтверждения
|
||||||
|
showLess: Показать меньше
|
||||||
|
exportAll: Экспортировать всё
|
||||||
|
exportSelected: Экспортировать выбранное
|
||||||
|
cannotAttachFileWhenAccountSwitched: Вы не можете прикрепить файл, перейдя в другую
|
||||||
|
учетную запись.
|
||||||
|
cannotSwitchAccountWhenFileAttached: Вы не можете переключать учетные записи, пока
|
||||||
|
файлы прикреплены.
|
||||||
|
deleteAccount: Удалить аккаунт
|
||||||
|
isSystemAccount: Учетная запись, созданная системой и автоматически управляемая ею.
|
||||||
|
oneDay: Один день
|
||||||
|
cropImage: Обрезать изображение
|
||||||
|
documentation: Документация
|
||||||
|
movedTo: Этот пользователь перешел на {handle}.
|
||||||
|
typeToConfirm: Пожалуйста введите {x} чтобы подтвердить
|
||||||
|
rateLimitExceeded: Лимит превышен
|
||||||
|
numberOfPageCache: Количество кэшированных страниц
|
||||||
|
numberOfPageCacheDescription: Увеличение этого числа повысит удобство для пользователей,
|
||||||
|
но приведет к увеличению нагрузки на сервер, а также к использованию большего объема
|
||||||
|
памяти.
|
||||||
|
file: Файл
|
||||||
|
unclip: Удалить из подборки
|
||||||
|
translationSettings: Настройки перевода
|
||||||
|
translationService: Служба перевода
|
||||||
|
threadMuteNotificationsDesc: Выберите уведомления, которые вы хотите просмотреть в
|
||||||
|
этом треде. Также применяются глобальные настройки уведомлений. Отключение имеет
|
||||||
|
приоритет.
|
||||||
|
reflectMayTakeTime: Это может занять некоторое время чтобы вступило в силу.
|
||||||
|
failedToFetchAccountInformation: Не удалось получить информацию о аккаунте
|
||||||
|
instanceDefaultThemeDescription: Введите код темы в формате объекта.
|
||||||
|
tenMinutes: 10 минут
|
||||||
|
oneHour: Один час
|
||||||
|
oneWeek: Одна неделя
|
||||||
|
cropImageAsk: Вы хотите обрезать это изображение?
|
||||||
|
recentNHours: Последние {n} часов
|
||||||
|
recentNDays: Последние {n} дней
|
||||||
|
confirmToUnclipAlreadyClippedNote: Эта заметка уже является частью подборки "{name}".
|
||||||
|
Вы хотите вместо этого удалить это из этой подборки?
|
||||||
|
noEmailServerWarning: Сервер электронной почты не настроен.
|
||||||
|
setTag: Установить метку
|
||||||
|
addTag: Добавить метку
|
||||||
|
removeTag: Удалить метку
|
||||||
|
externalCssSnippets: Несколько фрагментов CSS для вашего вдохновения (не управляются
|
||||||
|
FoundKey)
|
||||||
|
oauthErrorGoBack: Произошла ошибка при попытке аутентификации стороннего приложения.
|
||||||
|
Пожалуйста, вернитесь и попробуйте еще раз.
|
||||||
|
appAuthorization: Авторизация приложения
|
||||||
|
noPermissionsRequested: (Никаких разрешений не требуется.)
|
||||||
|
selectMode: Выберите несколько
|
||||||
|
selectAll: Выбрать все
|
||||||
|
setCategory: Установить категорию
|
||||||
|
thereIsUnresolvedAbuseReportWarning: Есть нерасмотренные жалобы.
|
||||||
|
recommended: Рекомендовано
|
||||||
|
check: Проверка
|
||||||
|
unlimited: Неограниченный
|
||||||
|
mutePeriod: Длительность глушения
|
||||||
|
uploadFailed: Загрузка не удалась
|
||||||
|
uploadFailedDescription: Файл не может быть загружен.
|
||||||
|
uploadFailedSize: Файл слишком большой для загрузки.
|
||||||
|
renoteUnmute: Показать репосты
|
||||||
|
stopActivityDeliveryDescription: Локальная активнось не будет отправлена на этот сервер.
|
||||||
|
Получение активностей работает как раньше.
|
||||||
|
renoteMute: Скрыть репосты
|
||||||
|
unrenoteAllConfirm: Вы уверены что хотите отменить все репосты данной замети?
|
||||||
|
unrenoteAll: Отменить все репосты
|
||||||
|
blockThisInstanceDescription: Локальная активность не будет отправлена на этот сервер.
|
||||||
|
Активность этого сервера будет выброшена.
|
||||||
|
attachedToNotes: Заметки с этим файлом
|
||||||
|
showAttachedNotes: Показать заметки с этим файлом
|
||||||
|
signinHistoryExpires: Данные о прошлых попытках войти будут автоматически удалены
|
||||||
|
после 60 дней для соблюдения правил конфиденциальности.
|
||||||
|
deleteAllFiles: Удалить все файлы
|
||||||
|
federateBlocks: Федерировать блоки
|
||||||
|
federateBlocksDescription: Если выключено, то активности типа "блок" не будут отправлены.
|
||||||
|
regexpErrorDescription: 'Произошла ошибка в регулярном выражении на строке {line}
|
||||||
|
ваших {tab} заглушенных слов:'
|
||||||
|
reporter: Подавший жалобу
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from '@/config/index.js';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { intersperse } from '@/prelude/array.js';
|
import { intersperse } from '@/prelude/array.js';
|
||||||
|
import { toPunyNullable } from '@/misc/convert-host.js';
|
||||||
|
|
||||||
// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
|
// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
|
||||||
// mentioned in the text. If the list of mentions is not given, all mentions
|
// mentioned in the text. If the list of mentions is not given, all mentions
|
||||||
|
@ -14,6 +15,19 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise<stri
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mentionedUsers = [];
|
||||||
|
const ids = mentions ?? extractMentions(nodes);
|
||||||
|
if (ids.length > 0) {
|
||||||
|
mentionedUsers = await UserProfiles.createQueryBuilder('user_profile')
|
||||||
|
.leftJoin('user_profile.user', 'user')
|
||||||
|
.select('user.usernameLower', 'username')
|
||||||
|
.addSelect('user.host', 'host')
|
||||||
|
// links should preferably use user friendly urls, only fall back to AP ids
|
||||||
|
.addSelect('COALESCE(user_profile.url, user.uri)', 'url')
|
||||||
|
.where('"userId" IN (:...ids)', { ids })
|
||||||
|
.getRawMany();
|
||||||
|
}
|
||||||
|
|
||||||
const doc = new JSDOM('').window.document;
|
const doc = new JSDOM('').window.document;
|
||||||
|
|
||||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => Promise<Node> } = {
|
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => Promise<Node> } = {
|
||||||
|
@ -103,30 +117,28 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise<stri
|
||||||
},
|
},
|
||||||
|
|
||||||
async mention(node): Promise<HTMLElement | Text> {
|
async mention(node): Promise<HTMLElement | Text> {
|
||||||
const { username, host, acct } = node.props;
|
let { username, host, acct } = node.props;
|
||||||
const ids = mentions ?? extractMentions(nodes);
|
// normalize username and host for searching the user
|
||||||
if (ids.length > 0) {
|
username = username.toLowerCase();
|
||||||
const mentionedUsers = await UserProfiles.createQueryBuilder('user_profile')
|
host = toPunyNullable(host);
|
||||||
.leftJoin('user_profile.user', 'user')
|
// Discard host if it is the local host. Otherwise mentions of local users where the
|
||||||
.select('user.username', 'username')
|
// hostname is not omitted are not handled correctly.
|
||||||
.addSelect('user.host', 'host')
|
if (host == config.hostname) {
|
||||||
// links should preferably use user friendly urls, only fall back to AP ids
|
host = null;
|
||||||
.addSelect('COALESCE(user_profile.url, user.uri)', 'url')
|
}
|
||||||
.where('"userId" IN (:...ids)', { ids })
|
const userInfo = mentionedUsers.find(user => user.username === username && user.host === host);
|
||||||
.getRawMany();
|
if (userInfo != null) {
|
||||||
const userInfo = mentionedUsers.find(user => user.username === username && user.host === host);
|
// Mastodon microformat: span.h-card > a.u-url.mention
|
||||||
if (userInfo != null) {
|
const a = doc.createElement('a');
|
||||||
// Mastodon microformat: span.h-card > a.u-url.mention
|
// The fallback will only be used for local users, so the host part can be discarded.
|
||||||
const a = doc.createElement('a');
|
a.href = userInfo.url ?? `${config.url}/@${username}`;
|
||||||
a.href = userInfo.url ?? `${config.url}/${acct}`;
|
a.className = 'u-url mention';
|
||||||
a.className = 'u-url mention';
|
a.textContent = acct;
|
||||||
a.textContent = acct;
|
|
||||||
|
|
||||||
const card = doc.createElement('span');
|
const card = doc.createElement('span');
|
||||||
card.className = 'h-card';
|
card.className = 'h-card';
|
||||||
card.appendChild(a);
|
card.appendChild(a);
|
||||||
return card;
|
return card;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// this user does not actually exist
|
// this user does not actually exist
|
||||||
return doc.createTextNode(acct);
|
return doc.createTextNode(acct);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
|
||||||
import { IActivity } from '@/remote/activitypub/type.js';
|
import { IActivity } from '@/remote/activitypub/type.js';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
import { MINUTE } from '@/const.js';
|
import { MINUTE } from '@/const.js';
|
||||||
|
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
||||||
|
|
||||||
import processDeliver from './processors/deliver.js';
|
import processDeliver from './processors/deliver.js';
|
||||||
import processInbox from './processors/inbox.js';
|
import processInbox from './processors/inbox.js';
|
||||||
|
@ -83,24 +84,47 @@ webhookDeliverQueue
|
||||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||||
|
|
||||||
export function deliver(content: unknown, to: string | null) {
|
export async function deliver(content: IActivity|IActivity[], to: string | null) {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
const data = {
|
// group activities by actor
|
||||||
content,
|
const contentArray = Array.isArray(content) ? content : [content];
|
||||||
to,
|
let byActor = contentArray.reduce((acc, activity) => {
|
||||||
};
|
if (activity.actor == null) throw new Error("Cannot deliver activity without actor.");
|
||||||
|
if (!(activity.actor in acc)) {
|
||||||
|
acc[activity.actor] = [];
|
||||||
|
}
|
||||||
|
acc[activity.actor].push(activity);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return deliverQueue.add(data, {
|
// add groups to deliver queue
|
||||||
attempts: config.deliverJobMaxAttempts,
|
const dbResolver = new DbResolver();
|
||||||
timeout: MINUTE,
|
for (const actor in byActor) {
|
||||||
backoff: {
|
// extract user from the Activity
|
||||||
type: 'apBackoff',
|
const user = await dbResolver.getUserFromApId(actor);
|
||||||
},
|
if (!user) throw new Error("Actor not found, cannot deliver.");
|
||||||
removeOnComplete: true,
|
if (user.host != null) throw new Error("Cannot deliver for remote actor.");
|
||||||
removeOnFail: true,
|
|
||||||
});
|
// add item to deliver queue
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
content: byActor[actor],
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
deliverQueue.add(data, {
|
||||||
|
attempts: config.deliverJobMaxAttempts,
|
||||||
|
timeout: MINUTE,
|
||||||
|
backoff: {
|
||||||
|
type: 'apBackoff',
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { toPuny } from '@/misc/convert-host.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
||||||
import { DeliverJobData } from '@/queue/types.js';
|
import { DeliverJobData } from '@/queue/types.js';
|
||||||
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
|
||||||
|
|
||||||
const logger = new Logger('deliver');
|
const logger = new Logger('deliver');
|
||||||
|
|
||||||
|
@ -19,20 +18,13 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
||||||
|
|
||||||
if (await shouldSkipInstance(puny)) return 'skip';
|
if (await shouldSkipInstance(puny)) return 'skip';
|
||||||
|
|
||||||
// get user/actor for signing
|
|
||||||
const userUri = job.data.content.actor;
|
|
||||||
if (userUri == null) return 'error: missing actor';
|
|
||||||
const user = await new DbResolver().getUserFromApId(userUri);
|
|
||||||
if (user == null) return 'error: actor not found';
|
|
||||||
if (user.host != null) return 'error: actor not local';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Array.isArray(job.data.content)) {
|
if (Array.isArray(job.data.content)) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
job.data.content.map(x => request(user, job.data.to, x))
|
job.data.content.map(x => request(job.data.user, job.data.to, x))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await request(user, job.data.to, job.data.content);
|
await request(job.data.user, job.data.to, job.data.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { Webhook } from '@/models/entities/webhook.js';
|
||||||
import { IActivity } from '@/remote/activitypub/type.js';
|
import { IActivity } from '@/remote/activitypub/type.js';
|
||||||
|
|
||||||
export type DeliverJobData = {
|
export type DeliverJobData = {
|
||||||
/** Activity, containing the actor URI */
|
/** Actor */
|
||||||
|
user: ThinUser;
|
||||||
|
/** Activity */
|
||||||
content: IActivity;
|
content: IActivity;
|
||||||
/** inbox URL to deliver */
|
/** inbox URL to deliver */
|
||||||
to: string;
|
to: string;
|
||||||
|
|
|
@ -156,7 +156,7 @@ export class DeliverManager {
|
||||||
// skip instances as indicated
|
// skip instances as indicated
|
||||||
if (instancesToSkip.includes(new URL(inbox).host)) continue;
|
if (instancesToSkip.includes(new URL(inbox).host)) continue;
|
||||||
|
|
||||||
deliver(this.activity, inbox);
|
await deliver(this.activity, inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const actor = await getInstanceActor();
|
const actor = await getInstanceActor();
|
||||||
const targetUser = await Users.findOneByOrFail({ id: report.targetUserId });
|
const targetUser = await Users.findOneByOrFail({ id: report.targetUserId });
|
||||||
|
|
||||||
deliver(renderActivity(renderFlag(actor, report)), targetUser.inbox);
|
await deliver(renderActivity(renderFlag(actor, report)), targetUser.inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
await AbuseUserReports.update(report.id, {
|
await AbuseUserReports.update(report.id, {
|
||||||
|
|
|
@ -133,7 +133,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (note.userHost != null) {
|
if (note.userHost != null) {
|
||||||
const pollOwner = await Users.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
const pollOwner = await Users.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
||||||
|
|
||||||
deliver(renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
|
await deliver(renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートフォロワーにUpdate配信
|
// リモートフォロワーにUpdate配信
|
||||||
|
|
|
@ -29,6 +29,6 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
|
||||||
// deliver if remote bloking
|
// deliver if remote bloking
|
||||||
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
|
||||||
const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
|
const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
|
||||||
deliver(content, blockee.inbox);
|
await deliver(content, blockee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export async function acceptFollowRequest(followee: User, follower: User): Promi
|
||||||
|
|
||||||
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
|
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
|
||||||
const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee));
|
const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee));
|
||||||
deliver(content, follower.inbox);
|
await deliver(content, follower.inbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
Users.pack(followee.id, followee, {
|
Users.pack(followee.id, followee, {
|
||||||
|
|
|
@ -17,7 +17,7 @@ export async function cancelFollowRequest(followee: User, follower: User): Promi
|
||||||
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
|
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
|
||||||
|
|
||||||
if (Users.isLocalUser(follower)) {
|
if (Users.isLocalUser(follower)) {
|
||||||
deliver(content, followee.inbox);
|
await deliver(content, followee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,6 @@ export async function createFollowRequest(follower: User, followee: User, reques
|
||||||
|
|
||||||
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
|
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
|
||||||
const content = renderActivity(renderFollow(follower, followee));
|
const content = renderActivity(renderFollow(follower, followee));
|
||||||
deliver(content, followee.inbox);
|
await deliver(content, followee.inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ export async function createMessage(user: { id: User['id']; host: User['host'];
|
||||||
|
|
||||||
const activity = renderActivity(renderCreate(await renderNote(note, false, true), note));
|
const activity = renderActivity(renderCreate(await renderNote(note, false, true), note));
|
||||||
|
|
||||||
deliver(activity, recipientUser.inbox);
|
await deliver(activity, recipientUser.inbox);
|
||||||
}
|
}
|
||||||
return messageObj;
|
return messageObj;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ async function postDeleteMessage(message: MessagingMessage): Promise<void> {
|
||||||
|
|
||||||
if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
|
if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) {
|
||||||
const activity = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${message.id}`), user));
|
const activity = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${message.id}`), user));
|
||||||
deliver(activity, recipient.inbox);
|
await deliver(activity, recipient.inbox);
|
||||||
}
|
}
|
||||||
} else if (message.groupId) {
|
} else if (message.groupId) {
|
||||||
publishGroupMessagingStream(message.groupId, 'deleted', message.id);
|
publishGroupMessagingStream(message.groupId, 'deleted', message.id);
|
||||||
|
|
|
@ -37,7 +37,7 @@ export async function addRelay(inbox: string): Promise<Relay> {
|
||||||
|
|
||||||
const relayActor = await getRelayActor();
|
const relayActor = await getRelayActor();
|
||||||
const activity = renderActivity(renderFollowRelay(relay, relayActor));
|
const activity = renderActivity(renderFollowRelay(relay, relayActor));
|
||||||
deliver(activity, relay.inbox);
|
await deliver(activity, relay.inbox);
|
||||||
|
|
||||||
return relay;
|
return relay;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export async function removeRelay(inbox: string): Promise<void> {
|
||||||
|
|
||||||
const relayActor = await getRelayActor();
|
const relayActor = await getRelayActor();
|
||||||
const activity = renderActivity(renderUndo(renderFollowRelay(relay, relayActor), relayActor));
|
const activity = renderActivity(renderUndo(renderFollowRelay(relay, relayActor), relayActor));
|
||||||
deliver(activity, relay.inbox);
|
await deliver(activity, relay.inbox);
|
||||||
|
|
||||||
await Relays.delete(relay.id);
|
await Relays.delete(relay.id);
|
||||||
}
|
}
|
||||||
|
@ -89,9 +89,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act
|
||||||
|
|
||||||
const signed = await attachLdSignature(copy, user);
|
const signed = await attachLdSignature(copy, user);
|
||||||
|
|
||||||
for (const relay of relays) {
|
await Promise.all(relays.map(relay =>
|
||||||
deliver(signed, relay.inbox);
|
deliver(signed, relay.inbox)
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deliverMultipleToRelays(user: User, activities: any[]): Promise<void> {
|
export async function deliverMultipleToRelays(user: User, activities: any[]): Promise<void> {
|
||||||
|
@ -107,7 +107,7 @@ export async function deliverMultipleToRelays(user: User, activities: any[]): Pr
|
||||||
return attachLdSignature(copy, user);
|
return attachLdSignature(copy, user);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const relay of relays) {
|
await Promise.all(relays.map(relay =>
|
||||||
deliver(content, relay.inbox);
|
deliver(content, relay.inbox)
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export async function doPostUnsuspend(user: User): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const inbox of queue) {
|
for (const inbox of queue) {
|
||||||
deliver(content, inbox);
|
await deliver(content, inbox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue