Merge branch 'develop' into ssmucny-events
This commit is contained in:
commit
f2db387d67
|
@ -6,7 +6,7 @@
|
|||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "18.16.0"
|
||||
"version": "20.3.1"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
|
|
|
@ -2,7 +2,7 @@ version: '3.8'
|
|||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ indent_size = 2
|
|||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
|
|
|
@ -54,7 +54,7 @@ Please include errors from the developer console and/or server log files if you
|
|||
|
||||
* Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
|
||||
* Misskey: 13.x.x
|
||||
* Node: 18.x.x
|
||||
* Node: 20.x.x
|
||||
* PostgreSQL: 15.x.x
|
||||
* Redis: 7.x.x
|
||||
* OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
|
||||
|
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- name: Use Node.js 18.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
|
@ -51,7 +51,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
browser: [chrome]
|
||||
|
||||
services:
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
|
|
|
@ -64,3 +64,6 @@ temp
|
|||
*.blend3
|
||||
*.blend4
|
||||
*.blend5
|
||||
|
||||
# VSCode addon
|
||||
.favorites.json
|
||||
|
|
|
@ -1 +1 @@
|
|||
18.16.0
|
||||
20.3.1
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -14,8 +14,34 @@
|
|||
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### General
|
||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
|
||||
### Client
|
||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||
- ドライブファイルのメニューで画像をクロップできるように
|
||||
- 画像を動画と同様に簡単に隠せるように
|
||||
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
|
||||
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
|
||||
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
|
||||
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
|
||||
- プロフィールURLをコピーできるボタンを追加 #11190
|
||||
- Fix: サーバーメトリクスが90度傾いている
|
||||
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||
- Fix: ZenUIでポップアップの表示位置がおかしい問題を修正
|
||||
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
|
||||
- Fix: フォルダーのページネーションが機能しない #11180
|
||||
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
|
||||
- Fix: システムフォント設定が正しく反映されない問題を修正
|
||||
|
||||
### Server
|
||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
||||
- 全体的なDBクエリのパフォーマンスを向上
|
||||
- featuredノートのsignedGet回数を減らしました
|
||||
|
||||
## 13.13.2
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ If your language is not listed in Crowdin, please open an issue.
|
|||
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
|
||||
|
||||
## Development
|
||||
During development, it is useful to use the
|
||||
During development, it is useful to use the
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
|
@ -150,7 +150,7 @@ Prepare DB/Redis for testing.
|
|||
```
|
||||
docker compose -f packages/backend/test/docker-compose.yml up
|
||||
```
|
||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||
|
||||
Run all test.
|
||||
```
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=18.16.0-bullseye
|
||||
ARG NODE_VERSION=20.3.1-bullseye
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<a href="https://misskey-hub.net">
|
||||
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
|
||||
</a>
|
||||
|
||||
|
||||
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
|
||||
---
|
||||
|
||||
<a href="https://misskey-hub.net/instances.html">
|
||||
|
@ -21,7 +21,7 @@
|
|||
|
||||
<a href="https://www.patreon.com/syuilo">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/></a>
|
||||
|
||||
|
||||
---
|
||||
|
||||
[![codecov](https://codecov.io/gh/misskey-dev/misskey/branch/develop/graph/badge.svg?token=R6IQZ3QJOL)](https://codecov.io/gh/misskey-dev/misskey)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
@ -56,7 +56,7 @@ describe('After setup instance', () => {
|
|||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
* Gulp tasks
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const gulp = require('gulp');
|
||||
const replace = require('gulp-replace');
|
||||
const terser = require('gulp-terser');
|
||||
const cssnano = require('gulp-cssnano');
|
||||
import * as fs from 'node:fs';
|
||||
import gulp from 'gulp';
|
||||
import replace from 'gulp-replace';
|
||||
import terser from 'gulp-terser';
|
||||
import cssnano from 'gulp-cssnano';
|
||||
|
||||
const locales = require('./locales');
|
||||
const meta = require('./package.json');
|
||||
import locales from './locales/index.js';
|
||||
import meta from './package.json' assert { type: "json" };
|
||||
|
||||
gulp.task('copy:backend:views', () =>
|
||||
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
|
|
@ -52,6 +52,8 @@ addToList: "أضفه إلى قائمة"
|
|||
sendMessage: "أرسل رسالة"
|
||||
copyRSS: "انسخ رابط RSS"
|
||||
copyUsername: "انسخ اسم المستخدم"
|
||||
copyUserId: "انسخ معرف المستخدم"
|
||||
copyNoteId: "انسخ معرف الملاحظة"
|
||||
searchUser: "ابحث عن مستخدمين"
|
||||
reply: "رد"
|
||||
loadMore: "عرض المزيد"
|
||||
|
@ -305,7 +307,6 @@ copyUrl: "انسخ الرابط"
|
|||
rename: "إعادة التسمية"
|
||||
avatar: "الصورة الرمزية"
|
||||
banner: "الصورة الرأسية"
|
||||
nsfw: "محتوى حساس"
|
||||
whenServerDisconnected: "عند فقدان الاتصال بالخادم"
|
||||
disconnectedFromServer: "قُطِع الإتصال بالخادم"
|
||||
reload: "انعش"
|
||||
|
@ -398,6 +399,7 @@ totp: "تطبيق استيثاق"
|
|||
moderator: "مشرِف"
|
||||
moderation: "الإشراف"
|
||||
nUsersMentioned: "{n} مستخدمين أُشير إليهم"
|
||||
securityKeyAndPasskey: "الأمن ومفاتيح الأمان"
|
||||
securityKey: "مفتاح الأمان"
|
||||
lastUsed: "آخر استخدام"
|
||||
lastUsedAt: "آخر استخدام: {t}"
|
||||
|
@ -452,6 +454,7 @@ language: "اللغة"
|
|||
uiLanguage: "لغة واجهة المستخدم"
|
||||
aboutX: "عن {x}"
|
||||
emojiStyle: "نمط الوجوه التعبيرية"
|
||||
showNoteActionsOnlyHover: "أظهر الإجراءات عند التمرير فوق الملاحظة"
|
||||
noHistory: "السجل فارغ"
|
||||
signinHistory: "تاريخ تسجيل الدخول"
|
||||
doing: "انتظر لحظة"
|
||||
|
@ -484,10 +487,12 @@ objectStoragePrefix: "البادئة"
|
|||
objectStoragePrefixDesc: "ستُحفظ الملفات في مجلدات تحوي اسماءها هذه البادئة."
|
||||
objectStorageEndpoint: "نقطة النهاية"
|
||||
objectStorageRegion: "المنطقة"
|
||||
objectStorageRegionDesc: "حدد منطقة مثل \"xx-east-1\". إذا كانت خدمتك لا تميز بين المناطق استخدم \"us-east-1\" أو اتركها فارغة إذا كنت تستخدم متغيرات البيئة أو ملفات ضبط AWS."
|
||||
objectStorageUseSSL: "استخدم SSL"
|
||||
objectStorageUseSSLDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر HTTPS"
|
||||
objectStorageUseProxy: "اتصل عبر وكيل"
|
||||
objectStorageUseProxyDesc: "عطل هذا الخيار إذا لم ترد استخدام API عبر وكيل"
|
||||
objectStorageSetPublicRead: "عينها ك\"علنية\" عند الرفع"
|
||||
serverLogs: "سجلات الخادم"
|
||||
deleteAll: "حذف الكل"
|
||||
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
|
||||
|
@ -531,6 +536,7 @@ accountDeletedDescription: "حُذف هذا الحساب."
|
|||
menu: "القائمة"
|
||||
divider: "فاصل"
|
||||
addItem: "إضافة عنصر"
|
||||
rearrange: "أعد الترتيب"
|
||||
relays: "المُرَحلات"
|
||||
addRelay: "إضافة مُرحّل"
|
||||
inboxUrl: "رابط صندوق الوارد"
|
||||
|
@ -554,6 +560,7 @@ leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المت
|
|||
manage: "إدارة "
|
||||
plugins: "الإضافات"
|
||||
preferencesBackups: "النُسخ الاحتياطية للإعدادات"
|
||||
useBlurEffectForModal: "استخدم تأثير الطمس في المشروط"
|
||||
useFullReactionPicker: "استخدم الحجم الكامل لمنتقي التفاعلات"
|
||||
width: "العرض"
|
||||
height: "الإرتفاع"
|
||||
|
@ -629,6 +636,7 @@ clip: "مِشبك"
|
|||
createNew: "أنشِئ جديد"
|
||||
optional: "اختياري"
|
||||
createNewClip: "أنشئ مِشبكَا جديدًا"
|
||||
confirmToUnclipAlreadyClippedNote: "هذه الملاحظة تنتمي للمشبك {name} سلفًا، أتريد حذفها منه⸮"
|
||||
public: "علني"
|
||||
i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}"
|
||||
manageAccessTokens: "إدارة رموز الوصول"
|
||||
|
@ -650,6 +658,7 @@ driveFilesCount: "عدد الملفات في قرص التخزين"
|
|||
driveUsage: "المستغل من قرص التخزين"
|
||||
noCrawle: "ارفض فهرسة زاحف الويب"
|
||||
noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه."
|
||||
lockedAccountInfo: "ستكون هذه الملاحظة مرئية للجميع مالم تحدد مرئتيها إلى \"للمتابعين فقط\""
|
||||
alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس"
|
||||
loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات"
|
||||
disableShowingAnimatedImages: "لا تشغّل الصور المتحركة"
|
||||
|
@ -668,6 +677,8 @@ developer: "المطور"
|
|||
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
||||
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
||||
showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني"
|
||||
left: "يسار"
|
||||
center: "وسط"
|
||||
wide: "عريض"
|
||||
narrow: "رفيع"
|
||||
reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟"
|
||||
|
@ -705,6 +716,7 @@ editCode: "حرر الشفرة"
|
|||
apply: "تطبيق"
|
||||
receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل"
|
||||
emailNotification: "إشعارات البريد الكتروني"
|
||||
publish: "علني"
|
||||
inChannelSearch: "ابحث عن قناة"
|
||||
useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن"
|
||||
typingUsers: "{users} يكتب(ون)..."
|
||||
|
@ -744,6 +756,7 @@ noMaintainerInformationWarning: "لم تُضبط معلومات المدير"
|
|||
noBotProtectionWarning: "لم تضبط الحماية من الحسابات الآلية"
|
||||
configure: "اضبط"
|
||||
postToGallery: "انشر في المعرض"
|
||||
postToHashtag: "انشر بهذا الوسم"
|
||||
gallery: "المعرض"
|
||||
recentPosts: "المشاركات الحديثة"
|
||||
popularPosts: "المشاركات المتداولة"
|
||||
|
@ -776,6 +789,7 @@ translate: "ترجم"
|
|||
translatedFrom: "تُرجم من {x}"
|
||||
accountDeletionInProgress: "حذف الحساب جارٍ"
|
||||
usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
|
||||
devMode: "وضع المُطوّر"
|
||||
keepCw: "أبقِ على تحذيرات المحتوى"
|
||||
lastCommunication: "آخر تواصل"
|
||||
resolved: "عولج"
|
||||
|
@ -784,6 +798,7 @@ breakFollow: "إلغاء الاشتراك"
|
|||
breakFollowConfirm: "أمتأكد من إزالة المتابِع ؟"
|
||||
itsOn: "مفعّل"
|
||||
itsOff: "معطّل"
|
||||
on: "مفعل"
|
||||
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
|
||||
unread: "غير مقروءة"
|
||||
filter: "رشّح"
|
||||
|
@ -840,6 +855,7 @@ statusbar: "شريط الحالة"
|
|||
pleaseSelect: "حدد خيارًا"
|
||||
reverse: "اقلب"
|
||||
colored: "ملوّن"
|
||||
refreshInterval: "مهلة التحديث"
|
||||
label: "التسمية"
|
||||
type: "نوع"
|
||||
speed: "سرعة"
|
||||
|
@ -852,6 +868,9 @@ cannotUploadBecauseInappropriate: "تعذر رفع الملف لوجود محت
|
|||
cannotUploadBecauseNoFreeSpace: "تعذر رفع الملف لنقص مساحة التخزين."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "تعذر رفع الملف بسبب تجاوز حجمه للحد المسموح"
|
||||
beta: "بيتا"
|
||||
enableAutoSensitive: "تعيين تلقائي كمحتوى حساس NSFW"
|
||||
enableAutoSensitiveDescription: "عند الاستطاعة يسمح باكتشاف المحتوى حساس NSFW تلقائيًا في الوسائط باستخدام تعلم الآلة ووسمها تبعًا لذلك. قد يكون هذا الخيار مفعلا من جهة الخادم وسيعمل حتى وان عُطل."
|
||||
activeEmailValidationDescription: "يتحقق من صحة عنوان البريد الإلكتروني بشكل أكثر حزمًا وذلك عبر تحديد ما إذا كان عنوان بريد إلكتروني مؤقت وإمكانية التواصل معه. إذا لم يحدد هذا الخيار فسيتحقق من نسق عنوان البريد الإلكتروني."
|
||||
navbar: "شريط التنقل"
|
||||
shuffle: "خلط"
|
||||
account: "الحسابات"
|
||||
|
@ -863,21 +882,30 @@ pushNotificationAlreadySubscribed: "إرسال الإشعارات مفعل سل
|
|||
pushNotificationNotSupported: "متصفحك لا يدعم إرسال الإشعارات أو المثيل لا يدعمها."
|
||||
sendPushNotificationReadMessage: "احذف الإشعارات فور قراءتها"
|
||||
sendPushNotificationReadMessageCaption: "هذا قد يزيد من معدل استهلاك الطاقة لجهازك."
|
||||
windowRestore: "استرجاع"
|
||||
caption: "التعليق التوضيحي"
|
||||
loggedInAsBot: "والج كآلي"
|
||||
tools: "أدوات"
|
||||
cannotLoad: "تعذر التحميل"
|
||||
numberOfProfileView: "مشاهدات الملف الشخصي"
|
||||
like: "أعجبني"
|
||||
unlike: "ألغِ الإعجاب"
|
||||
numberOfLikes: "الإعجابات"
|
||||
show: "المظهر"
|
||||
neverShow: "لا تظهره مجددًا"
|
||||
remindMeLater: "ربما لاحقا"
|
||||
didYouLikeMisskey: "هل أعجبك ميسكي؟"
|
||||
pleaseDonate: "يستخدم {host} البرمجية الحرة ميسكي. نتمنى أن تتبرعوا للمشروع مما سيسمح لنا متابعة تطويره!"
|
||||
roles: "الأدوار"
|
||||
role: "الدور"
|
||||
noRole: "لم يُعثر على دور"
|
||||
normalUser: "مستخدم عادي"
|
||||
undefined: "غير معرّف"
|
||||
assign: "أسند"
|
||||
unassign: "ألغ الإسناد"
|
||||
color: "اللون"
|
||||
manageCustomEmojis: "إدارة الإيموجي المخصصة"
|
||||
youCannotCreateAnymore: "وصلت لسقف الإنشاء."
|
||||
cannotPerformTemporary: "غير متاح مؤقتاً"
|
||||
permissionDeniedError: "رُفضة العملية"
|
||||
preset: "إعدادات مسبقة"
|
||||
|
@ -900,6 +928,8 @@ cannotBeChangedLater: "لا يمكن تغييره لاحقًا."
|
|||
reactionAcceptance: "قبول التفاعلات"
|
||||
rolesAssignedToMe: "الأدوار المسندة إلي"
|
||||
resetPasswordConfirm: "هل تريد إعادة تعيين كلمة السر؟"
|
||||
license: "الرخصة"
|
||||
unfavoriteConfirm: "أتريد إزالتها من المفضلة؟"
|
||||
noteIdOrUrl: "معرف الملاحظة أو رابطها"
|
||||
video: "فيديو"
|
||||
videos: "فيديوهات"
|
||||
|
@ -908,6 +938,11 @@ accountMoved: "نقل هذا المستخدم حسابه:"
|
|||
accountMovedShort: "رُحل هذا الحساب."
|
||||
operationForbidden: "عملية ممنوعة"
|
||||
forceShowAds: "أظهر الإعلانات التجارية دائما"
|
||||
leftTop: "أعلى اليسار"
|
||||
rightTop: "أعلى اليمين"
|
||||
leftBottom: "أسفل اليسار"
|
||||
rightBottom: "أسفل اليمين"
|
||||
stackAxis: "اتجاه التكديس"
|
||||
vertical: "عمودي"
|
||||
horizontal: "جانبي"
|
||||
position: "الموضع"
|
||||
|
@ -917,9 +952,101 @@ pleaseAgreeAllToContinue: "للمتابعة وافق على الحقول أعل
|
|||
continue: "متابعة"
|
||||
preservedUsernames: "أسماء المستخدمين المحجوزة"
|
||||
preservedUsernamesDescription: "قائمة بأسماء المستخدمين المحجوزة كلٌ في سطر. لن يُقبل التسجيل بهذه الأسماء وستبقى محصورة على التسجيل اليدوي بواسطة المديرين. لن يتأثر المستخدمون الذين يملكون هذه الأسماء سلفًا."
|
||||
createNoteFromTheFile: "أنشئ ملاحظة من هذا الملف"
|
||||
archive: "الأرشيف"
|
||||
channelArchiveConfirmTitle: "أتريد أرشفت {name}؟"
|
||||
channelArchiveConfirmDescription: "لن يمكنك نشر ملاحظات في القناة المأرشفة ولن تظهر في قائمة القنوات ولا في نتائج البحث."
|
||||
thisChannelArchived: "أُرشفت هذه القناة."
|
||||
displayOfNote: "عرض الملاحظة"
|
||||
initialAccountSetting: "إعداد الملف الشخصي"
|
||||
youFollowing: "متابَع"
|
||||
options: "خيارات"
|
||||
specifyUser: "مستخدم محدد"
|
||||
failedToPreviewUrl: "تتعذر المعاينة"
|
||||
update: "حدِّث"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "الأدوار التي يُسمح لأصحابها استخدام هذا اإيموجي في اللتفاعل"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "إذا لم تحدد دورًا يمكن للجميع استخدام هذا الإيموجي في التفاعل."
|
||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "يجب أن تكون الأدوار علنية."
|
||||
cancelReactionConfirm: "أتريد حذف تفاعلك؟"
|
||||
changeReactionConfirm: "أتريد تعديل تفاعلك؟"
|
||||
later: "لاحقاً"
|
||||
goToMisskey: "لميسكي"
|
||||
additionalEmojiDictionary: "قواميس إيموجي إضافية"
|
||||
installed: "مُثبت"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "نجح إنشاء حسابك!"
|
||||
letsStartAccountSetup: "إذا كنت جديدًا لنعدّ حسابك الشخصي."
|
||||
letsFillYourProfile: "أولًا لنعد ملفك الشخصي."
|
||||
profileSetting: "إعدادات الملف الشخصي"
|
||||
privacySetting: "إعدادات الخصوصية"
|
||||
theseSettingsCanEditLater: "يمكنك تغيير هذه الإعدادات لاحقًا."
|
||||
_accountMigration:
|
||||
moveFrom: "انقل حسابًا آخر لهذا الحساب"
|
||||
moveFromLabel: "الحساب الأصلي #{n}"
|
||||
moveTo: "انقل هذا الحساب لحساب آخر"
|
||||
moveToLabel: "الحساب الوجهة:"
|
||||
moveCannotBeUndone: "لا يمكن التراجع عن نقل الحساب."
|
||||
movedTo: "الحساب الوجهة:"
|
||||
_achievements:
|
||||
_types:
|
||||
_notes1:
|
||||
description: "انشر ملاحظتك الأولى"
|
||||
flavor: "تمتع باستخدام ميسكي!"
|
||||
_notes10:
|
||||
title: "بعض الملاحظات"
|
||||
description: "انشر 10 ملاحظات"
|
||||
_notes100:
|
||||
title: "كثير من الملاحظات"
|
||||
description: "انشر 100 ملاحظة"
|
||||
_notes500:
|
||||
description: "انشر 500 ملاحظة"
|
||||
_notes1000:
|
||||
title: "جبل ملاحظات"
|
||||
description: "انشر 1000 ملاحظة"
|
||||
_notes5000:
|
||||
description: "انشر 5000 ملاحظة"
|
||||
_notes10000:
|
||||
description: "انشر 10000 ملاحظة"
|
||||
_notes20000:
|
||||
title: "أريد...ملاحظات...أكثر"
|
||||
description: "انشر 20000 ملاحظة"
|
||||
_notes30000:
|
||||
title: "ملاحظات وملاحظات وملاحظات"
|
||||
description: "انشر 30000 ملاحظة"
|
||||
_notes40000:
|
||||
title: "مصنع ملاحظات"
|
||||
description: "انشر 40000 ملاحظة"
|
||||
_notes50000:
|
||||
title: "كوكب ملاحظات"
|
||||
description: "انشر 50000 ملاحظة"
|
||||
_notes60000:
|
||||
title: "نجم ملاحظات"
|
||||
description: "انشر 60000 ملاحظة"
|
||||
_notes70000:
|
||||
title: "ثقب أسود للملاحظات"
|
||||
description: "انشر 70000 ملاحظة"
|
||||
_notes80000:
|
||||
title: "مجرة ملاحظات"
|
||||
description: "انشر 80000 ملاحظة"
|
||||
_notes90000:
|
||||
title: "كوْن ملاحظات"
|
||||
description: "انشر 90000 ملاحظة"
|
||||
_notes100000:
|
||||
title: "كل ملاحظاتك لنا"
|
||||
description: "انشر 100000 ملاحظة"
|
||||
flavor: "حقًا لديك الكثير من القصص"
|
||||
_login3:
|
||||
title: "مبتدأ I"
|
||||
_noteFavorited1:
|
||||
description: "فضًِل ملاحظتك الأولى"
|
||||
_myNoteFavorited1:
|
||||
title: "ساعٍ للنجوم"
|
||||
description: "أعجب شخص آخر بإحدى ملاحظاتك"
|
||||
_profileFilled:
|
||||
title: "مستعد"
|
||||
description: "أعدّ حسابك"
|
||||
_markedAsCat:
|
||||
title: "أنا قط"
|
||||
_role:
|
||||
new: "دور جديد"
|
||||
edit: "حرر الأدوار"
|
||||
|
@ -996,10 +1123,6 @@ _aboutMisskey:
|
|||
donate: "تبرع لميسكي"
|
||||
morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا لكم 🥰"
|
||||
patrons: "الداعمون"
|
||||
_nsfw:
|
||||
respect: "اخف الوسائط ذات المحتوى الحساس"
|
||||
ignore: "اعرض الوسائط ذات المحتوى الحساس"
|
||||
force: "اخف كل الوسائط"
|
||||
_instanceTicker:
|
||||
none: "لا تظهره بتاتًا"
|
||||
remote: "أظهر للمستخدمين البِعاد"
|
||||
|
|
|
@ -294,7 +294,6 @@ copyUrl: "URL কপি করুন"
|
|||
rename: "পুনঃনামকরণ"
|
||||
avatar: "প্রোফাইল ছবি"
|
||||
banner: "ব্যানার"
|
||||
nsfw: "সংবেদনশীল বিষয়বস্তু"
|
||||
whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে"
|
||||
disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে"
|
||||
reload: "আবার লোড করুন"
|
||||
|
@ -902,10 +901,6 @@ _aboutMisskey:
|
|||
donate: "Misskey তে দান করুন"
|
||||
morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰"
|
||||
patrons: "সমর্থনকারী"
|
||||
_nsfw:
|
||||
respect: "স্পর্শকাতর মিডিয়া লুকান"
|
||||
ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না"
|
||||
force: "সকল মিডিয়া লুকান"
|
||||
_instanceTicker:
|
||||
none: "দেখাবেন না"
|
||||
remote: "রিমোট ব্যাবহারকারীদের জন্য দেখান"
|
||||
|
|
|
@ -263,7 +263,6 @@ emptyFolder: "La carpeta està buida"
|
|||
unableToDelete: "No es pot eliminar"
|
||||
copyUrl: "Copia l'URL"
|
||||
rename: "Canvia el nom"
|
||||
nsfw: "NSFW"
|
||||
reload: "Actualitza"
|
||||
doNothing: "Ignora"
|
||||
accept: "Accepta"
|
||||
|
|
|
@ -276,7 +276,6 @@ copyUrl: "Kopírovat URL"
|
|||
rename: "Přejmenovat"
|
||||
avatar: "Avatar"
|
||||
banner: "Baner"
|
||||
nsfw: "NSFW"
|
||||
disconnectedFromServer: "Spojení bylo přerušeno"
|
||||
reload: "Aktualizovat"
|
||||
doNothing: "Ignorovat"
|
||||
|
|
|
@ -54,6 +54,8 @@ copyRSS: "RSS kopieren"
|
|||
copyUsername: "Benutzernamen kopieren"
|
||||
copyUserId: "Benutzer-ID kopieren"
|
||||
copyNoteId: "Notiz-ID kopieren"
|
||||
copyFileId: "Datei-ID kopieren"
|
||||
copyFolderId: "Ordner-ID kopieren"
|
||||
searchUser: "Nach einem Benutzer suchen"
|
||||
reply: "Antworten"
|
||||
loadMore: "Mehr laden"
|
||||
|
@ -112,7 +114,7 @@ pinnedNote: "Angeheftete Notiz"
|
|||
pinned: "Angeheftet"
|
||||
you: "Du"
|
||||
clickToShow: "Zum Anzeigen anklicken"
|
||||
sensitive: "NSFW"
|
||||
sensitive: "Sensibel"
|
||||
add: "Hinzufügen"
|
||||
reaction: "Reaktionen"
|
||||
reactions: "Reaktionen"
|
||||
|
@ -120,8 +122,8 @@ reactionSetting: "In der Reaktionsauswahl anzuzeigende Reaktionen"
|
|||
reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
|
||||
rememberNoteVisibility: "Notizsichtbarkeit merken"
|
||||
attachCancel: "Anhang entfernen"
|
||||
markAsSensitive: "Als NSFW markieren"
|
||||
unmarkAsSensitive: "Als nicht NSFW markieren"
|
||||
markAsSensitive: "Als sensibel markieren"
|
||||
unmarkAsSensitive: "Als nicht sensibel markieren"
|
||||
enterFileName: "Dateinamen eingeben"
|
||||
mute: "Stummschalten"
|
||||
unmute: "Stummschaltung aufheben"
|
||||
|
@ -136,8 +138,10 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
|
|||
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
|
||||
unsuspendConfirm: "Möchtest du diesen Benutzer wirklich entsperren?"
|
||||
selectList: "Liste auswählen"
|
||||
editList: "Liste bearbeiten"
|
||||
selectChannel: "Kanal auswählen"
|
||||
selectAntenna: "Antenne auswählen"
|
||||
editAntenna: "Antenne bearbeiten"
|
||||
selectWidget: "Widget auswählen"
|
||||
editWidgets: "Widgets bearbeiten"
|
||||
editWidgetsExit: "Fertig"
|
||||
|
@ -311,7 +315,7 @@ copyUrl: "URL kopieren"
|
|||
rename: "Umbenennen"
|
||||
avatar: "Profilbild"
|
||||
banner: "Banner"
|
||||
nsfw: "NSFW"
|
||||
displayOfSensitiveMedia: "Anzeige von sensiblen Medien"
|
||||
whenServerDisconnected: "Bei Verbindungsverlust zum Server"
|
||||
disconnectedFromServer: "Die Verbindung zum Server wurde getrennt"
|
||||
reload: "Aktualisieren"
|
||||
|
@ -693,7 +697,7 @@ driveUsage: "Drive-Auslastung"
|
|||
noCrawle: "Crawler-Indexierung ablehnen"
|
||||
noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Notizen, Seiten usw. nicht zu indexieren."
|
||||
lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt, wird jede deiner Notizen öffentlich sichtbar sein, sofern du ihre Notizsichtbarkeit nicht auf \"Nur Follower\" setzt."
|
||||
alwaysMarkSensitive: "Medien standardmäßig als NSFW markieren"
|
||||
alwaysMarkSensitive: "Medien standardmäßig als sensibel markieren"
|
||||
loadRawImages: "Anstatt Vorschaubilder immer Originalbilder anzeigen"
|
||||
disableShowingAnimatedImages: "Animierte Bilder nicht abspielen"
|
||||
verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse versendet. Besuche den dort enthaltenen Link, um die Verifizierung abzuschließen."
|
||||
|
@ -912,16 +916,16 @@ type: "Art"
|
|||
speed: "Geschwindigkeit"
|
||||
slow: "Langsam"
|
||||
fast: "Schnell"
|
||||
sensitiveMediaDetection: "Erkennung von NSFW-Medien"
|
||||
sensitiveMediaDetection: "Erkennung von sensiblen Medien"
|
||||
localOnly: "Nur Lokal"
|
||||
remoteOnly: "Nur für fremde Instanzen"
|
||||
failedToUpload: "Hochladen fehlgeschlagen"
|
||||
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise NSFW festgestellt wurden."
|
||||
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden."
|
||||
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "NSFW-Automarkierung"
|
||||
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch NSFW-Markierungen für Medien, die NSFW-Anteile beinhalten. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
||||
enableAutoSensitive: "Automarkierung sensibler Medien"
|
||||
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
||||
activeEmailValidationDescription: "Aktivert strengere Überprüfung von E-Mail-Adressen, d.h. Testen auf Wegwerfadressen und darauf, ob mit der Adresse tatsächlich kommuniziert werden kann. Ist dies deaktiviert, so wird nur das Format der E-Mail überprüft."
|
||||
navbar: "Navigationsleiste"
|
||||
shuffle: "Mischen"
|
||||
|
@ -1063,6 +1067,9 @@ goToMisskey: "Zu Misskey"
|
|||
additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher"
|
||||
installed: "Installiert"
|
||||
branding: "Branding"
|
||||
enableServerMachineStats: "Hardwareinformationen des Servers veröffentlichen"
|
||||
enableIdenticonGeneration: "Generierung von Benutzer-Identicons aktivieren"
|
||||
turnOffToImprovePerformance: "Deaktivierung kann zu höherer Leistung führen."
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
||||
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
||||
|
@ -1403,10 +1410,10 @@ _role:
|
|||
or: "ODER-Bedingung"
|
||||
not: "NICHT-Bedingung"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von sensiblen Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||
sensitivity: "Erkennungssensitivität"
|
||||
sensitivityDescription: "Durch das Senken der Sensitivität kann die Anzahl an Fehlerkennungen (sog. false positives) reduziert werden. Durch ein Erhöhen dieser kann die Anzahl an verpassten Erkennungen (sog. false negatives) reduziert werden."
|
||||
setSensitiveFlagAutomatically: "Als NSFW markieren"
|
||||
setSensitiveFlagAutomatically: "Als sensibel markieren"
|
||||
setSensitiveFlagAutomaticallyDescription: "Die Resultate der internen Erkennung werden beibehalten, auch wenn diese Option deaktiviert ist."
|
||||
analyzeVideos: "Videoanalyse aktivieren"
|
||||
analyzeVideosDescription: "Analysiert zusätzlich zu Bildern auch Videos. Die Last des Servers wird hierdurch etwas erhöht."
|
||||
|
@ -1435,6 +1442,7 @@ _ad:
|
|||
back: "Zurück"
|
||||
reduceFrequencyOfThisAd: "Diese Werbung weniger anzeigen"
|
||||
hide: "Ausblenden"
|
||||
timezoneinfo: "Der Wochentag wird durch die Serverzeitzone bestimmt."
|
||||
_forgotPassword:
|
||||
enterEmail: "Gib die Email-Adresse ein, mit der du dich registriert hast. An diese wird ein Link gesendet, mit dem du dein Passwort zurücksetzen kannst."
|
||||
ifNoEmail: "Solltest du bei der Registrierung keine Email-Adresse angegeben haben, wende dich bitte an den Administrator."
|
||||
|
@ -1486,9 +1494,9 @@ _aboutMisskey:
|
|||
donate: "An Misskey spenden"
|
||||
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
|
||||
patrons: "UnterstützerInnen"
|
||||
_nsfw:
|
||||
respect: "Als NSFW markierte Bilder verbergen"
|
||||
ignore: "Als NSFW markierte Bilder nicht verbergen"
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "Sensible Medien verbergen"
|
||||
ignore: "Sensible Medien anzeigen"
|
||||
force: "Alle Medien verbergen"
|
||||
_instanceTicker:
|
||||
none: "Nie anzeigen"
|
||||
|
|
|
@ -54,6 +54,8 @@ copyRSS: "Copy RSS"
|
|||
copyUsername: "Copy username"
|
||||
copyUserId: "Copy user ID"
|
||||
copyNoteId: "Copy note ID"
|
||||
copyFileId: "Copy file ID"
|
||||
copyFolderId: "Copy folder ID"
|
||||
searchUser: "Search for a user"
|
||||
reply: "Reply"
|
||||
loadMore: "Load more"
|
||||
|
@ -112,7 +114,7 @@ pinnedNote: "Pinned note"
|
|||
pinned: "Pin to profile"
|
||||
you: "You"
|
||||
clickToShow: "Click to show"
|
||||
sensitive: "NSFW"
|
||||
sensitive: "Sensitive"
|
||||
add: "Add"
|
||||
reaction: "Reactions"
|
||||
reactions: "Reactions"
|
||||
|
@ -120,8 +122,8 @@ reactionSetting: "Reactions to show in the reaction picker"
|
|||
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
|
||||
rememberNoteVisibility: "Remember note visibility settings"
|
||||
attachCancel: "Remove attachment"
|
||||
markAsSensitive: "Mark as NSFW"
|
||||
unmarkAsSensitive: "Unmark as NSFW"
|
||||
markAsSensitive: "Mark as sensitive"
|
||||
unmarkAsSensitive: "Unmark as sensitive"
|
||||
enterFileName: "Enter filename"
|
||||
mute: "Mute"
|
||||
unmute: "Unmute"
|
||||
|
@ -136,8 +138,10 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
|
|||
suspendConfirm: "Are you sure that you want to suspend this account?"
|
||||
unsuspendConfirm: "Are you sure that you want to unsuspend this account?"
|
||||
selectList: "Select a list"
|
||||
editList: "Edit list"
|
||||
selectChannel: "Select a channel"
|
||||
selectAntenna: "Select an antenna"
|
||||
editAntenna: "Edit antenna"
|
||||
selectWidget: "Select a widget"
|
||||
editWidgets: "Edit widgets"
|
||||
editWidgetsExit: "Done"
|
||||
|
@ -311,7 +315,7 @@ copyUrl: "Copy URL"
|
|||
rename: "Rename"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "NSFW"
|
||||
displayOfSensitiveMedia: "Display of sensitive media"
|
||||
whenServerDisconnected: "When losing connection to the server"
|
||||
disconnectedFromServer: "Connection to server has been lost"
|
||||
reload: "Refresh"
|
||||
|
@ -693,7 +697,7 @@ driveUsage: "Drive space usage"
|
|||
noCrawle: "Reject crawler indexing"
|
||||
noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc."
|
||||
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
|
||||
alwaysMarkSensitive: "Mark as NSFW by default"
|
||||
alwaysMarkSensitive: "Mark as sensitive by default"
|
||||
loadRawImages: "Load original images instead of showing thumbnails"
|
||||
disableShowingAnimatedImages: "Don't play animated images"
|
||||
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
||||
|
@ -912,16 +916,16 @@ type: "Type"
|
|||
speed: "Speed"
|
||||
slow: "Slow"
|
||||
fast: "Fast"
|
||||
sensitiveMediaDetection: "Detection of NSFW media"
|
||||
sensitiveMediaDetection: "Detection of sensitive media"
|
||||
localOnly: "Local only"
|
||||
remoteOnly: "Remote only"
|
||||
failedToUpload: "Upload failed"
|
||||
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially NSFW."
|
||||
cannotUploadBecauseInappropriate: "This file could not be uploaded because parts of it have been detected as potentially inappropriate."
|
||||
cannotUploadBecauseNoFreeSpace: "Upload failed due to lack of Drive capacity."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "This file cannot be uploaded as it exceeds the file size limit."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Automatic NSFW-Marking"
|
||||
enableAutoSensitiveDescription: "Allows automatic detection and marking of NSFW media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
|
||||
enableAutoSensitive: "Automatic marking as sensitive"
|
||||
enableAutoSensitiveDescription: "Allows automatic detection and marking of sensitive media through Machine Learning where possible. Even if this option is disabled, it may be enabled instance-wide."
|
||||
activeEmailValidationDescription: "Enables stricter validation of email addresses, which includes checking for disposable addresses and by whether it can actually be communicated with. When unchecked, only the format of the email is validated."
|
||||
navbar: "Navigation bar"
|
||||
shuffle: "Shuffle"
|
||||
|
@ -1063,6 +1067,9 @@ goToMisskey: "To Misskey"
|
|||
additionalEmojiDictionary: "Additional emoji dictionaries"
|
||||
installed: "Installed"
|
||||
branding: "Branding"
|
||||
enableServerMachineStats: "Publish server hardware stats"
|
||||
enableIdenticonGeneration: "Enable user identicon generation"
|
||||
turnOffToImprovePerformance: "Turning this off can increase performance."
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Your account was successfully created!"
|
||||
letsStartAccountSetup: "For starters, let's set up your profile."
|
||||
|
@ -1403,10 +1410,10 @@ _role:
|
|||
or: "OR-Condition"
|
||||
not: "NOT-Condition"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
description: "Reduces the effort of server moderation through automatically recognizing sensitive media via Machine Learning. This will slightly increase the load on the server."
|
||||
sensitivity: "Detection sensitivity"
|
||||
sensitivityDescription: "Reducing the sensitivity will lead to fewer misdetections (false positives) whereas increasing it will lead to fewer missed detections (false negatives)."
|
||||
setSensitiveFlagAutomatically: "Mark as NSFW"
|
||||
setSensitiveFlagAutomatically: "Mark as sensitive"
|
||||
setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off."
|
||||
analyzeVideos: "Enable analysis of videos"
|
||||
analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server."
|
||||
|
@ -1435,6 +1442,7 @@ _ad:
|
|||
back: "Back"
|
||||
reduceFrequencyOfThisAd: "Show this ad less"
|
||||
hide: "Hide"
|
||||
timezoneinfo: "The day of the week is determined from the server's timezone."
|
||||
_forgotPassword:
|
||||
enterEmail: "Enter the email address you used to register. A link with which you can reset your password will then be sent to it."
|
||||
ifNoEmail: "If you did not use an email during registration, please contact the instance administrator instead."
|
||||
|
@ -1486,9 +1494,9 @@ _aboutMisskey:
|
|||
donate: "Donate to Misskey"
|
||||
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"
|
||||
patrons: "Patrons"
|
||||
_nsfw:
|
||||
respect: "Hide NSFW media"
|
||||
ignore: "Don't hide NSFW media"
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "Hide media marked as sensitive"
|
||||
ignore: "Display media marked as sensitive"
|
||||
force: "Hide all media"
|
||||
_instanceTicker:
|
||||
none: "Never show"
|
||||
|
|
|
@ -52,6 +52,8 @@ addToList: "Agregar a lista"
|
|||
sendMessage: "Enviar un mensaje"
|
||||
copyRSS: "Copiar RSS"
|
||||
copyUsername: "Copiar nombre de usuario"
|
||||
copyUserId: "Copiar ID del usuario"
|
||||
copyNoteId: "Copiar ID de la nota"
|
||||
searchUser: "Buscar un usuario"
|
||||
reply: "Responder"
|
||||
loadMore: "Ver más"
|
||||
|
@ -134,8 +136,10 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
|
|||
suspendConfirm: "¿Quiere suspender esta cuenta?"
|
||||
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
|
||||
selectList: "Seleccione una lista"
|
||||
editList: "Editar lista"
|
||||
selectChannel: "Seleccionar canal"
|
||||
selectAntenna: "Seleccionar antena"
|
||||
editAntenna: "Editar antena"
|
||||
selectWidget: "Seleccionar widget"
|
||||
editWidgets: "Editar widgets"
|
||||
editWidgetsExit: "Terminar edición"
|
||||
|
@ -229,10 +233,10 @@ instanceFollowers: "Seguidores de la instancia"
|
|||
instanceUsers: "Usuarios de la instancia"
|
||||
changePassword: "Cambiar contraseña"
|
||||
security: "Seguridad"
|
||||
retypedNotMatch: "No hay coincidencia"
|
||||
retypedNotMatch: "La información no coincide."
|
||||
currentPassword: "Contraseña actual"
|
||||
newPassword: "Contraseña nueva"
|
||||
newPasswordRetype: "Contraseña nueva (repetir)"
|
||||
newPasswordRetype: "Reescribe contraseña nueva"
|
||||
attachFile: "Añadir archivo"
|
||||
more: "¡Más!"
|
||||
featured: "Destacados"
|
||||
|
@ -309,7 +313,6 @@ copyUrl: "Copiar URL"
|
|||
rename: "Renombrar"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "Marcado como sensible"
|
||||
whenServerDisconnected: "Cuando se pierda la conexión con el servidor"
|
||||
disconnectedFromServer: "Desconectado del servidor"
|
||||
reload: "Recargar"
|
||||
|
@ -433,7 +436,7 @@ title: "Título"
|
|||
text: "Texto"
|
||||
enable: "Activar"
|
||||
next: "Siguiente"
|
||||
retype: "Intentar de nuevo"
|
||||
retype: "Ingrese de nuevo"
|
||||
noteOf: "Notas de {user}"
|
||||
quoteAttached: "Cita añadida"
|
||||
quoteQuestion: "¿Quiere añadir una cita?"
|
||||
|
@ -453,7 +456,7 @@ weakPassword: "Contraseña débil"
|
|||
normalPassword: "Buena contraseña"
|
||||
strongPassword: "Muy buena contraseña"
|
||||
passwordMatched: "Correcto"
|
||||
passwordNotMatched: "Las contraseñas no son las mismas"
|
||||
passwordNotMatched: "Las contraseñas no coinciden"
|
||||
signinWith: "Inicie sesión con {x}"
|
||||
signinFailed: "Autenticación fallida. Asegúrate de haber usado el nombre de usuario y contraseña correctos."
|
||||
or: "O"
|
||||
|
@ -790,8 +793,9 @@ noMaintainerInformationWarning: "No se ha establecido la información del admini
|
|||
noBotProtectionWarning: "La protección contra los bots no está configurada"
|
||||
configure: "Configurar"
|
||||
postToGallery: "Crear una nueva publicación en la galería"
|
||||
postToHashtag: "Publicar a este hashtag"
|
||||
gallery: "Galería"
|
||||
recentPosts: "Posts recientes"
|
||||
recentPosts: "Publicaciones recientes"
|
||||
popularPosts: "Más vistos"
|
||||
shareWithNote: "Compartir con una nota"
|
||||
ads: "Anuncios"
|
||||
|
@ -823,6 +827,7 @@ translatedFrom: "Traducido de {x}"
|
|||
accountDeletionInProgress: "La eliminación de la cuenta está en curso"
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
|
||||
aiChanMode: "Modo Ai"
|
||||
devMode: "Modo de desarrollador"
|
||||
keepCw: "Mantener la advertencia de contenido"
|
||||
pubSub: "Cuentas Pub/Sub"
|
||||
lastCommunication: "Última comunicación"
|
||||
|
@ -832,9 +837,11 @@ breakFollow: "Dejar de seguir"
|
|||
breakFollowConfirm: "¿Quieres dejar de seguir?"
|
||||
itsOn: "¡Está encendido!"
|
||||
itsOff: "¡Está apagado!"
|
||||
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro de la cuenta"
|
||||
on: "Activado"
|
||||
off: "Desactivado"
|
||||
emailRequiredForSignup: "Se requiere una dirección de correo electrónico para el registro de la cuenta"
|
||||
unread: "No leído"
|
||||
filter: "Filtro"
|
||||
filter: "Filtrar"
|
||||
controlPanel: "Panel de control"
|
||||
manageAccounts: "Administrar cuenta"
|
||||
makeReactionsPublic: "Hacer el historial de reacciones público"
|
||||
|
@ -912,6 +919,7 @@ remoteOnly: "Sólo remoto"
|
|||
failedToUpload: "La subida falló"
|
||||
cannotUploadBecauseInappropriate: "Este archivo no se puede subir debido a que algunas partes han sido detectadas comoNSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "La subida falló debido a falta de espacio libre en la unidad del usuario."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "Este archivo supera el peso máximo y no puede ser subido."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Marcar automáticamente contenido NSFW"
|
||||
enableAutoSensitiveDescription: "Permite la detección y marcado automático de contenido NSFW usando 'Machine Learning' cuando sea posible. Incluso si esta opción está desactivada, puede ser activado para toda la instancia."
|
||||
|
@ -942,18 +950,23 @@ show: "Apariencia"
|
|||
neverShow: "No mostrar de nuevo"
|
||||
remindMeLater: "Recordar después"
|
||||
didYouLikeMisskey: "¿Te gusta Misskey?"
|
||||
pleaseDonate: "Misskey es software libre, y es usado por {host} . Por favor, ¡considera donar al proyecto principal para que podamos continuar!"
|
||||
pleaseDonate: "{host} usa el software gratuito Misskey. Por favor ¡Considera donar al proyecto principal para que podamos continuar!"
|
||||
roles: "Roles"
|
||||
role: "Roles"
|
||||
role: "Rol"
|
||||
noRole: "Rol no encontrado"
|
||||
normalUser: "Usuario normal"
|
||||
undefined: "Indefinido"
|
||||
assign: "Asignar"
|
||||
unassign: "Quitar"
|
||||
color: "Color"
|
||||
manageCustomEmojis: "Administrar emojis personalizados"
|
||||
youCannotCreateAnymore: "Se alcanzó el límite de creación"
|
||||
cannotPerformTemporary: "Indisponible temporalmente"
|
||||
youCannotCreateAnymore: "Has llegado al límite de creaciones."
|
||||
cannotPerformTemporary: "Temporalmente no disponible"
|
||||
cannotPerformTemporaryDescription: "Esta acción no se puede realizar porque se excedió el límite de ejecución. Espera un poco y prueba de nuevo."
|
||||
invalidParamError: "Parámetros inválidos"
|
||||
invalidParamErrorDescription: "Los parámetros de la solicitud son inválidos. Normalmente se trata de un error, pero también puede haberse excedido algún límite o similares."
|
||||
permissionDeniedError: "Operación denegada"
|
||||
permissionDeniedErrorDescription: "Esta cuenta no tiene permisos para hacer esa acción."
|
||||
preset: "Predefinido"
|
||||
selectFromPresets: "Escoger desde predefinidos"
|
||||
achievements: "Logros"
|
||||
|
@ -969,7 +982,7 @@ internalServerErrorDescription: "El servidor tuvo un error inesperado."
|
|||
copyErrorInfo: "Copiar detalles del error"
|
||||
joinThisServer: "Registrarse en esta instancia"
|
||||
exploreOtherServers: "Buscar otra instancia"
|
||||
letsLookAtTimeline: "Mirar la línea de tiempo local"
|
||||
letsLookAtTimeline: "Mira la línea de tiempo"
|
||||
disableFederationConfirm: "¿Estas seguro que quieres desactivar la federación?"
|
||||
disableFederationConfirmWarn: "Aunque no exista federación los posts no serán marcados como privados. En la mayoría de los casos, no es necesario hacer los posts no federar."
|
||||
disableFederationOk: "Desactivar."
|
||||
|
@ -980,6 +993,7 @@ cannotBeChangedLater: "Esto no podrá ser cambiado después."
|
|||
reactionAcceptance: "Aceptación de reacciones"
|
||||
likeOnly: "Sólo 'me gusta'"
|
||||
likeOnlyForRemote: "Sólo reacciones de instancias remotas"
|
||||
nonSensitiveOnly: "Solo no sensible"
|
||||
rolesAssignedToMe: "Roles asignados a mí"
|
||||
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
|
||||
sensitiveWords: "Palabras sensibles"
|
||||
|
@ -997,14 +1011,40 @@ enableChartsForFederatedInstances: "Generar gráficos de servidores remotos"
|
|||
showClipButtonInNoteFooter: "Añadir \"Clip\" al menú de notas"
|
||||
largeNoteReactions: "Agrandar las reacciones de las notas"
|
||||
noteIdOrUrl: "ID o URL de la nota"
|
||||
video: "Video"
|
||||
videos: "Video"
|
||||
dataSaver: "Ahorro de datos"
|
||||
accountMigration: "Migración de cuenta"
|
||||
accountMoved: "Este usuario se ha mudado a una nueva cuenta:"
|
||||
accountMoved: "Este usuario se movió a una nueva cuenta:"
|
||||
accountMovedShort: "Esta cuenta ha sido migrada."
|
||||
addMemo: "Añadir nota"
|
||||
editMemo: "Editar nota"
|
||||
reactionsList: "Lista de reacciones"
|
||||
renotesList: "Renotas"
|
||||
stackAxis: "Dirección de apilado"
|
||||
horizontal: "Horizontal"
|
||||
position: "Posición"
|
||||
serverRules: "Reglas del servidor"
|
||||
continue: "Continuar"
|
||||
preservedUsernames: "Nombre de usuario reservado"
|
||||
archive: "Archivo"
|
||||
channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
|
||||
youFollowing: "Siguiendo"
|
||||
options: "Opción"
|
||||
update: "Actualizar"
|
||||
installed: "Instalado"
|
||||
branding: "Marca"
|
||||
enableServerMachineStats: "Publicar estadísticas de hardware del servidor"
|
||||
enableIdenticonGeneration: "Activar generación de identicon por usuario"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "¡La cuenta ha sido creada!"
|
||||
letsStartAccountSetup: "Para empezar, creemos tu perfil."
|
||||
letsFillYourProfile: "Primero, creemos tu perfil."
|
||||
profileSetting: "Configuración del perfil"
|
||||
privacySetting: "Configuración de privacidad"
|
||||
theseSettingsCanEditLater: "Puedes cambiar estos ajustes más tarde."
|
||||
youCanEditMoreSettingsInSettingsPageLater: "Desde la pestaña de \"Configuración\" puedes modificar más ajustes. Asegúrate de visitarla después."
|
||||
followUsers: "Comienza a seguir a usuarios que te interesen para construir tu línea de tiempo."
|
||||
_accountMigration:
|
||||
moveFrom: "Trasladar de otra cuenta a ésta"
|
||||
moveFromLabel: "Cuenta desde la que se realiza el traslado:"
|
||||
|
@ -1290,7 +1330,7 @@ _role:
|
|||
canPublicNote: "Permitir la publicación"
|
||||
canInvite: "Puede crear códigos de invitación"
|
||||
canManageCustomEmojis: "Administrar emojis personalizados"
|
||||
driveCapacity: "Capacidad de almacenamiento"
|
||||
driveCapacity: "Capacidad del drive"
|
||||
pinMax: "Máximo de notas fijadas"
|
||||
antennaMax: "Máximo de antenas"
|
||||
wordMuteMax: "Máximo de caracteres en palabras silenciadas"
|
||||
|
@ -1401,10 +1441,6 @@ _aboutMisskey:
|
|||
donate: "Donar a Misskey"
|
||||
morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰"
|
||||
patrons: "Patrocinadores"
|
||||
_nsfw:
|
||||
respect: "Ocultar medios NSFW"
|
||||
ignore: "No esconder medios NSFW "
|
||||
force: "Ocultar todos los medios"
|
||||
_instanceTicker:
|
||||
none: "No mostrar"
|
||||
remote: "Mostrar a usuarios remotos"
|
||||
|
@ -1526,7 +1562,7 @@ _sfx:
|
|||
channel: "Notificaciones del canal"
|
||||
_ago:
|
||||
future: "Futuro"
|
||||
justNow: "Recién ahora"
|
||||
justNow: "Justo ahora"
|
||||
secondsAgo: "Hace {n} segundos"
|
||||
minutesAgo: "Hace {n} minutos"
|
||||
hoursAgo: "Hace {n} horas"
|
||||
|
@ -1540,6 +1576,9 @@ _time:
|
|||
minute: "Minutos"
|
||||
hour: "Horas"
|
||||
day: "Días"
|
||||
_timelineTutorial:
|
||||
step4_1: "También puedes añadir \"Reacciones\" a notas."
|
||||
step4_2: "Para añadir una reacción selecciona el botón \"+\" en la nota y escoge el emoji que quieras para reaccionar."
|
||||
_2fa:
|
||||
alreadyRegistered: "Ya has completado la configuración."
|
||||
registerTOTP: "Registrar aplicación autenticadora"
|
||||
|
@ -1685,14 +1724,14 @@ _visibility:
|
|||
homeDescription: "Visible sólo en la linea de tiempo de inicio"
|
||||
followers: "Seguidores"
|
||||
followersDescription: "Visible sólo para tus seguidores"
|
||||
specified: "Mensaje directo"
|
||||
specified: "Nota directa"
|
||||
specifiedDescription: "Visible sólo para los usuarios elegidos"
|
||||
disableFederation: "No federado"
|
||||
disableFederationDescription: "No enviar a otras instancias"
|
||||
_postForm:
|
||||
replyPlaceholder: "Responder a esta nota"
|
||||
quotePlaceholder: "Citar esta nota"
|
||||
channelPlaceholder: "Postear en el canal"
|
||||
channelPlaceholder: "Publicar en el canal"
|
||||
_placeholders:
|
||||
a: "¿Qué haces?"
|
||||
b: "¿Te pasó algo?"
|
||||
|
@ -1876,7 +1915,7 @@ _deck:
|
|||
list: "Listas"
|
||||
channel: "Canal"
|
||||
mentions: "Menciones"
|
||||
direct: "Mensaje directo"
|
||||
direct: "Notas directas"
|
||||
_dialog:
|
||||
charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}."
|
||||
charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}."
|
||||
|
@ -1884,8 +1923,8 @@ _disabledTimeline:
|
|||
title: "Línea de tiempo deshabilitada"
|
||||
description: "No puedes usar esta línea de tiempo con tus roles actuales."
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "Más grandes"
|
||||
orderByCreatedAtAsc: "Más antiguos"
|
||||
orderBySizeDesc: "Tamaño descendiente"
|
||||
orderByCreatedAtAsc: "Fecha ascendente"
|
||||
_webhookSettings:
|
||||
createWebhook: "Crear Webhook"
|
||||
name: "Nombre"
|
||||
|
|
|
@ -302,7 +302,6 @@ copyUrl: "Copier l’URL"
|
|||
rename: "Renommer"
|
||||
avatar: "Avatar"
|
||||
banner: "Bannière"
|
||||
nsfw: "Contenu sensible"
|
||||
whenServerDisconnected: "Lorsque la connexion au serveur est perdue"
|
||||
disconnectedFromServer: "Déconnecté·e du serveur"
|
||||
reload: "Rafraîchir"
|
||||
|
@ -1085,10 +1084,6 @@ _aboutMisskey:
|
|||
donate: "Soutenir Misskey"
|
||||
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes non mentionnées ici. Merci à toutes et à tous ! 🥰"
|
||||
patrons: "Contributeurs"
|
||||
_nsfw:
|
||||
respect: "Cacher les médias marqués comme contenu sensible"
|
||||
ignore: "Afficher les médias sensibles"
|
||||
force: "Cacher tous les médias"
|
||||
_instanceTicker:
|
||||
none: "Cacher "
|
||||
remote: "Montrer pour les utilisateur·ice·s distant·e·s"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const ts = require('typescript');
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
function createMembers(record) {
|
||||
return Object.entries(record)
|
||||
|
@ -14,7 +14,7 @@ function createMembers(record) {
|
|||
));
|
||||
}
|
||||
|
||||
module.exports = function generateDTS() {
|
||||
export default function generateDTS() {
|
||||
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||
const members = createMembers(locale);
|
||||
const elements = [
|
||||
|
|
|
@ -5,7 +5,7 @@ introMisskey: "Selamat datang! Misskey adalah perangkat mikroblog tercatu bersif
|
|||
poweredByMisskeyDescription: "{name} adalah sebuah layanan (instance) yang menggunakan platform sumber terbuka <b>Misskey</b>."
|
||||
monthAndDay: "{day} {month}"
|
||||
search: "Penelusuran"
|
||||
notifications: "Pemberitahuan"
|
||||
notifications: "Notifikasi"
|
||||
username: "Nama Pengguna"
|
||||
password: "Kata sandi"
|
||||
forgotPassword: "Lupa Kata Sandi"
|
||||
|
@ -17,7 +17,7 @@ noThankYou: "Tidak sekarang."
|
|||
enterUsername: "Masukkan nama pengguna"
|
||||
renotedBy: "direnote oleh {user}"
|
||||
noNotes: "Tidak ada catatan"
|
||||
noNotifications: "Tidak ada pemberitahuan"
|
||||
noNotifications: "Tidak ada notifikasi"
|
||||
instance: "Instansi"
|
||||
settings: "Pengaturan"
|
||||
notificationSettings: "Atur Notifikasi"
|
||||
|
@ -25,7 +25,7 @@ basicSettings: "Pengaturan umum"
|
|||
otherSettings: "Pengaturan lainnya"
|
||||
openInWindow: "Buka di jendela"
|
||||
profile: "Profil"
|
||||
timeline: "Linimasa"
|
||||
timeline: "Lini masa"
|
||||
noAccountDescription: "Pengguna ini belum menulis bio"
|
||||
login: "Masuk"
|
||||
loggingIn: "Sedang masuk"
|
||||
|
@ -52,6 +52,8 @@ addToList: "Tambahkan ke daftar"
|
|||
sendMessage: "Kirim pesan"
|
||||
copyRSS: "Salin RSS"
|
||||
copyUsername: "Salin nama pengguna"
|
||||
copyUserId: "Salin ID pengguna"
|
||||
copyNoteId: "Salin ID catatan"
|
||||
searchUser: "Cari pengguna"
|
||||
reply: "Balas"
|
||||
loadMore: "Selebihnya"
|
||||
|
@ -90,7 +92,7 @@ serverIsDead: "Tidak ada respon dari peladen. Mohon tunggu dan coba beberapa saa
|
|||
youShouldUpgradeClient: "Untuk melihat halaman ini, mohon muat ulang untuk memutakhirkan klienmu."
|
||||
enterListName: "Masukkan nama daftar"
|
||||
privacy: "Privasi"
|
||||
makeFollowManuallyApprove: "Permintaan mengikuti membutuhkan persetujuan"
|
||||
makeFollowManuallyApprove: "Permintaan mengikuti butuh persetujuan"
|
||||
defaultNoteVisibility: "Privasi bawaan catatan"
|
||||
follow: "Ikuti"
|
||||
followRequest: "Permintaan mengikuti"
|
||||
|
@ -115,7 +117,7 @@ add: "Tambahkan"
|
|||
reaction: "Reaksi"
|
||||
reactions: "Reaksi"
|
||||
reactionSetting: "Reaksi untuk dimunculkan di bilah reaksi"
|
||||
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan"
|
||||
reactionSettingDescription2: "Geser untuk memindah urutan emoji, klik untuk menghapus, tekan \"+\" untuk menambahkan"
|
||||
rememberNoteVisibility: "Ingat pengaturan visibilitas catatan"
|
||||
attachCancel: "Hapus lampiran"
|
||||
markAsSensitive: "Tandai sebagai konten sensitif"
|
||||
|
@ -134,8 +136,10 @@ unblockConfirm: "Apakah kamu yakin ingin membuka blokir akun ini?"
|
|||
suspendConfirm: "Apakah kamu yakin ingin membekukan akun ini?"
|
||||
unsuspendConfirm: "Apakah kamu yakin ingin membuka pembekuan akun ini?"
|
||||
selectList: "Pilih daftar"
|
||||
editList: "Sunting daftar"
|
||||
selectChannel: "Pilih kanal"
|
||||
selectAntenna: "Pilih Antena"
|
||||
editAntenna: "Sunting antena"
|
||||
selectWidget: "Pilih gawit"
|
||||
editWidgets: "Sunting gawit"
|
||||
editWidgetsExit: "Selesai"
|
||||
|
@ -146,16 +150,17 @@ emojiName: "Nama emoji"
|
|||
emojiUrl: "URL Emoji"
|
||||
addEmoji: "Tambahkan emoji"
|
||||
settingGuide: "Pengaturan rekomendasi"
|
||||
cacheRemoteFiles: "Tembolokkan berkas remote"
|
||||
cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas luar akan dimuat langsung dari instansi luar. Menonaktifkan ini akan mengurangi penggunaan penyimpanan, namun dapat menyebabkan meningkatkan lalu lintas bandwidth, karena thumbnail tidak dihasilkan."
|
||||
cacheRemoteFiles: "Tembolokkan berkas dari instansi luar"
|
||||
cacheRemoteFilesDescription: "Ketika pengaturan ini dinonaktifkan, berkas dari instansi luar akan dimuat langsung. Menonaktifkan ini akan mengurangi penggunaan penyimpanan peladen, namun dapat menyebabkan peningkatan lalu lintas bandwidth, karena keluku tidak dihasilkan."
|
||||
flagAsBot: "Atur akun ini sebagai Bot"
|
||||
flagAsBotDescription: "Jika akun ini dikendalikan oleh program, tetapkanlah opsi ini. Jika diaktifkan, ini akan berfungsi sebagai tanda bagi pengembang lain untuk mencegah interaksi berantai dengan bot lain dan menyesuaikan sistem internal Misskey untuk memperlakukan akun ini sebagai bot."
|
||||
flagAsCat: "Atur akun ini sebagai kucing"
|
||||
flagAsCatDescription: "Nyalakan tanda ini untuk menandai akun ini sebagai kucing."
|
||||
flagShowTimelineReplies: "Tampilkan balasan di linimasa"
|
||||
flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari note pengguna lain di linimasa apabila dinyalakan."
|
||||
flagShowTimelineReplies: "Tampilkan balasan di lini masa"
|
||||
flagShowTimelineRepliesDescription: "Menampilkan balasan pengguna dari catatan pengguna lain di lini masa apabila dinyalakan."
|
||||
autoAcceptFollowed: "Setujui otomatis permintaan mengikuti dari pengguna yang kamu ikuti"
|
||||
addAccount: "Tambahkan akun"
|
||||
reloadAccountsList: "Muat ulang daftar akun"
|
||||
loginFailed: "Gagal untuk masuk"
|
||||
showOnRemote: "Lihat profil asli"
|
||||
general: "Umum"
|
||||
|
@ -166,7 +171,7 @@ searchWith: "Cari: {q}"
|
|||
youHaveNoLists: "Kamu tidak memiliki daftar apapun"
|
||||
followConfirm: "Apakah kamu yakin ingin mengikuti {name}?"
|
||||
proxyAccount: "Akun proksi"
|
||||
proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna luar ke dalam daftar, aktivitas dari pengguna luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya."
|
||||
proxyAccountDescription: "Akun proksi merupakan sebuah akun yang bertindak sebagai pengikut instansi luar untuk pengguna dalam kondisi tertentu. Sebagai contoh, ketika pengguna menambahkan seorang pengguna instansi luar ke dalam daftar, aktivitas dari pengguna instansi luar tidak akan disampaikan ke instansi apabila tidak ada pengguna lokal yang mengikuti pengguna tersebut, dengan begitu akun proksilah yang akan mengikutinya."
|
||||
host: "Host"
|
||||
selectUser: "Pilih pengguna"
|
||||
recipient: "Penerima"
|
||||
|
@ -198,7 +203,7 @@ clearQueue: "Bersihkan antrian"
|
|||
clearQueueConfirmTitle: "Apakah kamu yakin ingin membersihkan antrian?"
|
||||
clearQueueConfirmText: "Seluruh sisa catatan yang tidak tersampaikan di dalam antrian tidak akan difederasi. Biasanya operasi ini TIDAK dibutuhkan."
|
||||
clearCachedFiles: "Hapus tembolok"
|
||||
clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas remote?"
|
||||
clearCachedFilesConfirm: "Apakah kamu yakin ingin menghapus seluruh tembolok berkas instansi luar?"
|
||||
blockedInstances: "Instansi terblokir"
|
||||
blockedInstancesDescription: "Daftar nama host dari instansi yang diperlukan untuk diblokir. Instansi yang didaftarkan tidak akan dapat berkomunikasi dengan instansi ini."
|
||||
muteAndBlock: "Bisukan / Blokir"
|
||||
|
@ -223,9 +228,9 @@ all: "Semua"
|
|||
subscribing: "Berlangganan"
|
||||
publishing: "Sedang menyiarkan langsung"
|
||||
notResponding: "Tidak ada respon"
|
||||
instanceFollowing: "Mengikuti instance"
|
||||
instanceFollowers: "Pengikut instance"
|
||||
instanceUsers: "Pengguna pada instance ini"
|
||||
instanceFollowing: "Mengikuti instansi"
|
||||
instanceFollowers: "Pengikut instansi"
|
||||
instanceUsers: "Pengguna pada instansi ini"
|
||||
changePassword: "Ubah kata sandi"
|
||||
security: "Keamanan"
|
||||
retypedNotMatch: "Input tidak sama"
|
||||
|
@ -233,11 +238,11 @@ currentPassword: "Kata sandi saat ini"
|
|||
newPassword: "Kata sandi baru"
|
||||
newPasswordRetype: "Ulangi kata sandi baru"
|
||||
attachFile: "Lampirkan berkas"
|
||||
more: "Lagi !"
|
||||
more: "Lainnya"
|
||||
featured: "Sorotan"
|
||||
usernameOrUserId: "Nama pengguna atau User ID"
|
||||
noSuchUser: "Pengguna tidak ditemukan"
|
||||
lookup: "Mencari"
|
||||
lookup: "Cari"
|
||||
announcements: "Pengumuman"
|
||||
imageUrl: "URL Gambar"
|
||||
remove: "Hapus"
|
||||
|
@ -268,7 +273,7 @@ basicNotesBeforeCreateAccount: "Catatan penting"
|
|||
termsOfService: "Syarat dan ketentuan"
|
||||
start: "Mulai"
|
||||
home: "Beranda"
|
||||
remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari instansi luar."
|
||||
remoteUserCaution: "Informasi ini mungkin tidak mutakhir, karena pengguna ini berasal dari peladen instansi luar."
|
||||
activity: "Aktivitas"
|
||||
images: "Gambar"
|
||||
image: "Gambar"
|
||||
|
@ -308,19 +313,18 @@ copyUrl: "Salin tautan"
|
|||
rename: "Ubah nama"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "Konten sensitif"
|
||||
whenServerDisconnected: "Ketika kehilangan koneksi dengan peladen"
|
||||
disconnectedFromServer: "Terputus koneksi dari peladen"
|
||||
reload: "Muat ulang"
|
||||
doNothing: "Abaikan"
|
||||
reloadConfirm: "Apakah kamu ingin memuat ulang linimasa?"
|
||||
reloadConfirm: "Apakah kamu ingin memuat ulang lini masa?"
|
||||
watch: "Tonton"
|
||||
unwatch: "Batal tonton"
|
||||
accept: "Terima"
|
||||
reject: "Tolak"
|
||||
normal: "Normal"
|
||||
instanceName: "Nama instance"
|
||||
instanceDescription: "Tentang instance"
|
||||
instanceName: "Nama instansi"
|
||||
instanceDescription: "Tentang instansi"
|
||||
maintainerName: "Pengelola"
|
||||
maintainerEmail: "Surel pengelola"
|
||||
tosUrl: "URL Syarat dan Ketentuan"
|
||||
|
@ -334,9 +338,9 @@ pages: "Halaman"
|
|||
integration: "Integrasi"
|
||||
connectService: "Sambungkan"
|
||||
disconnectService: "Putuskan"
|
||||
enableLocalTimeline: "Nyalakan linimasa lokal"
|
||||
enableGlobalTimeline: "Nyalakan linimasa global"
|
||||
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua linimasa meskipun linimasa tersebut tidak diaktifkan."
|
||||
enableLocalTimeline: "Nyalakan lini masa lokal"
|
||||
enableGlobalTimeline: "Nyalakan lini masa global"
|
||||
disablingTimelinesInfo: "Admin dan Moderator akan selalu memiliki akses ke semua lini masa meskipun lini masa tersebut tidak diaktifkan."
|
||||
registration: "Pendaftaran"
|
||||
enableRegistration: "Nyalakan pendaftaran pengguna baru"
|
||||
invite: "Undang"
|
||||
|
@ -379,7 +383,7 @@ enableServiceworker: "Aktifkan ServiceWorker"
|
|||
antennaUsersDescription: "Tuliskan satu nama pengguna per baris"
|
||||
caseSensitive: "Peka huruf besar dan huruf kecil"
|
||||
withReplies: "Termasuk balasan"
|
||||
connectedTo: "Akun yang mengikuti telah terhubung"
|
||||
connectedTo: "Akun berikut terhubung"
|
||||
notesAndReplies: "Catatan dan balasan"
|
||||
withFiles: "Media"
|
||||
silence: "Bungkam"
|
||||
|
@ -398,6 +402,7 @@ about: "Informasi"
|
|||
aboutMisskey: "Tentang Misskey"
|
||||
administrator: "Admin"
|
||||
token: "Token"
|
||||
2fa: "Autentikasi 2-faktor"
|
||||
totp: "Aplikasi autentikator"
|
||||
totpDescription: "Gunakan aplikasi autentikator untuk mendapatkan kata sandi sekali pakai"
|
||||
moderator: "Moderator"
|
||||
|
@ -409,6 +414,7 @@ lastUsed: "Terakhir digunakan"
|
|||
lastUsedAt: "Penggunaan terakhir: {t}"
|
||||
unregister: "Batalkan pendaftaran"
|
||||
passwordLessLogin: "Setel login tanpa kata sandi"
|
||||
passwordLessLoginDescription: "Bolehkan masuk tanpa kata sandi dengan menggunakan hanya security-key atau passkey"
|
||||
resetPassword: "Atur ulang kata sandi"
|
||||
newPasswordIs: "Kata sandi baru adalah \"{password}\""
|
||||
reduceUiAnimation: "Kurangi animasi antarmuka"
|
||||
|
@ -417,7 +423,7 @@ notFound: "Tidak dapat ditemukan"
|
|||
notFoundDescription: "Tidak ada halaman sesuai dengan URL yang ditentukan."
|
||||
uploadFolder: "Lokasi unggah folder bawaan"
|
||||
cacheClear: "Bersihkan tembolok"
|
||||
markAsReadAllNotifications: "Tandai semua pemberitahuan telah dibaca"
|
||||
markAsReadAllNotifications: "Tandai semua notifikasi telah dibaca"
|
||||
markAsReadAllUnreadNotes: "Tandai semua catatan telah dibaca"
|
||||
markAsReadAllTalkMessages: "Tandai semua pesan telah dibaca"
|
||||
help: "Bantuan"
|
||||
|
@ -460,6 +466,7 @@ aboutX: "Tentang {x}"
|
|||
emojiStyle: "Gaya emoji"
|
||||
native: "Native"
|
||||
disableDrawer: "Jangan gunakan menu bergaya laci"
|
||||
showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
|
||||
noHistory: "Tidak ada riwayat"
|
||||
signinHistory: "Riwayat masuk"
|
||||
enableAdvancedMfm: "Nyalakan MFM tingkat lanjut"
|
||||
|
@ -472,6 +479,8 @@ createAccount: "Buat akun"
|
|||
existingAccount: "Akun yang ada"
|
||||
regenerate: "Buat ulang"
|
||||
fontSize: "Ukuran huruf"
|
||||
mediaListWithOneImageAppearance: "Tinggi daftar media dengan satu gambar saja"
|
||||
limitTo: "Batasi pada {x}"
|
||||
noFollowRequests: "Kamu tidak memiliki permintaan mengikuti yang menunggu"
|
||||
openImageInNewTab: "Buka gambar di tab baru"
|
||||
dashboard: "Dasbor"
|
||||
|
@ -487,7 +496,7 @@ promotion: "Promosi"
|
|||
promote: "Promosikan"
|
||||
numberOfDays: "Jumlah hari"
|
||||
hideThisNote: "Sembunyikan catatan ini"
|
||||
showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di linimasa"
|
||||
showFeaturedNotesInTimeline: "Tampilkan catatan yang diunggulkan di lini masa"
|
||||
objectStorage: "Object Storage"
|
||||
useObjectStorage: "Gunakan object storage"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
|
@ -505,9 +514,11 @@ objectStorageUseSSLDesc: "Matikan ini jika kamu tidak akan menggunakan HTTPS unt
|
|||
objectStorageUseProxy: "Hubungkan melalui Proxy"
|
||||
objectStorageUseProxyDesc: "Matikan ini jika kamu tidak akan menggunakan Proxy untuk koneksi ObjectStorage"
|
||||
objectStorageSetPublicRead: "Setel \"public-read\" disaat mengunggah"
|
||||
s3ForcePathStyleDesc: "Jika s3ForcePathStyle dinyalakan, nama bucket harus dimasukkan dalam path URL dan bukan URL nama host tersebut. Kamu perlu menyalakan pengaturan ini jika menggunakan layanan seperti instansi Minio yang self-hosted."
|
||||
serverLogs: "Log Peladen"
|
||||
deleteAll: "Hapus semua"
|
||||
showFixedPostForm: "Tampilkan form posting di atas linimasa."
|
||||
showFixedPostForm: "Tampilkan form posting di atas lini masa."
|
||||
showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)"
|
||||
newNoteRecived: "Kamu mendapat catatan baru"
|
||||
sounds: "Bunyi"
|
||||
sound: "Bunyi"
|
||||
|
@ -536,23 +547,28 @@ scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScr
|
|||
output: "Keluaran"
|
||||
script: "Script"
|
||||
disablePagesScript: "Nonaktifkan script pada halaman"
|
||||
updateRemoteUser: "Perbaharui informasi pengguna luar"
|
||||
updateRemoteUser: "Perbaharui informasi pengguna instansi luar"
|
||||
deleteAllFiles: "Hapus semua berkas"
|
||||
deleteAllFilesConfirm: "Apakah kamu yakin ingin menghapus semua berkas?"
|
||||
removeAllFollowing: "Tahan semua mengikuti"
|
||||
removeAllFollowing: "Batalkan mengikuti semua pengguna"
|
||||
removeAllFollowingDescription: "Batal mengikuti semua akun dari {host}. Mohon jalankan ini ketika instansi sudah tidak ada lagi."
|
||||
userSuspended: "Pengguna ini telah dibekukan."
|
||||
userSilenced: "Pengguna ini telah dibungkam."
|
||||
yourAccountSuspendedTitle: "Akun ini dibekukan"
|
||||
yourAccountSuspendedDescription: "Akun ini dibekukan karena melanggar ketentuan penggunaan layanan peladen atau semacamnya. Hubungi admin apabila ingin tahu alasan lebih lanjut. Mohon untuk tidak membuat akun baru."
|
||||
tokenRevoked: "Token tidak valid"
|
||||
tokenRevokedDescription: "Token ini telah kedaluwarsa. Mohon masuk lagi."
|
||||
accountDeleted: "Akun telah dihapus"
|
||||
accountDeletedDescription: "Akun ini telah dihapus."
|
||||
menu: "Menu"
|
||||
divider: "Pembagi"
|
||||
addItem: "Tambahkan item"
|
||||
rearrange: "Tata ulang"
|
||||
relays: "Relay"
|
||||
addRelay: "Tambahkan relay"
|
||||
inboxUrl: "URL Kotak masuk"
|
||||
addedRelays: "Relay yang ditambahkan"
|
||||
serviceworkerInfo: "Harus diaktifkan untuk pemberitahuan push."
|
||||
serviceworkerInfo: "Harus diaktifkan untuk notifikasi dorong."
|
||||
deletedNote: "Catatan yang dihapus"
|
||||
invisibleNote: "Catatan yang disembunyikan"
|
||||
enableInfiniteScroll: "Aktifkan gulir tak terbatas"
|
||||
|
@ -580,13 +596,13 @@ height: "Tinggi"
|
|||
large: "Besar"
|
||||
medium: "Sedang"
|
||||
small: "Kecil"
|
||||
generateAccessToken: "Buat access token"
|
||||
generateAccessToken: "Buat token akses"
|
||||
permission: "Izin"
|
||||
enableAll: "Aktifkan semua"
|
||||
disableAll: "Nonaktifkan semua"
|
||||
tokenRequested: "Berikan ijin akses ke akun"
|
||||
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
|
||||
notificationType: "Jenis pemberitahuan"
|
||||
notificationType: "Jenis notifikasi"
|
||||
edit: "Sunting"
|
||||
emailServer: "Peladen surel"
|
||||
enableEmail: "Nyalakan distribusi surel"
|
||||
|
@ -617,10 +633,10 @@ delayed: "Terlambat"
|
|||
database: "Basis data"
|
||||
channel: "Kanal"
|
||||
create: "Buat"
|
||||
notificationSetting: "Pengaturan Pemberitahuan"
|
||||
notificationSettingDesc: "Pilih tipe pemberitahuan untuk ditampilkan"
|
||||
notificationSetting: "Pengaturan Notifikasi"
|
||||
notificationSettingDesc: "Pilih tipe notifikasi untuk ditampilkan"
|
||||
useGlobalSetting: "Gunakan setelan global"
|
||||
useGlobalSettingDesc: "Jika dinyalakan, setelan pemberitahuan akun kamu akan digunakan. Jika dimatikan, konfigurasi secara individu dapat dibuat."
|
||||
useGlobalSettingDesc: "Jika dinyalakan, setelan notifikasi akun kamu akan digunakan. Jika dimatikan, pengaturan secara individu dapat dibuat."
|
||||
other: "Lainnya"
|
||||
regenerateLoginToken: "Perbarui token login"
|
||||
regenerateLoginTokenDescription: "Perbarui token yang digunakan secara internal saat login. Normalnya aksi ini tidak diperlukan. Jika diperbarui, semua perangkat akan dilogout."
|
||||
|
@ -657,8 +673,8 @@ createNewClip: "Buat klip baru"
|
|||
unclip: "Batalkan klip"
|
||||
confirmToUnclipAlreadyClippedNote: "Catatan ini sudah disertakan di klip \"{name}\". Yakin ingin membatalkan catatan dari klip ini?"
|
||||
public: "Publik"
|
||||
i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu dapat ikut membantu di {link}."
|
||||
manageAccessTokens: "Kelola access token"
|
||||
i18nInfo: "Misskey diterjemahkan ke dalam banyak bahasa oleh sukarelawan. Kamu juga dapat ikut membantu menerjemahkannya di {link}."
|
||||
manageAccessTokens: "Kelola token akses"
|
||||
accountInfo: "Informasi akun"
|
||||
notesCount: "Jumlah catatan"
|
||||
repliesCount: "Jumlah balasan terkirim"
|
||||
|
@ -675,7 +691,7 @@ yes: "Iya"
|
|||
no: "Tidak"
|
||||
driveFilesCount: "Jumlah berkas drive"
|
||||
driveUsage: "Penggunaan ruang penyimpanan drive"
|
||||
noCrawle: "Tolak pengindeksan crawler"
|
||||
noCrawle: "Tolak pengindeksan perayap web"
|
||||
noCrawleDescription: "Meminta mesin pencari untuk tidak mengindeks halaman profil kamu, catatan, Halaman, dll."
|
||||
lockedAccountInfo: "Kecuali kamu menyetel visibilitas catatan milikmu ke \"Hanya pengikut\", catatan milikmu akan dapat dilihat oleh siapa saja, bahkan jika kamu memerlukan pengikut untuk disetujui secara manual."
|
||||
alwaysMarkSensitive: "Tandai media dalam catatan sebagai media sensitif"
|
||||
|
@ -691,10 +707,12 @@ contact: "Kontak"
|
|||
useSystemFont: "Gunakan font bawaan sistem operasi"
|
||||
clips: "Klip"
|
||||
experimentalFeatures: "Fitur eksperimental"
|
||||
experimental: "Eksperimental"
|
||||
thisIsExperimentalFeature: "Fitur ini eksperimental. Fungsionalitas dari fitur ini dapat berubah sewaktu-waktu dan mungkin tidak bekerja sesuai semestinya."
|
||||
developer: "Pengembang"
|
||||
makeExplorable: "Buat akun tampil di \"Jelajahi\""
|
||||
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di bagian \"Jelajahi:"
|
||||
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada linimasa"
|
||||
makeExplorableDescription: "Jika kamu mematikan ini, akun kamu tidak akan muncul di menu \"Jelajahi\""
|
||||
showGapBetweenNotesInTimeline: "Tampilkan jarak diantara catatan pada lini masa"
|
||||
duplicate: "Duplikat"
|
||||
left: "Kiri"
|
||||
center: "Tengah"
|
||||
|
@ -733,14 +751,14 @@ capacity: "Kapasitas"
|
|||
inUse: "Digunakan"
|
||||
editCode: "Sunting kode"
|
||||
apply: "Terapkan"
|
||||
receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini"
|
||||
emailNotification: "Pemberitahuan surel"
|
||||
receiveAnnouncementFromInstance: "Terima pengumuman dari instansi ini"
|
||||
emailNotification: "Notifikasi surel"
|
||||
publish: "Terbitkan"
|
||||
inChannelSearch: "Cari di kanal"
|
||||
useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan"
|
||||
typingUsers: "{users} sedang mengetik..."
|
||||
jumpToSpecifiedDate: "Loncat ke tanggal spesifik"
|
||||
showingPastTimeline: "Sedang menampilkan linimasa lama"
|
||||
showingPastTimeline: "Sedang menampilkan lini masa lama"
|
||||
clear: "Bersihkan"
|
||||
markAllAsRead: "Tandai semua telah dibaca"
|
||||
goBack: "Kembali"
|
||||
|
@ -755,7 +773,7 @@ userInfo: "Informasi pengguna"
|
|||
unknown: "Tidak diketahui"
|
||||
onlineStatus: "Status daring"
|
||||
hideOnlineStatus: "Sembunyikan status daring"
|
||||
hideOnlineStatusDescription: "Menyembunyikan status daring kamu umengurangi kenyamanan untuk beberapa fungsi seperti contohnya pencarian."
|
||||
hideOnlineStatusDescription: "Menyembunyikan status daring kamu akan mengurangi kenyamanan untuk beberapa fungsi, seperti contohnya pencarian."
|
||||
online: "Daring"
|
||||
active: "Aktif"
|
||||
offline: "Luring"
|
||||
|
@ -775,12 +793,14 @@ noMaintainerInformationWarning: "Informasi pengelola belum disetel."
|
|||
noBotProtectionWarning: "Proteksi bot belum disetel."
|
||||
configure: "Setel"
|
||||
postToGallery: "Posting ke galeri"
|
||||
postToHashtag: "Catat ke tagar ini"
|
||||
gallery: "Galeri"
|
||||
recentPosts: "Postingan terbaru"
|
||||
popularPosts: "Postingan populer"
|
||||
shareWithNote: "Bagikan dengan catatan"
|
||||
ads: "Iklan"
|
||||
expiration: "Batas akhir"
|
||||
startingperiod: "Mulai"
|
||||
memo: "Memo"
|
||||
priority: "Prioritas"
|
||||
high: "Tinggi"
|
||||
|
@ -807,14 +827,18 @@ translatedFrom: "Terjemahkan dari {x}"
|
|||
accountDeletionInProgress: "Penghapusan akun sedang dalam proses"
|
||||
usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada peladen ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya."
|
||||
aiChanMode: "Mode Ai"
|
||||
keepCw: "Biarkan Peringatan Konten"
|
||||
devMode: "Mode pengembang"
|
||||
keepCw: "Biarkan peringatan konten"
|
||||
pubSub: "Akun Pub/Sub"
|
||||
lastCommunication: "Komunikasi terakhir"
|
||||
resolved: "Selesai"
|
||||
unresolved: "Belum selesai"
|
||||
breakFollow: "Batalkan mengikuti"
|
||||
breakFollow: "Hapus pengikut"
|
||||
breakFollowConfirm: "Yakin untuk menghapus pengikut ini?"
|
||||
itsOn: "Aktif"
|
||||
itsOff: "Nonaktif"
|
||||
on: "Nyala"
|
||||
off: "Mati"
|
||||
emailRequiredForSignup: "Membutuhkan alamat surel untuk mendaftar"
|
||||
unread: "Belum dibaca"
|
||||
filter: "Saring"
|
||||
|
@ -891,25 +915,26 @@ slow: "Lambat"
|
|||
fast: "Cepat"
|
||||
sensitiveMediaDetection: "Deteksi media NSFW"
|
||||
localOnly: "Hanya lokal"
|
||||
remoteOnly: "Hanya remot"
|
||||
remoteOnly: "Hanya luar instansi"
|
||||
failedToUpload: "Gagal mengunggah"
|
||||
cannotUploadBecauseInappropriate: "Berkas ini tidak dapat diunggah karena sebagian dari berkas terdeteksi berpotensi NSFW."
|
||||
cannotUploadBecauseNoFreeSpace: "Gagal mengunggah karena kekurangan kapasitas Drive."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "Berkas ini tidak dapat diunggah karena melebihi batas ukuran berkas."
|
||||
beta: "Beta"
|
||||
enableAutoSensitive: "Penandaan NSFW otomatis"
|
||||
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Machine Learning jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
|
||||
enableAutoSensitiveDescription: "Mendeteksi otomatis dan menandai media NSFW menggunakan Pembelajaran Mesin jika memungkinkan. Meskipun opsi ini dimatikan, ada kemungkinan dinyalakan secara menyeluruh pada instansi peladen."
|
||||
activeEmailValidationDescription: "Membolehkan validasi alamat surel ketat dengan mengecek apakah alamat surel tersebut temporer dan bisa berkomunikasi dengan surel tersebut. Ketidak tidak dicentang, hanya format surel yang divalidasi."
|
||||
navbar: "Bilah navigasi"
|
||||
shuffle: "Acak"
|
||||
account: "Akun"
|
||||
move: "Pindah"
|
||||
pushNotification: "Pemberitahuan push"
|
||||
subscribePushNotification: "Nyalakan pemberitahuan push"
|
||||
unsubscribePushNotification: "Matikan pemberitahuan push"
|
||||
pushNotificationAlreadySubscribed: "Pemberitahuan push telah dinyalakan"
|
||||
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung pemberitahuan push"
|
||||
sendPushNotificationReadMessage: "Hapus pemberitahuan push ketika pemberitahuan relevan atau pesan telah dibaca"
|
||||
sendPushNotificationReadMessageCaption: "Pemberitahuan berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
|
||||
pushNotification: "Notifikasi dorong"
|
||||
subscribePushNotification: "Nyalakan notifikasi dorong"
|
||||
unsubscribePushNotification: "Matikan notifikasi dorong"
|
||||
pushNotificationAlreadySubscribed: "Notifikasi dorong telah dinyalakan"
|
||||
pushNotificationNotSupported: "Browser atau instansi kamu tidak mendukung notifikasi dorong"
|
||||
sendPushNotificationReadMessage: "Hapus notifikasi dorong ketika notifikasi relevan atau pesan telah dibaca"
|
||||
sendPushNotificationReadMessageCaption: "Notifikasi berisi teks「{emptyPushNotificationMessage}」akan ditampilkan dalam waktu pendek. Ini mungkin dapat menambah pemakaian baterai pada perangkat kamu."
|
||||
windowMaximize: "Maksimalkan"
|
||||
windowMinimize: "Minimalkan"
|
||||
windowRestore: "Kembalikan"
|
||||
|
@ -928,6 +953,7 @@ didYouLikeMisskey: "Apakah kamu mulai menyukai Misskey?"
|
|||
pleaseDonate: "{host} menggunakan perangkat lunak bebas yaitu Misskey. Kami sangat mengapresiasi sekali donasi dari kamu agar pengembangan Misskey tetap dapat berlanjut!"
|
||||
roles: "Peran"
|
||||
role: "Peran"
|
||||
noRole: "Peran tidak temukan"
|
||||
normalUser: "Pengguna umum"
|
||||
undefined: "Tak terdefinisi"
|
||||
assign: "Tetapkan\n"
|
||||
|
@ -937,28 +963,141 @@ manageCustomEmojis: "Kelola Emoji Kustom"
|
|||
youCannotCreateAnymore: "Kamu melewati batas pembuatan."
|
||||
cannotPerformTemporary: "Sementara Tidak Tersedia"
|
||||
cannotPerformTemporaryDescription: "Aksi ini tidak dapat dilakukan sementara karena melewati batas eksekusi. Mohon tunggu sejenak dan coba lagi."
|
||||
invalidParamError: "Parameter tidak valid"
|
||||
invalidParamErrorDescription: "Parameter permintaan tidak valid. Hal ini biasanya disebabkan oleh bug, namun juga dapat terjadi karena input melebihi batas ukuran atau semacamnya."
|
||||
permissionDeniedError: "Operasi ditolak"
|
||||
permissionDeniedErrorDescription: "Akun ini tidak memiliki izin untuk melakukan aksi ini."
|
||||
preset: "Prasetel"
|
||||
selectFromPresets: "Pilih dari prasetel"
|
||||
achievements: "Pencapaian"
|
||||
gotInvalidResponseError: "Respon peladen tidak valid"
|
||||
gotInvalidResponseErrorDescription: "Peladen tidak dapat dijangkau atau sedang dalam perawatan. Mohon coba lagi nanti."
|
||||
thisPostMayBeAnnoying: "Catatan ini mungkin dapat mengganggu orang lain."
|
||||
thisPostMayBeAnnoyingHome: "Catat ke linimasa beranda"
|
||||
thisPostMayBeAnnoyingHome: "Catat ke lini masa beranda"
|
||||
thisPostMayBeAnnoyingCancel: "Batalkan"
|
||||
thisPostMayBeAnnoyingIgnore: "Tetap catat"
|
||||
collapseRenotes: "Tutup renote yang sudah kamu lihat"
|
||||
internalServerError: "Kesalahan internal peladen"
|
||||
internalServerErrorDescription: "Peladen sedang mengalami galat tak terduga"
|
||||
copyErrorInfo: "Salin detil galat"
|
||||
joinThisServer: "Gabung server ini"
|
||||
exploreOtherServers: "Cari server lain"
|
||||
joinThisServer: "Gabung peladen ini"
|
||||
exploreOtherServers: "Cari peladen lain"
|
||||
letsLookAtTimeline: "LIhat timeline"
|
||||
disableFederationConfirm: "Matikan federasi?"
|
||||
disableFederationConfirmWarn: "Mematikan federasi tidak membuat kiriman menjadi privat. Umumnya, mematikan federasi tidak diperlukan."
|
||||
disableFederationOk: "Matikan federasi"
|
||||
invitationRequiredToRegister: "Instansi ini dalam mode undangan-saja. Kamu harus memasukkan kode undangan yang valid untuk mendaftar."
|
||||
emailNotSupported: "Instansi ini tidak mendukung mengirim surel"
|
||||
postToTheChannel: "Catat ke kanal"
|
||||
cannotBeChangedLater: "Hal ini nantinya tidak dapat diubah lagi."
|
||||
reactionAcceptance: "Penerimaan reaksi"
|
||||
likeOnly: "Hanya suka"
|
||||
likeOnlyForRemote: "Semua (Hanya suka dari instansi luar)"
|
||||
nonSensitiveOnly: "Hanya non-sensitif"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Hanya non-sensitif (Hanya suka dari instansi luar)"
|
||||
rolesAssignedToMe: "Peran yang ditugaskan ke saya"
|
||||
resetPasswordConfirm: "Yakin untuk mereset kata sandimu?"
|
||||
sensitiveWords: "Kata sensitif"
|
||||
sensitiveWordsDescription: "Visibilitas dari semua catatan mengandung kata yang telah diatur akan dijadikan \"Beranda\" secara otomatis. Kamu dapat mendaftarkan kata tersebut lebih dari satu dengan menuliskannya di baris baru."
|
||||
sensitiveWordsDescription2: "Menggunakan spasi akan membuat ekspresi AND dan kata kunci disekitarnya dengan garis miring akan mengubahnya menjadi ekspresi reguler."
|
||||
notesSearchNotAvailable: "Pencarian catatan tidak tersedia."
|
||||
license: "Lisensi"
|
||||
unfavoriteConfirm: "Yakin ingin menghapusnya dari favorit?"
|
||||
myClips: "Klip saya"
|
||||
drivecleaner: "Pembersih Drive"
|
||||
retryAllQueuesNow: "Coba jalankan lagi semua antrian"
|
||||
retryAllQueuesConfirmTitle: "Yakin ingin mencoba lagi semuanya?"
|
||||
retryAllQueuesConfirmText: "Hal ini akan meningkatkan beban sementara ke peladen."
|
||||
enableChartsForRemoteUser: "Buat bagan data pengguna instansi luar"
|
||||
enableChartsForFederatedInstances: "Buat bagan data peladen instansi luar"
|
||||
showClipButtonInNoteFooter: "Tambahkan \"Klip\" ke menu aksi catatan"
|
||||
largeNoteReactions: "Besarkan reaksi yang ditampilkan"
|
||||
noteIdOrUrl: "ID catatan atau URL"
|
||||
video: "Video"
|
||||
videos: "Video"
|
||||
dataSaver: "Penghemat data"
|
||||
accountMigration: "Pemindahan akun"
|
||||
accountMoved: "Pengguna ini telah berpindah ke akun baru:"
|
||||
accountMovedShort: "Akun ini telah dipindahkan."
|
||||
operationForbidden: "Operasi dilarang"
|
||||
forceShowAds: "Selalu tampilkan iklan"
|
||||
addMemo: "Tambahkan memo"
|
||||
editMemo: "Sunting memo"
|
||||
reactionsList: "Reaksi"
|
||||
renotesList: "Renote"
|
||||
notificationDisplay: "Notifikasi"
|
||||
leftTop: "Kiri atas"
|
||||
rightTop: "Kanan atas"
|
||||
leftBottom: "Kiri bawah"
|
||||
rightBottom: "Kanan bawah"
|
||||
stackAxis: "Arah tumpukan"
|
||||
vertical: "Vertikal"
|
||||
horizontal: "Horisontal"
|
||||
position: "Posisi"
|
||||
serverRules: "Aturan peladen"
|
||||
pleaseConfirmBelowBeforeSignup: "Mohon konfirmasi di bawah ini sebelum mendaftar."
|
||||
pleaseAgreeAllToContinue: "Kamu harus menyetujui semua kolom di atas untuk melanjutkan."
|
||||
continue: "Lanjutkan"
|
||||
preservedUsernames: "Nama pengguna tercadangkan"
|
||||
preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah dengan baris baru. Nama pengguna berikut akan tidak dapat dipakai pada pembuatan akun normal, namun dapat digunakan oleh admin untuk membuat akun baru. Akun yang sudah ada dengan menggunakan nama pengguna ini tidak akan terpengaruh."
|
||||
createNoteFromTheFile: "Buat catatan dari berkas ini"
|
||||
archive: "Arsipkan"
|
||||
channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
|
||||
channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
|
||||
thisChannelArchived: "Kanal ini telah diarsipkan."
|
||||
displayOfNote: "Tampilan catatan"
|
||||
initialAccountSetting: "Atur profil"
|
||||
youFollowing: "Mengikuti"
|
||||
preventAiLearning: "Tolak penggunaan Pembelajaran Mesin (AI Generatif)"
|
||||
preventAiLearningDescription: "Minta perayap web untuk tidak menggunakan materi teks atau gambar yang telah diposting ke dalam set data Pembelajaran Mesin (Prediktif / Generatif). Hal ini dicapai dengan menambahkan flag HTML-Response \"noai\" ke masing-masing konten. Pencegahan penuh mungkin tidak dapat dicapai dengan flag ini, karena juga dapat diabaikan begitu saja."
|
||||
options: "Opsi peran"
|
||||
specifyUser: "Pengguna spesifik"
|
||||
failedToPreviewUrl: "Tidak dapat dipratinjau"
|
||||
update: "Perbarui"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "Peran yang dapat menggunakan emoji ini sebagai reaksi"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Jika peran tidak ditentukan, semua pengguna dapat menggunakan emoji ini sebagai reaksi."
|
||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Peran ini harus publik."
|
||||
cancelReactionConfirm: "Yakin untuk menghapus reaksimu?"
|
||||
changeReactionConfirm: "Yakin untuk mengganti reaksimu?"
|
||||
later: "Nanti saja"
|
||||
goToMisskey: "Ke Misskey"
|
||||
additionalEmojiDictionary: "Kamus emoji tambahan"
|
||||
installed: "Terpasang"
|
||||
branding: "Merek"
|
||||
enableServerMachineStats: "Tampilkan informasi mesin peladen menjadi publik"
|
||||
enableIdenticonGeneration: "Nyalakan pembuatan Identicon per pengguna"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Akun kamu telah sukses dibuat!"
|
||||
letsStartAccountSetup: "Untuk pemula, ayo atur profilmu dulu."
|
||||
letsFillYourProfile: "Pertama, ayo atur profilmu dulu."
|
||||
profileSetting: "Pengaturan profil"
|
||||
privacySetting: "Pengaturan privasi"
|
||||
theseSettingsCanEditLater: "Kamu selalu bisa mengganti pengaturan ini lain kali."
|
||||
youCanEditMoreSettingsInSettingsPageLater: "Ada banyak pengaturan yang dapat kamu atur dari halaman \"Pengaturan\". Pastikan untuk mengunjungi halaman tersebut nanti."
|
||||
followUsers: "Coba ikuti beberapa pengguna yang menarik bagimu untuk membangun lini masa akunmu."
|
||||
pushNotificationDescription: "Menyalakan notifikasi dorong akan membuatmu menerima notifikasi dari {name} secara langsung ke perangkatmu."
|
||||
initialAccountSettingCompleted: "Pengaturan profil selesai!"
|
||||
haveFun: "Selamat menikmati, {name}!"
|
||||
ifYouNeedLearnMore: "Kalau kamu ingin mempelajari lebih lanjut bagaimana cara menggunakan {name} (Misskey), silahkan kunjungi {link}."
|
||||
skipAreYouSure: "Yakin melewati atur profil?"
|
||||
laterAreYouSure: "Yakin banget untuk atur profil nanti?"
|
||||
_serverRules:
|
||||
description: "Daftar peraturan akan ditampilkan sebelum pendaftaran. Mengatur ringkasan dari Syarat dan Ketentuan sangat direkomendasikan."
|
||||
_accountMigration:
|
||||
moveFrom: "Pindahkan akun lain ke akun ini"
|
||||
moveFromSub: "Buat alias ke akun lain"
|
||||
moveFromLabel: "Akun asli #{n}"
|
||||
moveFromDescription: "Kamu harus membuat alias untuk akun asal kamu berpindah ke akun ini\nMasukkan alias akun asal kamu berpindah ke dalam format berikut: @namapengguna@nama.server.com\nUntuk menghapus alias, kosongkan kolom ini (tidak direkomendasikan)."
|
||||
moveTo: "Pindahkan akun ini ke akun lain"
|
||||
moveToLabel: "Akun tujuan pindah:"
|
||||
moveCannotBeUndone: "Pemindahan akun tidak dapat diurungkan."
|
||||
moveAccountDescription: "Hal ini akan memindahkan akun kamu ke akun lain.\n ・Pengikut dari akun ini akan secara otomatis dipindahkan ke akun baru\n ・Akun ini akan berhenti mengikuti dari semua pengguna yang sedang kamu ikuti\n ・Kamu akan tidak dapat membuat catatan baru dan lain-lain pada akun ini\n\nMeskipun pemindahan pengikut dilakukan secara otomatis, kamu harus mempersiapkan beberapa langkah secara manual untuk memindahkan daftar pengguna yang sedang kamu ikuti. Untuk melakukan tersebut, lakukan ekspor daftar ikuti yang nantinya dapat kamu impor pada menu pengaturan di akun baru kamu. Prosedur yang sama juga dapat diterapkan pada daftar seperti pengguna yang kamu bisukan atau blokir.\n\n(Penjelasan ini hanya berlaku pada Misskey versi 13.12.0 dan setelahnya. Perangkat lunak ActivityPub lainnya seperti Mastodon berkemungkinan befungsi berbeda.)"
|
||||
moveAccountHowTo: "Untuk pindah, pertama buat alias untuk akun ini pada akun tujuan kamu berpindah.\nSetelah kamu membuat alias, masukkan akun tujuan kamu berpindah ke dalam format berikut:\n@namapengguna@nama.server.com"
|
||||
startMigration: "Pindahkan"
|
||||
migrationConfirm: "Yakin untuk memindahkan akun ini ke {account}? Sekali dimulai, proses ini tidak dapat dihentikan atau ditarik kembali, dan kamu tidak dapat menggunakan akun ini lagi dalam keadaan asli semula."
|
||||
movedAndCannotBeUndone: "\nAkun ini telah dipindahkan.\nPemindahan tidak dapat diurungkan."
|
||||
postMigrationNote: "24 jam setelah pemindahan akun selesai, akun ini akan berhenti mengikuti semua akun yang sedang diikuti. Angka mengikut dan pengikut akan menjadi nol. Untuk menghindari pengikut kamu tidak dapat melihat postingan hanya pengikut saja dalam postingan ini, mereka akan tetap mengikuti akun ini."
|
||||
movedTo: "Akun baru tujuan pindah:"
|
||||
_achievements:
|
||||
earnedAt: "Terbuka pada"
|
||||
_types:
|
||||
|
@ -1130,6 +1269,9 @@ _achievements:
|
|||
_client30min:
|
||||
title: "Istirahat pendek"
|
||||
description: "Habiskan waktu 30 menit di Misskey"
|
||||
_client60min:
|
||||
title: "Tidak ada \"Miss\" dalam Misskey"
|
||||
description: "Biarkan Misskey tetap terbuka setidaknya selama 60 menit"
|
||||
_noteDeletedWithin1min:
|
||||
title: "Eh, salah coy!"
|
||||
description: "Hapus catatan kurang dari semenit kamu catat"
|
||||
|
@ -1145,8 +1287,8 @@ _achievements:
|
|||
title: "Rujukan mandiri"
|
||||
description: "Kutip catatanmu sendiri"
|
||||
_htl20npm:
|
||||
title: "Linimasa mengalir"
|
||||
description: "Memiliki linimasa beranda dengan kecepatan melebihi 20 cpm (catatan per menit)"
|
||||
title: "Lini masa mengalir"
|
||||
description: "Memiliki lini masa beranda dengan kecepatan melebihi 20 cpm (catatan per menit)"
|
||||
_viewInstanceChart:
|
||||
title: "Analis"
|
||||
description: "Lihat bagan instansimu"
|
||||
|
@ -1218,6 +1360,10 @@ _role:
|
|||
iconUrl: "URL ikon"
|
||||
asBadge: "Tampilkan sebagai lencana"
|
||||
descriptionOfAsBadge: "Ikon peran ini akan ditampilkan bersebelahan dengan username pengguna yang memiliki peran ini jika dinyalakan."
|
||||
isExplorable: "Buat peran dapat terjelajahi"
|
||||
descriptionOfIsExplorable: "Lini masa peran ini dan daftar pengguna dengan peran ini akan dibuat publik apabila dinyalakan."
|
||||
displayOrder: "Urutan"
|
||||
descriptionOfDisplayOrder: "Semakin tinggi angka, semakin tinggi posisi antarmukanya."
|
||||
canEditMembersByModerator: "Perbolehkan moderator untuk menyunting daftar anggota untuk peran ini"
|
||||
descriptionOfCanEditMembersByModerator: "Ketika dinyalakan, moderator beserta administrator dapat menugaskan ataupun mencabut pengguna ke peran ini. Ketika dimatikan, hanya administrator saja yang dapat menugaskan pengguna ke peran ini."
|
||||
priority: "Prioritas"
|
||||
|
@ -1226,12 +1372,13 @@ _role:
|
|||
middle: "Sedang"
|
||||
high: "Tinggi"
|
||||
_options:
|
||||
gtlAvailable: "Dapat melihat linimasa global"
|
||||
ltlAvailable: "Dapat melihat linimasa lokal"
|
||||
gtlAvailable: "Dapat melihat lini masa global"
|
||||
ltlAvailable: "Dapat melihat lini masa lokal"
|
||||
canPublicNote: "Dapat mengirim catatan publik"
|
||||
canInvite: "Dapat membuat kode undangan instansi"
|
||||
canManageCustomEmojis: "Dapat mengelola Emoji kustom"
|
||||
driveCapacity: "Kapasitas Drive"
|
||||
alwaysMarkNsfw: "Selalu tandai berkas sebagai NSFW"
|
||||
pinMax: "Jumlah maksimal catatan yang disematkan"
|
||||
antennaMax: "Jumlah maksimum antena"
|
||||
wordMuteMax: "Jumlah maksimum karakter yang diperbolehkan dalam membisukan kata"
|
||||
|
@ -1243,6 +1390,7 @@ _role:
|
|||
rateLimitFactor: "Batas kecepatan"
|
||||
descriptionOfRateLimitFactor: "Batas kecepatan yang rendah tidak begitu membatasi, batas kecepatan tinggi lebih membatasi. "
|
||||
canHideAds: "Dapat menyembunyikan iklan"
|
||||
canSearchNotes: "Penggunaan pencarian catatan"
|
||||
_condition:
|
||||
isLocal: "Pengguna lokal"
|
||||
isRemote: "Pengguna remote"
|
||||
|
@ -1252,11 +1400,13 @@ _role:
|
|||
followersMoreThanOrEq: "Memiliki pengikut X atau lebih dari tersebut"
|
||||
followingLessThanOrEq: "Mengikuti X pengguna atau kurang dari itu"
|
||||
followingMoreThanOrEq: "Mengikuti X pengguna atau lebih dari itu"
|
||||
notesLessThanOrEq: "Jumlah postingan kurang dari sama dengan"
|
||||
notesMoreThanOrEq: "Jumlah postingan lebih dari sama dengan"
|
||||
and: "Kondisi-AND"
|
||||
or: "Kondisi-OR"
|
||||
not: "Kondisi-NOT"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Mengurangi usaha moderasi server dengan mengenali media NSFW srcara otomatis menggunakan Machine Learning. Fungsi ini akan sedikit menaikkan beban peladen."
|
||||
description: "Mengurangi usaha moderasi peladen dengan mengenali media NSFW secara otomatis menggunakan Pembelajaran Mesin. Fungsi ini akan sedikit menaikkan beban peladen."
|
||||
sensitivity: "Sensitivitas deteksi"
|
||||
sensitivityDescription: "Mengurangi sensitivitas akan mengurangi misdeteksi (false positive) sedangkan meningkatkannya akan menambah misdeteksi (false positive)."
|
||||
setSensitiveFlagAutomatically: "Tandai sebagai NSFW"
|
||||
|
@ -1339,21 +1489,17 @@ _aboutMisskey:
|
|||
donate: "Donasi ke Misskey"
|
||||
morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang tidak tercantum disini. Terima kasih! 🥰"
|
||||
patrons: "Pendukung"
|
||||
_nsfw:
|
||||
respect: "Sembunyikan media NSFW"
|
||||
ignore: "Jangan sembunyikan media NSFW"
|
||||
force: "Sembunyikan semua media"
|
||||
_instanceTicker:
|
||||
none: "Jangan tampilkan"
|
||||
remote: "Tampilkan untuk pengguna luar"
|
||||
remote: "Tampilkan untuk pengguna instansi luar"
|
||||
always: "Selalu tampilkan"
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "Muat ulang otomatis"
|
||||
dialog: "Tampilkan dialog peringatan"
|
||||
quiet: "Tampilkan peringatan tidak mengganggu"
|
||||
_channel:
|
||||
create: "Buat saluran"
|
||||
edit: "Sunting saluran"
|
||||
create: "Buat Kanal"
|
||||
edit: "Sunting Kanal"
|
||||
setBanner: "Setel banner"
|
||||
removeBanner: "Hapus banner"
|
||||
featured: "Tren"
|
||||
|
@ -1361,6 +1507,8 @@ _channel:
|
|||
following: "Mengikuti"
|
||||
usersCount: "{n} Partisipan"
|
||||
notesCount: "terdapat {n} catatan"
|
||||
nameAndDescription: "Nama dan deskripsi"
|
||||
nameOnly: "Hanya nama"
|
||||
_menuDisplay:
|
||||
sideFull: "Horisontal"
|
||||
sideIcon: "Horisontal (Ikon)"
|
||||
|
@ -1369,9 +1517,9 @@ _menuDisplay:
|
|||
_wordMute:
|
||||
muteWords: "Kata yang dibisukan"
|
||||
muteWordsDescription: "Pisahkan dengan spasi untuk kondisi AND. Pisahkan dengan baris baru untuk kondisi OR."
|
||||
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan regular expressions."
|
||||
softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari linimasa."
|
||||
hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke linimasa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke linimasa meskipun jika kondisi tersebut diubah."
|
||||
muteWordsDescription2: "Kurung kata kunci dengan garis miring untuk menggunakan ekspresi reguler."
|
||||
softDescription: "Sembunyikan catatan yang memenuhi aturan kondisi dari lini masa."
|
||||
hardDescription: "Cegah catatan memenuhi aturan kondisi dari ditambahkan ke lini masa. Dengan tambahan, catatan berikut tidak akan ditambahkan ke lini masa meskipun jika kondisi tersebut diubah."
|
||||
soft: "Lembut"
|
||||
hard: "Keras"
|
||||
mutedNotes: "Catatan yang dibisukan"
|
||||
|
@ -1441,8 +1589,8 @@ _theme:
|
|||
cwBg: "Latar belakang tombol Sembunyikan Konten"
|
||||
cwFg: "Teks tombol Sembunyikan Konten"
|
||||
cwHoverBg: "Latar belakang tombol Sembunyikan Konten (Mengambang)"
|
||||
toastBg: "Latar belakang pemberitahuan"
|
||||
toastFg: "Teks pemberitahuan"
|
||||
toastBg: "Latar belakang notifikasi"
|
||||
toastFg: "Teks notifikasi"
|
||||
buttonBg: "Latar belakang tombol"
|
||||
buttonHoverBg: "Latar belakang tombol (Mengambang)"
|
||||
inputBorder: "Batas bidang masukan"
|
||||
|
@ -1457,11 +1605,11 @@ _theme:
|
|||
_sfx:
|
||||
note: "Catatan"
|
||||
noteMy: "Catatan (Saya)"
|
||||
notification: "Pemberitahuan"
|
||||
notification: "Notifikasi"
|
||||
chat: "Pesan"
|
||||
chatBg: "Obrolan (Latar Belakang)"
|
||||
antenna: "Penerimaan Antenna"
|
||||
channel: "Pemberitahuan saluran"
|
||||
channel: "Notifikasi Kanal"
|
||||
_ago:
|
||||
future: "Masa depan"
|
||||
justNow: "Baru saja"
|
||||
|
@ -1478,15 +1626,40 @@ _time:
|
|||
minute: "menit"
|
||||
hour: "jam"
|
||||
day: "hari"
|
||||
_timelineTutorial:
|
||||
title: "Bagaimana cara menggunakan Misskey"
|
||||
step1_1: "Ini adalah \"lini masa\". Semua \"catatan\" yang dikirimkan oleh {name} akan dimunculkan secara kronologis di sini."
|
||||
step1_2: "Ada beberapa lini masa yang berbeda. Seperti contoh, \"Lini masa Beranda\" berisi catatan dari pengguna yang kamu ikuti, dan \"Lini masa lokal\" berisi catatan dari semua pengguna dari {name}."
|
||||
step2_1: "Selanjutnya, mari kita coba memposting sebuah catatan. Kamu dapat melakukanya dengan menekan tombol dengan ikon pensil."
|
||||
step2_2: "Bagaimana dengan menuliskan sedikit perkenalan diri, atau hanya \"Hello {name}\" kalau kamu lagi ngga feeling?"
|
||||
step3_1: "Udah selesai memposting catatan pertamamu?"
|
||||
step3_2: "Catatan pertamamu seharusnya sekarang sudah tampil di lini masa kamu."
|
||||
step4_1: "Kamu dapat menyisipkan \"Reaksi\" ke dalam catatan."
|
||||
step4_2: "Untuk menyisipkan reaksi, tekan tanda \"+\" dalam catatan dan pilih emoji yang kamu suka untuk mereaksi catatan tersebut."
|
||||
_2fa:
|
||||
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
||||
registerTOTP: "Daftarkan aplikasi autentikator"
|
||||
passwordToTOTP: "Masukkan kata sandimu"
|
||||
step1: "Pertama, pasang aplikasi otentikasi (seperti {a} atau {b}) di perangkat kamu."
|
||||
step2: "Lalu, pindai kode QR yang ada di layar."
|
||||
step2Click: "Mengeklik kode QR ini akan membolehkanmu untuk mendaftarkan 2FA ke security-key atau aplikasi autentikator ponsel."
|
||||
step2Url: "Di aplikasi desktop, masukkan URL berikut:"
|
||||
step3Title: "Masukkan kode autentikasi"
|
||||
step3: "Masukkan token yang telah disediakan oleh aplikasimu untuk menyelesaikan pemasangan."
|
||||
step4: "Mulai sekarang, upaya login apapun akan meminta token login dari aplikasi otentikasi kamu."
|
||||
securityKeyNotSupported: "Peramban kamu tidak mendukung security key."
|
||||
registerTOTPBeforeKey: "Mohon atur aplikasi autentikator untuk mendaftarkan security key atau passkey."
|
||||
securityKeyInfo: "Kamu dapat memasang otentikasi WebAuthN untuk mengamankan proses login lebih lanjut dengan tidak hanya perangkat keras kunci keamanan yang mendukung FIDO2, namun juga sidik jari atau otentikasi PIN pada perangkatmu."
|
||||
chromePasskeyNotSupported: "Passkey Chrome saat ini tidak didukung."
|
||||
registerSecurityKey: "Daftarkan security key atau passkey."
|
||||
securityKeyName: "Masukkan nama key."
|
||||
tapSecurityKey: "Mohon ikuti peramban kamu untuk mendaftarkan security key atau passkey"
|
||||
removeKey: "Hapus security key"
|
||||
removeKeyConfirm: "Hapus cadangan {name}?"
|
||||
whyTOTPOnlyRenew: "Aplikasi autentikator tidak dapat dihapus selama security key masih terdaftar."
|
||||
renewTOTP: "Atur ulang aplikasi autentikator"
|
||||
renewTOTPConfirm: "Hal ini akan menyebabkan kode verifikasi dari aplikasi autentikator sebelumnya berhenti bekerja"
|
||||
renewTOTPOk: "Atur ulang"
|
||||
renewTOTPCancel: "Tidak sekarang."
|
||||
_permissions:
|
||||
"read:account": "Lihat informasi akun"
|
||||
|
@ -1504,8 +1677,8 @@ _permissions:
|
|||
"read:mutes": "Lihat daftar orang yang dibisukan"
|
||||
"write:mutes": "Sunting daftar orang yang dibisukan"
|
||||
"write:notes": "Buat atau hapus catatan"
|
||||
"read:notifications": "Lihat pemberitahuan"
|
||||
"write:notifications": "Sunting pemberitahuan"
|
||||
"read:notifications": "Lihat notifikasi"
|
||||
"write:notifications": "Sunting notifikasi"
|
||||
"read:reactions": "Lihat reaksi"
|
||||
"write:reactions": "Sunting reaksi"
|
||||
"write:votes": "Beri suara"
|
||||
|
@ -1515,8 +1688,8 @@ _permissions:
|
|||
"write:page-likes": "Sunting suka pada Halaman"
|
||||
"read:user-groups": "Lihat grup pengguna"
|
||||
"write:user-groups": "Sunting atau hapus grup pengguna"
|
||||
"read:channels": "Lihat saluran"
|
||||
"write:channels": "Sunting saluran"
|
||||
"read:channels": "Lihat Kanal"
|
||||
"write:channels": "Sunting Kanal"
|
||||
"read:gallery": "Lihat galeri"
|
||||
"write:gallery": "Sunting galeri"
|
||||
"read:gallery-likes": "Lihat daftar postingan galeri yang disukai"
|
||||
|
@ -1548,8 +1721,8 @@ _widgets:
|
|||
profile: "Profil"
|
||||
instanceInfo: "Informasi Instansi"
|
||||
memo: "Catatan memo"
|
||||
notifications: "Pemberitahuan"
|
||||
timeline: "Linimasa"
|
||||
notifications: "Notifikasi"
|
||||
timeline: "Lini masa"
|
||||
calendar: "Kalender"
|
||||
trends: "Tren"
|
||||
clock: "Jam"
|
||||
|
@ -1603,13 +1776,15 @@ _poll:
|
|||
remainingSeconds: "Berakhir dalam {s} detik"
|
||||
_visibility:
|
||||
public: "Publik"
|
||||
publicDescription: "Catat ke linimasa global"
|
||||
publicDescription: "Catat ke lini masa global"
|
||||
home: "Beranda"
|
||||
homeDescription: "Catat ke linimasa beranda saja"
|
||||
homeDescription: "Catat ke lini masa beranda saja"
|
||||
followers: "Pengikut"
|
||||
followersDescription: "Catat ke pengikut saja"
|
||||
specified: "Langsung"
|
||||
specifiedDescription: "Catat ke pengguna yang ditentukan saja"
|
||||
disableFederation: "Matikan federasi"
|
||||
disableFederationDescription: "Jangan kirimkan ke instansi lain"
|
||||
_postForm:
|
||||
replyPlaceholder: "Balas ke catatan ini..."
|
||||
quotePlaceholder: "Kutip catatan ini..."
|
||||
|
@ -1650,7 +1825,7 @@ _charts:
|
|||
activeUsers: "Pengguna aktif"
|
||||
notesIncDec: "Perbedaan # dalam catatan"
|
||||
localNotesIncDec: "Perbedaan # dalam catatan lokal"
|
||||
remoteNotesIncDec: "Perbedaan # dalam catatan luar"
|
||||
remoteNotesIncDec: "Perbedaan # dalam catatan instansi luar"
|
||||
notesTotal: "Total # catatan"
|
||||
filesIncDec: "Perbedaan # dalam berkas"
|
||||
filesTotal: "Jumlah # berkas"
|
||||
|
@ -1765,7 +1940,8 @@ _notification:
|
|||
pollEnded: "Jajak pendapat berakhir"
|
||||
receiveFollowRequest: "Permintaan mengikuti diterima"
|
||||
followRequestAccepted: "Permintaan mengikuti disetujui"
|
||||
app: "Pemberitahuan dari aplikasi"
|
||||
achievementEarned: "Pencapaian didapatkan"
|
||||
app: "Notifikasi dari aplikasi tertaut"
|
||||
_actions:
|
||||
followBack: "Ikuti Kembali"
|
||||
reply: "Balas"
|
||||
|
@ -1790,13 +1966,34 @@ _deck:
|
|||
_columns:
|
||||
main: "Utama"
|
||||
widgets: "Widget"
|
||||
notifications: "Pemberitahuan"
|
||||
tl: "Linimasa"
|
||||
notifications: "Notifikasi"
|
||||
tl: "Lini masa"
|
||||
antenna: "Antena"
|
||||
list: "Daftar"
|
||||
channel: "Kanal"
|
||||
mentions: "Sebutan"
|
||||
direct: "Langsung"
|
||||
roleTimeline: "Lini masa peran"
|
||||
_dialog:
|
||||
charactersExceeded: "Kamu telah melebihi batas karakter maksimum! Saat ini pada {current} dari {max}."
|
||||
charactersBelow: "Kamu berada di bawah batas minimum karakter! Saat ini pada {current} dari {min}."
|
||||
_disabledTimeline:
|
||||
title: "Lini masa dinonaktifkan"
|
||||
description: "Saat ini kamu tidak dapat menggunakan lini masa ini karena peran kamu saat ini."
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "Ukuran berkas (Turun)"
|
||||
orderByCreatedAtAsc: "Tanggal (Naik)"
|
||||
_webhookSettings:
|
||||
createWebhook: "Buat Webhook"
|
||||
name: "Nama"
|
||||
secret: "Secret"
|
||||
events: "Webhook Events"
|
||||
active: "Aktif"
|
||||
_events:
|
||||
follow: "Ketika mengikuti pengguna"
|
||||
followed: "Ketika diikuti pengguna"
|
||||
note: "Ketika memposting catatan"
|
||||
reply: "Ketika menerima balasan"
|
||||
renote: "Ketika direnote"
|
||||
reaction: "Ketika menerima reaksi"
|
||||
mention: "Ketika sedang disebut"
|
||||
|
|
|
@ -57,6 +57,9 @@ export interface Locale {
|
|||
"copyUsername": string;
|
||||
"copyUserId": string;
|
||||
"copyNoteId": string;
|
||||
"copyFileId": string;
|
||||
"copyFolderId": string;
|
||||
"copyProfileUrl": string;
|
||||
"searchUser": string;
|
||||
"reply": string;
|
||||
"loadMore": string;
|
||||
|
@ -139,8 +142,10 @@ export interface Locale {
|
|||
"suspendConfirm": string;
|
||||
"unsuspendConfirm": string;
|
||||
"selectList": string;
|
||||
"editList": string;
|
||||
"selectChannel": string;
|
||||
"selectAntenna": string;
|
||||
"editAntenna": string;
|
||||
"selectWidget": string;
|
||||
"editWidgets": string;
|
||||
"editWidgetsExit": string;
|
||||
|
@ -314,7 +319,7 @@ export interface Locale {
|
|||
"rename": string;
|
||||
"avatar": string;
|
||||
"banner": string;
|
||||
"nsfw": string;
|
||||
"displayOfSensitiveMedia": string;
|
||||
"whenServerDisconnected": string;
|
||||
"disconnectedFromServer": string;
|
||||
"reload": string;
|
||||
|
@ -1069,6 +1074,9 @@ export interface Locale {
|
|||
"additionalEmojiDictionary": string;
|
||||
"installed": string;
|
||||
"branding": string;
|
||||
"enableServerMachineStats": string;
|
||||
"enableIdenticonGeneration": string;
|
||||
"turnOffToImprovePerformance": string;
|
||||
"_initialAccountSetting": {
|
||||
"accountCreated": string;
|
||||
"letsStartAccountSetup": string;
|
||||
|
@ -1558,6 +1566,7 @@ export interface Locale {
|
|||
"back": string;
|
||||
"reduceFrequencyOfThisAd": string;
|
||||
"hide": string;
|
||||
"timezoneinfo": string;
|
||||
};
|
||||
"_forgotPassword": {
|
||||
"enterEmail": string;
|
||||
|
@ -1619,7 +1628,7 @@ export interface Locale {
|
|||
"morePatrons": string;
|
||||
"patrons": string;
|
||||
};
|
||||
"_nsfw": {
|
||||
"_displayOfSensitiveMedia": {
|
||||
"respect": string;
|
||||
"ignore": string;
|
||||
"force": string;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
* Languages Loader
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
|
@ -51,9 +51,9 @@ const primaries = {
|
|||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||
|
||||
module.exports = Object.entries(locales)
|
||||
export default Object.entries(locales)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
|
|
|
@ -26,15 +26,15 @@ otherSettings: "Altre impostazioni"
|
|||
openInWindow: "Apri in una finestra"
|
||||
profile: "Profilo"
|
||||
timeline: "Timeline"
|
||||
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
|
||||
noAccountDescription: "La persona non ha ancora scritto alcuna autobiografia."
|
||||
login: "Accedi"
|
||||
loggingIn: "Accesso in corso..."
|
||||
logout: "Uscita"
|
||||
signup: "Iscriviti"
|
||||
uploading: "Caricamento..."
|
||||
save: "Salva"
|
||||
users: "Utente"
|
||||
addUser: "Aggiungi utente"
|
||||
users: "Profili"
|
||||
addUser: "Aggiungi profilo"
|
||||
favorite: "Preferiti"
|
||||
favorites: "Preferiti"
|
||||
unfavorite: "Rimuovi nota dai preferiti"
|
||||
|
@ -52,7 +52,9 @@ addToList: "Aggiungi alla lista"
|
|||
sendMessage: "Invia messaggio"
|
||||
copyRSS: "Copia RSS"
|
||||
copyUsername: "Copia nome utente"
|
||||
searchUser: "Cerca utente"
|
||||
copyUserId: "Copia ID del profilo"
|
||||
copyNoteId: "Copia ID della Nota"
|
||||
searchUser: "Cerca profilo"
|
||||
reply: "Rispondi"
|
||||
loadMore: "Mostra di più"
|
||||
showMore: "Espandi"
|
||||
|
@ -153,7 +155,7 @@ flagAsBotDescription: "Attiva questo campo se il profilo esegue principalmente o
|
|||
flagAsCat: "Sono un gatto"
|
||||
flagAsCatDescription: "La modalità \"sono un gatto\" aggiunge le orecchie al tuo profilo"
|
||||
flagShowTimelineReplies: "Mostra le risposte alle note sulla timeline."
|
||||
flagShowTimelineRepliesDescription: "Se è attiva, la timeline mostra le risposte alle altre note dell'utente oltre a quelle dell'utente stesso."
|
||||
flagShowTimelineRepliesDescription: "Attivando, la timeline mostra le Note del profilo ed anche le risposte ad altre Note"
|
||||
autoAcceptFollowed: "Accetta automaticamente le richieste di follow da utenti che già segui"
|
||||
addAccount: "Aggiungi profilo"
|
||||
reloadAccountsList: "Ricarica l'elenco dei profili"
|
||||
|
@ -205,7 +207,7 @@ blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga.
|
|||
muteAndBlock: "Silenziati / Bloccati"
|
||||
mutedUsers: "Profili silenziati"
|
||||
blockedUsers: "Profili bloccati"
|
||||
noUsers: "Nessun utente trovato"
|
||||
noUsers: "Non ci sono profili"
|
||||
editProfile: "Modifica profilo"
|
||||
noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?"
|
||||
pinLimitExceeded: "Non puoi fissare altre note "
|
||||
|
@ -236,8 +238,8 @@ newPasswordRetype: "Conferma password"
|
|||
attachFile: "Allega file"
|
||||
more: "Di più!"
|
||||
featured: "Tendenze"
|
||||
usernameOrUserId: "Nome utente o ID utente"
|
||||
noSuchUser: "Nessun utente trovato"
|
||||
usernameOrUserId: "Nome utente o ID"
|
||||
noSuchUser: "Profilo non trovato"
|
||||
lookup: "Ricerca remota"
|
||||
announcements: "Annunci"
|
||||
imageUrl: "URL dell'immagine"
|
||||
|
@ -269,7 +271,7 @@ basicNotesBeforeCreateAccount: "Note importanti"
|
|||
termsOfService: "Informativa Privacy"
|
||||
start: "Inizia!"
|
||||
home: "Home"
|
||||
remoteUserCaution: "Può darsi che le informazioni siano incomplete perché questo è un utente remoto."
|
||||
remoteUserCaution: "Le informazioni potrebbero essere incomplete poiché questo profilo remoto potrebbe non essere completamente federato."
|
||||
activity: "Attività"
|
||||
images: "Immagini"
|
||||
image: "Immagini"
|
||||
|
@ -309,7 +311,6 @@ copyUrl: "Copia URL"
|
|||
rename: "Modifica nome"
|
||||
avatar: "Foto del profilo"
|
||||
banner: "Intestazione"
|
||||
nsfw: "Contenuti sensibili"
|
||||
whenServerDisconnected: "Quando la connessione col server è persa"
|
||||
disconnectedFromServer: "Il server si è disconnesso"
|
||||
reload: "Ricarica"
|
||||
|
@ -444,7 +445,7 @@ signinRequired: "Occorre avere un profilo registrato su questa istanza"
|
|||
invitations: "Invita"
|
||||
invitationCode: "Codice di invito"
|
||||
checking: "Confermando"
|
||||
available: "Consigliati"
|
||||
available: "Disponibile"
|
||||
unavailable: "Il nome utente è già in uso"
|
||||
usernameInvalidFormat: "Il nome utente può contenere solo lettere, numeri e '_'"
|
||||
tooShort: "Troppo breve"
|
||||
|
@ -560,6 +561,7 @@ accountDeletedDescription: "Questo profilo è stato eliminato."
|
|||
menu: "Menù"
|
||||
divider: "Linea di separazione"
|
||||
addItem: "Aggiungi elemento"
|
||||
rearrange: "Riordina"
|
||||
relays: "Ripetitori"
|
||||
addRelay: "Aggiungi ripetitore"
|
||||
inboxUrl: "Inbox URL"
|
||||
|
@ -660,7 +662,7 @@ instanceTicker: "Informazioni sull'istanza da cui vengono le note"
|
|||
waitingFor: "Aspettando {x}"
|
||||
random: "Casuale"
|
||||
system: "Sistema"
|
||||
switchUi: "Cambiare interfaccia"
|
||||
switchUi: "Cambia interfaccia grafica"
|
||||
desktop: "Desktop"
|
||||
clip: "Clip"
|
||||
createNew: "Crea"
|
||||
|
@ -703,6 +705,8 @@ contact: "Contatti"
|
|||
useSystemFont: "Usa il carattere predefinito del sistema"
|
||||
clips: "Clip"
|
||||
experimentalFeatures: "Funzioni sperimentali"
|
||||
experimental: "Sperimentale"
|
||||
thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe essere malfunzionante o cambiare in futuro."
|
||||
developer: "Sviluppatore"
|
||||
makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\""
|
||||
makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"."
|
||||
|
@ -779,7 +783,7 @@ switchAccount: "Cambia profilo"
|
|||
enabled: "Attivo"
|
||||
disabled: "Inattivo"
|
||||
quickAction: "Azioni rapide"
|
||||
user: "Utente"
|
||||
user: "Profilo"
|
||||
administration: "Gestione"
|
||||
accounts: "Profilo"
|
||||
switch: "Cambia"
|
||||
|
@ -787,6 +791,7 @@ noMaintainerInformationWarning: "Le informazioni amministratore non sono imposta
|
|||
noBotProtectionWarning: "Nessuna protezione impostata contro i bot."
|
||||
configure: "Imposta"
|
||||
postToGallery: "Pubblicare nella galleria"
|
||||
postToHashtag: "Pubblica a questo hashtag"
|
||||
gallery: "Galleria"
|
||||
recentPosts: "Le più recenti"
|
||||
popularPosts: "Le più visualizzate"
|
||||
|
@ -811,7 +816,7 @@ received: "Ricevuto"
|
|||
searchResult: "Risultati della Ricerca"
|
||||
hashtags: "Hashtag"
|
||||
troubleshooting: "Risoluzione problemi"
|
||||
useBlurEffect: "Utilizza effetto sfocatura nell'interfaccia"
|
||||
useBlurEffect: "Utilizza effetto sfocatura"
|
||||
learnMore: "Più dettagli"
|
||||
misskeyUpdated: "Misskey è stato aggiornato!"
|
||||
whatIsNew: "Visualizza le informazioni sull'aggiornamento"
|
||||
|
@ -820,6 +825,7 @@ translatedFrom: "Tradotto da {x}"
|
|||
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
|
||||
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
|
||||
aiChanMode: "Modalità Ai"
|
||||
devMode: "Modalità sviluppatori"
|
||||
keepCw: "Mantieni il Content Warning"
|
||||
pubSub: "Publish/Subscribe del profilo"
|
||||
lastCommunication: "La comunicazione più recente"
|
||||
|
@ -829,6 +835,8 @@ breakFollow: "Non seguire"
|
|||
breakFollowConfirm: "Vuoi davvero togliere follower?"
|
||||
itsOn: "Abilitato"
|
||||
itsOff: "Disabilitato"
|
||||
on: "Acceso"
|
||||
off: "Spento"
|
||||
emailRequiredForSignup: "L'ndirizzo e-mail è obbligatorio per registrarsi"
|
||||
unread: "Non lette"
|
||||
filter: "Filtri"
|
||||
|
@ -848,7 +856,7 @@ voteConfirm: "Votare per「{choice}」?"
|
|||
hide: "Nascondere"
|
||||
useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
|
||||
welcomeBackWithName: "Ciao, {name}! Eccoti di nuovo!"
|
||||
clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo email."
|
||||
clickToFinishEmailVerification: "Premi il bottone \"{ok}\" per completare la verifica dell'indirizzo email."
|
||||
overridedDeviceKind: "Tipo di dispositivo"
|
||||
smartphone: "Smartphone"
|
||||
tablet: "Tablet"
|
||||
|
@ -909,6 +917,7 @@ remoteOnly: "Solo remoto"
|
|||
failedToUpload: "errore di caricamento"
|
||||
cannotUploadBecauseInappropriate: "Non è possibile caricarlo perché è stato stabilito che potrebbe contenere contenuti inappropriati."
|
||||
cannotUploadBecauseNoFreeSpace: "Impossibile caricare a causa della mancanza di spazio libero sul drive."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "Il file non può essere caricato perché eccede le dimensioni consentite."
|
||||
beta: "Versione beta"
|
||||
enableAutoSensitive: "Determinazione automatica del NSFW"
|
||||
enableAutoSensitiveDescription: "Se disponibile, il flag NSFW viene impostato automaticamente sui media utilizzando l'apprendimento automatico. Anche se questa funzione è disattivata, in alcuni casi può essere impostata automaticamente."
|
||||
|
@ -942,6 +951,7 @@ didYouLikeMisskey: "Ti piace Misskey?"
|
|||
pleaseDonate: "Misskey è il software libero utilizzato su {host}. Offrendo una donazione è più facile continuare a svilupparlo!"
|
||||
roles: "Ruoli"
|
||||
role: "Ruolo"
|
||||
noRole: "Ruolo non trovato"
|
||||
normalUser: "Profilo standard"
|
||||
undefined: "Indefinito"
|
||||
assign: "Assegna"
|
||||
|
@ -981,10 +991,13 @@ cannotBeChangedLater: "Non sarà più modificabile"
|
|||
reactionAcceptance: "Reazioni consentite"
|
||||
likeOnly: "Solo i Like"
|
||||
likeOnlyForRemote: "Solo Like remoti"
|
||||
nonSensitiveOnly: "Solamente non sensibili"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Solamente non sensibili (solo Mi piace remoti)"
|
||||
rolesAssignedToMe: "I miei ruoli"
|
||||
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
|
||||
sensitiveWords: "Parole sensibili"
|
||||
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
|
||||
sensitiveWordsDescription2: "Gli spazi creano la relazione \"E\" tra parole (questo E quello). Racchiudere una parola nelle slash \"/\" la trasforma in Espressione Regolare."
|
||||
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
|
||||
license: "Licenza"
|
||||
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
|
||||
|
@ -1003,9 +1016,13 @@ videos: "Video"
|
|||
dataSaver: "Risparmia dati"
|
||||
accountMigration: "Migrazione del profilo"
|
||||
accountMoved: "Questo profilo ha migrato altrove:"
|
||||
accountMovedShort: "Questo profilo è stato migrato"
|
||||
operationForbidden: "Operazione non consentita"
|
||||
forceShowAds: "Mostra sempre i banner"
|
||||
addMemo: "Aggiungi Memo"
|
||||
editMemo: "Modifica Memo"
|
||||
reactionsList: "Elenco delle reazioni"
|
||||
renotesList: "Elenco di Rinota"
|
||||
notificationDisplay: "Stile delle notifiche"
|
||||
leftTop: "In alto a sinistra"
|
||||
rightTop: "In alto a destra"
|
||||
|
@ -1019,18 +1036,65 @@ serverRules: "Regolamento"
|
|||
pleaseConfirmBelowBeforeSignup: "Ai sensi del regolamento EU 679/2016 GDPR, autorizzo il trattamento dati personali come descritto nella informativa Privacy."
|
||||
pleaseAgreeAllToContinue: "Per continuare, occorre selezionare ed essere d'accordo su tutto."
|
||||
continue: "Continua"
|
||||
preservedUsernames: "Nomi utente riservati"
|
||||
preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti."
|
||||
createNoteFromTheFile: "Crea Nota da questo file"
|
||||
archive: "Archivio"
|
||||
channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?"
|
||||
channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note."
|
||||
thisChannelArchived: "Questo canale è stato archiviato."
|
||||
displayOfNote: "Visualizzazione delle Note"
|
||||
initialAccountSetting: "Impostazioni iniziali del profilo"
|
||||
youFollowing: "Seguiti"
|
||||
preventAiLearning: "Impedisci l'apprendimento della IA"
|
||||
preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata."
|
||||
options: "Opzioni del ruolo"
|
||||
specifyUser: "Profilo specifico"
|
||||
failedToPreviewUrl: "Anteprima non disponibile"
|
||||
update: "Aggiorna"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se non viene specificato alcun ruolo, chiunque può reagire con questa emoji."
|
||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Questi ruoli devono essere pubblici"
|
||||
cancelReactionConfirm: "Vuoi annullare la tua reazione?"
|
||||
changeReactionConfirm: "Vuoi cambiare la tua reazione?"
|
||||
later: "Non ora"
|
||||
goToMisskey: "Vai a Misskey"
|
||||
additionalEmojiDictionary: "Dizionario aggiuntivo emoji"
|
||||
installed: "Installazione avvenuta"
|
||||
branding: "Branding"
|
||||
enableServerMachineStats: "Pubblicare le informazioni sul server"
|
||||
enableIdenticonGeneration: "Generazione automatica delle Identicon"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Il tuo profilo è stato creato!"
|
||||
letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo."
|
||||
letsFillYourProfile: "Innanzitutto, compila il tuo profilo."
|
||||
profileSetting: "Impostazioni del profilo"
|
||||
privacySetting: "Impostazioni sulla privacy"
|
||||
theseSettingsCanEditLater: "In seguito, potrai cambiare la tua scelta."
|
||||
youCanEditMoreSettingsInSettingsPageLater: "Nella pagina \"Impostazioni\", è possibile personalizzare di più il tuo profilo. Dacci un'occhiata dopo!"
|
||||
followUsers: "Per comporre la tua Timeline Home (personale) segui i profili delle persone che ti interessano."
|
||||
pushNotificationDescription: "Attivare le notifiche push ti permettera di ricevere informazioni sulla attività di {name} direttamente sul tuo dispositivo."
|
||||
initialAccountSettingCompleted: "Hai completato la configurazione iniziale!"
|
||||
haveFun: "Divertiti con {name}!"
|
||||
ifYouNeedLearnMore: "Per saperne di più su come usare {name} (Misskey), visita la pagina {link}"
|
||||
skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?"
|
||||
laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?"
|
||||
_serverRules:
|
||||
description: "In Europa è necessario mostrare l'informativa sul trattamento dei dati personali, prima della registrazione al servizio."
|
||||
_accountMigration:
|
||||
moveFrom: "Migra un altro profilo dentro a questo"
|
||||
moveFromSub: "Crea un alias verso un altro profilo remoto"
|
||||
moveFromLabel: "Profilo da cui migrare:"
|
||||
moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it"
|
||||
moveTo: "Migrare questo profilo verso un un altro"
|
||||
moveToLabel: "Profilo verso cui migrare"
|
||||
moveCannotBeUndone: "La migrazione è irreversibile, non può essere interrotta o annullata."
|
||||
moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it"
|
||||
moveAccountHowTo: "Per migrare su un profilo remoto, crea prima un alias di questo profilo, sulla istanza di destinazione.\nDopo aver creato l'alias, inserisci l'indirizzo di destinazione, indicando ad esempio: @profilo@altra.istanza"
|
||||
startMigration: "Avvia la migrazione"
|
||||
migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo."
|
||||
movedAndCannotBeUndone: "Il tuo profilo è stato migrato.\nLa migrazione non può essere annullata."
|
||||
postMigrationNote: "Questo profilo smetterà di seguire gli altri profili remoti a 24 ore dal termine della migrazione.\nSia i Follow che i Follower scenderanno a zero. I tuoi follower saranno comunque in grado di vedere le Note per soli follower, poiché non smetteranno di seguirti."
|
||||
movedTo: "Profilo verso cui migrare"
|
||||
_achievements:
|
||||
earnedAt: "Data di conseguimento"
|
||||
|
@ -1312,6 +1376,7 @@ _role:
|
|||
canInvite: "Genera codici di invito all'istanza"
|
||||
canManageCustomEmojis: "Gestire le emoji personalizzate"
|
||||
driveCapacity: "Capienza del Drive"
|
||||
alwaysMarkNsfw: "Imposta sempre come NSFW"
|
||||
pinMax: "Quantità massima di Note in primo piano"
|
||||
antennaMax: "Quantità massima di Antenne"
|
||||
wordMuteMax: "Lunghezza massima del filtro parole"
|
||||
|
@ -1422,10 +1487,6 @@ _aboutMisskey:
|
|||
donate: "Sostieni Misskey"
|
||||
morePatrons: "Apprezziamo sinceramente il supporto di tante altre persone. Grazie mille! 🥰"
|
||||
patrons: "Sostenitori"
|
||||
_nsfw:
|
||||
respect: "Nascondere i media segnati come sensibli"
|
||||
ignore: "Visualizzare i media segnati come sensibili"
|
||||
force: "Nascondere tutti i media"
|
||||
_instanceTicker:
|
||||
none: "Nascondi"
|
||||
remote: "Mostra solo per i profili remoti"
|
||||
|
@ -1563,6 +1624,16 @@ _time:
|
|||
minute: "min"
|
||||
hour: "ore"
|
||||
day: "giorni"
|
||||
_timelineTutorial:
|
||||
title: "Come usare Misskey"
|
||||
step1_1: "Questa è la \"Timeline\". tutte le \"Note\" pubblicate su {name} vengono elencate in ordine cronologico."
|
||||
step1_2: "Le Timeline sono diverse, ad esempio, la \"Home\" elenca le Note dei profili che segui. Quella \"Locale\" elenca quelle di tutti i profili attivi su {name}."
|
||||
step2_1: "Prova a pubblicare una Nota. Semplicemente premendo il bottone con l'icona di una matita."
|
||||
step2_2: "Potresti scrivere la tua presentazione, oppure semplicemente \"Ciao da {name}!\""
|
||||
step3_1: "Hai pubblicato qualcosa?"
|
||||
step3_2: "In tal caso, dovrebbe comparire subito nella tua \"Home\""
|
||||
step4_1: "Puoi reagire con un emoji alle Note."
|
||||
step4_2: "To attach a reaction, press the \"+\" mark on a note and choose an emoji you'd like to react with.\nPer reagire con una emoji, premi il bottone \"+\" (più) visibile vicino ad ogni Nota e scegli dall'elenco la emoji che rappresenta la tua reazione."
|
||||
_2fa:
|
||||
alreadyRegistered: "La configurazione è stata già completata."
|
||||
registerTOTP: "Registra un'app di autenticazione"
|
||||
|
@ -1710,7 +1781,7 @@ _visibility:
|
|||
followersDescription: "Visibile solo ai tuoi follower"
|
||||
specified: "Nota diretta"
|
||||
specifiedDescription: "Visibile solo ai profili menzionati"
|
||||
disableFederation: "Federazione disabilitata"
|
||||
disableFederation: "Non federare"
|
||||
disableFederationDescription: "Non spedire attività alle altre istanze remote"
|
||||
_postForm:
|
||||
replyPlaceholder: "Rispondi a questa nota..."
|
||||
|
|
|
@ -54,6 +54,9 @@ copyRSS: "RSSをコピー"
|
|||
copyUsername: "ユーザー名をコピー"
|
||||
copyUserId: "ユーザーIDをコピー"
|
||||
copyNoteId: "ノートIDをコピー"
|
||||
copyFileId: "ファイルIDをコピー"
|
||||
copyFolderId: "フォルダーIDをコピー"
|
||||
copyProfileUrl: "プロフィールURLをコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
reply: "返信"
|
||||
loadMore: "もっと見る"
|
||||
|
@ -112,7 +115,7 @@ pinnedNote: "ピン留めされたノート"
|
|||
pinned: "ピン留め"
|
||||
you: "あなた"
|
||||
clickToShow: "クリックして表示"
|
||||
sensitive: "閲覧注意"
|
||||
sensitive: "センシティブ"
|
||||
add: "追加"
|
||||
reaction: "リアクション"
|
||||
reactions: "リアクション"
|
||||
|
@ -120,8 +123,8 @@ reactionSetting: "ピッカーに表示するリアクション"
|
|||
reactionSettingDescription2: "ドラッグして並び替え、クリックして削除、+を押して追加します。"
|
||||
rememberNoteVisibility: "公開範囲を記憶する"
|
||||
attachCancel: "添付取り消し"
|
||||
markAsSensitive: "閲覧注意にする"
|
||||
unmarkAsSensitive: "閲覧注意を解除する"
|
||||
markAsSensitive: "センシティブとして設定"
|
||||
unmarkAsSensitive: "センシティブを解除する"
|
||||
enterFileName: "ファイル名を入力"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュート解除"
|
||||
|
@ -136,8 +139,10 @@ unblockConfirm: "ブロック解除しますか?"
|
|||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
editList: "リストを編集"
|
||||
selectChannel: "チャンネルを選択"
|
||||
selectAntenna: "アンテナを選択"
|
||||
editAntenna: "アンテナを編集"
|
||||
selectWidget: "ウィジェットを選択"
|
||||
editWidgets: "ウィジェットを編集"
|
||||
editWidgetsExit: "編集を終了"
|
||||
|
@ -311,7 +316,7 @@ copyUrl: "URLをコピー"
|
|||
rename: "名前を変更"
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "閲覧注意"
|
||||
displayOfSensitiveMedia: "センシティブなメディアの表示"
|
||||
whenServerDisconnected: "サーバーとの接続が失われたとき"
|
||||
disconnectedFromServer: "サーバーから切断されました"
|
||||
reload: "リロード"
|
||||
|
@ -693,7 +698,7 @@ driveUsage: "ドライブ使用量"
|
|||
noCrawle: "クローラーによるインデックスを拒否"
|
||||
noCrawleDescription: "外部の検索エンジンにあなたのユーザーページ、ノート、Pagesなどのコンテンツを登録(インデックス)しないよう要求します。"
|
||||
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
|
||||
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
|
||||
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
|
||||
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
|
||||
disableShowingAnimatedImages: "アニメーション画像を再生しない"
|
||||
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
|
||||
|
@ -920,8 +925,8 @@ cannotUploadBecauseInappropriate: "不適切な内容を含む可能性がある
|
|||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
|
||||
beta: "ベータ"
|
||||
enableAutoSensitive: "自動NSFW判定"
|
||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにNSFWフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||
enableAutoSensitive: "自動センシティブ判定"
|
||||
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
|
||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかなどを判定しより積極的に行います。オフにすると単に文字列として正しいかどうかのみチェックされます。"
|
||||
navbar: "ナビゲーションバー"
|
||||
shuffle: "シャッフル"
|
||||
|
@ -1066,6 +1071,9 @@ goToMisskey: "Misskeyへ"
|
|||
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||
installed: "インストール済み"
|
||||
branding: "ブランディング"
|
||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
||||
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウントの作成が完了しました!"
|
||||
|
@ -1444,7 +1452,7 @@ _sensitiveMediaDetection:
|
|||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||
sensitivity: "検出感度"
|
||||
sensitivityDescription: "感度を低くすると、誤検知(偽陽性)が減ります。感度を高くすると、検知漏れ(偽陰性)が減ります。"
|
||||
setSensitiveFlagAutomatically: "NSFWフラグを設定する"
|
||||
setSensitiveFlagAutomatically: "センシティブフラグを設定する"
|
||||
setSensitiveFlagAutomaticallyDescription: "この設定をオフにしても内部的に判定結果は保持されます。"
|
||||
analyzeVideos: "動画の解析を有効化"
|
||||
analyzeVideosDescription: "静止画に加えて動画も解析するようにします。サーバーの負荷が少し増えます。"
|
||||
|
@ -1478,6 +1486,7 @@ _ad:
|
|||
back: "戻る"
|
||||
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"
|
||||
hide: "表示しない"
|
||||
timezoneinfo: "曜日はサーバーのタイムゾーンを元に指定されます。"
|
||||
|
||||
_forgotPassword:
|
||||
enterEmail: "アカウントに登録したメールアドレスを入力してください。そのアドレス宛てに、パスワードリセット用のリンクが送信されます。"
|
||||
|
@ -1537,9 +1546,9 @@ _aboutMisskey:
|
|||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"
|
||||
patrons: "支援者"
|
||||
|
||||
_nsfw:
|
||||
respect: "閲覧注意のメディアは隠す"
|
||||
ignore: "閲覧注意のメディアを隠さない"
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "センシティブ設定されたメディアを隠す"
|
||||
ignore: "センシティブ設定されたメディアを隠さない"
|
||||
force: "常にメディアを隠す"
|
||||
|
||||
_instanceTicker:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
_lang_: "日本語 (関西弁)"
|
||||
headlineMisskey: "ノートでつながるネットワーク"
|
||||
introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか🚀"
|
||||
introMisskey: "ようお越し!Misskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「ツッコミ」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな、新しい世界を探検しよか🚀"
|
||||
poweredByMisskeyDescription: "{name}は、オープンソースのプラットフォーム<b>Misskey</b>のサーバーのひとつなんやで。"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "探す"
|
||||
|
@ -136,8 +136,10 @@ unblockConfirm: "ブロックやめたるってほんまか?"
|
|||
suspendConfirm: "凍結してしもうてええか?"
|
||||
unsuspendConfirm: "解凍するけどええか?"
|
||||
selectList: "リストを選ぶ"
|
||||
editList: "リスト直すで"
|
||||
selectChannel: "チャンネルを選ぶ"
|
||||
selectAntenna: "アンテナを選ぶ"
|
||||
editAntenna: "アンテナを編集"
|
||||
selectWidget: "ウィジェットを選ぶ"
|
||||
editWidgets: "ウィジェットをいじる"
|
||||
editWidgetsExit: "編集終ったで"
|
||||
|
@ -311,7 +313,6 @@ copyUrl: "URLをコピー"
|
|||
rename: "名前を変えるで"
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "見るんは気いつけてな"
|
||||
whenServerDisconnected: "サーバーとの接続が失くなってしもうたとき"
|
||||
disconnectedFromServer: "サーバーが機嫌悪いねん"
|
||||
reload: "リロード"
|
||||
|
@ -1062,6 +1063,9 @@ later: "あとで"
|
|||
goToMisskey: "Misskeyへ"
|
||||
additionalEmojiDictionary: "絵文字の追加辞書"
|
||||
installed: "インストール済み"
|
||||
branding: "あ"
|
||||
enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウント作り終わったで。"
|
||||
letsStartAccountSetup: "アカウントの初期設定をしよか。"
|
||||
|
@ -1485,10 +1489,6 @@ _aboutMisskey:
|
|||
donate: "Misskeyに寄付"
|
||||
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
|
||||
patrons: "支援者"
|
||||
_nsfw:
|
||||
respect: "閲覧注意のメディアは隠すで"
|
||||
ignore: "閲覧注意のメディアは隠さへんで"
|
||||
force: "常にメディアを隠すで"
|
||||
_instanceTicker:
|
||||
none: "表示せん"
|
||||
remote: "リモートユーザーに表示"
|
||||
|
@ -1635,7 +1635,7 @@ _timelineTutorial:
|
|||
step3_1: "投稿できた?"
|
||||
step3_2: "あんたのノートがタイムラインに出てきたら成功や。"
|
||||
step4_1: "ノートには、「ツッコミ」を付けれるで。"
|
||||
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶで。"
|
||||
step4_2: "ツッコむんやったら、ノートの「+」マークを押して、好きな絵文字を選ぶんやで。"
|
||||
_2fa:
|
||||
alreadyRegistered: "もう設定終わっとるわ。"
|
||||
registerTOTP: "認証アプリの設定はじめる"
|
||||
|
|
|
@ -136,8 +136,10 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?"
|
|||
suspendConfirm: "이 계정을 정지하시겠습니까?"
|
||||
unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?"
|
||||
selectList: "리스트 선택"
|
||||
editList: "리스트 편집"
|
||||
selectChannel: "채널 선택"
|
||||
selectAntenna: "안테나 선택"
|
||||
editAntenna: "안테나 편집"
|
||||
selectWidget: "위젯 선택"
|
||||
editWidgets: "위젯 편집"
|
||||
editWidgetsExit: "편집 종료"
|
||||
|
@ -311,7 +313,6 @@ copyUrl: "URL 복사"
|
|||
rename: "이름 변경"
|
||||
avatar: "아바타"
|
||||
banner: "배너"
|
||||
nsfw: "열람주의"
|
||||
whenServerDisconnected: "서버와의 접속이 끊겼을 때"
|
||||
disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
|
||||
reload: "새로고침"
|
||||
|
@ -992,8 +993,8 @@ cannotBeChangedLater: "나중에 변경할 수 없습니다."
|
|||
reactionAcceptance: "리액션 수신"
|
||||
likeOnly: "좋아요만 받기"
|
||||
likeOnlyForRemote: "리모트에서는 좋아요만 받기"
|
||||
nonSensitiveOnly: "열람 주의로 설정되지 않았을 때만 받기"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "열람 주의로 설정되지 않았을 때만 받기 (리모트에서는 좋아요만 받기)"
|
||||
nonSensitiveOnly: "민감한 이모지를 제외하고 받기"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "민감한 이모지를 제외하고 받기 (리모트에서는 좋아요만 받기)"
|
||||
rolesAssignedToMe: "나에게 할당된 역할"
|
||||
resetPasswordConfirm: "비밀번호를 재설정하시겠습니까?"
|
||||
sensitiveWords: "민감한 단어"
|
||||
|
@ -1062,6 +1063,9 @@ later: "나중에"
|
|||
goToMisskey: "Misskey로"
|
||||
additionalEmojiDictionary: "이모지 추가 사전"
|
||||
installed: "설치됨"
|
||||
branding: "브랜딩"
|
||||
enableServerMachineStats: "서버의 머신 사양을 공개하기"
|
||||
enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "계정 생성이 완료되었습니다!"
|
||||
letsStartAccountSetup: "계정의 초기 설정을 진행합니다."
|
||||
|
@ -1485,10 +1489,6 @@ _aboutMisskey:
|
|||
donate: "Misskey에 기부하기"
|
||||
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
|
||||
patrons: "후원자"
|
||||
_nsfw:
|
||||
respect: "열람주의로 설정된 미디어 숨기기"
|
||||
ignore: "열람 주의 미디어 항상 표시"
|
||||
force: "미디어 항상 숨기기"
|
||||
_instanceTicker:
|
||||
none: "보이지 않음"
|
||||
remote: "리모트 유저에게만 보이기"
|
||||
|
|
|
@ -248,7 +248,6 @@ inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
|
|||
inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
|
||||
circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
|
||||
rename: "ປ່ຽນຊື່"
|
||||
nsfw: "NSFW"
|
||||
watch: "ເບິ່ງ"
|
||||
unwatch: "ຢຸດເບິ່ງ"
|
||||
accept: "ອະນຸຍາດ"
|
||||
|
|
|
@ -296,7 +296,6 @@ copyUrl: "URL kopiëren"
|
|||
rename: "Hernoemen"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken"
|
||||
disconnectedFromServer: "Verbinding met de server onderbroken."
|
||||
reload: "Verversen"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
|
@ -299,7 +299,6 @@ copyUrl: "Skopiuj adres URL"
|
|||
rename: "Zmień nazwę"
|
||||
avatar: "Awatar"
|
||||
banner: "Baner"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Po utracie połączenia z serwerem"
|
||||
disconnectedFromServer: "Utracono połączenie z serwerem."
|
||||
reload: "Odśwież"
|
||||
|
@ -955,10 +954,6 @@ _aboutMisskey:
|
|||
donate: "Przekaż darowiznę na Misskey"
|
||||
morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób. Dziękuję! 🥰"
|
||||
patrons: "Wspierający"
|
||||
_nsfw:
|
||||
respect: "Ukrywaj media NSFW"
|
||||
ignore: "Nie ukrywaj mediów NSFW"
|
||||
force: "Ukrywaj wszystkie media"
|
||||
_instanceTicker:
|
||||
none: "Nigdy nie pokazuj"
|
||||
remote: "Pokaż dla zdalnych użytkowników"
|
||||
|
|
|
@ -294,7 +294,6 @@ copyUrl: "Copiar URL"
|
|||
rename: "Renomear"
|
||||
avatar: "Avatar"
|
||||
banner: "Capa"
|
||||
nsfw: "Conteúdo sensível"
|
||||
whenServerDisconnected: "Quando a conexão com o servidor é perdida"
|
||||
disconnectedFromServer: "Desconectado do servidor"
|
||||
reload: "Recarregar"
|
||||
|
|
|
@ -294,7 +294,6 @@ copyUrl: "Copiază URL"
|
|||
rename: "Redenumește"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Când pierzi conexiunea cu serverul"
|
||||
disconnectedFromServer: "Conecțiunea cu serverul a fost pierdută"
|
||||
reload: "Reîncarcă"
|
||||
|
|
|
@ -52,6 +52,8 @@ addToList: "Добавить в список"
|
|||
sendMessage: "Отправить сообщение"
|
||||
copyRSS: "Скопировать RSS"
|
||||
copyUsername: "Скопировать имя пользователя"
|
||||
copyUserId: "Скопировать ID пользователя"
|
||||
copyNoteId: "Скопировать ID заметки"
|
||||
searchUser: "Поиск людей"
|
||||
reply: "Ответить"
|
||||
loadMore: "Показать еще"
|
||||
|
@ -309,7 +311,6 @@ copyUrl: "Копировать ссылку"
|
|||
rename: "Переименовать"
|
||||
avatar: "Аватар"
|
||||
banner: "Шапка"
|
||||
nsfw: "Содержимое не для всех"
|
||||
whenServerDisconnected: "Когда соединение с сервером потеряно"
|
||||
disconnectedFromServer: "Разорвано соединение с сервером"
|
||||
reload: "Перезагрузить"
|
||||
|
@ -790,6 +791,7 @@ noMaintainerInformationWarning: "Не заполнены сведения об
|
|||
noBotProtectionWarning: "Ботозащита не настроена"
|
||||
configure: "Настроить"
|
||||
postToGallery: "Опубликовать в галерею"
|
||||
postToHashtag: "Опубликовать пост с этим хештегом"
|
||||
gallery: "Галерея"
|
||||
recentPosts: "Недавние публикации"
|
||||
popularPosts: "Популярные публикации"
|
||||
|
@ -1395,10 +1397,6 @@ _aboutMisskey:
|
|||
donate: "Пожертвование на Misskey"
|
||||
morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте! 🥰"
|
||||
patrons: "Материальная поддержка"
|
||||
_nsfw:
|
||||
respect: "Скрывать содержимое не для всех"
|
||||
ignore: "Показывать содержимое не для всех"
|
||||
force: "Скрывать вообще все файлы"
|
||||
_instanceTicker:
|
||||
none: "Не показывать"
|
||||
remote: "Только для других сайтов"
|
||||
|
|
|
@ -303,7 +303,6 @@ copyUrl: "Kopírovať URL"
|
|||
rename: "Premenovať"
|
||||
avatar: "Avatar"
|
||||
banner: "BAnner"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Keď sa stratí spojenie so serverom"
|
||||
disconnectedFromServer: "Spojenie so serverom bolo prerušené"
|
||||
reload: "Obnoviť"
|
||||
|
@ -1009,10 +1008,6 @@ _aboutMisskey:
|
|||
donate: "Podporiť Misskey"
|
||||
morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení. Ďakujeme! 🥰"
|
||||
patrons: "Prispievatelia"
|
||||
_nsfw:
|
||||
respect: "Skryť NSFW médiá"
|
||||
ignore: "Neskrývať NSFW médiá"
|
||||
force: "Skryť všetky médiá"
|
||||
_instanceTicker:
|
||||
none: "Nikdy nezobrazovať"
|
||||
remote: "Zobraziť pre vzdialených používateľov"
|
||||
|
|
|
@ -51,6 +51,8 @@ addToList: "Lägg till i lista"
|
|||
sendMessage: "Skicka ett meddelande"
|
||||
copyRSS: "Kopiera RSS"
|
||||
copyUsername: "Kopiera användarnamn"
|
||||
copyUserId: "Kopiera användar-ID"
|
||||
copyNoteId: "Kopiera noter-ID"
|
||||
searchUser: "Sök användare"
|
||||
reply: "Svara"
|
||||
loadMore: "Ladda mer"
|
||||
|
@ -103,6 +105,7 @@ renoted: "Omnoterad."
|
|||
cantRenote: "Inlägget kunde inte bli omnoterat."
|
||||
cantReRenote: "En omnotering kan inte bli omnoterad."
|
||||
quote: "Citat"
|
||||
inChannelRenote: "Omnotera inom kanalen"
|
||||
pinnedNote: "Fästad not"
|
||||
pinned: "Fäst till profil"
|
||||
you: "Du"
|
||||
|
@ -129,7 +132,10 @@ unblockConfirm: "Är du säkert att du vill avblockera kontot?"
|
|||
suspendConfirm: "Är du säker att du vill suspendera detta konto?"
|
||||
unsuspendConfirm: "Är du säker att du vill avsuspendera detta konto?"
|
||||
selectList: "Välj lista"
|
||||
editList: "Redigera lista"
|
||||
selectChannel: "Välj en kanal"
|
||||
selectAntenna: "Välj en antenn"
|
||||
editAntenna: "Redigera en antenn"
|
||||
selectWidget: "Välj en widget"
|
||||
editWidgets: "Redigera widgets"
|
||||
editWidgetsExit: "Avsluta redigering"
|
||||
|
@ -256,6 +262,9 @@ noMoreHistory: "Det finns ingen mer historik"
|
|||
startMessaging: "Starta en chatt"
|
||||
nUsersRead: "läst av {n}"
|
||||
agreeTo: "Jag accepterar {0}"
|
||||
agree: "Överens"
|
||||
termsOfService: "Användarvillkor"
|
||||
start: "Kom igång"
|
||||
home: "Hem"
|
||||
remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig."
|
||||
activity: "Aktivitet"
|
||||
|
@ -297,7 +306,6 @@ copyUrl: "Kopiera URL"
|
|||
rename: "Byt namn"
|
||||
avatar: "Profilbild"
|
||||
banner: "Banner"
|
||||
nsfw: "Känsligt innehåll"
|
||||
reload: "Ladda om"
|
||||
doNothing: "Ignorera"
|
||||
reloadConfirm: "Vill du ladda om tidslinjen?"
|
||||
|
@ -320,16 +328,22 @@ connectService: "Anslut"
|
|||
disconnectService: "Koppla från"
|
||||
enableLocalTimeline: "Aktivera lokal tidslinje"
|
||||
enableGlobalTimeline: "Aktivera global tidslinje"
|
||||
registration: "Registrera"
|
||||
enableRegistration: "Aktivera registrering av nya användare"
|
||||
invite: "Inbjudan"
|
||||
inMb: "I megabyte"
|
||||
iconUrl: "URL till profilbilden"
|
||||
bannerUrl: "URL till banner-bilden"
|
||||
pinnedNotes: "Fästad not"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "Aktivera hCaptcha"
|
||||
hcaptchaSiteKey: "Webbplatsnyckel"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Aktivera reCAPTCHA"
|
||||
enableTurnstile: "Aktivera Turnstile"
|
||||
antennas: "Antenner"
|
||||
manageAntennas: "Hantera Antenner"
|
||||
name: "Namn"
|
||||
antennaSource: "Antennkälla"
|
||||
antennaKeywords: "Nyckelord att lyssna efter"
|
||||
antennaExcludeKeywords: "Nyckelord att exkludera"
|
||||
|
@ -338,9 +352,14 @@ notifyAntenna: "Notifiera om nya noter"
|
|||
withFileAntenna: "Endast noter med filer"
|
||||
enableServiceworker: "Aktivera pushnotiser i denna webbläsaren"
|
||||
antennaUsersDescription: "Ange ett användarnamn per linje"
|
||||
notesAndReplies: "Inlägg och svar"
|
||||
silence: "Tystnad"
|
||||
recentlyUpdatedUsers: "Nyligen aktiva användare"
|
||||
recentlyRegisteredUsers: "Nyligen registrerade användare"
|
||||
exploreFediverse: "Utforska Fediverse"
|
||||
popularTags: "Populära taggar"
|
||||
userList: "Listor"
|
||||
about: "Om"
|
||||
aboutMisskey: "Om Misskey"
|
||||
administrator: "Administratör"
|
||||
passwordLessLogin: "Lösenordsfri inloggning"
|
||||
|
@ -348,29 +367,90 @@ passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en
|
|||
resetPassword: "Återställ Lösenord"
|
||||
newPasswordIs: "Det nya lösenordet är \"{password}\""
|
||||
share: "Dela"
|
||||
help: "Hjälp"
|
||||
close: "Stäng"
|
||||
invites: "Inbjudan"
|
||||
members: "Medlemmar"
|
||||
text: "Text"
|
||||
enable: "Aktivera"
|
||||
next: "Nästa"
|
||||
invitations: "Inbjudan"
|
||||
weakPassword: "Svagt Lösenord"
|
||||
normalPassword: "Medel Lösenord"
|
||||
strongPassword: "Starkt Lösenord"
|
||||
signinFailed: "Kan inte logga in. Det angivna användarnamnet eller lösenordet är felaktigt."
|
||||
or: "eller"
|
||||
language: "Språk"
|
||||
aboutX: "Om {x}"
|
||||
category: "Kategori"
|
||||
tags: "Taggar"
|
||||
createAccount: "Skapa ett konto"
|
||||
existingAccount: "Existerande konto"
|
||||
regenerate: "Regenerera"
|
||||
fontSize: "Textstorlek"
|
||||
openImageInNewTab: "Öppna bild i ny flik"
|
||||
clientSettings: "Klientinställningar"
|
||||
accountSettings: "Kontoinställningar"
|
||||
numberOfDays: "Antal dagar"
|
||||
deleteAll: "Radera alla"
|
||||
sounds: "Ljud"
|
||||
sound: "Ljud"
|
||||
listen: "Lyssna"
|
||||
none: "Ingen"
|
||||
volume: "Volym"
|
||||
chooseEmoji: "Välj en emoji"
|
||||
recentUsed: "Senast använd"
|
||||
install: "Installera"
|
||||
uninstall: "Avinstallera"
|
||||
menu: "Meny"
|
||||
serviceworkerInfo: "Måste vara aktiverad för pushnotiser."
|
||||
enableInfiniteScroll: "Ladda mer automatiskt"
|
||||
enablePlayer: "Öppna videospelare"
|
||||
permission: "Behörigheter"
|
||||
enableAll: "Aktivera alla"
|
||||
edit: "Ändra"
|
||||
enableEmail: "Aktivera epost-utskick"
|
||||
email: "E-post"
|
||||
smtpHost: "Värd"
|
||||
smtpUser: "Användarnamn"
|
||||
smtpPass: "Lösenord"
|
||||
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
|
||||
logs: "Logg"
|
||||
channel: "kanal"
|
||||
create: "Skapa"
|
||||
other: "Mer"
|
||||
send: "Skicka"
|
||||
openInNewTab: "Öppna i ny flik"
|
||||
createNew: "Skapa ny"
|
||||
i18nInfo: "Misskey översätts till många olika språk av volontärer. Du kan hjälpa till med översättningen på {link}."
|
||||
accountInfo: "Kontoinformation"
|
||||
clips: "Klipp"
|
||||
duplicate: "Duplicera"
|
||||
reloadToApplySetting: "Inställningen tillämpas efter sidan laddas om. Vill du göra det nu?"
|
||||
clearCache: "Rensa cache"
|
||||
onlineUsersCount: "{n} användare är online"
|
||||
nNotes: "{n} Noter"
|
||||
backgroundColor: "Bakgrundsbild"
|
||||
textColor: "Text"
|
||||
youAreRunningUpToDateClient: "Klienten du använder är uppdaterat."
|
||||
newVersionOfClientAvailable: "Ny version av klienten är tillgänglig."
|
||||
publish: "Publicera"
|
||||
typingUsers: "{users} skriver"
|
||||
info: "Om"
|
||||
enabled: "Aktiverad"
|
||||
user: "Användare"
|
||||
customCssWarn: "Den här inställningen borde bara ändrats av en som har rätta kunskaper. Om du ställer in det här fel så kan klienten sluta fungera rätt."
|
||||
global: "Global"
|
||||
squareAvatars: "Visa fyrkantiga profilbilder"
|
||||
sent: "Skicka"
|
||||
misskeyUpdated: "Misskey har uppdaterats!"
|
||||
incorrectPassword: "Fel lösenord."
|
||||
welcomeBackWithName: "Välkommen tillbaka, {name}"
|
||||
clickToFinishEmailVerification: "Tryck på [{ok}] för att slutföra bekräftelsen på e-postadressen."
|
||||
searchByGoogle: "Sök"
|
||||
file: "Filer"
|
||||
cannotUploadBecauseNoFreeSpace: "Kan inte ladda upp filen för att det finns inget lagringsutrymme kvar."
|
||||
cannotUploadBecauseExceedsFileSizeLimit: "Kan inte ladda upp filen för att den är större än filstorleksgränsen."
|
||||
enableAutoSensitive: "Automatisk NSFW markering"
|
||||
enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen."
|
||||
pushNotification: "Pushnotiser"
|
||||
|
@ -381,12 +461,15 @@ pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för
|
|||
windowMaximize: "Maximera"
|
||||
windowMinimize: "Minimera"
|
||||
windowRestore: "Återställ"
|
||||
pleaseDonate: "Misskey är en gratis programvara som används på {host}. Donera gärna för att göra utvecklingen ständigt, tack!"
|
||||
resetPasswordConfirm: "Återställ verkligen ditt lösenord?"
|
||||
_achievements:
|
||||
_types:
|
||||
_open3windows:
|
||||
title: "Flera Fönster"
|
||||
description: "Ha minst 3 fönster öppna samtidigt"
|
||||
_ffVisibility:
|
||||
public: "Publicera"
|
||||
_email:
|
||||
_follow:
|
||||
title: "följde dig"
|
||||
|
@ -426,6 +509,7 @@ _visibility:
|
|||
home: "Hem"
|
||||
followers: "Följare"
|
||||
_profile:
|
||||
name: "Namn"
|
||||
username: "Användarnamn"
|
||||
changeAvatar: "Ändra profilbild"
|
||||
changeBanner: "Ändra banner"
|
||||
|
@ -461,6 +545,8 @@ _deck:
|
|||
tl: "Tidslinje"
|
||||
antenna: "Antenner"
|
||||
list: "Listor"
|
||||
channel: "kanal"
|
||||
mentions: "Omnämningar"
|
||||
_webhookSettings:
|
||||
name: "Namn"
|
||||
active: "Aktiverad"
|
||||
|
|
|
@ -52,6 +52,8 @@ addToList: "เพิ่มในลิสต์"
|
|||
sendMessage: "ส่งข้อความ"
|
||||
copyRSS: "คัดลอก RSS"
|
||||
copyUsername: "คัดลอกชื่อผู้ใช้"
|
||||
copyUserId: "คัดลอก ID ผู้ใช้"
|
||||
copyNoteId: "คัดลอก ID โน้ต "
|
||||
searchUser: "ค้นหาผู้ใช้งาน"
|
||||
reply: "ตอบกลับ"
|
||||
loadMore: "โหลดเพิ่มเติม"
|
||||
|
@ -73,8 +75,8 @@ unfollowConfirm: "นายแน่ใจแล้วหรอว่าต้
|
|||
exportRequested: "เมื่อคุณได้ร้องขอการส่งออก อาจจะต้องใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว"
|
||||
importRequested: "เมื่อคุณได้ร้องขอการนำเข้า อาจจะต้องใช้เวลาสักครู่นะ"
|
||||
lists: "รายการ"
|
||||
noLists: "คุณไม่มีลิสต์ใดๆนะ"
|
||||
note: "ตัวโน้ต"
|
||||
noLists: "คุณไม่มีลิสต์ใด ๆ"
|
||||
note: " โน้ต"
|
||||
notes: "ตัวโน้ต"
|
||||
following: "กำลังติดตาม"
|
||||
followers: "ผู้ติดตาม"
|
||||
|
@ -100,7 +102,7 @@ followRequestPending: "กำลังรอดำเนินการร้อ
|
|||
enterEmoji: "ใส่อีโมจิ"
|
||||
renote: "รีโน้ต"
|
||||
unrenote: "เลิกรีโน้ต"
|
||||
renoted: "รีโน้ตแล้วนะ"
|
||||
renoted: "รีโน้ตแล้ว"
|
||||
cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตไว้ใหม่ได้นะ"
|
||||
cantReRenote: "ไม่สามารถรีโน้ตเอาไว้ใหม่ได้นะ"
|
||||
quote: "อ้างคำพูด"
|
||||
|
@ -108,7 +110,7 @@ inChannelRenote: "รีโน้ตช่องแชลแนลเท่า
|
|||
inChannelQuote: "อ้างช่องเท่านั้น"
|
||||
pinnedNote: "โน้ตที่ปักหมุดเอาไว้"
|
||||
pinned: "ปักหมุดไปยังโปรไฟล์"
|
||||
you: "ตัวเอง"
|
||||
you: "คุณ"
|
||||
clickToShow: "คลิกเพื่อแสดง"
|
||||
sensitive: "เนื้อหาที่ละเอียดอ่อน NSFW"
|
||||
add: "เพิ่ม"
|
||||
|
@ -134,8 +136,10 @@ unblockConfirm: "คุณแน่ใจแล้วเหรอ? ว่าต
|
|||
suspendConfirm: "นายแน่ใจแล้วเหรอว่าต้องการระงับบัญชีนี้อ่ะ?"
|
||||
unsuspendConfirm: "นายแน่ใจแล้วหรอ? ว่าต้องการยกเลิกการระงับบัญชีนี้"
|
||||
selectList: "เลือกรายการ"
|
||||
editList: "แก้ไขรายการ"
|
||||
selectChannel: "เลือกแชนแนล"
|
||||
selectAntenna: "เลือกเสาอากาศ"
|
||||
editAntenna: "แก้ไขเสาอากาศ"
|
||||
selectWidget: "เลือกวิดเจ็ต"
|
||||
editWidgets: "แก้ไขวิดเจ็ต"
|
||||
editWidgetsExit: "เรียบร้อย"
|
||||
|
@ -164,7 +168,7 @@ wallpaper: "วอลล์เปเปอร์"
|
|||
setWallpaper: "ตั้งวอลเปเปอร์"
|
||||
removeWallpaper: "นำวอลเปเปอร์ออก"
|
||||
searchWith: "ค้นหา: {q}"
|
||||
youHaveNoLists: "รายการนี้ว่างเปล่า"
|
||||
youHaveNoLists: "คุณไม่มีลิสต์ใด ๆ "
|
||||
followConfirm: "คุณแน่ใจแล้วหรอว่าต้องการที่จะติดตาม {name}?"
|
||||
proxyAccount: "บัญชี พร็อกซี่"
|
||||
proxyAccountDescription: "บัญชีพร็อกซี่ คือ บัญชีที่จะทำหน้าที่เป็นผู้ติดตามระยะไกลสำหรับผู้ใช้งานที่อยู่ภายใต้ด้วยเงื่อนไขบางอย่าง ยกตัวอย่าง เช่น เมื่อมีผู้ใช้งานนั้นได้เพิ่มผู้ใช้งานจากระยะไกลลงในรายการ แต่กิจกรรมของผู้ใช้ในระยะไกลนั้นจะไม่ถูกส่งไปยังอินสแตนซ์หากไม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัญชีพร็อกซีนี้จะติดตามแทน"
|
||||
|
@ -186,7 +190,7 @@ blockThisInstance: "บล็อกอินสแตนซ์นี้"
|
|||
operations: "ดำเนินการ"
|
||||
software: "ซอฟต์แวร์"
|
||||
version: "เวอร์ชั่น"
|
||||
metadata: "ข้อมูลเมตา"
|
||||
metadata: "Metadata"
|
||||
withNFiles: "{n} ไฟล์(s)"
|
||||
monitor: "มอนิเตอร์"
|
||||
jobQueue: "คิวงาน"
|
||||
|
@ -213,7 +217,7 @@ intro: "การติดตั้ง Misskey เสร็จสิ้นแล
|
|||
done: "เสร็จสิ้น"
|
||||
processing: "กำลังประมวลผล..."
|
||||
preview: "แสดงตัวอย่าง"
|
||||
default: "ค่าตั้งต้น"
|
||||
default: "ค่าเริ่มต้น"
|
||||
defaultValueIs: "ค่าเริ่มต้น: {value}"
|
||||
noCustomEmojis: "ไม่มีอีโมจิ"
|
||||
noJobs: "ไม่มีชิ้นงาน"
|
||||
|
@ -234,10 +238,10 @@ currentPassword: "รหัสผ่านปัจจุบัน"
|
|||
newPassword: "รหัสผ่านใหม่"
|
||||
newPasswordRetype: "ใส่รหัสผ่านใหม่อีกครั้ง"
|
||||
attachFile: "แนบไฟล์"
|
||||
more: "เพิ่มเติม!"
|
||||
more: "เพิ่มเติม"
|
||||
featured: "ไฮไลท์"
|
||||
usernameOrUserId: "ชื่อผู้ใช้หรือรหัสผู้ใช้งาน"
|
||||
noSuchUser: "ไม่มีผู้ใช้นี้อยู่ในระบบ"
|
||||
noSuchUser: "ไม่พบผู้ใช้"
|
||||
lookup: "การค้นหา"
|
||||
announcements: "ประกาศ"
|
||||
imageUrl: "url รูปภาพ"
|
||||
|
@ -282,7 +286,7 @@ themeForLightMode: "ธีมที่จะใช้ในโหมดแสง
|
|||
themeForDarkMode: "ธีมที่จะใช้ในโหมดมืด"
|
||||
light: "สว่าง"
|
||||
dark: "มืด"
|
||||
lightThemes: "ธีมสีสว่าง"
|
||||
lightThemes: "ธีมสว่าง"
|
||||
darkThemes: "ธีมมืด"
|
||||
syncDeviceDarkMode: "ซิงค์โหมดมืดด้วยการตั้งค่ากับอุปกรณ์"
|
||||
drive: "ไดรฟ์"
|
||||
|
@ -309,7 +313,6 @@ copyUrl: "คัดลอก URL"
|
|||
rename: "เปลี่ยนชื่อ"
|
||||
avatar: "ไอคอน"
|
||||
banner: "แบนเนอร์"
|
||||
nsfw: "เนื้อหาที่ละเอียดอ่อน NSFW"
|
||||
whenServerDisconnected: "สูญเสียการเชื่อมต่อกับเซิร์ฟเวอร์"
|
||||
disconnectedFromServer: "ถูกตัดการเชื่อมต่อออกจากเซิร์ฟเวอร์"
|
||||
reload: "รีโหลด"
|
||||
|
@ -319,7 +322,7 @@ watch: "ดู"
|
|||
unwatch: "หยุดดู"
|
||||
accept: "ยอมรับ"
|
||||
reject: "ปฏิเสธ"
|
||||
normal: "โหมดปกติ"
|
||||
normal: "ปกติ"
|
||||
instanceName: "ชื่อ อินสแตนซ์"
|
||||
instanceDescription: "คำอธิบายอินสแตนซ์"
|
||||
maintainerName: "ผู้ดูแล"
|
||||
|
@ -790,6 +793,7 @@ noMaintainerInformationWarning: "ข้อมูลผู้ดูแลไม
|
|||
noBotProtectionWarning: "ไม่ได้กำหนดค่าการป้องกันบอทนะ"
|
||||
configure: "กำหนดค่า"
|
||||
postToGallery: "สร้างโพสต์แกลเลอรี่ใหม่"
|
||||
postToHashtag: "โพสต์ไปที่แฮชแท็กนี้"
|
||||
gallery: "แกลเลอรี่"
|
||||
recentPosts: "โพสต์ล่าสุด"
|
||||
popularPosts: "โพสต์ติดอันดับ"
|
||||
|
@ -823,6 +827,7 @@ translatedFrom: "แปลมาจาก {x}"
|
|||
accountDeletionInProgress: "กำลังดำเนินการลบบัญชีอยู่"
|
||||
usernameInfo: "ชื่อที่ระบุบัญชีของคุณจากผู้อื่นในเซิร์ฟเวอร์นี้ คุณสามารถใช้ตัวอักษร (a~z, A~Z), ตัวเลข (0~9) หรือขีดล่าง (_) ชื่อผู้ใช้ไม่สามารถเปลี่ยนแปลงได้ในภายหลัง"
|
||||
aiChanMode: "โหมด Ai "
|
||||
devMode: "โหมดนักพัฒนา"
|
||||
keepCw: "เก็บคำเตือนเนื้อหา"
|
||||
pubSub: "บัญชีผับ/ย่อย"
|
||||
lastCommunication: "การสื่อสารครั้งสุดท้ายล่าสุด"
|
||||
|
@ -832,6 +837,8 @@ breakFollow: "ลบผู้ติดตาม"
|
|||
breakFollowConfirm: "ลบผู้ติดตามนี้ออกจริงหรอ?"
|
||||
itsOn: "เปิดใช้งาน"
|
||||
itsOff: "ปิดใช้งาน"
|
||||
on: "เปิด"
|
||||
off: "ปิด"
|
||||
emailRequiredForSignup: "จำเป็นต้องการใช้ที่อยู่อีเมลสำหรับการสมัคร"
|
||||
unread: "ไม่ได้อ่าน"
|
||||
filter: "กรอง"
|
||||
|
@ -850,7 +857,7 @@ incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
|
|||
voteConfirm: "ยืนยันการโหวต \"{choice}\" มั้ย?"
|
||||
hide: "ซ่อน"
|
||||
useDrawerReactionPickerForMobile: "แสดงผล ตัวเลือกปฏิกิริยาเป็นลิ้นชักบนมือถือ"
|
||||
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะค่ะ, {name}"
|
||||
welcomeBackWithName: "ยินดีต้อนรับการกลับมานะคะ, {name}"
|
||||
clickToFinishEmailVerification: "กรุณาคลิก [{ok}] เพื่อดำเนินการยืนยันอีเมลให้เสร็จสมบูรณ์นะ"
|
||||
overridedDeviceKind: "ประเภทอุปกรณ์"
|
||||
smartphone: "สมาร์ทโฟน"
|
||||
|
@ -955,7 +962,7 @@ color: "สี"
|
|||
manageCustomEmojis: "จัดการอีโมจิแบบกำหนดเอง"
|
||||
youCannotCreateAnymore: "คุณถึงขีดจํากัดการสร้างแล้วนะ"
|
||||
cannotPerformTemporary: "ไม่สามารถใช้การได้ชั่วคราว"
|
||||
cannotPerformTemporaryDescription: "การดําเนินการนี้ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้งนะค่ะ"
|
||||
cannotPerformTemporaryDescription: "ไม่สามารถดําเนินการได้ชั่วคราว เนื่องจากเกินขีดจํากัดการดําเนินการ กรุณารอสักครู่แล้วลองใหม่อีกครั้ง"
|
||||
invalidParamError: "ข้อผิดพลาดพารามิเตอร์"
|
||||
invalidParamErrorDescription: "คำขอพารามิเตอร์ไม่ถูกต้อง สิ่งนี้มักจะเกิดจากข้อผิดพลาด แต่อาจเกิดจากอินพุตเกินขีดจำกัดของขนาดหรือที่คล้ายกัน"
|
||||
permissionDeniedError: "การดำเนินถูกปฏิเสธ"
|
||||
|
@ -977,20 +984,23 @@ joinThisServer: "ลงชื่อสมัครใช้ในอินสแ
|
|||
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
|
||||
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
|
||||
disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรอแน่ใจแล้วนะ?"
|
||||
disableFederationConfirmWarn: "แม้ว่าจะถูกยกเลิกเอาไว้โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป เว้นแต่ว่า...จะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องทำตรงนี้หรอกนะค่ะ"
|
||||
disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น"
|
||||
disableFederationOk: "ปิดการใช้งาน"
|
||||
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
|
||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
||||
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญ เพื่องลงทะเบียนเข้าใช้งาน"
|
||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมล"
|
||||
postToTheChannel: "โพสต์ลงช่อง"
|
||||
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
||||
reactionAcceptance: "การยอมรับรีแอคชั่น"
|
||||
likeOnly: "ที่ชอบเท่านั้น"
|
||||
likeOnlyForRemote: "ไลค์สำหรับอินสแตนซ์ระยะไกลเท่านั้น"
|
||||
nonSensitiveOnly: "ไม่มีความอ่อนไหวเท่านั้น"
|
||||
nonSensitiveOnlyForLocalLikeOnlyForRemote: "ไม่มีความอ่อนไหวเท่านั้น (เฉพาะไลค์จากระยะไกลเท่านั้น)"
|
||||
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
|
||||
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
|
||||
sensitiveWords: "คำที่ละเอียดอ่อน"
|
||||
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
|
||||
sensitiveWordsDescription2: "การใช้ช่องว่างนั้นอาจจะสร้างนิพจน์ AND และคำหลักที่มีเครื่องหมายทับล้อมรอบจะเปลี่ยนเป็นนิพจน์ทั่วไปนะ"
|
||||
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งาน"
|
||||
license: "ใบอนุญาต"
|
||||
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||
myClips: "คลิปของฉัน"
|
||||
|
@ -1025,29 +1035,60 @@ vertical: "แนวตั้ง"
|
|||
horizontal: "ด้านข้าง"
|
||||
position: "ตำแหน่ง"
|
||||
serverRules: "กฎของเซิฟเวอร์"
|
||||
pleaseConfirmBelowBeforeSignup: "โปรดยืนยันด้านล่างก่อนกำลังลงชื่อสมัครนะค่ะ"
|
||||
pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างก่อนสมัครใช้งาน"
|
||||
pleaseAgreeAllToContinue: "คุณต้องยอมรับทุกช่องตรงด้านบนเพื่อดำเนินการต่อค่ะ"
|
||||
continue: "ดำเนินการต่อ"
|
||||
preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว้"
|
||||
preservedUsernamesDescription: "ลิสต์ชื่อผู้ใช้ที่จะสำรองโดยคั่นด้วยการแบ่งบรรทัดนั้น เพราะสิ่งเหล่านี้จะไม่สามารถทำได้ในระหว่างการสร้างบัญชีตามปกติ บัญชีที่มีอยู่แล้วนั้นโดยใช้ชื่อผู้ใช้เหล่านี้จะไม่ได้รับผลกระทบอะไร"
|
||||
createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้"
|
||||
archive: "เก็บถาวร"
|
||||
channelArchiveConfirmTitle: "เก็บถาวรจริงๆ {name} มั้ย?"
|
||||
thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ"
|
||||
displayOfNote: "การแสดงโน้ต"
|
||||
initialAccountSetting: "ตั้งค่าโปรไฟล์"
|
||||
youFollowing: "ติดตามแล้ว"
|
||||
preventAiLearning: "ปฏิเสธการใช้งาน ในการเรียนรู้ของเครื่อง (Generative AI)"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
specifyUser: "ผู้ใช้เฉพาะ"
|
||||
failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้"
|
||||
update: "อัปเดต"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้อิโมจินี้เป็นรีแอคชั่นได้"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "บทบาทเหล่านี้ต้องเป็นสาธารณะ"
|
||||
cancelReactionConfirm: "ต้องการลบรีแอคชั่นของคุณจริงๆหรอ?"
|
||||
changeReactionConfirm: "ต้องการเปลี่ยนรีแอคชั่นของคุณจริงๆหรอ?"
|
||||
later: "ไว้ทีหลัง"
|
||||
goToMisskey: "ถึง Misskey"
|
||||
additionalEmojiDictionary: "พจนานุกรมอีโมจิเพิ่มเติม"
|
||||
installed: "ติดตั้งแล้ว"
|
||||
branding: "แบรนดิ้ง"
|
||||
enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์"
|
||||
enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ"
|
||||
turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!"
|
||||
letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ"
|
||||
letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ"
|
||||
profileSetting: "ตั้งค่าโปรไฟล์"
|
||||
privacySetting: "ตั้งค่าความเป็นส่วนตัว"
|
||||
theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
|
||||
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
|
||||
haveFun: "สนุกกับ {name}!"
|
||||
skipAreYouSure: "ต้องการข้ามการตั้งค่าโปรไฟล์จริงๆแบบนั้นหรอ?"
|
||||
laterAreYouSure: "ต้องการตั้งค่าโปรไฟล์ในภายหลังจริงๆอย่างงั้นหรอ?"
|
||||
_serverRules:
|
||||
description: "ชุดของกฎที่จะแสดงก่อนการลงทะเบียนเราขอแนะนำให้ตั้งค่าสรุปข้อกำหนดในการให้บริการ"
|
||||
_accountMigration:
|
||||
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง"
|
||||
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
|
||||
moveFromLabel: "บัญชีที่จะย้ายจาก:"
|
||||
moveFromDescription: "สร้างนามแฝงสำหรับบัญชีที่จะย้ายจากบัญชีนี้ ถ้าหากคุณต้องการโอนผู้ติดตาม สิ่งนี้ต้องทำก่อนโอนก่อนนะค่ะ! หลังจากนั้น ป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
|
||||
moveFromDescription: "ถ้าหากคุณต้องการโอนข้อมูล คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี หลังจากนั้นป้อนบัญชีที่จะย้ายไปในรูปแบบต่อไปนี้: @person@instance.com"
|
||||
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง"
|
||||
moveToLabel: "บัญชีที่จะย้ายไปที่:"
|
||||
moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้"
|
||||
moveAccountDescription: "การกระทำนี้ไม่สามารถย้อนกลับได้นะ ขั้นตอนแรก ต้องสร้างนามแฝงสำหรับบัญชีนี้ในบัญชีที่คุณต้องการย้ายไป หลังจากนั้นแล้ว ป้อนบัญชีที่จะย้ายไปในรูปแบบดังต่อไปนี้: @person@instance.com"
|
||||
moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com"
|
||||
startMigration: "โอนย้าย"
|
||||
migrationConfirm: "ย้ายข้อมูลบัญชีนี้ไปที่ {account} จริงๆนะ เมื่อมีการเริ่มต้นแล้ว กระบวนการนี้จะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ เพื่อให้แน่ใจยืนยันว่าคุณได้สร้างนามแฝงในบัญชีที่จะย้ายข้อมูลนะค่ะ"
|
||||
migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี"
|
||||
movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้"
|
||||
postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป"
|
||||
movedTo: "บัญชีที่จะย้ายไปที่:"
|
||||
|
@ -1223,7 +1264,7 @@ _achievements:
|
|||
title: "พักผ่อนสักหน่อย"
|
||||
description: "ใช้เวลา 30 นาทีบน Misskey"
|
||||
_client60min:
|
||||
title: "ไม่มี \"Miss\" ใน Misskey นะค่ะ !"
|
||||
title: "ไม่พบ \"Miss\" ใน Misskey "
|
||||
description: "เปิด Misskey ค้างไว้แล้วอย่างน้อย 60 นาที"
|
||||
_noteDeletedWithin1min:
|
||||
title: "ไม่เป็นไร"
|
||||
|
@ -1391,6 +1432,7 @@ _ad:
|
|||
back: "ย้อนกลับ"
|
||||
reduceFrequencyOfThisAd: "แสดงโฆษณานี้ให้น้อยลง"
|
||||
hide: "ไม่ต้องแสดง"
|
||||
timezoneinfo: "วันในสัปดาห์นี้จะถูกกำหนดจากโซนเวลาของเซิร์ฟเวอร์"
|
||||
_forgotPassword:
|
||||
enterEmail: "ป้อนที่อยู่อีเมลที่คุณเคยใช้ในการลงทะเบียนไว้ ลิงก์ที่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูกส่งไปนะ"
|
||||
ifNoEmail: "ถ้าหากคุณไม่ได้ใช้อีเมลระหว่างการลงทะเบียน กรุณาติดต่อผู้ดูแลระบบอินสแตนซ์แทนนะ"
|
||||
|
@ -1442,10 +1484,6 @@ _aboutMisskey:
|
|||
donate: "บริจาคให้กับ Misskey"
|
||||
morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ ขอขอบคุณ! 🥰"
|
||||
patrons: "สมาชิกพันธมิตร"
|
||||
_nsfw:
|
||||
respect: "ซ่อนสื่อ NSFW"
|
||||
ignore: "อย่าซ่อนสื่อ NSFW"
|
||||
force: "ซ่อนสื่อทั้งหมด"
|
||||
_instanceTicker:
|
||||
none: "ไม่ต้องแสดง"
|
||||
remote: "แสดงสำหรับผู้ใช้ระยะไกล"
|
||||
|
@ -1585,9 +1623,11 @@ _time:
|
|||
day: "วัน"
|
||||
_timelineTutorial:
|
||||
title: "วิธีใช้งาน Misskey"
|
||||
step2_1: "มาลองโพสต์โน้ตต่อไปกัน คุณสามารถทำได้โดยการกดปุ่มที่มีไอคอนดินสอ"
|
||||
step2_2: "ยังไงไหนลองเขียนแนะนำตัวเองหรือแค่ \"สวัสดี {name}!\" ถ้าคุณไม่รู้สึกเหมือนมัน?"
|
||||
step3_1: "เสร็จสิ้นการโพสต์โน้ตย่อแรกของคุณแล้วอย่างงั้นหรอ?"
|
||||
step3_2: "ไชโย! ตอนนี้โน้ตย่อแรกของคุณได้ปรากฏบนไทม์ไลน์ของคุณแล้วนะ"
|
||||
step4_1: "คุณยังสามารถแนบ \"ปฏิกิริยา\" ไปกับโน้ตได้อีกด้วยนะค่ะ"
|
||||
step4_1: "คุณสามารถเพิ่ม \"การตอบสนอง\" ในโน้ตได้"
|
||||
step4_2: "หากต้องการแนบการแสดงความรู้สึก ให้กดเครื่องหมาย \"+\" บนโน้ตแล้วเลือกอิโมจิที่คุณต้องการแสดงความรู้สึกที่ตนเองชอบได้เลย"
|
||||
_2fa:
|
||||
alreadyRegistered: "คุณได้ลงทะเบียนอุปกรณ์ยืนยันตัวตนแบบ 2 ชั้นแล้ว"
|
||||
|
|
|
@ -300,7 +300,6 @@ copyUrl: "Копіювати URL"
|
|||
rename: "Перейменувати"
|
||||
avatar: "Аватар"
|
||||
banner: "Банер"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Коли зв’язок із сервером втрачено"
|
||||
disconnectedFromServer: "Зв’язок із сервером було перервано"
|
||||
reload: "Оновити"
|
||||
|
@ -1200,10 +1199,6 @@ _aboutMisskey:
|
|||
donate: "Пожертвувати Misskey"
|
||||
morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених тут. Дякуємо! 🥰"
|
||||
patrons: "Підтримали"
|
||||
_nsfw:
|
||||
respect: "Приховувати NSFW медіа"
|
||||
ignore: "Не приховувати NSFW медіа"
|
||||
force: "Приховувати всі медіа файли"
|
||||
_instanceTicker:
|
||||
none: "Не відображати"
|
||||
remote: "Відображати для віддалених користувачів"
|
||||
|
|
|
@ -303,7 +303,6 @@ copyUrl: "Sao chép URL"
|
|||
rename: "Đổi tên"
|
||||
avatar: "Ảnh đại diện"
|
||||
banner: "Ảnh bìa"
|
||||
nsfw: "Nhạy cảm"
|
||||
whenServerDisconnected: "Khi mất kết nối tới máy chủ"
|
||||
disconnectedFromServer: "Mất kết nối tới máy chủ"
|
||||
reload: "Tải lại"
|
||||
|
@ -1212,10 +1211,6 @@ _aboutMisskey:
|
|||
donate: "Ủng hộ Misskey"
|
||||
morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác không được liệt kê ở đây. Cảm ơn! 🥰"
|
||||
patrons: "Người ủng hộ"
|
||||
_nsfw:
|
||||
respect: "Ẩn nội dung NSFW"
|
||||
ignore: "Hiện nội dung NSFW"
|
||||
force: "Ẩn mọi media"
|
||||
_instanceTicker:
|
||||
none: "Không hiển thị"
|
||||
remote: "Hiện cho người dùng từ máy chủ khác"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -54,6 +54,8 @@ copyRSS: "複製RSS"
|
|||
copyUsername: "複製使用者名稱"
|
||||
copyUserId: "複製使用者ID"
|
||||
copyNoteId: "複製貼文ID"
|
||||
copyFileId: "複製檔案ID"
|
||||
copyFolderId: "複製資料夾ID"
|
||||
searchUser: "搜尋使用者"
|
||||
reply: "回覆"
|
||||
loadMore: "載入更多"
|
||||
|
@ -136,8 +138,10 @@ unblockConfirm: "確定解除封鎖此用戶?"
|
|||
suspendConfirm: "確定凍結此帳戶?"
|
||||
unsuspendConfirm: "確定解凍此帳戶?"
|
||||
selectList: "選擇清單"
|
||||
editList: "編輯清單"
|
||||
selectChannel: "選擇頻道"
|
||||
selectAntenna: "選擇天線"
|
||||
editAntenna: "編輯天線"
|
||||
selectWidget: "選擇小工具"
|
||||
editWidgets: "編輯小工具"
|
||||
editWidgetsExit: "完成"
|
||||
|
@ -311,7 +315,7 @@ copyUrl: "複製URL"
|
|||
rename: "重新命名"
|
||||
avatar: "大頭貼"
|
||||
banner: "橫幅"
|
||||
nsfw: "敏感內容"
|
||||
displayOfSensitiveMedia: "敏感性媒體的顯示"
|
||||
whenServerDisconnected: "與伺服器的連接中斷時"
|
||||
disconnectedFromServer: "與伺服器中斷連線"
|
||||
reload: "重新整理"
|
||||
|
@ -752,7 +756,7 @@ editCode: "編輯代碼"
|
|||
apply: "套用"
|
||||
receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
|
||||
emailNotification: "郵件通知"
|
||||
publish: "發佈"
|
||||
publish: "發布"
|
||||
inChannelSearch: "頻道内搜尋"
|
||||
useReactionPickerForContextMenu: "點擊右鍵開啟反應工具欄"
|
||||
typingUsers: "{users}輸入中..."
|
||||
|
@ -810,7 +814,7 @@ ratio: "%"
|
|||
previewNoteText: "預覽文本"
|
||||
customCss: "自定義 CSS"
|
||||
customCssWarn: "這個設定必須由具備相關知識的人員操作,不當的設定可能导致客戶端無法正常使用。"
|
||||
global: "公開"
|
||||
global: "全域"
|
||||
squareAvatars: "頭像以方形顯示"
|
||||
sent: "發送"
|
||||
received: "收取"
|
||||
|
@ -848,7 +852,7 @@ makeReactionsPublicDescription: "將您做過的反應設為公開可見。"
|
|||
classic: "經典"
|
||||
muteThread: "將貼文串設為靜音"
|
||||
unmuteThread: "將貼文串的靜音解除"
|
||||
ffVisibility: "連接的公開範圍"
|
||||
ffVisibility: "連繫的可見性"
|
||||
ffVisibilityDescription: "您可以設定您的關注/關注者資訊的公開範圍"
|
||||
continueThread: "查看更多貼文"
|
||||
deleteAccountConfirm: "將要刪除帳戶。是否確定?"
|
||||
|
@ -1053,8 +1057,8 @@ options: "選項"
|
|||
specifyUser: "指定使用者"
|
||||
failedToPreviewUrl: "無法預覽"
|
||||
update: "更新"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "可以當成反應使用的角色"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "如果是未指定角色的情況,則任何人都可以被當成反應來使用。"
|
||||
rolesThatCanBeUsedThisEmojiAsReaction: "可以用這個做為反應的角色"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "未指定角色的情況,則任何人都可以將它用做反應。"
|
||||
rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "角色必須是公開的角色。"
|
||||
cancelReactionConfirm: "要取消做出的反應嗎?"
|
||||
changeReactionConfirm: "要變更做出的反應嗎?"
|
||||
|
@ -1063,6 +1067,9 @@ goToMisskey: "往Misskey"
|
|||
additionalEmojiDictionary: "表情符號的附加辭典"
|
||||
installed: "已安裝"
|
||||
branding: "品牌宣傳"
|
||||
enableServerMachineStats: "公布伺服器的機器資訊"
|
||||
enableIdenticonGeneration: "啟用每個使用者的Identicon"
|
||||
turnOffToImprovePerformance: "關閉時會提高性能。"
|
||||
_initialAccountSetting:
|
||||
accountCreated: "帳戶已建立完成!"
|
||||
letsStartAccountSetup: "來進行帳戶的初始設定吧。"
|
||||
|
@ -1357,7 +1364,7 @@ _role:
|
|||
iconUrl: "圖示的URL"
|
||||
asBadge: "顯示為徽章"
|
||||
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
|
||||
isExplorable: "公開角色時間軸"
|
||||
isExplorable: "讓使用者更容易找到您"
|
||||
descriptionOfIsExplorable: "若開啟則公開角色時間軸。若角色不是公開的,則無法公開時間軸。"
|
||||
displayOrder: "顯示順序"
|
||||
descriptionOfDisplayOrder: "數字越大,顯示在UI上的越上面。"
|
||||
|
@ -1417,7 +1424,7 @@ _emailUnavailable:
|
|||
mx: "郵件伺服器不正確"
|
||||
smtp: "郵件伺服器沒有應答"
|
||||
_ffVisibility:
|
||||
public: "發佈"
|
||||
public: "公開"
|
||||
followers: "只有關注你的用戶能看到"
|
||||
private: "私密"
|
||||
_signup:
|
||||
|
@ -1435,6 +1442,7 @@ _ad:
|
|||
back: "返回"
|
||||
reduceFrequencyOfThisAd: "降低此廣告的頻率 "
|
||||
hide: "隱藏"
|
||||
timezoneinfo: "星期幾是由伺服器的時區指定的。"
|
||||
_forgotPassword:
|
||||
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
|
||||
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 "
|
||||
|
@ -1486,10 +1494,10 @@ _aboutMisskey:
|
|||
donate: "贊助Misskey"
|
||||
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
|
||||
patrons: "贊助者"
|
||||
_nsfw:
|
||||
respect: "隱藏敏感內容"
|
||||
ignore: "不隱藏敏感內容"
|
||||
force: "隱藏所有內容"
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "隱藏設定為敏感的媒體"
|
||||
ignore: "不隱藏設定為敏感的媒體"
|
||||
force: "隱藏所有媒體"
|
||||
_instanceTicker:
|
||||
none: "隱藏"
|
||||
remote: "向遠端使用者顯示"
|
||||
|
@ -1779,11 +1787,11 @@ _visibility:
|
|||
public: "公開"
|
||||
publicDescription: "發布給所有用戶 "
|
||||
home: "首頁"
|
||||
homeDescription: "僅發送至首頁的時間軸"
|
||||
homeDescription: "僅發布至首頁的時間軸"
|
||||
followers: "追隨者"
|
||||
followersDescription: "僅發送至關注者"
|
||||
followersDescription: "僅發布至關注者"
|
||||
specified: "指定使用者"
|
||||
specifiedDescription: "僅發送至指定使用者"
|
||||
specifiedDescription: "僅發布至指定使用者"
|
||||
disableFederation: "停用聯邦"
|
||||
disableFederationDescription: "不要傳遞給其他實例"
|
||||
_postForm:
|
||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "13.13.2",
|
||||
"version": "13.14.0-beta.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -25,7 +25,7 @@
|
|||
"migrateandstart": "pnpm migrate && pnpm start",
|
||||
"gulp": "pnpm exec gulp build",
|
||||
"watch": "pnpm dev",
|
||||
"dev": "node ./scripts/dev.js",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"lint": "pnpm -r lint",
|
||||
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "pnpm cypress run",
|
||||
|
@ -44,23 +44,23 @@
|
|||
"lodash": "4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "5.1.1",
|
||||
"execa": "7.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "5.1.3"
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.15.0",
|
||||
"eslint": "8.43.0",
|
||||
"cypress": "12.17.0",
|
||||
"eslint": "8.44.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"paths": {
|
||||
"@/*": ["*"]
|
||||
},
|
||||
"target": "es2021"
|
||||
"target": "es2022"
|
||||
},
|
||||
"minify": false
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,9 @@
|
|||
export class ad1677054292210 {
|
||||
name = 'ad1677054292210';
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "ad" ADD "dayOfWeek" integer NOT NULL Default 0`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "dayOfWeek"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
export class AddMetaOptions1688280713783 {
|
||||
name = 'AddMetaOptions1688280713783'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableServerMachineStats" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIdenticonGeneration" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIdenticonGeneration"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServerMachineStats"`);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@
|
|||
"main": "./index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||
|
@ -51,12 +54,12 @@
|
|||
"utf-8-validate": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.321.1",
|
||||
"@aws-sdk/lib-storage": "3.321.1",
|
||||
"@aws-sdk/node-http-handler": "3.321.1",
|
||||
"@bull-board/api": "5.5.3",
|
||||
"@bull-board/fastify": "5.5.3",
|
||||
"@bull-board/ui": "5.5.3",
|
||||
"@aws-sdk/client-s3": "3.367.0",
|
||||
"@aws-sdk/lib-storage": "3.367.0",
|
||||
"@aws-sdk/node-http-handler": "3.360.0",
|
||||
"@bull-board/api": "5.6.0",
|
||||
"@bull-board/fastify": "5.6.0",
|
||||
"@bull-board/ui": "5.6.0",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.2.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
|
@ -64,22 +67,23 @@
|
|||
"@fastify/http-proxy": "9.2.1",
|
||||
"@fastify/multipart": "7.7.0",
|
||||
"@fastify/static": "6.10.2",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "10.0.3",
|
||||
"@nestjs/core": "10.0.3",
|
||||
"@nestjs/testing": "10.0.3",
|
||||
"@fastify/view": "8.0.0",
|
||||
"@nestjs/common": "10.0.5",
|
||||
"@nestjs/core": "10.0.5",
|
||||
"@nestjs/testing": "10.0.5",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.3.0",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.66",
|
||||
"@swc/core": "1.3.68",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"async-mutex": "^0.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bullmq": "4.1.0",
|
||||
"cacheable-lookup": "6.1.0",
|
||||
"bullmq": "4.2.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.0",
|
||||
"chalk": "5.2.0",
|
||||
"chalk-template": "0.4.0",
|
||||
|
@ -90,18 +94,18 @@
|
|||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.18.0",
|
||||
"fastify": "4.19.2",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.5.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.6.0",
|
||||
"happy-dom": "9.20.3",
|
||||
"got": "13.0.0",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
"ipaddr.js": "2.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"is-svg": "5.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "22.1.0",
|
||||
"json5": "2.2.3",
|
||||
|
@ -118,9 +122,9 @@
|
|||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.1.2",
|
||||
"otpauth": "9.1.3",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.0",
|
||||
"pg": "8.11.1",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
|
@ -137,7 +141,6 @@
|
|||
"rxjs": "7.8.1",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.11.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.5.3",
|
||||
"sharp": "0.32.1",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
|
@ -145,14 +148,14 @@
|
|||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.18.4",
|
||||
"systeminformation": "5.18.6",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.6",
|
||||
"tsc-alias": "1.8.7",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.17",
|
||||
"typescript": "5.1.3",
|
||||
"typescript": "5.1.6",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.14",
|
||||
"uuid": "9.0.0",
|
||||
|
@ -162,7 +165,7 @@
|
|||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.5.0",
|
||||
"@jest/globals": "29.6.1",
|
||||
"@swc/jest": "0.2.26",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.2",
|
||||
|
@ -178,14 +181,15 @@
|
|||
"@types/jsonld": "1.5.9",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "20.3.1",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.8",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/qrcode": "1.5.1",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
"@types/redis": "4.0.11",
|
||||
|
@ -202,14 +206,14 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
"aws-sdk-client-mock": "2.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.43.0",
|
||||
"eslint": "8.44.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.5.0",
|
||||
"jest-mock": "29.5.0"
|
||||
"execa": "7.1.1",
|
||||
"jest": "29.6.1",
|
||||
"jest-mock": "29.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,12 +96,6 @@ function showNodejsVersion(): void {
|
|||
const nodejsLogger = bootLogger.createSubLogger('nodejs');
|
||||
|
||||
nodejsLogger.info(`Version ${process.version} detected.`);
|
||||
|
||||
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
|
||||
if (semver.lt(process.version, minVersion)) {
|
||||
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfigBoot(): Config {
|
||||
|
|
|
@ -295,7 +295,7 @@ export class AccountMoveService {
|
|||
* dstユーザーのalsoKnownAsをfetchPersonしていき、本当にmovedToUrlをdstに指定するユーザーが存在するのかを調べる
|
||||
*
|
||||
* @param dst movedToUrlを指定するユーザー
|
||||
* @param check
|
||||
* @param check
|
||||
* @param instant checkがtrueであるユーザーが最初に見つかったら即座にreturnするかどうか
|
||||
* @returns Promise<LocalUser | RemoteUser | null>
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,7 @@ import { dirname } from 'node:path';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as nsfw from 'nsfwjs';
|
||||
import si from 'systeminformation';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -17,6 +18,7 @@ let isSupportedCpu: undefined | boolean = undefined;
|
|||
@Injectable()
|
||||
export class AiService {
|
||||
private model: nsfw.NSFWJS;
|
||||
private modelLoadMutex: Mutex = new Mutex();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
|
@ -31,16 +33,22 @@ export class AiService {
|
|||
const cpuFlags = await this.getCpuFlags();
|
||||
isSupportedCpu = REQUIRED_CPU_FLAGS.every(required => cpuFlags.includes(required));
|
||||
}
|
||||
|
||||
|
||||
if (!isSupportedCpu) {
|
||||
console.error('This CPU cannot use TensorFlow.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const tf = await import('@tensorflow/tfjs-node');
|
||||
|
||||
if (this.model == null) this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
||||
|
||||
|
||||
if (this.model == null) {
|
||||
await this.modelLoadMutex.runExclusive(async () => {
|
||||
if (this.model == null) {
|
||||
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = await fs.promises.readFile(path);
|
||||
const image = await tf.node.decodeImage(buffer, 3) as any;
|
||||
try {
|
||||
|
|
|
@ -99,7 +99,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
'MAXLEN', '~', '200',
|
||||
'*',
|
||||
'note', note.id);
|
||||
|
||||
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
}
|
||||
|
||||
|
@ -112,16 +112,16 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
if (note.visibility === 'followers') return false;
|
||||
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
|
||||
if (antenna.src === 'home') {
|
||||
// TODO
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await this.userListJoiningsRepository.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'users') {
|
||||
const accts = antenna.users.map(x => {
|
||||
|
@ -130,32 +130,32 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
});
|
||||
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
||||
}
|
||||
|
||||
|
||||
const keywords = antenna.keywords
|
||||
// Clean up
|
||||
.map(xs => xs.filter(x => x !== ''))
|
||||
.filter(xs => xs.length > 0);
|
||||
|
||||
|
||||
if (keywords.length > 0) {
|
||||
if (note.text == null && note.cw == null) return false;
|
||||
|
||||
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
|
||||
|
||||
|
||||
const matched = keywords.some(and =>
|
||||
and.every(keyword =>
|
||||
antenna.caseSensitive
|
||||
? _text.includes(keyword)
|
||||
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
||||
));
|
||||
|
||||
|
||||
if (!matched) return false;
|
||||
}
|
||||
|
||||
|
||||
const excludeKeywords = antenna.excludeKeywords
|
||||
// Clean up
|
||||
.map(xs => xs.filter(x => x !== ''))
|
||||
.filter(xs => xs.length > 0);
|
||||
|
||||
|
||||
if (excludeKeywords.length > 0) {
|
||||
if (note.text == null && note.cw == null) return false;
|
||||
|
||||
|
@ -167,16 +167,16 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
? _text.includes(keyword)
|
||||
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
||||
));
|
||||
|
||||
|
||||
if (matched) return false;
|
||||
}
|
||||
|
||||
|
||||
if (antenna.withFile) {
|
||||
if (note.fileIds && note.fileIds.length === 0) return false;
|
||||
}
|
||||
|
||||
|
||||
// TODO: eval expression
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -188,7 +188,7 @@ export class AntennaService implements OnApplicationShutdown {
|
|||
});
|
||||
this.antennasFetched = true;
|
||||
}
|
||||
|
||||
|
||||
return this.antennas;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,11 +32,6 @@ export class AppLockService {
|
|||
return this.lock(`ap-object:${uri}`, timeout);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000): Promise<() => void> {
|
||||
return this.lock(`instance:${host}`, timeout);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getChartInsertLock(lockKey: string, timeout = 30 * 1000): Promise<() => void> {
|
||||
return this.lock(`chart-insert:${lockKey}`, timeout);
|
||||
|
|
|
@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
|||
|
||||
@Injectable()
|
||||
export class CacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: MemoryKVCache<User>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
|
||||
public userByIdCache: MemoryKVCache<User, User | string>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>;
|
||||
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||
public uriPersonCache: MemoryKVCache<User | null>;
|
||||
public uriPersonCache: MemoryKVCache<User | null, string | null>;
|
||||
public userProfileCache: RedisKVCache<UserProfile>;
|
||||
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||
|
@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown {
|
|||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.userByIdCache = new MemoryKVCache<User>(Infinity);
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
|
||||
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
|
||||
const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */);
|
||||
this.localUserByIdCache = localUserByIdCache;
|
||||
|
||||
// ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する
|
||||
const userByIdCache = new MemoryKVCache<User, User | string>(1000 * 60 * 60 * 6 /* 6h */, {
|
||||
toMapConverter: user => {
|
||||
if (user.host === null) {
|
||||
localUserByIdCache.set(user.id, user as LocalUser);
|
||||
return user.id;
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
|
||||
});
|
||||
this.userByIdCache = userByIdCache;
|
||||
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
localUserByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
|
||||
});
|
||||
this.uriPersonCache = new MemoryKVCache<User | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
userByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : userByIdCache.get(id),
|
||||
});
|
||||
|
||||
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
|
@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
|
||||
this.userByIdCache.set(user.id, user);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value?.id === user.id) {
|
||||
if (v.value === user.id) {
|
||||
this.uriPersonCache.set(k, user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export class CaptchaService {
|
|||
secret,
|
||||
response,
|
||||
});
|
||||
|
||||
|
||||
const res = await this.httpRequestService.send(url, {
|
||||
method: 'POST',
|
||||
body: params.toString(),
|
||||
|
@ -28,14 +28,14 @@ export class CaptchaService {
|
|||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}, { throwErrorWhenResponseNotOk: false });
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status}`);
|
||||
}
|
||||
|
||||
|
||||
return await res.json() as CaptchaResponse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
|
@ -73,7 +73,7 @@ export class CaptchaService {
|
|||
if (response == null) {
|
||||
throw new Error('turnstile-failed: no response provided');
|
||||
}
|
||||
|
||||
|
||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||
throw new Error(`turnstile-request-failed: ${err}`);
|
||||
});
|
||||
|
|
|
@ -25,27 +25,27 @@ export class CreateSystemUserService {
|
|||
@bindThis
|
||||
public async createSystemUser(username: string): Promise<User> {
|
||||
const password = uuid();
|
||||
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
|
||||
// Generate secret
|
||||
const secret = generateNativeUserToken();
|
||||
|
||||
|
||||
const keyPair = await genRsaKeyPair(4096);
|
||||
|
||||
|
||||
let account!: User;
|
||||
|
||||
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
});
|
||||
|
||||
|
||||
if (exist) throw new Error('the user is already exists');
|
||||
|
||||
|
||||
account = await transactionalEntityManager.insert(User, {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
|
@ -58,25 +58,25 @@ export class CreateSystemUserService {
|
|||
isExplorable: false,
|
||||
isBot: true,
|
||||
}).then(x => transactionalEntityManager.findOneByOrFail(User, x.identifiers[0]));
|
||||
|
||||
|
||||
await transactionalEntityManager.insert(UserKeypair, {
|
||||
publicKey: keyPair.publicKey,
|
||||
privateKey: keyPair.privateKey,
|
||||
userId: account.id,
|
||||
});
|
||||
|
||||
|
||||
await transactionalEntityManager.insert(UserProfile, {
|
||||
userId: account.id,
|
||||
autoAcceptFollowed: false,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
|
||||
await transactionalEntityManager.insert(UsedUsername, {
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: updated,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +194,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
|
@ -215,7 +215,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
|
||||
await this.emojisRepository.update({
|
||||
|
|
|
@ -28,11 +28,11 @@ export class DeleteAccountService {
|
|||
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||
|
||||
|
||||
this.queueService.createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
|
|
@ -29,12 +29,12 @@ export class EmailService {
|
|||
@bindThis
|
||||
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
||||
const meta = await this.metaService.fetch(true);
|
||||
|
||||
|
||||
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
||||
const emailSettingUrl = `${this.config.url}/settings/email`;
|
||||
|
||||
|
||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtpHost,
|
||||
port: meta.smtpPort,
|
||||
|
@ -46,7 +46,7 @@ export class EmailService {
|
|||
pass: meta.smtpPass,
|
||||
} : undefined,
|
||||
} as any);
|
||||
|
||||
|
||||
try {
|
||||
// TODO: htmlサニタイズ
|
||||
const info = await transporter.sendMail({
|
||||
|
@ -135,7 +135,7 @@ export class EmailService {
|
|||
</body>
|
||||
</html>`,
|
||||
});
|
||||
|
||||
|
||||
this.logger.info(`Message sent: ${info.messageId}`);
|
||||
} catch (err) {
|
||||
this.logger.error(err as Error);
|
||||
|
@ -149,12 +149,12 @@ export class EmailService {
|
|||
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
|
||||
}> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
|
||||
const exist = await this.userProfilesRepository.countBy({
|
||||
emailVerified: true,
|
||||
email: emailAddress,
|
||||
});
|
||||
|
||||
|
||||
const validated = meta.enableActiveEmailValidation ? await validateEmail({
|
||||
email: emailAddress,
|
||||
validateRegex: true,
|
||||
|
@ -163,9 +163,9 @@ export class EmailService {
|
|||
validateDisposable: true, // 捨てアドかどうかチェック
|
||||
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
||||
}) : { valid: true, reason: null };
|
||||
|
||||
|
||||
const available = exist === 0 && validated.valid;
|
||||
|
||||
|
||||
return {
|
||||
available,
|
||||
reason: available ? null :
|
||||
|
|
|
@ -43,19 +43,19 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async fetch(host: string): Promise<Instance> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached) return cached;
|
||||
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
|
||||
if (index == null) {
|
||||
const i = await this.instancesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
host,
|
||||
firstRetrievedAt: new Date(),
|
||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
|
||||
this.federatedInstanceCache.set(host, i);
|
||||
return i;
|
||||
} else {
|
||||
|
@ -74,7 +74,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
|
||||
this.federatedInstanceCache.set(result.host, result);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { JSDOM } from 'jsdom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
@ -12,6 +10,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import type { DOMWindow } from 'jsdom';
|
||||
import * as Redis from 'ioredis';
|
||||
|
||||
type NodeInfo = {
|
||||
openRegistrations?: unknown;
|
||||
|
@ -37,39 +36,49 @@ export class FetchInstanceMetadataService {
|
|||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private appLockService: AppLockService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async tryLock(host: string): Promise<boolean> {
|
||||
const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET');
|
||||
return mutex !== '1';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public unlock(host: string): Promise<'OK'> {
|
||||
return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchInstanceMetadata(instance: Instance, force = false): Promise<void> {
|
||||
const unlock = await this.appLockService.getFetchInstanceMetadataLock(instance.host);
|
||||
|
||||
if (!force) {
|
||||
const _instance = await this.instancesRepository.findOneBy({ host: instance.host });
|
||||
const now = Date.now();
|
||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
||||
unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Fetching metadata of ${instance.host} ...`);
|
||||
|
||||
const host = instance.host;
|
||||
// Acquire mutex to ensure no parallel runs
|
||||
if (!await this.tryLock(host)) return;
|
||||
try {
|
||||
if (!force) {
|
||||
const _instance = await this.federatedInstanceService.fetch(host);
|
||||
const now = Date.now();
|
||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
||||
// unlock at the finally caluse
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Fetching metadata of ${instance.host} ...`);
|
||||
|
||||
const [info, dom, manifest] = await Promise.all([
|
||||
this.fetchNodeinfo(instance).catch(() => null),
|
||||
this.fetchDom(instance).catch(() => null),
|
||||
this.fetchManifest(instance).catch(() => null),
|
||||
]);
|
||||
|
||||
|
||||
const [favicon, icon, themeColor, name, description] = await Promise.all([
|
||||
this.fetchFaviconUrl(instance, dom).catch(() => null),
|
||||
this.fetchIconUrl(instance, dom, manifest).catch(() => null),
|
||||
|
@ -77,13 +86,13 @@ export class FetchInstanceMetadataService {
|
|||
this.getSiteName(info, dom, manifest).catch(() => null),
|
||||
this.getDescription(info, dom, manifest).catch(() => null),
|
||||
]);
|
||||
|
||||
|
||||
this.logger.succ(`Successfuly fetched metadata of ${instance.host}`);
|
||||
|
||||
|
||||
const updates = {
|
||||
infoUpdatedAt: new Date(),
|
||||
} as Record<string, any>;
|
||||
|
||||
|
||||
if (info) {
|
||||
updates.softwareName = typeof info.software?.name === 'string' ? info.software.name.toLowerCase() : '?';
|
||||
updates.softwareVersion = info.software?.version;
|
||||
|
@ -91,27 +100,27 @@ export class FetchInstanceMetadataService {
|
|||
updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null;
|
||||
updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null;
|
||||
}
|
||||
|
||||
|
||||
if (name) updates.name = name;
|
||||
if (description) updates.description = description;
|
||||
if (icon || favicon) updates.iconUrl = icon ?? favicon;
|
||||
if (favicon) updates.faviconUrl = favicon;
|
||||
if (themeColor) updates.themeColor = themeColor;
|
||||
|
||||
|
||||
await this.federatedInstanceService.update(instance.id, updates);
|
||||
|
||||
|
||||
this.logger.succ(`Successfuly updated metadata of ${instance.host}`);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
|
||||
} finally {
|
||||
unlock();
|
||||
await this.unlock(host);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
|
||||
this.logger.info(`Fetching nodeinfo of ${instance.host} ...`);
|
||||
|
||||
|
||||
try {
|
||||
const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo')
|
||||
.catch(err => {
|
||||
|
@ -121,33 +130,33 @@ export class FetchInstanceMetadataService {
|
|||
throw err.statusCode ?? err.message;
|
||||
}
|
||||
}) as Record<string, unknown>;
|
||||
|
||||
|
||||
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
|
||||
throw new Error('No wellknown links');
|
||||
}
|
||||
|
||||
|
||||
const links = wellknown.links as any[];
|
||||
|
||||
|
||||
const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
|
||||
const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
|
||||
const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
|
||||
const link = lnik2_1 ?? lnik2_0 ?? lnik1_0;
|
||||
|
||||
|
||||
if (link == null) {
|
||||
throw new Error('No nodeinfo link provided');
|
||||
}
|
||||
|
||||
|
||||
const info = await this.httpRequestService.getJson(link.href)
|
||||
.catch(err => {
|
||||
throw err.statusCode ?? err.message;
|
||||
});
|
||||
|
||||
|
||||
this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
|
||||
|
||||
|
||||
return info as NodeInfo;
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${err}`);
|
||||
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
@ -155,51 +164,51 @@ export class FetchInstanceMetadataService {
|
|||
@bindThis
|
||||
private async fetchDom(instance: Instance): Promise<DOMWindow['document']> {
|
||||
this.logger.info(`Fetching HTML of ${instance.host} ...`);
|
||||
|
||||
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
|
||||
const html = await this.httpRequestService.getHtml(url);
|
||||
|
||||
|
||||
const { window } = new JSDOM(html);
|
||||
const doc = window.document;
|
||||
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchManifest(instance: Instance): Promise<Record<string, unknown> | null> {
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
|
||||
const manifestUrl = url + '/manifest.json';
|
||||
|
||||
|
||||
const manifest = await this.httpRequestService.getJson(manifestUrl) as Record<string, unknown>;
|
||||
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | null): Promise<string | null> {
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
|
||||
if (doc) {
|
||||
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
||||
const href = Array.from(doc.getElementsByTagName('link')).reverse().find(link => link.relList.contains('icon'))?.href;
|
||||
|
||||
|
||||
if (href) {
|
||||
return (new URL(href, url)).href;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const faviconUrl = url + '/favicon.ico';
|
||||
|
||||
|
||||
const favicon = await this.httpRequestService.send(faviconUrl, {
|
||||
method: 'HEAD',
|
||||
}, { throwErrorWhenResponseNotOk: false });
|
||||
|
||||
|
||||
if (favicon.ok) {
|
||||
return faviconUrl;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -209,38 +218,38 @@ export class FetchInstanceMetadataService {
|
|||
const url = 'https://' + instance.host;
|
||||
return (new URL(manifest.icons[0].src, url)).href;
|
||||
}
|
||||
|
||||
|
||||
if (doc) {
|
||||
const url = 'https://' + instance.host;
|
||||
|
||||
|
||||
// https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043
|
||||
const links = Array.from(doc.getElementsByTagName('link')).reverse();
|
||||
// https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559
|
||||
const href =
|
||||
const href =
|
||||
[
|
||||
links.find(link => link.relList.contains('apple-touch-icon-precomposed'))?.href,
|
||||
links.find(link => link.relList.contains('apple-touch-icon'))?.href,
|
||||
links.find(link => link.relList.contains('icon'))?.href,
|
||||
]
|
||||
.find(href => href);
|
||||
|
||||
|
||||
if (href) {
|
||||
return (new URL(href, url)).href;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getThemeColor(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
const themeColor = info?.metadata?.themeColor ?? doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') ?? manifest?.theme_color;
|
||||
|
||||
|
||||
if (themeColor) {
|
||||
const color = new tinycolor(themeColor);
|
||||
if (color.isValid()) return color.toHexString();
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -253,19 +262,19 @@ export class FetchInstanceMetadataService {
|
|||
return info.metadata.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (doc) {
|
||||
const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content');
|
||||
|
||||
|
||||
if (og) {
|
||||
return og;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (manifest) {
|
||||
return manifest.name ?? manifest.short_name;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -278,23 +287,23 @@ export class FetchInstanceMetadataService {
|
|||
return info.metadata.description;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (doc) {
|
||||
const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content');
|
||||
if (meta) {
|
||||
return meta;
|
||||
}
|
||||
|
||||
|
||||
const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content');
|
||||
if (og) {
|
||||
return og;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (manifest) {
|
||||
return manifest.name ?? manifest.short_name;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,20 +161,20 @@ export class FileInfoService {
|
|||
private async detectSensitivity(source: string, mime: string, sensitiveThreshold: number, sensitiveThresholdForPorn: number, analyzeVideo: boolean): Promise<[sensitive: boolean, porn: boolean]> {
|
||||
let sensitive = false;
|
||||
let porn = false;
|
||||
|
||||
|
||||
function judgePrediction(result: readonly predictionType[]): [sensitive: boolean, porn: boolean] {
|
||||
let sensitive = false;
|
||||
let porn = false;
|
||||
|
||||
|
||||
if ((result.find(x => x.className === 'Sexy')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
if ((result.find(x => x.className === 'Hentai')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThreshold) sensitive = true;
|
||||
|
||||
|
||||
if ((result.find(x => x.className === 'Porn')?.probability ?? 0) > sensitiveThresholdForPorn) porn = true;
|
||||
|
||||
|
||||
return [sensitive, porn];
|
||||
}
|
||||
|
||||
|
||||
if ([
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
|
@ -253,10 +253,10 @@ export class FileInfoService {
|
|||
disposeOutDir();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [sensitive, porn];
|
||||
}
|
||||
|
||||
|
||||
private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
|
||||
const watcher = new FSWatcher({
|
||||
cwd,
|
||||
|
@ -295,7 +295,7 @@ export class FileInfoService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private exists(path: string): Promise<boolean> {
|
||||
return fs.promises.access(path).then(() => true, () => false);
|
||||
|
@ -304,11 +304,11 @@ export class FileInfoService {
|
|||
@bindThis
|
||||
public fixMime(mime: string | fileType.MimeType): string {
|
||||
// see https://github.com/misskey-dev/misskey/pull/10686
|
||||
if (mime === "audio/x-flac") {
|
||||
return "audio/flac";
|
||||
if (mime === 'audio/x-flac') {
|
||||
return 'audio/flac';
|
||||
}
|
||||
if (mime === "audio/vnd.wave") {
|
||||
return "audio/wav";
|
||||
if (mime === 'audio/vnd.wave') {
|
||||
return 'audio/wav';
|
||||
}
|
||||
|
||||
return mime;
|
||||
|
@ -355,11 +355,12 @@ export class FileInfoService {
|
|||
* Check the file is SVG or not
|
||||
*/
|
||||
@bindThis
|
||||
public async checkSvg(path: string) {
|
||||
public async checkSvg(path: string): Promise<boolean> {
|
||||
try {
|
||||
const size = await this.getFileSize(path);
|
||||
if (size > 1 * 1024 * 1024) return false;
|
||||
return isSvg(fs.readFileSync(path));
|
||||
const buffer = await fs.promises.readFile(path);
|
||||
return isSvg(buffer.toString());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Role } from '@/models';
|
||||
import { Role } from '@/models/index.js';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalEventService {
|
||||
|
|
|
@ -42,21 +42,21 @@ export class HttpRequestService {
|
|||
errorTtl: 30, // 30secs
|
||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||
});
|
||||
|
||||
|
||||
this.http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
} as http.AgentOptions);
|
||||
|
||||
|
||||
this.https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
} as https.AgentOptions);
|
||||
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||
|
||||
|
||||
this.httpAgent = config.proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
|
|
|
@ -23,7 +23,7 @@ export class IdService {
|
|||
@bindThis
|
||||
public genId(date?: Date): string {
|
||||
if (!date || (date > new Date())) date = new Date();
|
||||
|
||||
|
||||
switch (this.method) {
|
||||
case 'aid': return genAid(date);
|
||||
case 'meid': return genMeid(date);
|
||||
|
|
|
@ -26,12 +26,12 @@ export class InstanceActorService {
|
|||
public async getInstanceActor(): Promise<LocalUser> {
|
||||
const cached = this.cache.get();
|
||||
if (cached) return cached;
|
||||
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
host: IsNull(),
|
||||
username: ACTOR_USERNAME,
|
||||
}) as LocalUser | undefined;
|
||||
|
||||
|
||||
if (user) {
|
||||
this.cache.set(user);
|
||||
return user;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { KEYWORD } from 'color-convert/conversions';
|
||||
import type { KEYWORD } from 'color-convert/conversions.js';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerService {
|
||||
|
|
|
@ -56,7 +56,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async fetch(noCache = false): Promise<Meta> {
|
||||
if (!noCache && this.cache) return this.cache;
|
||||
|
||||
|
||||
return await this.db.transaction(async transactionalEntityManager => {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
|
@ -64,9 +64,9 @@ export class MetaService implements OnApplicationShutdown {
|
|||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const meta = metas[0];
|
||||
|
||||
|
||||
if (meta) {
|
||||
this.cache = meta;
|
||||
return meta;
|
||||
|
@ -81,7 +81,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
['id'],
|
||||
)
|
||||
.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
|
||||
|
||||
|
||||
this.cache = saved;
|
||||
return saved;
|
||||
}
|
||||
|
|
|
@ -27,29 +27,29 @@ export class MfmService {
|
|||
public fromHtml(html: string, hashtagNames?: string[]): string {
|
||||
// some AP servers like Pixelfed use br tags as well as newlines
|
||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||
|
||||
|
||||
const dom = parse5.parseFragment(html);
|
||||
|
||||
|
||||
let text = '';
|
||||
|
||||
|
||||
for (const n of dom.childNodes) {
|
||||
analyze(n);
|
||||
}
|
||||
|
||||
|
||||
return text.trim();
|
||||
|
||||
|
||||
function getText(node: TreeAdapter.Node): string {
|
||||
if (treeAdapter.isTextNode(node)) return node.value;
|
||||
if (!treeAdapter.isElementNode(node)) return '';
|
||||
if (node.nodeName === 'br') return '\n';
|
||||
|
||||
|
||||
if (node.childNodes) {
|
||||
return node.childNodes.map(n => getText(n)).join('');
|
||||
}
|
||||
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||
if (childNodes) {
|
||||
for (const n of childNodes) {
|
||||
|
@ -57,35 +57,35 @@ export class MfmService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function analyze(node: TreeAdapter.Node) {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
text += node.value;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Skip comment or document type node
|
||||
if (!treeAdapter.isElementNode(node)) return;
|
||||
|
||||
|
||||
switch (node.nodeName) {
|
||||
case 'br': {
|
||||
text += '\n';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'a':
|
||||
{
|
||||
const txt = getText(node);
|
||||
const rel = node.attrs.find(x => x.name === 'rel');
|
||||
const href = node.attrs.find(x => x.name === 'href');
|
||||
|
||||
|
||||
// ハッシュタグ
|
||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
|
||||
const part = txt.split('@');
|
||||
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
//#region ホスト名部分が省略されているので復元する
|
||||
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
||||
|
@ -116,12 +116,12 @@ export class MfmService {
|
|||
return `[${txt}](${href.value})`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
text += generateLink();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'h1':
|
||||
{
|
||||
text += '【';
|
||||
|
@ -129,7 +129,7 @@ export class MfmService {
|
|||
text += '】\n';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'b':
|
||||
case 'strong':
|
||||
{
|
||||
|
@ -138,7 +138,7 @@ export class MfmService {
|
|||
text += '**';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'small':
|
||||
{
|
||||
text += '<small>';
|
||||
|
@ -146,7 +146,7 @@ export class MfmService {
|
|||
text += '</small>';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 's':
|
||||
case 'del':
|
||||
{
|
||||
|
@ -155,7 +155,7 @@ export class MfmService {
|
|||
text += '~~';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'i':
|
||||
case 'em':
|
||||
{
|
||||
|
@ -164,7 +164,7 @@ export class MfmService {
|
|||
text += '</i>';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// block code (<pre><code>)
|
||||
case 'pre': {
|
||||
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
||||
|
@ -176,7 +176,7 @@ export class MfmService {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// inline code (<code>)
|
||||
case 'code': {
|
||||
text += '`';
|
||||
|
@ -184,7 +184,7 @@ export class MfmService {
|
|||
text += '`';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'blockquote': {
|
||||
const t = getText(node);
|
||||
if (t) {
|
||||
|
@ -193,7 +193,7 @@ export class MfmService {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case 'p':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
|
@ -205,7 +205,7 @@ export class MfmService {
|
|||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// other block elements
|
||||
case 'div':
|
||||
case 'header':
|
||||
|
@ -219,7 +219,7 @@ export class MfmService {
|
|||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
default: // includes inline elements
|
||||
{
|
||||
appendChildren(node.childNodes);
|
||||
|
@ -234,48 +234,48 @@ export class MfmService {
|
|||
if (nodes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const { window } = new Window();
|
||||
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
|
||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||
if (children) {
|
||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||
bold: (node) => {
|
||||
const el = doc.createElement('b');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
small: (node) => {
|
||||
const el = doc.createElement('small');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
strike: (node) => {
|
||||
const el = doc.createElement('del');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
italic: (node) => {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
fn: (node) => {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
blockCode: (node) => {
|
||||
const pre = doc.createElement('pre');
|
||||
const inner = doc.createElement('code');
|
||||
|
@ -283,21 +283,21 @@ export class MfmService {
|
|||
pre.appendChild(inner);
|
||||
return pre;
|
||||
},
|
||||
|
||||
|
||||
center: (node) => {
|
||||
const el = doc.createElement('div');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
emojiCode: (node) => {
|
||||
return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
|
||||
},
|
||||
|
||||
|
||||
unicodeEmoji: (node) => {
|
||||
return doc.createTextNode(node.props.emoji);
|
||||
},
|
||||
|
||||
|
||||
hashtag: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
|
||||
|
@ -305,32 +305,32 @@ export class MfmService {
|
|||
a.setAttribute('rel', 'tag');
|
||||
return a;
|
||||
},
|
||||
|
||||
|
||||
inlineCode: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.code;
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
mathInline: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
mathBlock: (node) => {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
link: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', node.props.url);
|
||||
appendChildren(node.children, a);
|
||||
return a;
|
||||
},
|
||||
|
||||
|
||||
mention: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
const { username, host, acct } = node.props;
|
||||
|
@ -340,47 +340,47 @@ export class MfmService {
|
|||
a.textContent = acct;
|
||||
return a;
|
||||
},
|
||||
|
||||
|
||||
quote: (node) => {
|
||||
const el = doc.createElement('blockquote');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
text: (node) => {
|
||||
const el = doc.createElement('span');
|
||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||
|
||||
|
||||
for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
|
||||
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
||||
}
|
||||
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
|
||||
url: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', node.props.url);
|
||||
a.textContent = node.props.url;
|
||||
return a;
|
||||
},
|
||||
|
||||
|
||||
search: (node) => {
|
||||
const a = doc.createElement('a');
|
||||
a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
|
||||
a.textContent = node.props.content;
|
||||
return a;
|
||||
},
|
||||
|
||||
|
||||
plain: (node) => {
|
||||
const el = doc.createElement('span');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
appendChildren(nodes, doc.body);
|
||||
|
||||
|
||||
return `<p>${doc.body.innerHTML}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -692,7 +692,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
// Register to search database
|
||||
this.index(note);
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
|
||||
if (sensitiveWord.length > 0) {
|
||||
|
@ -778,7 +778,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
private index(note: Note) {
|
||||
if (note.text == null && note.cw == null) return;
|
||||
|
||||
|
||||
this.searchService.indexNote(note);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
|
@ -41,11 +42,12 @@ export class NoteDeleteService {
|
|||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
* 投稿を削除します。
|
||||
* @param user 投稿者
|
||||
|
@ -53,6 +55,7 @@ export class NoteDeleteService {
|
|||
*/
|
||||
async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) {
|
||||
const deletedAt = new Date();
|
||||
const cascadingNotes = await this.findCascadingNotes(note);
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
|
||||
|
@ -88,8 +91,8 @@ export class NoteDeleteService {
|
|||
}
|
||||
|
||||
// also deliever delete activity to cascaded notes
|
||||
const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
|
||||
for (const cascadingNote of federatedLocalCascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
||||
|
@ -114,6 +117,11 @@ export class NoteDeleteService {
|
|||
}
|
||||
}
|
||||
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
this.searchService.unindexNote(cascadingNote);
|
||||
}
|
||||
this.searchService.unindexNote(note);
|
||||
|
||||
await this.notesRepository.delete({
|
||||
id: note.id,
|
||||
userId: user.id,
|
||||
|
@ -121,10 +129,8 @@ export class NoteDeleteService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async findCascadingNotes(note: Note) {
|
||||
const cascadingNotes: Note[] = [];
|
||||
|
||||
const recursive = async (noteId: string) => {
|
||||
private async findCascadingNotes(note: Note): Promise<Note[]> {
|
||||
const recursive = async (noteId: string): Promise<Note[]> => {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.replyId = :noteId', { noteId })
|
||||
.orWhere(new Brackets(q => {
|
||||
|
@ -133,14 +139,16 @@ export class NoteDeleteService {
|
|||
}))
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
const replies = await query.getMany();
|
||||
for (const reply of replies) {
|
||||
cascadingNotes.push(reply);
|
||||
await recursive(reply.id);
|
||||
}
|
||||
};
|
||||
await recursive(note.id);
|
||||
|
||||
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
|
||||
return [
|
||||
replies,
|
||||
...await Promise.all(replies.map(reply => recursive(reply.id))),
|
||||
].flat();
|
||||
};
|
||||
|
||||
const cascadingNotes: Note[] = await recursive(note.id);
|
||||
|
||||
return cascadingNotes;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -99,7 +99,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
});
|
||||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
|
@ -109,7 +109,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
|
|
|
@ -46,7 +46,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
force = false,
|
||||
) {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||
|
||||
|
||||
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'+',
|
||||
|
|
|
@ -39,12 +39,12 @@ export class PollService {
|
|||
@bindThis
|
||||
public async vote(user: User, note: Note, choice: number) {
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
|
||||
|
||||
if (poll == null) throw new Error('poll not found');
|
||||
|
||||
|
||||
// Check whether is valid choice
|
||||
if (poll.choices[choice] == null) throw new Error('invalid choice param');
|
||||
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
|
@ -52,13 +52,13 @@ export class PollService {
|
|||
throw new Error('blocked');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// if already voted
|
||||
const exist = await this.pollVotesRepository.findBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
||||
if (poll.multiple) {
|
||||
if (exist.some(x => x.choice === choice)) {
|
||||
throw new Error('already voted');
|
||||
|
@ -66,7 +66,7 @@ export class PollService {
|
|||
} else if (exist.length !== 0) {
|
||||
throw new Error('already voted');
|
||||
}
|
||||
|
||||
|
||||
// Create vote
|
||||
await this.pollVotesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
|
@ -75,11 +75,11 @@ export class PollService {
|
|||
userId: user.id,
|
||||
choice: choice,
|
||||
});
|
||||
|
||||
|
||||
// Increment votes count
|
||||
const index = choice + 1; // In SQL, array index is 1 based
|
||||
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
|
||||
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
|
||||
choice: choice,
|
||||
userId: user.id,
|
||||
|
@ -90,10 +90,10 @@ export class PollService {
|
|||
public async deliverQuestionUpdate(noteId: Note['id']) {
|
||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||
if (note == null) throw new Error('note not found');
|
||||
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: note.userId });
|
||||
if (user == null) throw new Error('note not found');
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user));
|
||||
this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
|
|
|
@ -3,7 +3,7 @@ import push from 'web-push';
|
|||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { Packed } from '@/misc/json-schema';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import type { SwSubscription, SwSubscriptionsRepository } from '@/models/index.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
@ -31,7 +31,7 @@ function truncateBody<T extends keyof PushNotificationsTypes>(type: T, body: Pus
|
|||
...body.note,
|
||||
// textをgetNoteSummaryしたものに置き換える
|
||||
text: getNoteSummary(('type' in body && body.type === 'renote') ? body.note.renote as Packed<'Note'> : body.note),
|
||||
|
||||
|
||||
cw: undefined,
|
||||
reply: undefined,
|
||||
renote: undefined,
|
||||
|
@ -69,16 +69,16 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
|
||||
if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return;
|
||||
|
||||
|
||||
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
|
||||
push.setVapidDetails(this.config.url,
|
||||
meta.swPublicKey,
|
||||
meta.swPrivateKey);
|
||||
|
||||
|
||||
const subscriptions = await this.subscriptionsCache.fetch(userId);
|
||||
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
if ([
|
||||
'readAllNotifications',
|
||||
|
@ -103,7 +103,7 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||
//swLogger.info(err.statusCode);
|
||||
//swLogger.info(err.headers);
|
||||
//swLogger.info(err.body);
|
||||
|
||||
|
||||
if (err.statusCode === 410) {
|
||||
this.swSubscriptionsRepository.delete({
|
||||
userId: userId,
|
||||
|
|
|
@ -60,8 +60,8 @@ export class QueryService {
|
|||
q.orderBy(`${q.alias}.id`, 'DESC');
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ここでいうBlockedは被Blockedの意
|
||||
@bindThis
|
||||
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }): void {
|
||||
|
@ -109,18 +109,18 @@ export class QueryService {
|
|||
q.andWhere('note.channelId IS NULL');
|
||||
} else {
|
||||
q.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
|
||||
const channelFollowingQuery = this.channelFollowingsRepository.createQueryBuilder('channelFollowing')
|
||||
.select('channelFollowing.followeeId')
|
||||
.where('channelFollowing.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
// チャンネルのノートではない
|
||||
.where('note.channelId IS NULL')
|
||||
// または自分がフォローしているチャンネルのノート
|
||||
.orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
|
||||
q.setParameters(channelFollowingQuery.getParameters());
|
||||
}
|
||||
}
|
||||
|
@ -130,9 +130,9 @@ export class QueryService {
|
|||
const mutedQuery = this.mutedNotesRepository.createQueryBuilder('muted')
|
||||
.select('muted.noteId')
|
||||
.where('muted.userId = :userId', { userId: me.id });
|
||||
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||
|
||||
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
}
|
||||
|
||||
|
@ -141,13 +141,13 @@ export class QueryService {
|
|||
const mutedQuery = this.noteThreadMutingsRepository.createQueryBuilder('threadMuted')
|
||||
.select('threadMuted.threadId')
|
||||
.where('threadMuted.userId = :userId', { userId: me.id });
|
||||
|
||||
|
||||
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
.where('note.threadId IS NULL')
|
||||
.orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
|
||||
q.setParameters(mutedQuery.getParameters());
|
||||
}
|
||||
|
||||
|
@ -156,15 +156,15 @@ export class QueryService {
|
|||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
|
||||
if (exclude) {
|
||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
||||
}
|
||||
|
||||
|
||||
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
|
||||
.select('user_profile.mutedInstances')
|
||||
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||
|
||||
|
||||
// 投稿の作者をミュートしていない かつ
|
||||
// 投稿の返信先の作者をミュートしていない かつ
|
||||
// 投稿の引用元の作者をミュートしていない
|
||||
|
@ -191,7 +191,7 @@ export class QueryService {
|
|||
.where('note.renoteUserHost IS NULL')
|
||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
||||
}));
|
||||
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
q.setParameters(mutingInstanceQuery.getParameters());
|
||||
}
|
||||
|
@ -201,9 +201,9 @@ export class QueryService {
|
|||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
||||
.select('muting.muteeId')
|
||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
|
||||
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
||||
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ export class QueryService {
|
|||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :meId');
|
||||
|
||||
|
||||
q.andWhere(new Brackets(qb => { qb
|
||||
// 公開投稿である
|
||||
.where(new Brackets(qb => { qb
|
||||
|
@ -268,7 +268,7 @@ export class QueryService {
|
|||
}));
|
||||
}));
|
||||
}));
|
||||
|
||||
|
||||
q.setParameters({ meId: me.id });
|
||||
}
|
||||
}
|
||||
|
@ -278,10 +278,10 @@ export class QueryService {
|
|||
const mutingQuery = this.renoteMutingsRepository.createQueryBuilder('renote_muting')
|
||||
.select('renote_muting.muteeId')
|
||||
.where('renote_muting.muterId = :muterId', { muterId: me.id });
|
||||
|
||||
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(new Brackets(qb => {
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NOT NULL');
|
||||
qb.andWhere('note.text IS NULL');
|
||||
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
|
@ -289,7 +289,7 @@ export class QueryService {
|
|||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL');
|
||||
}));
|
||||
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js';
|
||||
import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class QueueService {
|
|||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
|
||||
const data = {
|
||||
const data: DeliverJobData = {
|
||||
user: {
|
||||
id: user.id,
|
||||
},
|
||||
|
@ -88,6 +88,40 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ApDeliverManager-DeliverManager.execute()からinboxesを突っ込んでaddBulkしたい
|
||||
* @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください
|
||||
* @param content IActivity | null
|
||||
* @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox)
|
||||
* @returns void
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||
if (content == null) return null;
|
||||
|
||||
const opts = {
|
||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||
backoff: {
|
||||
type: 'custom',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
};
|
||||
|
||||
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
|
||||
name: d[0],
|
||||
data: {
|
||||
user,
|
||||
content,
|
||||
to: d[0],
|
||||
isSharedInbox: d[1],
|
||||
} as DeliverJobData,
|
||||
opts,
|
||||
})));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||
const data = {
|
||||
|
|
|
@ -39,9 +39,9 @@ export class RelayService {
|
|||
host: IsNull(),
|
||||
username: ACTOR_USERNAME,
|
||||
});
|
||||
|
||||
|
||||
if (user) return user as LocalUser;
|
||||
|
||||
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME);
|
||||
return created as LocalUser;
|
||||
}
|
||||
|
@ -53,12 +53,12 @@ export class RelayService {
|
|||
inbox,
|
||||
status: 'requesting',
|
||||
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const activity = this.apRendererService.addContext(follow);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||
|
||||
|
||||
return relay;
|
||||
}
|
||||
|
||||
|
@ -67,17 +67,17 @@ export class RelayService {
|
|||
const relay = await this.relaysRepository.findOneBy({
|
||||
inbox,
|
||||
});
|
||||
|
||||
|
||||
if (relay == null) {
|
||||
throw new Error('relay not found');
|
||||
}
|
||||
|
||||
|
||||
const relayActor = await this.getRelayActor();
|
||||
const follow = this.apRendererService.renderFollowRelay(relay, relayActor);
|
||||
const undo = this.apRendererService.renderUndo(follow, relayActor);
|
||||
const activity = this.apRendererService.addContext(undo);
|
||||
this.queueService.deliver(relayActor, activity, relay.inbox, false);
|
||||
|
||||
|
||||
await this.relaysRepository.delete(relay.id);
|
||||
}
|
||||
|
||||
|
@ -86,13 +86,13 @@ export class RelayService {
|
|||
const relays = await this.relaysRepository.find();
|
||||
return relays;
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public async relayAccepted(id: string): Promise<string> {
|
||||
const result = await this.relaysRepository.update(id, {
|
||||
status: 'accepted',
|
||||
});
|
||||
|
||||
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
|
@ -101,24 +101,24 @@ export class RelayService {
|
|||
const result = await this.relaysRepository.update(id, {
|
||||
status: 'rejected',
|
||||
});
|
||||
|
||||
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||
if (activity == null) return;
|
||||
|
||||
|
||||
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
|
||||
status: 'accepted',
|
||||
}));
|
||||
if (relays.length === 0) return;
|
||||
|
||||
|
||||
const copy = deepClone(activity);
|
||||
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
|
||||
|
||||
const signed = await this.apRendererService.attachLdSignature(copy, user);
|
||||
|
||||
|
||||
for (const relay of relays) {
|
||||
this.queueService.deliver(user, signed, relay.inbox, false);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class RemoteUserResolveService {
|
|||
@bindThis
|
||||
public async resolveUser(username: string, host: string | null): Promise<LocalUser | RemoteUser> {
|
||||
const usernameLower = username.toLowerCase();
|
||||
|
||||
|
||||
if (host == null) {
|
||||
this.logger.info(`return local user: ${usernameLower}`);
|
||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||
|
@ -46,9 +46,9 @@ export class RemoteUserResolveService {
|
|||
}
|
||||
}) as LocalUser;
|
||||
}
|
||||
|
||||
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
|
||||
if (this.config.host === host) {
|
||||
this.logger.info(`return local user: ${usernameLower}`);
|
||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||
|
@ -59,39 +59,39 @@ export class RemoteUserResolveService {
|
|||
}
|
||||
}) as LocalUser;
|
||||
}
|
||||
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ usernameLower, host }) as RemoteUser | null;
|
||||
|
||||
|
||||
const acctLower = `${usernameLower}@${host}`;
|
||||
|
||||
|
||||
if (user == null) {
|
||||
const self = await this.resolveSelf(acctLower);
|
||||
|
||||
|
||||
this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
||||
return await this.apPersonService.createPerson(self.href);
|
||||
}
|
||||
|
||||
|
||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||
await this.usersRepository.update(user.id, {
|
||||
lastFetchedAt: new Date(),
|
||||
});
|
||||
|
||||
|
||||
this.logger.info(`try resync: ${acctLower}`);
|
||||
const self = await this.resolveSelf(acctLower);
|
||||
|
||||
|
||||
if (user.uri !== self.href) {
|
||||
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
|
||||
this.logger.info(`uri missmatch: ${acctLower}`);
|
||||
this.logger.info(`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`);
|
||||
|
||||
|
||||
// validate uri
|
||||
const uri = new URL(self.href);
|
||||
if (uri.hostname !== host) {
|
||||
throw new Error('Invalid uri');
|
||||
}
|
||||
|
||||
|
||||
await this.usersRepository.update({
|
||||
usernameLower,
|
||||
host: host,
|
||||
|
@ -101,9 +101,9 @@ export class RemoteUserResolveService {
|
|||
} else {
|
||||
this.logger.info(`uri is fine: ${acctLower}`);
|
||||
}
|
||||
|
||||
|
||||
await this.apPersonService.updatePerson(self.href);
|
||||
|
||||
|
||||
this.logger.info(`return resynced remote user: ${acctLower}`);
|
||||
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
|
||||
if (u == null) {
|
||||
|
@ -113,7 +113,7 @@ export class RemoteUserResolveService {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.logger.info(`return existing remote user: ${acctLower}`);
|
||||
return user;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Packed } from '@/misc/json-schema';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export type RolePolicies = {
|
||||
|
@ -392,7 +392,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||
@bindThis
|
||||
public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
|
||||
const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
|
||||
if (existing == null) {
|
||||
throw new RoleService.NotAssignedError();
|
||||
|
|
|
@ -115,6 +115,15 @@ export class SearchService {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unindexNote(note: Note): Promise<void> {
|
||||
if (!['home', 'public'].includes(note.visibility)) return;
|
||||
|
||||
if (this.meilisearch) {
|
||||
this.meilisearchNoteIndex!.deleteDocument(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async searchNote(q: string, me: User | null, opts: {
|
||||
userId?: Note['userId'] | null;
|
||||
|
@ -174,7 +183,7 @@ export class SearchService {
|
|||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
|
||||
return await query.take(pagination.limit).getMany();
|
||||
return await query.limit(pagination.limit).getMany();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,31 +50,31 @@ export class SignupService {
|
|||
}) {
|
||||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
|
||||
|
||||
// Validate username
|
||||
if (!this.userEntityService.validateLocalUsername(username)) {
|
||||
throw new Error('INVALID_USERNAME');
|
||||
}
|
||||
|
||||
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!this.userEntityService.validatePassword(password)) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
hash = await bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
||||
|
||||
// Check username duplication
|
||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
throw new Error('DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
throw new Error('USED_USERNAME');
|
||||
|
@ -106,18 +106,18 @@ export class SignupService {
|
|||
}, (err, publicKey, privateKey) =>
|
||||
err ? rej(err) : res([publicKey, privateKey]),
|
||||
));
|
||||
|
||||
|
||||
let account!: User;
|
||||
|
||||
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
const exist = await transactionalEntityManager.findOneBy(User, {
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
});
|
||||
|
||||
|
||||
if (exist) throw new Error(' the username is already used');
|
||||
|
||||
|
||||
account = await transactionalEntityManager.save(new User({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
|
@ -127,27 +127,27 @@ export class SignupService {
|
|||
token: secret,
|
||||
isRoot: isTheFirstUser,
|
||||
}));
|
||||
|
||||
|
||||
await transactionalEntityManager.save(new UserKeypair({
|
||||
publicKey: keyPair[0],
|
||||
privateKey: keyPair[1],
|
||||
userId: account.id,
|
||||
}));
|
||||
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
userId: account.id,
|
||||
autoAcceptFollowed: true,
|
||||
password: hash,
|
||||
}));
|
||||
|
||||
|
||||
await transactionalEntityManager.save(new UsedUsername({
|
||||
createdAt: new Date(),
|
||||
username: username.toLowerCase(),
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
this.usersChart.update(account, true);
|
||||
|
||||
|
||||
return { account, secret };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ function verifyCertificateChain(certificates: string[]) {
|
|||
|
||||
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
|
||||
if (certStruct == null) throw new Error('certStruct is null');
|
||||
|
||||
|
||||
const algorithm = certificate.getSignatureAlgorithmField();
|
||||
const signatureHex = certificate.getSignatureValueHex();
|
||||
|
||||
|
@ -143,19 +143,19 @@ export class TwoFactorAuthenticationService {
|
|||
if (clientData.type !== 'webauthn.get') {
|
||||
throw new Error('type is not webauthn.get');
|
||||
}
|
||||
|
||||
|
||||
if (this.hash(clientData.challenge).toString('hex') !== challenge) {
|
||||
throw new Error('challenge mismatch');
|
||||
}
|
||||
if (clientData.origin !== this.config.scheme + '://' + this.config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
|
||||
const verificationData = Buffer.concat(
|
||||
[authenticatorData, this.hash(clientDataJSON)],
|
||||
32 + authenticatorData.length,
|
||||
);
|
||||
|
||||
|
||||
return crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
|
@ -168,7 +168,7 @@ export class TwoFactorAuthenticationService {
|
|||
none: {
|
||||
verify({ publicKey }: { publicKey: Map<number, Buffer> }) {
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
|
@ -176,12 +176,12 @@ export class TwoFactorAuthenticationService {
|
|||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
||||
const publicKeyU2F = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32,
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
publicKey: publicKeyU2F,
|
||||
valid: true,
|
||||
|
@ -207,16 +207,16 @@ export class TwoFactorAuthenticationService {
|
|||
if (attStmt.alg !== -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash,
|
||||
]);
|
||||
|
||||
|
||||
const attCert: Buffer = attStmt.x5c[0];
|
||||
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
|
@ -224,23 +224,23 @@ export class TwoFactorAuthenticationService {
|
|||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32,
|
||||
);
|
||||
|
||||
|
||||
if (!attCert.equals(publicKeyData)) {
|
||||
throw new Error('public key mismatch');
|
||||
}
|
||||
|
||||
|
||||
const isValid = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
|
||||
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
||||
|
||||
|
||||
return {
|
||||
valid: isValid,
|
||||
publicKey: publicKeyData,
|
||||
|
@ -267,43 +267,43 @@ export class TwoFactorAuthenticationService {
|
|||
const verificationData = this.hash(
|
||||
Buffer.concat([authenticatorData, clientDataHash]),
|
||||
);
|
||||
|
||||
|
||||
const jwsParts = attStmt.response.toString('utf-8').split('.');
|
||||
|
||||
|
||||
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
|
||||
const response = JSON.parse(
|
||||
base64URLDecode(jwsParts[1]).toString('utf-8'),
|
||||
);
|
||||
const signature = jwsParts[2];
|
||||
|
||||
|
||||
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
|
||||
throw new Error('invalid nonce');
|
||||
}
|
||||
|
||||
|
||||
const certificateChain = header.x5c
|
||||
.map((key: any) => PEMString(key))
|
||||
.concat([GSR2]);
|
||||
|
||||
|
||||
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
|
||||
throw new Error('invalid common name');
|
||||
}
|
||||
|
||||
|
||||
if (!verifyCertificateChain(certificateChain)) {
|
||||
throw new Error('Invalid certificate chain!');
|
||||
}
|
||||
|
||||
|
||||
const signatureBase = Buffer.from(
|
||||
jwsParts[0] + '.' + jwsParts[1],
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
|
||||
const valid = crypto
|
||||
.createVerify('sha256')
|
||||
.update(signatureBase)
|
||||
.verify(certificateChain[0], base64URLDecode(signature));
|
||||
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
|
@ -311,7 +311,7 @@ export class TwoFactorAuthenticationService {
|
|||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32,
|
||||
|
@ -342,17 +342,17 @@ export class TwoFactorAuthenticationService {
|
|||
authenticatorData,
|
||||
clientDataHash,
|
||||
]);
|
||||
|
||||
|
||||
if (attStmt.x5c) {
|
||||
const attCert = attStmt.x5c[0];
|
||||
|
||||
|
||||
const validSignature = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
|
@ -360,12 +360,12 @@ export class TwoFactorAuthenticationService {
|
|||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32,
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyData,
|
||||
|
@ -375,12 +375,12 @@ export class TwoFactorAuthenticationService {
|
|||
throw new Error('ECDAA-Verify is not supported');
|
||||
} else {
|
||||
if (attStmt.alg !== -7) throw new Error('alg mismatch');
|
||||
|
||||
|
||||
throw new Error('self attestation is not supported');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
'fido-u2f': {
|
||||
verify({
|
||||
attStmt,
|
||||
|
@ -401,13 +401,13 @@ export class TwoFactorAuthenticationService {
|
|||
if (x5c.length !== 1) {
|
||||
throw new Error('x5c length does not match expectation');
|
||||
}
|
||||
|
||||
|
||||
const attCert = x5c[0];
|
||||
|
||||
|
||||
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
||||
|
||||
|
||||
const negTwo: Buffer = publicKey.get(-2);
|
||||
|
||||
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
|
@ -415,12 +415,12 @@ export class TwoFactorAuthenticationService {
|
|||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
||||
const publicKeyU2F = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32,
|
||||
);
|
||||
|
||||
|
||||
const verificationData = Buffer.concat([
|
||||
NULL_BYTE,
|
||||
rpIdHash,
|
||||
|
@ -428,12 +428,12 @@ export class TwoFactorAuthenticationService {
|
|||
credentialId,
|
||||
publicKeyU2F,
|
||||
]);
|
||||
|
||||
|
||||
const validSignature = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyU2F,
|
||||
|
|
|
@ -32,13 +32,13 @@ export class UserSuspendService {
|
|||
@bindThis
|
||||
public async doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));
|
||||
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
|
@ -46,13 +46,13 @@ export class UserSuspendService {
|
|||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user, content, inbox, true);
|
||||
}
|
||||
|
@ -62,13 +62,13 @@ export class UserSuspendService {
|
|||
@bindThis
|
||||
public async doPostUnsuspend(user: User): Promise<void> {
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
|
||||
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにUndo Delete配信
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));
|
||||
|
||||
|
||||
const queue: string[] = [];
|
||||
|
||||
|
||||
const followings = await this.followingsRepository.find({
|
||||
where: [
|
||||
{ followerSharedInbox: Not(IsNull()) },
|
||||
|
@ -76,13 +76,13 @@ export class UserSuspendService {
|
|||
],
|
||||
select: ['followerSharedInbox', 'followeeSharedInbox'],
|
||||
});
|
||||
|
||||
|
||||
const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox);
|
||||
|
||||
|
||||
for (const inbox of inboxes) {
|
||||
if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
|
||||
}
|
||||
|
||||
|
||||
for (const inbox of queue) {
|
||||
this.queueService.deliver(user as any, content, inbox, true);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class VideoProcessingService {
|
|||
@bindThis
|
||||
public async generateVideoThumbnail(source: string): Promise<IImage> {
|
||||
const [dir, cleanup] = await createTempDir();
|
||||
|
||||
|
||||
try {
|
||||
await new Promise((res, rej) => {
|
||||
FFmpeg({
|
||||
|
|
|
@ -31,7 +31,7 @@ export class WebhookService implements OnApplicationShutdown {
|
|||
});
|
||||
this.webhooksFetched = true;
|
||||
}
|
||||
|
||||
|
||||
return this.webhooks;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,14 +27,14 @@ export class ApAudienceService {
|
|||
public async parseAudience(actor: RemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||
const toGroups = this.groupingAudience(getApIds(to), actor);
|
||||
const ccGroups = this.groupingAudience(getApIds(cc), actor);
|
||||
|
||||
|
||||
const others = unique(concat([toGroups.other, ccGroups.other]));
|
||||
|
||||
|
||||
const limit = promiseLimit<User | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
|
||||
)).filter((x): x is User => x != null);
|
||||
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
visibility: 'public',
|
||||
|
@ -42,7 +42,7 @@ export class ApAudienceService {
|
|||
visibleUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (ccGroups.public.length > 0) {
|
||||
return {
|
||||
visibility: 'home',
|
||||
|
@ -50,7 +50,7 @@ export class ApAudienceService {
|
|||
visibleUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (toGroups.followers.length > 0) {
|
||||
return {
|
||||
visibility: 'followers',
|
||||
|
@ -58,14 +58,14 @@ export class ApAudienceService {
|
|||
visibleUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
visibility: 'specified',
|
||||
mentionedUsers,
|
||||
visibleUsers: mentionedUsers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
||||
const groups = {
|
||||
|
@ -73,7 +73,7 @@ export class ApAudienceService {
|
|||
followers: [] as string[],
|
||||
other: [] as string[],
|
||||
};
|
||||
|
||||
|
||||
for (const id of ids) {
|
||||
if (this.isPublic(id)) {
|
||||
groups.public.push(id);
|
||||
|
@ -83,12 +83,12 @@ export class ApAudienceService {
|
|||
groups.other.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
groups.other = unique(groups.other);
|
||||
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private isPublic(id: string) {
|
||||
return [
|
||||
|
@ -97,7 +97,7 @@ export class ApAudienceService {
|
|||
'Public',
|
||||
].includes(id);
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
private isFollowers(id: string, actor: RemoteUser) {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import escapeRegexp from 'escape-regexp';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -56,25 +55,18 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||
|
||||
@bindThis
|
||||
public parseUri(value: string | IObject): UriParseResult {
|
||||
const uri = getApId(value);
|
||||
|
||||
// the host part of a URL is case insensitive, so use the 'i' flag.
|
||||
const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
|
||||
const matchLocal = uri.match(localRegex);
|
||||
|
||||
if (matchLocal) {
|
||||
return {
|
||||
local: true,
|
||||
type: matchLocal[1],
|
||||
id: matchLocal[2],
|
||||
rest: matchLocal[3],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
local: false,
|
||||
uri,
|
||||
};
|
||||
}
|
||||
const separator = '/';
|
||||
|
||||
const uri = new URL(getApId(value));
|
||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
||||
|
||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||
return {
|
||||
local: true,
|
||||
type,
|
||||
id,
|
||||
rest: rest.length === 0 ? undefined : rest.join(separator),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,7 +121,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||
const key = await this.userPublickeysRepository.findOneBy({
|
||||
keyId,
|
||||
});
|
||||
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return key;
|
||||
|
@ -155,7 +147,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||
|
||||
if (user == null) return null;
|
||||
|
||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
|
|
@ -7,6 +7,8 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
|||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import { ThinUser } from '@/queue/types.js';
|
||||
|
||||
interface IRecipe {
|
||||
type: string;
|
||||
|
@ -21,10 +23,10 @@ interface IDirectRecipe extends IRecipe {
|
|||
to: RemoteUser;
|
||||
}
|
||||
|
||||
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
|
||||
const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||
recipe.type === 'Followers';
|
||||
|
||||
const isDirect = (recipe: any): recipe is IDirectRecipe =>
|
||||
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||
recipe.type === 'Direct';
|
||||
|
||||
@Injectable()
|
||||
|
@ -46,11 +48,11 @@ export class ApDeliverManagerService {
|
|||
|
||||
/**
|
||||
* Deliver activity to followers
|
||||
* @param actor
|
||||
* @param activity Activity
|
||||
* @param from Followee
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: any) {
|
||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
|
@ -64,11 +66,12 @@ export class ApDeliverManagerService {
|
|||
|
||||
/**
|
||||
* Deliver activity to user
|
||||
* @param actor
|
||||
* @param activity Activity
|
||||
* @param to Target user
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: any, to: RemoteUser) {
|
||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
|
@ -81,25 +84,28 @@ export class ApDeliverManagerService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: any) {
|
||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
||||
return new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
this.queueService,
|
||||
|
||||
actor,
|
||||
actor,
|
||||
activity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeliverManager {
|
||||
private actor: { id: User['id']; host: null; };
|
||||
private activity: any;
|
||||
private actor: ThinUser;
|
||||
private activity: IActivity | null;
|
||||
private recipes: IRecipe[] = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param userEntityService
|
||||
* @param followingsRepository
|
||||
* @param queueService
|
||||
* @param actor Actor
|
||||
* @param activity Activity to deliver
|
||||
*/
|
||||
|
@ -109,9 +115,15 @@ class DeliverManager {
|
|||
private queueService: QueueService,
|
||||
|
||||
actor: { id: User['id']; host: null; },
|
||||
activity: any,
|
||||
activity: IActivity | null,
|
||||
) {
|
||||
this.actor = actor;
|
||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||
if (actor.host != null) throw new Error('actor.host must be null');
|
||||
|
||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||
this.actor = {
|
||||
id: actor.id,
|
||||
};
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
|
@ -155,9 +167,8 @@ class DeliverManager {
|
|||
*/
|
||||
@bindThis
|
||||
public async execute() {
|
||||
if (!this.userEntityService.isLocalUser(this.actor)) return;
|
||||
|
||||
// The value flags whether it is shared or not.
|
||||
// key: inbox URL, value: whether it is sharedInbox
|
||||
const inboxes = new Map<string, boolean>();
|
||||
|
||||
/*
|
||||
|
@ -201,9 +212,6 @@ class DeliverManager {
|
|||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||
|
||||
// deliver
|
||||
for (const inbox of inboxes) {
|
||||
// inbox[0]: inbox, inbox[1]: whether it is sharedInbox
|
||||
this.queueService.deliver(this.actor, this.activity, inbox[0], inbox[1]);
|
||||
}
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class ApMfmService {
|
|||
@bindThis
|
||||
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
||||
|
||||
|
||||
return this.mfmService.fromHtml(html, hashtagNames);
|
||||
}
|
||||
|
||||
|
@ -29,5 +29,5 @@ export class ApMfmService {
|
|||
public getNoteHtml(note: Note) {
|
||||
if (!note.text) return '';
|
||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
|
|||
import { DriveService } from '@/core/DriveService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import type { IObject } from '../type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApImageService {
|
||||
|
@ -32,23 +33,27 @@ export class ApImageService {
|
|||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Imageを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
public async createImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||
public async createImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
}
|
||||
|
||||
const image = await this.apResolverService.createResolver().resolve(value) as any;
|
||||
const image = await this.apResolverService.createResolver().resolve(value);
|
||||
|
||||
if (image.url == null) {
|
||||
throw new Error('invalid image: url not privided');
|
||||
}
|
||||
|
||||
if (typeof image.url !== 'string') {
|
||||
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
|
||||
}
|
||||
|
||||
if (!checkHttps(image.url)) {
|
||||
throw new Error('invalid image: unexpected schema of url: ' + image.url);
|
||||
}
|
||||
|
@ -57,29 +62,19 @@ export class ApImageService {
|
|||
|
||||
const instance = await this.metaService.fetch();
|
||||
|
||||
let file = await this.driveService.uploadFromUrl({
|
||||
const file = await this.driveService.uploadFromUrl({
|
||||
url: image.url,
|
||||
user: actor,
|
||||
uri: image.url,
|
||||
sensitive: image.sensitive,
|
||||
isLink: !instance.cacheRemoteFiles,
|
||||
comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||
});
|
||||
if (!file.isLink || file.url === image.url) return file;
|
||||
|
||||
if (file.isLink) {
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
// URLを更新する
|
||||
if (file.url !== image.url) {
|
||||
await this.driveFilesRepository.update({ id: file.id }, {
|
||||
url: image.url,
|
||||
uri: image.url,
|
||||
});
|
||||
|
||||
file = await this.driveFilesRepository.findOneByOrFail({ id: file.id });
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、URLを更新する
|
||||
await this.driveFilesRepository.update({ id: file.id }, { url: image.url, uri: image.url });
|
||||
return await this.driveFilesRepository.findOneByOrFail({ id: file.id });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +84,7 @@ export class ApImageService {
|
|||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolveImage(actor: RemoteUser, value: any): Promise<DriveFile> {
|
||||
public async resolveImage(actor: RemoteUser, value: string | IObject): Promise<DriveFile> {
|
||||
// TODO
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
|
|
|
@ -22,17 +22,17 @@ export class ApMentionService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver) {
|
||||
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href as string));
|
||||
public async extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise<User[]> {
|
||||
const hrefs = unique(this.extractApMentionObjects(tags).map(x => x.href));
|
||||
|
||||
const limit = promiseLimit<User | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter((x): x is User => x != null);
|
||||
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
|
||||
if (tags == null) return [];
|
||||
|
|
|
@ -20,7 +20,6 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApMfmService } from '../ApMfmService.js';
|
||||
import { ApDbResolverService } from '../ApDbResolverService.js';
|
||||
|
@ -56,7 +55,7 @@ export class ApNoteService {
|
|||
// 循環参照のため / for circular dependency
|
||||
@Inject(forwardRef(() => ApPersonService))
|
||||
private apPersonService: ApPersonService,
|
||||
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private apAudienceService: ApAudienceService,
|
||||
private apMentionService: ApMentionService,
|
||||
|
@ -74,17 +73,13 @@ export class ApNoteService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public validateNote(object: IObject, uri: string) {
|
||||
public validateNote(object: IObject, uri: string): Error | null {
|
||||
const expectHost = this.utilityService.extractDbHost(uri);
|
||||
|
||||
if (object == null) {
|
||||
return new Error('invalid Note: object is null');
|
||||
}
|
||||
|
||||
|
||||
if (!validPost.includes(getApType(object))) {
|
||||
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
|
||||
}
|
||||
|
||||
|
||||
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
|
||||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
|
||||
}
|
||||
|
@ -93,10 +88,10 @@ export class ApNoteService {
|
|||
if (object.attributedTo && actualHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Noteをフェッチします。
|
||||
*
|
||||
|
@ -106,31 +101,30 @@ export class ApNoteService {
|
|||
public async fetchNote(object: string | IObject): Promise<Note | null> {
|
||||
return await this.apDbResolverService.getNoteFromApId(object);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Noteを作成します。
|
||||
*/
|
||||
@bindThis
|
||||
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
|
||||
|
||||
const object = await resolver.resolve(value);
|
||||
|
||||
|
||||
const entryUri = getApId(value);
|
||||
const err = this.validateNote(object, entryUri);
|
||||
if (err) {
|
||||
this.logger.error(`${err.message}`, {
|
||||
resolver: {
|
||||
history: resolver.getHistory(),
|
||||
},
|
||||
value: value,
|
||||
object: object,
|
||||
this.logger.error(err.message, {
|
||||
resolver: { history: resolver.getHistory() },
|
||||
value,
|
||||
object,
|
||||
});
|
||||
throw new Error('invalid note');
|
||||
}
|
||||
|
||||
|
||||
const note = object as IPost;
|
||||
|
||||
|
||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||
|
||||
if (note.id && !checkHttps(note.id)) {
|
||||
|
@ -142,21 +136,25 @@ export class ApNoteService {
|
|||
if (url && !checkHttps(url)) {
|
||||
throw new Error('unexpected shcema of note url: ' + url);
|
||||
}
|
||||
|
||||
|
||||
this.logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
|
||||
// 投稿者をフェッチ
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser;
|
||||
|
||||
if (note.attributedTo == null) {
|
||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
||||
}
|
||||
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser;
|
||||
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
}
|
||||
|
||||
|
||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||
let visibility = noteAudience.visibility;
|
||||
const visibleUsers = noteAudience.visibleUsers;
|
||||
|
||||
|
||||
// Audience (to, cc) が指定されてなかった場合
|
||||
if (visibility === 'specified' && visibleUsers.length === 0) {
|
||||
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
|
||||
|
@ -164,81 +162,71 @@ export class ApNoteService {
|
|||
visibility = 'public';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||
const apHashtags = await extractApHashtags(note.tag);
|
||||
|
||||
const apHashtags = extractApHashtags(note.tag);
|
||||
|
||||
// 添付ファイル
|
||||
// TODO: attachmentは必ずしもImageではない
|
||||
// TODO: attachmentは必ずしも配列ではない
|
||||
// Noteがsensitiveなら添付もsensitiveにする
|
||||
const limit = promiseLimit(2);
|
||||
|
||||
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
|
||||
const files = note.attachment
|
||||
.map(attach => attach.sensitive = note.sensitive)
|
||||
? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise<DriveFile>)))
|
||||
.filter(image => image != null)
|
||||
: [];
|
||||
|
||||
const limit = promiseLimit<DriveFile>(2);
|
||||
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
||||
limit(() => this.apImageService.resolveImage(actor, {
|
||||
...attach,
|
||||
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
||||
}))
|
||||
))));
|
||||
|
||||
// リプライ
|
||||
const reply: Note | null = note.inReplyTo
|
||||
? await this.resolveNote(note.inReplyTo, resolver).then(x => {
|
||||
if (x == null) {
|
||||
this.logger.warn('Specified inReplyTo, but not found');
|
||||
throw new Error('inReplyTo not found');
|
||||
} else {
|
||||
? await this.resolveNote(note.inReplyTo, { resolver })
|
||||
.then(x => {
|
||||
if (x == null) {
|
||||
this.logger.warn('Specified inReplyTo, but not found');
|
||||
throw new Error('inReplyTo not found');
|
||||
}
|
||||
|
||||
return x;
|
||||
}
|
||||
}).catch(async err => {
|
||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
||||
throw err;
|
||||
})
|
||||
})
|
||||
.catch(async err => {
|
||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
||||
throw err;
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
// 引用
|
||||
let quote: Note | undefined | null;
|
||||
|
||||
let quote: Note | undefined | null = null;
|
||||
|
||||
if (note._misskey_quote || note.quoteUrl) {
|
||||
const tryResolveNote = async (uri: string): Promise<{
|
||||
status: 'ok';
|
||||
res: Note | null;
|
||||
} | {
|
||||
status: 'permerror' | 'temperror';
|
||||
}> => {
|
||||
if (typeof uri !== 'string' || !uri.match(/^https?:/)) return { status: 'permerror' };
|
||||
const tryResolveNote = async (uri: string): Promise<
|
||||
| { status: 'ok'; res: Note }
|
||||
| { status: 'permerror' | 'temperror' }
|
||||
> => {
|
||||
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
||||
try {
|
||||
const res = await this.resolveNote(uri);
|
||||
if (res) {
|
||||
return {
|
||||
status: 'ok',
|
||||
res,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'permerror',
|
||||
};
|
||||
}
|
||||
if (res == null) return { status: 'permerror' };
|
||||
return { status: 'ok', res };
|
||||
} catch (e) {
|
||||
return {
|
||||
status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
||||
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||
const results = await Promise.all(uris.map(tryResolveNote));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0);
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw new Error('quote resolve failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const cw = note.summary === '' ? null : note.summary;
|
||||
|
||||
|
||||
// テキストのパース
|
||||
let text: string | null = null;
|
||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
||||
|
@ -248,39 +236,39 @@ export class ApNoteService {
|
|||
} else if (typeof note.content === 'string') {
|
||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||
}
|
||||
|
||||
|
||||
// vote
|
||||
if (reply && reply.hasPoll) {
|
||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id });
|
||||
|
||||
|
||||
const tryCreateVote = async (name: string, index: number): Promise<null> => {
|
||||
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
|
||||
this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||
} else if (index >= 0) {
|
||||
this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
|
||||
await this.pollService.vote(actor, reply, index);
|
||||
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
this.pollService.deliverQuestionUpdate(reply.id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
if (note.name) {
|
||||
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
||||
this.logger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
return [];
|
||||
});
|
||||
|
||||
|
||||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
|
||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
const event = await this.apEventService.extractEventFromNote(note, resolver).catch(() => undefined);
|
||||
|
||||
|
||||
return await this.noteCreateService.create(actor, {
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
files,
|
||||
|
@ -301,7 +289,7 @@ export class ApNoteService {
|
|||
url: url,
|
||||
}, silent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Noteを解決します。
|
||||
*
|
||||
|
@ -309,94 +297,91 @@ export class ApNoteService {
|
|||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
|
||||
const uri = typeof value === 'string' ? value : value.id;
|
||||
if (uri == null) throw new Error('missing uri');
|
||||
|
||||
// ブロックしてたら中断
|
||||
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<Note | null> {
|
||||
const uri = getApId(value);
|
||||
|
||||
// ブロックしていたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451);
|
||||
|
||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
||||
throw new StatusError('blocked host', 451);
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
|
||||
try {
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await this.fetchNote(uri);
|
||||
|
||||
if (exist) {
|
||||
return exist;
|
||||
}
|
||||
if (exist) return exist;
|
||||
//#endregion
|
||||
|
||||
|
||||
if (uri.startsWith(this.config.url)) {
|
||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
||||
}
|
||||
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||
return await this.createNote(uri, resolver, true);
|
||||
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
||||
return await this.createNote(createFrom, options.resolver, true);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@bindThis
|
||||
public async extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
if (!tags) return [];
|
||||
|
||||
|
||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
||||
|
||||
const existingEmojis = await this.emojisRepository.findBy({
|
||||
host,
|
||||
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
|
||||
name: In(eomjiTags.map(tag => tag.name.replaceAll(':', ''))),
|
||||
});
|
||||
|
||||
|
||||
return await Promise.all(eomjiTags.map(async tag => {
|
||||
const name = tag.name!.replaceAll(':', '');
|
||||
const name = tag.name.replaceAll(':', '');
|
||||
tag.icon = toSingle(tag.icon);
|
||||
|
||||
|
||||
const exists = existingEmojis.find(x => x.name === name);
|
||||
|
||||
|
||||
if (exists) {
|
||||
if ((tag.updated != null && exists.updatedAt == null)
|
||||
if ((exists.updatedAt == null)
|
||||
|| (tag.id != null && exists.uri == null)
|
||||
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
|
||||
|| (tag.icon!.url !== exists.originalUrl)
|
||||
|| (new Date(tag.updated) > exists.updatedAt)
|
||||
|| (tag.icon.url !== exists.originalUrl)
|
||||
) {
|
||||
await this.emojisRepository.update({
|
||||
host,
|
||||
name,
|
||||
}, {
|
||||
uri: tag.id,
|
||||
originalUrl: tag.icon!.url,
|
||||
publicUrl: tag.icon!.url,
|
||||
originalUrl: tag.icon.url,
|
||||
publicUrl: tag.icon.url,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
return await this.emojisRepository.findOneBy({
|
||||
host,
|
||||
name,
|
||||
}) as Emoji;
|
||||
|
||||
const emoji = await this.emojisRepository.findOneBy({ host, name });
|
||||
if (emoji == null) throw new Error('emoji update failed');
|
||||
return emoji;
|
||||
}
|
||||
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
|
||||
this.logger.info(`register emoji host=${host}, name=${name}`);
|
||||
|
||||
|
||||
return await this.emojisRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
host,
|
||||
name,
|
||||
uri: tag.id,
|
||||
originalUrl: tag.icon!.url,
|
||||
publicUrl: tag.icon!.url,
|
||||
originalUrl: tag.icon.url,
|
||||
publicUrl: tag.icon.url,
|
||||
updatedAt: new Date(),
|
||||
aliases: [],
|
||||
} as Partial<Emoji>).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import promiseLimit from 'promise-limit';
|
|||
import { DataSource } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { BlockingsRepository, MutingsRepository, FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { LocalUser, RemoteUser } from '@/models/entities/User.js';
|
||||
import { User } from '@/models/entities/User.js';
|
||||
|
@ -15,7 +15,6 @@ import type Logger from '@/logger.js';
|
|||
import type { Note } from '@/models/entities/Note.js';
|
||||
import type { IdService } from '@/core/IdService.js';
|
||||
import type { MfmService } from '@/core/MfmService.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
import type { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
|
@ -48,6 +47,8 @@ import type { IActor, IObject } from '../type.js';
|
|||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
||||
type Field = Record<'name' | 'value', string>;
|
||||
|
||||
@Injectable()
|
||||
export class ApPersonService implements OnModuleInit {
|
||||
private utilityService: UtilityService;
|
||||
|
@ -94,28 +95,10 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
//private utilityService: UtilityService,
|
||||
//private userEntityService: UserEntityService,
|
||||
//private idService: IdService,
|
||||
//private globalEventService: GlobalEventService,
|
||||
//private metaService: MetaService,
|
||||
//private federatedInstanceService: FederatedInstanceService,
|
||||
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||
//private cacheService: CacheService,
|
||||
//private apResolverService: ApResolverService,
|
||||
//private apNoteService: ApNoteService,
|
||||
//private apImageService: ApImageService,
|
||||
//private apMfmService: ApMfmService,
|
||||
//private mfmService: MfmService,
|
||||
//private hashtagService: HashtagService,
|
||||
//private usersChart: UsersChart,
|
||||
//private instanceChart: InstanceChart,
|
||||
//private apLoggerService: ApLoggerService,
|
||||
) {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
onModuleInit(): void {
|
||||
this.utilityService = this.moduleRef.get('UtilityService');
|
||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
|
@ -153,10 +136,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
private validateActor(x: IObject, uri: string): IActor {
|
||||
const expectHost = this.punyHost(uri);
|
||||
|
||||
if (x == null) {
|
||||
throw new Error('invalid Actor: object is null');
|
||||
}
|
||||
|
||||
if (!isActor(x)) {
|
||||
throw new Error(`invalid Actor type '${x.type}'`);
|
||||
}
|
||||
|
@ -218,21 +197,19 @@ export class ApPersonService implements OnModuleInit {
|
|||
*/
|
||||
@bindThis
|
||||
public async fetchPerson(uri: string): Promise<LocalUser | RemoteUser | null> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null;
|
||||
const cached = this.cacheService.uriPersonCache.get(uri) as LocalUser | RemoteUser | null | undefined;
|
||||
if (cached) return cached;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
if (uri.startsWith(`${this.config.url}/`)) {
|
||||
const id = uri.split('/').pop();
|
||||
const u = await this.usersRepository.findOneBy({ id }) as LocalUser;
|
||||
const u = await this.usersRepository.findOneBy({ id }) as LocalUser | null;
|
||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||
return u;
|
||||
}
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser;
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as LocalUser | RemoteUser | null;
|
||||
|
||||
if (exist) {
|
||||
this.cacheService.uriPersonCache.set(uri, exist);
|
||||
|
@ -254,9 +231,11 @@ export class ApPersonService implements OnModuleInit {
|
|||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(uri) as any;
|
||||
const object = await resolver.resolve(uri);
|
||||
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
||||
|
||||
const person = this.validateActor(object, uri);
|
||||
|
||||
|
@ -264,9 +243,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const host = this.punyHost(object.id);
|
||||
|
||||
const { fields } = this.analyzeAttachments(person.attachment ?? []);
|
||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
||||
|
||||
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
||||
const isBot = getApType(object) === 'Service';
|
||||
|
||||
|
@ -279,9 +258,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
}
|
||||
|
||||
// Create user
|
||||
let user: RemoteUser;
|
||||
let user: RemoteUser | null = null;
|
||||
try {
|
||||
// Start transaction
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
user = await transactionalEntityManager.save(new User({
|
||||
id: this.idService.genId(),
|
||||
|
@ -290,16 +269,16 @@ export class ApPersonService implements OnModuleInit {
|
|||
createdAt: new Date(),
|
||||
lastFetchedAt: new Date(),
|
||||
name: truncate(person.name, nameLength),
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
isLocked: person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo,
|
||||
movedAt: person.movedTo ? new Date() : null,
|
||||
alsoKnownAs: person.alsoKnownAs,
|
||||
isExplorable: !!person.discoverable,
|
||||
isExplorable: person.discoverable,
|
||||
username: person.preferredUsername,
|
||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||
usernameLower: person.preferredUsername?.toLowerCase(),
|
||||
host,
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||
featured: person.featured ? getApId(person.featured) : undefined,
|
||||
uri: person.id,
|
||||
|
@ -311,9 +290,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
await transactionalEntityManager.save(new UserProfile({
|
||||
userId: user.id,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
url: url,
|
||||
url,
|
||||
fields,
|
||||
birthday: bday ? bday[0] : null,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
userHost: host,
|
||||
}));
|
||||
|
@ -327,24 +306,21 @@ export class ApPersonService implements OnModuleInit {
|
|||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||
const u = await this.usersRepository.findOneBy({
|
||||
uri: person.id,
|
||||
});
|
||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
||||
if (u == null) throw new Error('already registered');
|
||||
|
||||
if (u) {
|
||||
user = u as RemoteUser;
|
||||
} else {
|
||||
throw new Error('already registered');
|
||||
}
|
||||
user = u as RemoteUser;
|
||||
} else {
|
||||
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
|
@ -354,29 +330,26 @@ export class ApPersonService implements OnModuleInit {
|
|||
}
|
||||
});
|
||||
|
||||
this.usersChart.update(user!, true);
|
||||
this.usersChart.update(user, true);
|
||||
|
||||
// ハッシュタグ更新
|
||||
this.hashtagService.updateUsertags(user!, tags);
|
||||
this.hashtagService.updateUsertags(user, tags);
|
||||
|
||||
//#region アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = await Promise.all([
|
||||
person.icon,
|
||||
person.image,
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: this.apImageService.resolveImage(user!, img).catch(() => null),
|
||||
));
|
||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||
if (img == null) return null;
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||
}));
|
||||
|
||||
const avatarId = avatar ? avatar.id : null;
|
||||
const bannerId = banner ? banner.id : null;
|
||||
const avatarId = avatar?.id ?? null;
|
||||
const bannerId = banner?.id ?? null;
|
||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||
const avatarBlurhash = avatar?.blurhash ?? null;
|
||||
const bannerBlurhash = banner?.blurhash ?? null;
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
await this.usersRepository.update(user.id, {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
|
@ -385,37 +358,35 @@ export class ApPersonService implements OnModuleInit {
|
|||
bannerBlurhash,
|
||||
});
|
||||
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
user!.avatarUrl = avatarUrl;
|
||||
user!.bannerUrl = bannerUrl;
|
||||
user!.avatarBlurhash = avatarBlurhash;
|
||||
user!.bannerBlurhash = bannerBlurhash;
|
||||
user.avatarId = avatarId;
|
||||
user.bannerId = bannerId;
|
||||
user.avatarUrl = avatarUrl;
|
||||
user.bannerUrl = bannerUrl;
|
||||
user.avatarBlurhash = avatarBlurhash;
|
||||
user.bannerBlurhash = bannerBlurhash;
|
||||
//#endregion
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [] as Emoji[];
|
||||
return [];
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
emojis: emojiNames,
|
||||
});
|
||||
await this.usersRepository.update(user.id, { emojis: emojiNames });
|
||||
//#endregion
|
||||
|
||||
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
|
||||
|
||||
return user!;
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Personの情報を更新します。
|
||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||
* もしアカウントの移行が確認された場合、アカウント移行処理を行います。
|
||||
*
|
||||
*
|
||||
* @param uri URI of Person
|
||||
* @param resolver Resolver
|
||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
||||
|
@ -426,18 +397,14 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (uri.startsWith(`${this.config.url}/`)) {
|
||||
return;
|
||||
}
|
||||
if (uri.startsWith(`${this.config.url}/`)) return;
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||
|
||||
if (exist === null) {
|
||||
return;
|
||||
}
|
||||
if (exist === null) return;
|
||||
//#endregion
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
|
||||
const object = hint ?? await resolver.resolve(uri);
|
||||
|
@ -447,26 +414,22 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.logger.info(`Updating the Person: ${person.id}`);
|
||||
|
||||
// アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = await Promise.all([
|
||||
person.icon,
|
||||
person.image,
|
||||
].map(img =>
|
||||
img == null
|
||||
? Promise.resolve(null)
|
||||
: this.apImageService.resolveImage(exist, img).catch(() => null),
|
||||
));
|
||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||
if (img == null) return null;
|
||||
return this.apImageService.resolveImage(exist, img).catch(() => null);
|
||||
}));
|
||||
|
||||
// カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
||||
this.logger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
return [];
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
|
||||
const { fields } = this.analyzeAttachments(person.attachment ?? []);
|
||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
||||
|
||||
const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
|
@ -479,7 +442,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||
featured: person.featured,
|
||||
emojis: emojiNames,
|
||||
|
@ -487,18 +450,29 @@ export class ApPersonService implements OnModuleInit {
|
|||
tags,
|
||||
isBot: getApType(object) === 'Service',
|
||||
isCat: (person as any).isCat === true,
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
isLocked: person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo ?? null,
|
||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||
isExplorable: !!person.discoverable,
|
||||
isExplorable: person.discoverable,
|
||||
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||
|
||||
const moving =
|
||||
const moving = ((): boolean => {
|
||||
// 移行先がない→ある
|
||||
(!exist.movedToUri && updates.movedToUri) ||
|
||||
if (
|
||||
exist.movedToUri === null &&
|
||||
updates.movedToUri
|
||||
) return true;
|
||||
|
||||
// 移行先がある→別のもの
|
||||
(exist.movedToUri !== updates.movedToUri && exist.movedToUri && updates.movedToUri);
|
||||
if (
|
||||
exist.movedToUri !== null &&
|
||||
updates.movedToUri !== null &&
|
||||
exist.movedToUri !== updates.movedToUri
|
||||
) return true;
|
||||
|
||||
// 移行先がある→ない、ない→ないは無視
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (moving) updates.movedAt = new Date();
|
||||
|
||||
|
@ -525,10 +499,10 @@ export class ApPersonService implements OnModuleInit {
|
|||
}
|
||||
|
||||
await this.userProfilesRepository.update({ userId: exist.id }, {
|
||||
url: url,
|
||||
url,
|
||||
fields,
|
||||
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
|
||||
birthday: bday ? bday[0] : null,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
|
||||
|
@ -538,11 +512,10 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.hashtagService.updateUsertags(exist, tags);
|
||||
|
||||
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||
await this.followingsRepository.update({
|
||||
followerId: exist.id,
|
||||
}, {
|
||||
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
});
|
||||
await this.followingsRepository.update(
|
||||
{ followerId: exist.id },
|
||||
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
|
||||
);
|
||||
|
||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||
|
||||
|
@ -580,27 +553,22 @@ export class ApPersonService implements OnModuleInit {
|
|||
*/
|
||||
@bindThis
|
||||
public async resolvePerson(uri: string, resolver?: Resolver): Promise<LocalUser | RemoteUser> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await this.fetchPerson(uri);
|
||||
|
||||
if (exist) {
|
||||
return exist;
|
||||
}
|
||||
if (exist) return exist;
|
||||
//#endregion
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
return await this.createPerson(uri, resolver);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public analyzeAttachments(attachments: IObject | IObject[] | undefined) {
|
||||
const fields: {
|
||||
name: string,
|
||||
value: string
|
||||
}[] = [];
|
||||
// TODO: `attachments`が`IObject`だった場合、返り値が`[]`になるようだが構わないのか?
|
||||
public analyzeAttachments(attachments: IObject | IObject[] | undefined): Field[] {
|
||||
const fields: Field[] = [];
|
||||
|
||||
if (Array.isArray(attachments)) {
|
||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||
fields.push({
|
||||
|
@ -610,11 +578,11 @@ export class ApPersonService implements OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
return { fields };
|
||||
return fields;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateFeatured(userId: User['id'], resolver?: Resolver) {
|
||||
public async updateFeatured(userId: User['id'], resolver?: Resolver): Promise<void> {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||
if (!user.featured) return;
|
||||
|
@ -636,20 +604,23 @@ export class ApPersonService implements OnModuleInit {
|
|||
const featuredNotes = await Promise.all(items
|
||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||
.slice(0, 5)
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, _resolver))));
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, {
|
||||
resolver: _resolver,
|
||||
sentFrom: new URL(user.uri),
|
||||
}))));
|
||||
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||
|
||||
// とりあえずidを別の時間で生成して順番を維持
|
||||
let td = 0;
|
||||
for (const note of featuredNotes.filter(note => note != null)) {
|
||||
for (const note of featuredNotes.filter((note): note is Note => note != null)) {
|
||||
td -= 1000;
|
||||
transactionalEntityManager.insert(UserNotePining, {
|
||||
id: this.idService.genId(new Date(Date.now() + td)),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
noteId: note!.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -688,7 +659,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
// (uriが存在しなかったり応答がなかったりする場合resolvePersonはthrow Errorする)
|
||||
dst = await this.resolvePerson(src.movedToUri);
|
||||
}
|
||||
|
||||
|
||||
if (dst.movedToUri === dst.uri) return 'skip: movedTo itself (dst)'; // ???
|
||||
if (src.movedToUri !== dst.uri) return 'skip: missmatch uri'; // ???
|
||||
if (dst.movedToUri === src.uri) return 'skip: dst.movedToUri === src.uri';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue