forked from FoundKeyGang/FoundKey
Compare commits
126 commits
feature/ve
...
main
Author | SHA1 | Date | |
---|---|---|---|
Johann150 | be950f5313 | ||
Johann150 | a9c6e51051 | ||
Johann150 | c18a7a8d30 | ||
Johann150 | d34f7c7faa | ||
Johann150 | 095802d059 | ||
Johann150 | f245b6e517 | ||
Johann150 | c9759a3a79 | ||
Johann150 | a7a663e939 | ||
Johann150 | 7458550f7a | ||
Johann150 | 5444ca9aca | ||
Johann150 | d293fc1dc7 | ||
Johann150 | e2779befe6 | ||
Johann150 | 2218936af3 | ||
Johann150 | b8b69f825a | ||
Johann150 | 01f8c5d7da | ||
Johann150 | 7e37a8fd88 | ||
Johann150 | e2311a6f4b | ||
Johann150 | ac1ef641f5 | ||
Johann150 | 1af0687423 | ||
Johann150 | 09ff7f0c7d | ||
Johann150 | f285281b5a | ||
Johann150 | 624157f03e | ||
Johann150 | e366116ac1 | ||
Johann150 | 2b5a35147a | ||
Johann150 | 1098b3a038 | ||
Johann150 | 6501c542b2 | ||
Johann150 | ab22a1afa0 | ||
Johann150 | 5f09a44dbb | ||
Johann150 | 2c55f8968c | ||
Johann150 | fc733a4a86 | ||
Johann150 | 5636534d03 | ||
Johann150 | 4b121e7615 | ||
Johann150 | 5664c9fdf7 | ||
Johann150 | d82c72a111 | ||
Johann150 | f751941a30 | ||
Johann150 | 76aef3de74 | ||
Johann150 | dbdb2b70f1 | ||
Johann150 | d4a5ed29db | ||
Johann150 | fba8536743 | ||
Johann150 | 4b3154c22c | ||
Johann150 | 47b3277201 | ||
c8f8e4c01d | |||
Johann150 | 6ee8a369b3 | ||
Johann150 | c504091c61 | ||
Johann150 | aac1c40657 | ||
Johann150 | 83bce62672 | ||
Johann150 | 6fd422f2b0 | ||
Johann150 | b94aeb2df2 | ||
Johann150 | ada577bde6 | ||
Johann150 | 3968a6ca07 | ||
Johann150 | 86565cd25b | ||
Johann150 | 24f6177b94 | ||
Johann150 | 78359daac6 | ||
Johann150 | 2cf80a8ccf | ||
Johann150 | 6bd42ab3f9 | ||
Johann150 | d24967c36c | ||
Johann150 | 5d60ba6c50 | ||
Ignas Kiela | 66560f9977 | ||
Johann150 | c67ff44207 | ||
Johann150 | bed6a1e2d8 | ||
Johann150 | 5f9fb28fc2 | ||
Johann150 | 2917fdcb34 | ||
Johann150 | 2a83a6ae8c | ||
Johann150 | 89761c86ab | ||
Johann150 | d1cde9c75e | ||
Johann150 | b7dc3cca22 | ||
2bb91f34df | |||
Johann150 | 8b16ead35c | ||
Johann150 | 101ea21747 | ||
Johann150 | 8d78113907 | ||
Johann150 | 451c674906 | ||
Johann150 | 6367fcca79 | ||
Johann150 | f4e234d108 | ||
Johann150 | a6c5e9f358 | ||
Johann150 | 0bcbb38ecc | ||
Johann150 | 0bdc24b34e | ||
1c0c75d6ca | |||
Johann150 | 2fcea24817 | ||
Johann150 | 5bb10de1e0 | ||
Johann150 | f6c3d44265 | ||
Johann150 | 1e7d2cf54c | ||
Johann150 | 37658f5162 | ||
Johann150 | 0cb4529ed0 | ||
Johann150 | 2c69cb4a92 | ||
Johann150 | c669e9212f | ||
Johann150 | b39716a199 | ||
Norm | dbe2b7611d | ||
Johann150 | 650b797fd6 | ||
Johann150 | 9968b21da7 | ||
Johann150 | 6dd782d4d5 | ||
Johann150 | b61e477c0c | ||
Johann150 | 75ab4de41f | ||
Johann150 | 456a86af8d | ||
Johann150 | 2fbd31abe6 | ||
Johann150 | 9b4e976bda | ||
Johann150 | cc776a6b9b | ||
Johann150 | bf698987c3 | ||
Johann150 | 45112158b0 | ||
Johann150 | 46660abb6a | ||
Johann150 | 415e84daf0 | ||
Norm | 29320a751d | ||
remi | a5080f80a1 | ||
Johann150 | 3e4c37eb31 | ||
Norm | 2cb89a6bf0 | ||
Chloe Kudryavtsev | d3eb6a3340 | ||
Chloe Kudryavtsev | af1893d71f | ||
Johann150 | 77358c8f4b | ||
Johann150 | 62cd1e7ed6 | ||
Johann150 | 796adc8599 | ||
Johann150 | ff8b9b6651 | ||
Johann150 | c1268c04f8 | ||
Johann150 | 42b555e5e4 | ||
Johann150 | 5c3e7c132a | ||
Johann150 | 76c8e6b11b | ||
Johann150 | ca24080596 | ||
Johann150 | a12debb7b6 | ||
Johann150 | f760426142 | ||
Johann150 | 2f30af1812 | ||
Johann150 | 2d46cf7c1e | ||
Johann150 | 2ea6daaf7a | ||
Johann150 | 597de07465 | ||
Johann150 | 9289b0e8ed | ||
Hélène | b600efae0d | ||
Johann150 | ecca5a164e | ||
Johann150 | 1125a623a7 | ||
Johann150 | 51a319e8ca |
|
@ -153,3 +153,9 @@ redis:
|
|||
# info: /twemoji/1f440.svg
|
||||
# notFound: /twemoji/2049.svg
|
||||
# error: /twemoji/1f480.svg
|
||||
|
||||
# Whether it should be allowed to fetch content in ActivityPub form without HTTP signatures.
|
||||
# It is recommended to leave this as default to improve the effectiveness of instance blocks.s
|
||||
# However, note that while this prevents fetching in ActivityPub form, it could still be scraped
|
||||
# from the API or other representations if the other side is determined to do so.
|
||||
#allowUnsignedFetches: false
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -11,6 +11,33 @@ Unreleased changes should not be listed in this file.
|
|||
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
||||
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||
|
||||
## 13.0.0-preview6 - 2023-07-02
|
||||
|
||||
## Added
|
||||
- **BREAKING** activitypub: validate fetch signatures
|
||||
Fetching the ActivityPub representation of something now requires a valid HTTP signature.
|
||||
- client: add MFM functions `position`, `scale`, `fg`, `bg`
|
||||
- server: add webhook stat to nodeinfo
|
||||
- activitypub: handle incoming Update Note activities
|
||||
|
||||
## Changed
|
||||
- client: change followers only icon to closed lock
|
||||
- client: disable sound for received note by default
|
||||
- client: always forbid MFM overflow
|
||||
- make mutes case insensitive
|
||||
- activitypub: improve JSON-LD context
|
||||
The context now properly notes the `@type`s of defined attributes.
|
||||
- docker: only publish port on localhost
|
||||
|
||||
## Fixed
|
||||
- server: fix internal download in emoji import
|
||||
- server: replace unzipper with decompress
|
||||
|
||||
## Removed
|
||||
- migrate note favorites to clips
|
||||
If you previously had favorites they will now be in a clip called "⭐".
|
||||
If you want to add a note as a "favorite" you can use the menu item "Clip".
|
||||
|
||||
## 13.0.0-preview5 - 2023-05-23
|
||||
This release contains 6 breaking changes and 1 security update.
|
||||
|
||||
|
|
|
@ -62,6 +62,8 @@ representative at an online or offline event.
|
|||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement via email at
|
||||
johann<EFBFBD>qwertqwefsday.eu and/or toast<73>bunkerlabs.net .
|
||||
(The at sign has been replaced so that spammers do not find these email addresses easily.
|
||||
If you are a human you hopefully know what to do.)
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
|
|
@ -11,13 +11,9 @@ Please understand that in such cases we might edit your issue to translate it, t
|
|||
## Development platform
|
||||
FoundKey generally assumes that it is running on a Unix-like platform (e.g. Linux or macOS). If you are using Windows for development, we highly suggest using the Windows Subsystem for Linux (WSL) as the development environment.
|
||||
|
||||
## Roadmap
|
||||
See [ROADMAP.md](./ROADMAP.md)
|
||||
|
||||
## Issues
|
||||
Issues are intended for feature requests and bug tracking.
|
||||
|
||||
For technical support or if you are not sure if what you are experiencing is a bug you can talk to people on the [IRC server](https://irc.akkoma.dev) in the `#foundkey` channel first.
|
||||
Please note that in general, we are not looking for completely new features to add, but quality of life improvements will be considered.
|
||||
|
||||
Please do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||
|
||||
|
@ -25,7 +21,6 @@ Please do not close issues that are about to be resolved. It should remain open
|
|||
branch|what it's for
|
||||
---|---
|
||||
main|development branch
|
||||
translate|managed by weblate, see [section about translation](#Translation)
|
||||
|
||||
For a production environment you might not want to follow the `main` branch directly but instead check out one of the git tags.
|
||||
|
||||
|
@ -154,8 +149,6 @@ Here is the step by step checklist:
|
|||
|
||||
<small>a.k.a. Localization (l10n) or Internationalization (i18n)</small>
|
||||
|
||||
To translate text used in Foundkey, we use weblate at <https://translate.akkoma.dev/projects/foundkey/>.
|
||||
|
||||
Localization files are found in `/locales/` and are YAML files using the `yml` file extension.
|
||||
The file name consists of the [IETF BCP 47](https://www.rfc-editor.org/info/bcp47) language code.
|
||||
|
||||
|
|
|
@ -7,6 +7,10 @@ Look further up in the section to find the "base path" it is relative to.
|
|||
|
||||
All the backend code is in `/packages/backend/src`.
|
||||
|
||||
The backend is started via `index.ts` which in turn starts `boot/index.ts`.
|
||||
In the "boot" code is where the process is forked from the main process into additional and separate worker and frontend processes.
|
||||
If you look into your operating system's process overview or similar, you might be able to see that the processes rename themselves accordingly.
|
||||
|
||||
### Database
|
||||
|
||||
For connecting to the database an ORM (object–relational mapping) is used.
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<div align="center"><img src="./logo.svg" height="200" alt="Foundkey logo, an owl holding a key"/></div>
|
||||
|
||||
# FoundKey
|
||||
FoundKey is a free and open source microblogging server compatible with ActivityPub. Forked from Misskey, FoundKey improves on maintainability and behaviour, while also bringing in useful features.
|
||||
FoundKey is a free and open source microblogging server compatible with ActivityPub.
|
||||
It is currently under **LIMITED MAINTENANCE** and is not well suited for large instances.
|
||||
No more than 20 users per instance are recommended.
|
||||
|
||||
Forked from Misskey, FoundKey improves on maintainability and behaviour, while also bringing in useful features.
|
||||
|
||||
See the [changelog](./CHANGELOG.md) and [roadmap](./ROADMAP.md) for more on what's changed and future plans.
|
||||
|
||||
## Documentation
|
||||
FoundKey's documentation is a work in progress, which can be found in the `docs/` folder.
|
||||
|
||||
In the meantime, much of the documentation on the [Misskey Hub](https://misskey-hub.net/) will also apply to FoundKey.
|
||||
Feel free to contribute some documentation.
|
||||
|
||||
## Contributing
|
||||
If you're interested in helping out with the project, please read the [contributing guide](./CONTRIBUTING.md).
|
||||
|
|
|
@ -74,6 +74,9 @@ The fields of `Meta` are currently not used or checked when importing emoji, exc
|
|||
For each `Emoji`:
|
||||
- `downloaded`: should always be true. If the field is missing or not truthy, the emoji will not be imported.
|
||||
- `fileName`: name of the image file inside the packed file.
|
||||
The filename has to match the following ECMAScript RegExp: `/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/`
|
||||
(i.e. composed of latin letters, digits, underscores or dots, not starting with a dot and not ending with an underscore)
|
||||
If the file does not match this RegExp, the respective emoji will not be imported!
|
||||
- `emoji`: data associated with the emoji as it was stored in the database. Currently most of these fields are
|
||||
not even checked for existence. The following are currently used:
|
||||
- `name`: name of the emoji for the user, e.g. `blobfox` if a user should type in `:blobfox:` to get the emoji.
|
||||
|
|
|
@ -21,10 +21,12 @@ LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n nsfwDetection16
|
|||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||
|
||||
for i in $(seq 1 $NUM_MIGRATIONS); do
|
||||
npx typeorm migration:revert -d ormconfig.js
|
||||
npx typeorm migration:revert -d ormconfig.js || continue
|
||||
done
|
||||
```
|
||||
|
||||
**Note:** TypeORM might hang when reverting a migration. If it says that the migration was reverted successfully, you can force close TypeORM using Ctrl-C. The script will continue until all of the migrations have been reverted.
|
||||
|
||||
## Switching repositories
|
||||
To switch to the FoundKey repository, do the following in your Misskey install location:
|
||||
```sh
|
||||
|
|
|
@ -220,7 +220,6 @@ uploadFromUrl: "ارفع عبر رابط"
|
|||
uploadFromUrlDescription: "رابط الملف المراد رفعه"
|
||||
uploadFromUrlRequested: "الرفع مطلوب"
|
||||
uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع"
|
||||
explore: "استكشاف"
|
||||
messageRead: "مقروءة"
|
||||
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
||||
startMessaging: "ابدأ محادثة"
|
||||
|
@ -300,9 +299,6 @@ inMb: "بالميغابايت"
|
|||
iconUrl: "رابط الأيقونة"
|
||||
bannerUrl: "رابط صورة اللافتة"
|
||||
backgroundImageUrl: "رابط صورة الخلفية"
|
||||
pinnedUsers: "المستخدمون المدبسون"
|
||||
pinnedUsersDescription: "قائمة المستخدمين المدبسين في لسان \"استكشف\" ، اجعل كل اسم\
|
||||
\ مستخدم في سطر لوحده."
|
||||
hcaptchaSiteKey: "مفتاح الموقع"
|
||||
hcaptchaSecretKey: "المفتاح السري"
|
||||
recaptchaSiteKey: "مفتاح الموقع"
|
||||
|
@ -327,11 +323,6 @@ silence: "اكتم"
|
|||
silenceConfirm: "أمتأكد من كتم هذا المستخدم؟"
|
||||
unsilence: "إلغاء الكتم"
|
||||
unsilenceConfirm: "أمتأكد من إلغاء كتم هذا المستخدم؟"
|
||||
popularUsers: "المستخدمون الرائدون"
|
||||
recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة"
|
||||
recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا"
|
||||
recentlyDiscoveredUsers: "المستخدمون المكتشفون حديثًا"
|
||||
popularTags: "الوسوم الرائجة"
|
||||
userList: "القوائم"
|
||||
aboutMisskey: "عن FoundKey"
|
||||
administrator: "المدير"
|
||||
|
@ -372,7 +363,6 @@ messagingWithGroup: "محادثة جماعية"
|
|||
title: "العنوان"
|
||||
text: "النص"
|
||||
enable: "تشغيل"
|
||||
next: "التالية"
|
||||
retype: "أعد الكتابة"
|
||||
noteOf: "ملاحظات {user}"
|
||||
inviteToGroup: "دعوة إلى فريق"
|
||||
|
@ -533,7 +523,6 @@ abuseReports: "البلاغات"
|
|||
reportAbuse: "أبلغ"
|
||||
reportAbuseOf: "أبلغ عن {name}"
|
||||
fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ"
|
||||
abuseReported: "أُرسل البلاغ، شكرًا لك"
|
||||
reporter: "المُبلّغ"
|
||||
reporteeOrigin: "أصل البلاغ"
|
||||
reporterOrigin: "أصل المُبلّغ"
|
||||
|
@ -881,35 +870,6 @@ _time:
|
|||
minute: "د"
|
||||
hour: "سا"
|
||||
day: "ي"
|
||||
_tutorial:
|
||||
title: "كيف تستخدم FoundKey"
|
||||
step1_1: "مرحبًا!"
|
||||
step1_2: "تدعى هذه الصفحة 'الخيط الزمني' وهي تحوي ملاحظات الأشخاص الذي تتابعهم مرتبة\
|
||||
\ حسب تاريخ نشرها."
|
||||
step1_3: "خيطك الزمني فارغ حاليًا بما أنك لا تتابع أي شخص ولم تنشر أي ملاحظة."
|
||||
step2_1: "لننهي إعداد ملفك الشخصي قبل كتابة ملاحظة أو متابعة أشخاص."
|
||||
step2_2: "أعطاء معلومات عن شخصيتك يمنح من له نفس إهتماماتك فرصة متابعتك والتفاعل\
|
||||
\ مع ملاحظاتك."
|
||||
step3_1: "هل أنهيت إعداد حسابك؟"
|
||||
step3_2: "إذا تاليًا لتنشر ملاحظة. أنقر على أيقونة القلم في أعلى الشاشة"
|
||||
step3_3: "املأ النموذج وانقر الزرّ الموجود في أعلى اليمين للإرسال."
|
||||
step3_4: "ليس لديك ما تقوله؟ إذا اكتب \"بدأتُ استخدم ميسكي\"."
|
||||
step4_1: "هل نشرت ملاحظتك الأولى؟"
|
||||
step4_2: "مرحى! يمكنك الآن رؤية ملاحظتك في الخيط الزمني."
|
||||
step5_1: "والآن، لنجعل الخيط الزمني أكثر حيوية وذلك بمتابعة بعض المستخدمين."
|
||||
step5_2: "تعرض صفحة {features} الملاحظات المتداولة في هذا المثيل ويتيح لك {Explore}\
|
||||
\ العثور على المستخدمين الرائدين. اعثر على الأشخاص الذين يثيرون إهتمامك وتابعهم!"
|
||||
step5_3: "لمتابعة مستخدمين ادخل ملفهم الشخصي بالنقر على صورتهم الشخصية ثم اضغط زر\
|
||||
\ 'تابع'."
|
||||
step5_4: "إذا كان لدى المستخدم رمز قفل بجوار اسمه ، وجب عليك انتظاره ليقبل طلب المتابعة\
|
||||
\ يدويًا."
|
||||
step6_1: "الآن ستتمكن من رؤية ملاحظات المستخدمين المتابَعين في الخيط الزمني."
|
||||
step6_2: "يمكنك التفاعل بسرعة مع الملاحظات عن طريق إضافة \"تفاعل\"."
|
||||
step6_3: "لإضافة تفاعل لملاحظة ، انقر فوق علامة \"+\" أسفل للملاحظة واختر الإيموجي\
|
||||
\ المطلوب."
|
||||
step7_1: "مبارك ! أنهيت الدورة التعليمية الأساسية لاستخدام ميسكي."
|
||||
step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}."
|
||||
step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
|
||||
registerDevice: "سجّل جهازًا جديدًا"
|
||||
|
|
|
@ -234,7 +234,6 @@ uploadFromUrl: "URL হতে আপলোড"
|
|||
uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL"
|
||||
uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে"
|
||||
uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।"
|
||||
explore: "ঘুরে দেখুন"
|
||||
messageRead: "পড়া"
|
||||
noMoreHistory: "আর কোন ইতিহাস নেই"
|
||||
startMessaging: "চ্যাট শুরু করুন"
|
||||
|
@ -315,9 +314,6 @@ inMb: "মেগাবাইটে লিখুন"
|
|||
iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)"
|
||||
bannerUrl: "ব্যানার ছবির URL"
|
||||
backgroundImageUrl: "পটভূমির চিত্রের URL"
|
||||
pinnedUsers: "পিন করা ব্যাবহারকারীগণ"
|
||||
pinnedUsersDescription: "আপনি যেসব ব্যবহারকারীদের \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে\
|
||||
\ চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন"
|
||||
hcaptchaSiteKey: "সাইট কী"
|
||||
hcaptchaSecretKey: "সিক্রেট কী"
|
||||
recaptchaSiteKey: "সাইট কী"
|
||||
|
@ -342,11 +338,6 @@ silence: "নীরব"
|
|||
silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?"
|
||||
unsilence: "সরব"
|
||||
unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?"
|
||||
popularUsers: "জনপ্রিয় ব্যবহারকারীগন"
|
||||
recentlyUpdatedUsers: "সম্প্রতি পোস্ট করা ব্যবহারকারীগন"
|
||||
recentlyRegisteredUsers: "নতুন যোগ দেওয়া ব্যবহারকারীগন"
|
||||
recentlyDiscoveredUsers: "নতুন খুঁজে পাওয়া ব্যবহারকারীগন"
|
||||
popularTags: "জনপ্রিয় ট্যাগগুলি"
|
||||
userList: "লিস্ট"
|
||||
aboutMisskey: "FoundKey সম্পর্কে"
|
||||
administrator: "প্রশাসক"
|
||||
|
@ -387,7 +378,6 @@ messagingWithGroup: "গ্রুপ চ্যাট"
|
|||
title: "শিরোনাম"
|
||||
text: "পাঠ্য"
|
||||
enable: "সক্রিয়"
|
||||
next: "পরবর্তী"
|
||||
retype: "পুনঃ প্রবেশ"
|
||||
noteOf: "{user} এর নোট"
|
||||
inviteToGroup: "গ্রুপে আমন্ত্রণ জানান"
|
||||
|
@ -579,7 +569,6 @@ abuseReports: "অভিযোগ"
|
|||
reportAbuse: "অভিযোগ"
|
||||
reportAbuseOf: "{name} এ অভিযোগ করুন"
|
||||
fillAbuseReportDescription: "রিপোর্টের কারণ বর্ণনা করুন."
|
||||
abuseReported: "আপনার অভিযোগটি দাখিল করা হয়েছে। আপনাকে ধন্যবাদ।"
|
||||
reporter: "অভিযোগকারী"
|
||||
reporteeOrigin: "অভিযোগটির উৎস"
|
||||
reporterOrigin: "অভিযোগকারীর উৎস"
|
||||
|
@ -975,41 +964,6 @@ _time:
|
|||
minute: "মিনিট"
|
||||
hour: "ঘণ্টা"
|
||||
day: "দিন"
|
||||
_tutorial:
|
||||
title: "FoundKey কিভাবে ব্যাবহার করবেন"
|
||||
step1_1: "স্বাগতম!"
|
||||
step1_2: "এই স্ক্রীনটিকে \"টাইমলাইন\" বলা হয় এবং কালানুক্রমিক ক্রমে আপনার এবং আপনি\
|
||||
\ যাদের \"অনুসরণ করেন\" তাদের \"নোটগুলি\" দেখায়৷"
|
||||
step1_3: "আপনি আপনার টাইমলাইনে কিছু দেখতে পাবেন না কারণ আপনি এখনও কোনো নোট পোস্ট\
|
||||
\ করেননি এবং আপনি কাউকে অনুসরণ করছেন না৷"
|
||||
step2_1: "নোট তৈরি করার আগে বা কাউকে অনুসরণ করার আগে প্রথমে আপনার প্রোফাইলটি সম্পূর্ণ\
|
||||
\ করুন।"
|
||||
step2_2: "আপনি কে তা জানা অনেক লোকের জন্য আপনার নোটগুলি দেখা এবং অনুসরণ করাকে সহজ\
|
||||
\ করে তোলে৷"
|
||||
step3_1: "আপনি কি সফলভাবে আপনার প্রোফাইল সেট আপ করেছেন?"
|
||||
step3_2: "এখন, কিছু নোট পোস্ট করার চেষ্টা করুন। পোস্ট ফর্ম খুলতে পেন্সিল চিহ্নযুক্ত\
|
||||
\ বাটনে ক্লিক করুন।"
|
||||
step3_3: "বিষয়বস্তু লেখার পরে, আপনি ফর্মের উপরের ডানদিকের বাটনে ক্লিক করে পোস্ট\
|
||||
\ করতে পারেন।"
|
||||
step3_4: "পোস্ট করার মত কিছু মনে পরছে না? \"আমি মিসকি সেট আপ করছি\" বললে কেমন হয়?"
|
||||
step4_1: "পোস্ট করেছেন?"
|
||||
step4_2: "সাবাশ! এখন আপনার নোট টাইমলাইনে দেখা যাবে।"
|
||||
step5_1: "এখন অন্যদেরকে অনুসরণ করে আপনার টাইমলাইনকে প্রাণবন্ত করে তুলুন।"
|
||||
step5_2: "আপনি {featured}-এ জনপ্রিয় নোটগুলি দেখতে পারেন, যাতে আপনি যে ব্যক্তিকে\
|
||||
\ পছন্দ করেন তাকে বেছে নিতে এবং অনুসরণ করতে পারেন, অথবা {explore}-এ জনপ্রিয় ব্যবহারকারীদের\
|
||||
\ দেখতে পারেন৷"
|
||||
step5_3: "একজন ব্যবহারকারীকে অনুসরণ করতে, ব্যবহারকারীর আইকনে ক্লিক করুন এবং ব্যবহারকারীর\
|
||||
\ পৃষ্ঠাতে \"অনুসরণ করুন\" বাটনে ক্লিক করুন।"
|
||||
step5_4: "যদি ব্যবহারকারীর নামের পাশে একটি লক আইকন থাকে তাহলে আপনার অনুসরণের অনুরোধ\
|
||||
\ গ্রহণ করার জন্য তারা কিছু সময় নিতে পারে।"
|
||||
step6_1: "সবকিছু ঠিক থাকলে আপনি টাইমলাইনে অন্য ব্যবহারকারীদের নোট দেখতে পাবেন।"
|
||||
step6_2: "আপনি সহজেই আপনার প্রতিক্রিয়া জানাতে অন্য ব্যক্তির নোটে \"রিঅ্যাকশন\"\
|
||||
\ যোগ করতে পারেন।"
|
||||
step6_3: "একটি রিঅ্যাকশন যোগ করতে, নোটে \"+\" চিহ্নে ক্লিক করুন এবং আপনার পছন্দের\
|
||||
\ রিঅ্যাকশন নির্বাচন করুন।"
|
||||
step7_1: "অভিনন্দন! আপনি এখন FoundKey-র প্রাথমিক টিউটোরিয়ালটি শেষ করেছেন।"
|
||||
step7_2: "আপনি যদি FoundKey সম্পর্কে আরও জানতে চান, তাহলে {help} এ দেখুন।"
|
||||
step7_3: "এখন FoundKey উপভোগ করুন \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷"
|
||||
registerDevice: "নতুন ডিভাইস নিবন্ধন করুন"
|
||||
|
|
|
@ -210,7 +210,6 @@ fromUrl: "Z URL"
|
|||
uploadFromUrl: "Nahrát z URL adresy"
|
||||
uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát"
|
||||
uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání."
|
||||
explore: "Objevovat"
|
||||
messageRead: "Přečtené"
|
||||
noMoreHistory: "To je vše"
|
||||
startMessaging: "Zahájit chat"
|
||||
|
@ -281,7 +280,6 @@ inMb: "V megabajtech"
|
|||
iconUrl: "Favicon URL"
|
||||
bannerUrl: "Baner URL"
|
||||
backgroundImageUrl: "Adresa URL obrázku pozadí"
|
||||
pinnedUsers: "Připnutí uživatelé"
|
||||
hcaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
||||
recaptchaSecretKey: "Tajný Klíč (Secret Key)"
|
||||
antennas: "Antény"
|
||||
|
@ -290,7 +288,6 @@ name: "Jméno"
|
|||
antennaSource: "Zdroj Antény"
|
||||
caseSensitive: "Rozlišuje malá a velká písmena"
|
||||
connectedTo: "Následující účty jsou připojeny"
|
||||
popularTags: "Populární tagy"
|
||||
userList: "Seznamy"
|
||||
aboutMisskey: "O FoundKey"
|
||||
administrator: "Administrátor"
|
||||
|
@ -328,7 +325,6 @@ transfer: "Převod"
|
|||
title: "Titulek"
|
||||
text: "Text"
|
||||
enable: "Povolit"
|
||||
next: "Další"
|
||||
retype: "Zadejte znovu"
|
||||
noteOf: "{user} poznámky"
|
||||
inviteToGroup: "Pozvat do skupiny"
|
||||
|
|
|
@ -245,7 +245,6 @@ uploadFromUrlDescription: "URL der hochzuladenden Datei"
|
|||
uploadFromUrlRequested: "Upload angefordert"
|
||||
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen\
|
||||
\ ist."
|
||||
explore: "Erkunden"
|
||||
messageRead: "Gelesen"
|
||||
noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
||||
startMessaging: "Neuen Chat erstellen"
|
||||
|
@ -328,9 +327,6 @@ inMb: "In Megabytes"
|
|||
iconUrl: "Icon-URL (favicon etc)"
|
||||
bannerUrl: "Banner-URL"
|
||||
backgroundImageUrl: "Hintergrundbild-URL"
|
||||
pinnedUsers: "Angeheftete Benutzer"
|
||||
pinnedUsersDescription: "Gib durch Leerzeichen getrennte Benutzer an, die an die \"\
|
||||
Erkunden\"-Seite angeheftet werden sollen."
|
||||
hcaptchaSiteKey: "Site key"
|
||||
hcaptchaSecretKey: "Geheimer Schlüssel"
|
||||
recaptchaSiteKey: "Site-Schlüssel"
|
||||
|
@ -357,11 +353,6 @@ silenceConfirm: "Möchtest du diesen Benutzer wirklich instanzweit stummschalten
|
|||
unsilence: "Instanzweite Stummschaltung aufheben"
|
||||
unsilenceConfirm: "Möchtest du die instanzweite Stummschaltung dieses Benutzers wirklich\
|
||||
\ aufheben?"
|
||||
popularUsers: "Beliebte Benutzer"
|
||||
recentlyUpdatedUsers: "Vor kurzem aktive Benutzer"
|
||||
recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer"
|
||||
recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer"
|
||||
popularTags: "Beliebte Schlagwörter"
|
||||
userList: "Liste"
|
||||
aboutMisskey: "Über FoundKey"
|
||||
administrator: "Administrator"
|
||||
|
@ -402,7 +393,6 @@ messagingWithGroup: "Gruppenchat"
|
|||
title: "Titel"
|
||||
text: "Text"
|
||||
enable: "Aktivieren"
|
||||
next: "Weiter"
|
||||
retype: "Erneut eingeben"
|
||||
noteOf: "Notiz von {user}"
|
||||
inviteToGroup: "Zu Gruppe einladen"
|
||||
|
@ -600,7 +590,6 @@ reportAbuse: "Melden"
|
|||
reportAbuseOf: "{name} melden"
|
||||
fillAbuseReportDescription: "Bitte gib zusätzliche Informationen zu dieser Meldung\
|
||||
\ an."
|
||||
abuseReported: "Deine Meldung wurde versendet. Vielen Dank."
|
||||
reporter: "Melder"
|
||||
reporteeOrigin: "Herkunft des Gemeldeten"
|
||||
reporterOrigin: "Herkunft des Meldenden"
|
||||
|
@ -1014,44 +1003,6 @@ _time:
|
|||
minute: "Minute(n)"
|
||||
hour: "Stunde(n)"
|
||||
day: "Tag(en)"
|
||||
_tutorial:
|
||||
title: "Wie du FoundKey verwendest"
|
||||
step1_1: "Willkommen!"
|
||||
step1_2: "Diese Seite ist die „Chronik“. Sie zeigt dir deine geschrieben „Notizen“\
|
||||
\ sowie die aller Benutzer, denen du „folgst“, in chronologischer Reihenfolge."
|
||||
step1_3: "Deine Chronik sollte momentan leer sein, da du bis jetzt noch keine Notizen\
|
||||
\ geschrieben hast und auch noch keinen Benutzern folgst."
|
||||
step2_1: "Lass uns zuerst dein Profil vervollständigen, bevor du Notizen schreibst\
|
||||
\ oder jemandem folgst."
|
||||
step2_2: "Informationen darüber, was für eine Person du bist, macht es anderen leichter\
|
||||
\ zu wissen, ob sie deine Notizen sehen wollen und ob sie dir folgen möchten."
|
||||
step3_1: "Mit dem Einrichten deines Profils fertig?"
|
||||
step3_2: "Dann lass uns als nächstes versuchen, eine Notiz zu schreiben. Dies kannst\
|
||||
\ du tun, indem du auf den Knopf mit dem Stift-Icon auf dem Bildschirm drückst."
|
||||
step3_3: "Fülle das Fenster aus und drücke auf den Knopf oben rechts zum Senden."
|
||||
step3_4: "Fällt dir nichts ein, das du schreiben möchtest? Versuch's mit \"Hallo\
|
||||
\ FoundKey!\""
|
||||
step4_1: "Fertig mit dem Senden deiner ersten Notiz?"
|
||||
step4_2: "Falls deine Notiz nun in deiner Chronik auftaucht, hast du alles richtig\
|
||||
\ gemacht."
|
||||
step5_1: "Lass uns nun deiner Chronik etwas mehr Leben einhauchen, indem du einigen\
|
||||
\ anderen Benutzern folgst."
|
||||
step5_2: "{featured} zeigt dir beliebte Notizen dieser Instanz. In {explore} kannst\
|
||||
\ du beliebte Benutzer finden. Schau dort, ob du Benutzer findest, die dich interessieren!"
|
||||
step5_3: "Klicke zum Anzeigen des Profils eines Benutzers auf dessen Profilbild\
|
||||
\ und dann auf den \"Folgen\"-Knopf, um diesem zu folgen."
|
||||
step5_4: "Je nach Benutzer kann es etwas Zeit in Anspruch nehmen, bis dieser deine\
|
||||
\ Follow-Anfrage bestätigt."
|
||||
step6_1: "Wenn du nun auch die Notizen anderer Benutzer in deiner Chronik siehst,\
|
||||
\ hast du auch diesmal alles richtig gemacht."
|
||||
step6_2: "Du kannst ebenso „Reaktionen“ verwenden, um schnell auf Notizen anderer\
|
||||
\ Benutzer zu reagieren."
|
||||
step6_3: "Um eine Reaktion anzufügen, klicke auf das „+“-Symbol in der Notiz und\
|
||||
\ wähle ein Emoji aus, mit dem du reagieren möchtest."
|
||||
step7_1: "Glückwunsch! Du hast die Einführung in die Verwendung von FoundKey abgeschlossen."
|
||||
step7_2: "Wenn du mehr über FoundKey lernen möchtest, schau dich im {help}-Bereich\
|
||||
\ um."
|
||||
step7_3: "Und nun, viel Spaß mit FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung\
|
||||
\ registriert."
|
||||
|
@ -1309,6 +1260,7 @@ _notification:
|
|||
groupInvited: "Erhaltene Gruppeneinladungen"
|
||||
app: "Benachrichtigungen von Apps"
|
||||
move: Account-Umzüge
|
||||
update: Beobachtete Notiz wurde bearbeitet
|
||||
_actions:
|
||||
followBack: "folgt dir nun auch"
|
||||
reply: "Antworten"
|
||||
|
|
|
@ -242,7 +242,6 @@ uploadFromUrl: "Upload from a URL"
|
|||
uploadFromUrlDescription: "URL of the file you want to upload"
|
||||
uploadFromUrlRequested: "Upload requested"
|
||||
uploadFromUrlMayTakeTime: "It may take some time until the upload is complete."
|
||||
explore: "Explore"
|
||||
messageRead: "Read"
|
||||
noMoreHistory: "There is no further history"
|
||||
startMessaging: "Start a new chat"
|
||||
|
@ -324,9 +323,6 @@ inMb: "In megabytes"
|
|||
iconUrl: "Icon URL"
|
||||
bannerUrl: "Banner image URL"
|
||||
backgroundImageUrl: "Background image URL"
|
||||
pinnedUsers: "Pinned users"
|
||||
pinnedUsersDescription: "List usernames separated by line breaks to be pinned in the\
|
||||
\ \"Explore\" tab."
|
||||
hcaptchaSiteKey: "Site key"
|
||||
hcaptchaSecretKey: "Secret key"
|
||||
recaptchaSiteKey: "Site key"
|
||||
|
@ -353,11 +349,6 @@ silence: "Silence"
|
|||
silenceConfirm: "Are you sure that you want to silence this user?"
|
||||
unsilence: "Undo silencing"
|
||||
unsilenceConfirm: "Are you sure that you want to undo the silencing of this user?"
|
||||
popularUsers: "Popular users"
|
||||
recentlyUpdatedUsers: "Recently active users"
|
||||
recentlyRegisteredUsers: "Newly joined users"
|
||||
recentlyDiscoveredUsers: "Newly discovered users"
|
||||
popularTags: "Popular tags"
|
||||
userList: "Lists"
|
||||
aboutMisskey: "About FoundKey"
|
||||
administrator: "Administrator"
|
||||
|
@ -398,7 +389,6 @@ messagingWithGroup: "Group chat"
|
|||
title: "Title"
|
||||
text: "Text"
|
||||
enable: "Enable"
|
||||
next: "Next"
|
||||
retype: "Enter again"
|
||||
noteOf: "Note by {user}"
|
||||
inviteToGroup: "Invite to group"
|
||||
|
@ -504,6 +494,8 @@ output: "Output"
|
|||
updateRemoteUser: "Update remote user information"
|
||||
deleteAllFiles: "Delete all files"
|
||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||
deleteAllNotes: "Delete all notes"
|
||||
deleteAllNotesConfirm: "Are you sure that you want to delete all visible notes of this clip?"
|
||||
removeAllFollowing: "Unfollow all followed users"
|
||||
removeAllFollowingDescription: "Executing this unfollows all accounts from {host}.\
|
||||
\ Please run this if the instance e.g. no longer exists."
|
||||
|
@ -594,7 +586,6 @@ abuseReports: "Reports"
|
|||
reportAbuse: "Report"
|
||||
reportAbuseOf: "Report {name}"
|
||||
fillAbuseReportDescription: "Please fill in details regarding this report."
|
||||
abuseReported: "Your report has been sent. Thank you very much."
|
||||
reporter: "Reporter"
|
||||
reporteeOrigin: "Reportee Origin"
|
||||
reporterOrigin: "Reporter Origin"
|
||||
|
@ -1029,39 +1020,6 @@ _time:
|
|||
minute: "Minute(s)"
|
||||
hour: "Hour(s)"
|
||||
day: "Day(s)"
|
||||
_tutorial:
|
||||
title: "How to use FoundKey"
|
||||
step1_1: "Welcome!"
|
||||
step1_2: "This page is called the \"timeline\". It shows chronologically ordered\
|
||||
\ \"notes\" of people who you \"follow\"."
|
||||
step1_3: "Your timeline is currently empty, since you have not posted any notes\
|
||||
\ or followed anyone yet."
|
||||
step2_1: "Let's finish setting up your profile before writing a note or following\
|
||||
\ anyone."
|
||||
step2_2: "Providing some information about who you are will make it easier for others\
|
||||
\ to tell if they want to see your notes or follow you."
|
||||
step3_1: "Finished setting up your profile?"
|
||||
step3_2: "Then let's try posting a note next. You can do so by pressing the button\
|
||||
\ with a pencil icon on the screen."
|
||||
step3_3: "Fill in the modal and press the button on the top right to post."
|
||||
step3_4: "Have nothing to say? Try \"just setting up my msky\"!"
|
||||
step4_1: "Finished posting your first note?"
|
||||
step4_2: "Hurray! Now your first note should be displayed on your timeline."
|
||||
step5_1: "Now, let's try making your timeline more lively by following other people."
|
||||
step5_2: "{featured} will show you popular notes in this instance. {explore} will\
|
||||
\ let you find popular users. Try finding people you'd like to follow there!"
|
||||
step5_3: "To follow other users, click on their icon and press the \"Follow\" button\
|
||||
\ on their profile."
|
||||
step5_4: "If the other user has a lock icon next to their name, it may take some\
|
||||
\ time for that user to manually approve your follow request."
|
||||
step6_1: "You should be able to see other users' notes on your timeline now."
|
||||
step6_2: "You can also put \"reactions\" on other people's notes to quickly respond\
|
||||
\ to them."
|
||||
step6_3: "To attach a \"reaction\", press the \"+\" mark on another user's note\
|
||||
\ and choose an emoji you'd like to react with."
|
||||
step7_1: "Congratulations! You have now finished FoundKey's basic tutorial."
|
||||
step7_2: "If you would like to learn more about FoundKey, try the {help} section."
|
||||
step7_3: "Now then, good luck and have fun with FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "You have already registered a 2-factor authentication device."
|
||||
registerDevice: "Register a new device"
|
||||
|
@ -1247,6 +1205,7 @@ _timelines:
|
|||
local: "Local"
|
||||
social: "Social"
|
||||
global: "Global"
|
||||
shuffled: "Shuffled"
|
||||
_pages:
|
||||
newPage: "Create a new Page"
|
||||
editPage: "Edit this Page"
|
||||
|
@ -1304,6 +1263,7 @@ _notification:
|
|||
reaction: "Reactions"
|
||||
pollVote: "Votes on polls"
|
||||
pollEnded: "Polls ending"
|
||||
update: "Watched Note was updated"
|
||||
receiveFollowRequest: "Received follow requests"
|
||||
followRequestAccepted: "Accepted follow requests"
|
||||
groupInvited: "Group invitations"
|
||||
|
|
|
@ -238,7 +238,6 @@ uploadFromUrl: "Subir desde una URL"
|
|||
uploadFromUrlDescription: "URL del fichero que quieres subir"
|
||||
uploadFromUrlRequested: "Subida solicitada"
|
||||
uploadFromUrlMayTakeTime: "Subir el fichero puede tardar un tiempo."
|
||||
explore: "Explorar"
|
||||
messageRead: "Ya leído"
|
||||
noMoreHistory: "El historial se ha acabado"
|
||||
startMessaging: "Iniciar chat"
|
||||
|
@ -319,9 +318,6 @@ inMb: "En megabytes"
|
|||
iconUrl: "URL de la imagen del avatar"
|
||||
bannerUrl: "URL de la imagen del banner"
|
||||
backgroundImageUrl: "URL de la imagen de fondo"
|
||||
pinnedUsers: "Usuarios fijados"
|
||||
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\"\
|
||||
\ separados por una linea nueva"
|
||||
hcaptchaSiteKey: "Clave del sitio"
|
||||
hcaptchaSecretKey: "Clave secreta"
|
||||
recaptchaSiteKey: "Clave del sitio"
|
||||
|
@ -346,11 +342,6 @@ silence: "Silenciar"
|
|||
silenceConfirm: "¿Desea silenciar al usuario?"
|
||||
unsilence: "Dejar de silenciar"
|
||||
unsilenceConfirm: "¿Desea dejar de silenciar al usuario?"
|
||||
popularUsers: "Usuarios populares"
|
||||
recentlyUpdatedUsers: "Usuarios activos recientemente"
|
||||
recentlyRegisteredUsers: "Usuarios registrados recientemente"
|
||||
recentlyDiscoveredUsers: "Usuarios descubiertos recientemente"
|
||||
popularTags: "Etiquetas populares"
|
||||
userList: "Lista"
|
||||
aboutMisskey: "Sobre FoundKey"
|
||||
administrator: "Administrador"
|
||||
|
@ -391,7 +382,6 @@ messagingWithGroup: "Chatear en grupo"
|
|||
title: "Título"
|
||||
text: "Texto"
|
||||
enable: "Activar"
|
||||
next: "Siguiente"
|
||||
retype: "Intentar de nuevo"
|
||||
noteOf: "Notas de {user}"
|
||||
inviteToGroup: "Invitar al grupo"
|
||||
|
@ -583,7 +573,6 @@ abuseReports: "Reportes"
|
|||
reportAbuse: "Reportar"
|
||||
reportAbuseOf: "Reportar a {name}"
|
||||
fillAbuseReportDescription: "Ingrese los detalles del reporte."
|
||||
abuseReported: "Se ha enviado el reporte. Muchas gracias."
|
||||
reporteeOrigin: "Informar a"
|
||||
reporterOrigin: "Origen del informe"
|
||||
forwardReport: "Transferir un informe a una instancia remota"
|
||||
|
@ -936,41 +925,6 @@ _time:
|
|||
minute: "Minutos"
|
||||
hour: "Horas"
|
||||
day: "Días"
|
||||
_tutorial:
|
||||
title: "Cómo usar FoundKey"
|
||||
step1_1: "Bienvenido"
|
||||
step1_2: "Esta imagen se llama \"Linea de tiempo\" y muestra en orden cronológico\
|
||||
\ las \"notas\" tuyas y de la gente que \"sigues\""
|
||||
step1_3: "Si no estás escribiendo ninguna nota y no estás siguiendo a nadie, es\
|
||||
\ esperable que no se muestre nada en la linea de tiempo"
|
||||
step2_1: "Antes de crear notas y seguir a alguien, primero vamos a crear tu perfil"
|
||||
step2_2: "Si provees información sobre quien eres, será más fácil para que otros\
|
||||
\ usuarios te sigan"
|
||||
step3_1: "¿Has podido crear tu perfil sin problemas?"
|
||||
step3_2: "Con esto, prueba hacer una nota. Aprieta el botón con forma de lápiz que\
|
||||
\ está arriba de la imagen y abre el formulario."
|
||||
step3_3: "Si has escrito el contenido, aprieta el botón que está arriba a la derecha\
|
||||
\ del formulario para postear."
|
||||
step3_4: "¿No se te ocurre un contenido? Prueba con decir \"Empecé a usar FoundKey\""
|
||||
step4_1: "¿Has posteado?"
|
||||
step4_2: "Si tu nota puede verse en la linea de tiempo, fue todo un éxito."
|
||||
step5_1: "Luego, ponte a seguir a otra gente y haz que tu linea de tiempo esté más\
|
||||
\ animada."
|
||||
step5_2: "Puedes ver las notas destacadas en {featured} y desde allí seguir a usuarios\
|
||||
\ que te importan. También puedes buscar usuario destacados en {explore}."
|
||||
step5_3: "Para seguir a un usuario, haz click en su avatar para ver su página de\
|
||||
\ usuario y allí apretar el botón \"seguir\""
|
||||
step5_4: "De esa manera, puede pasar un tiempo hasta que el usuario apruebe al seguidor."
|
||||
step6_1: "Si puedes ver en la linea de tiempo las notas de otros usuarios, fue todo\
|
||||
\ un éxito."
|
||||
step6_2: "En las notas de otros usuarios puedes añadir una \"reacción\", para poder\
|
||||
\ responder rápidamente."
|
||||
step6_3: "Para añadir una reacción, haz click en el botón \"+\" de la nota y elige\
|
||||
\ la reacción que prefieras."
|
||||
step7_1: "Así terminó la explicación del funcionamiento básico de FoundKey. Eso\
|
||||
\ fue todo."
|
||||
step7_2: "Si quieres conocer más sobre FoundKey, prueba con la sección {help}."
|
||||
step7_3: "Así, disfruta de FoundKey \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Ya has completado la configuración."
|
||||
registerDevice: "Registrar dispositivo"
|
||||
|
|
|
@ -238,7 +238,6 @@ uploadFromUrlDescription: "URL du fichier que vous souhaitez téléverser"
|
|||
uploadFromUrlRequested: "Téléversement demandé"
|
||||
uploadFromUrlMayTakeTime: "Le téléversement de votre fichier peut prendre un certain\
|
||||
\ temps."
|
||||
explore: "Découvrir"
|
||||
messageRead: "Lu"
|
||||
noMoreHistory: "Il n’y a plus d’historique"
|
||||
startMessaging: "Commencer à discuter"
|
||||
|
@ -320,9 +319,6 @@ inMb: "en mégaoctets"
|
|||
iconUrl: "URL de l'icône"
|
||||
bannerUrl: "URL de l’image de la bannière"
|
||||
backgroundImageUrl: "URL de l'image d'arrière-plan"
|
||||
pinnedUsers: "Utilisateur·rice épinglé·e"
|
||||
pinnedUsersDescription: "Listez les utilisateur·rice·s que vous souhaitez voir épinglé·e·s\
|
||||
\ sur la page \"Découvrir\", un·e par ligne."
|
||||
hcaptchaSiteKey: "Clé du site"
|
||||
hcaptchaSecretKey: "Clé secrète"
|
||||
recaptchaSiteKey: "Clé du site"
|
||||
|
@ -349,11 +345,6 @@ silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en s
|
|||
unsilence: "Annuler la sourdine"
|
||||
unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te\
|
||||
\ utilisateur·rice ?"
|
||||
popularUsers: "Utilisateur·rice·s populaires"
|
||||
recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment"
|
||||
recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s"
|
||||
recentlyDiscoveredUsers: "Utilisateur·rice·s récemment découvert·e·s"
|
||||
popularTags: "Mots-clés populaires"
|
||||
userList: "Listes"
|
||||
aboutMisskey: "À propos de FoundKey"
|
||||
administrator: "Administrateur"
|
||||
|
@ -394,7 +385,6 @@ messagingWithGroup: "Discuter avec un groupe"
|
|||
title: "Titre"
|
||||
text: "Texte"
|
||||
enable: "Activer"
|
||||
next: "Suivant"
|
||||
retype: "Confirmation"
|
||||
noteOf: "Notes de {user}"
|
||||
inviteToGroup: "Inviter dans un groupe"
|
||||
|
@ -595,7 +585,6 @@ abuseReports: "Signalements"
|
|||
reportAbuse: "Signaler"
|
||||
reportAbuseOf: "Signaler {name}"
|
||||
fillAbuseReportDescription: "Veuillez expliquer les raisons du signalement."
|
||||
abuseReported: "Le rapport est envoyé. Merci."
|
||||
reporter: "Signalé par"
|
||||
reporteeOrigin: "Origine du signalement"
|
||||
reporterOrigin: "Signalé par"
|
||||
|
@ -993,46 +982,6 @@ _time:
|
|||
minute: "min"
|
||||
hour: "h"
|
||||
day: "j"
|
||||
_tutorial:
|
||||
title: "Comment utiliser FoundKey"
|
||||
step1_1: "Bienvenue !"
|
||||
step1_2: "Cette page est appelée « un fil ». Elle affiche les « notes » des personnes\
|
||||
\ auxquelles vous êtes abonné dans un ordre chronologique."
|
||||
step1_3: "Votre fil est actuellement vide vu que vous ne suivez aucun compte et\
|
||||
\ que vous n’avez publié aucune note, pour l’instant."
|
||||
step2_1: "Procédons d’abord à la préparation de votre profil avant d’écrire une\
|
||||
\ note et/ou de vous abonner à un compte."
|
||||
step2_2: "En fournissant quelques informations sur vous, il sera plus facile pour\
|
||||
\ les autres de s’abonner à votre compte."
|
||||
step3_1: "Vous avez fini de créer votre profil ?"
|
||||
step3_2: "L’étape suivante consiste à créer une note. Vous pouvez commencer en cliquant\
|
||||
\ sur l’icône crayon sur l’écran."
|
||||
step3_3: "Remplissez le cadran et cliquez sur le bouton en haut à droite pour envoyer."
|
||||
step3_4: "Vous n’avez rien à dire ? Essayez d’écrire « J’ai commencé à utiliser\
|
||||
\ FoundKey » !"
|
||||
step4_1: "Avez-vous publié votre première note ?"
|
||||
step4_2: "Youpi ! Celle-ci est maintenant affichée sur votre fil d’actualité."
|
||||
step5_1: "Maintenant, essayons de nous abonner à d’autres personnes afin de rendre\
|
||||
\ votre fil plus vivant."
|
||||
step5_2: "La page {featured} affiche les notes en tendance sur la présente instance\
|
||||
\ et {explore} vous permet de trouver des utilisateur·rice·s en tendance. Essayez\
|
||||
\ de vous abonner aux gens que vous aimez !"
|
||||
step5_3: "Pour pouvoir suivre d’autres utilisateur·rice, cliquez sur leur avatar\
|
||||
\ afin d’afficher la page du profil utilisateur ensuite appuyez sur le bouton\
|
||||
\ « S’abonner »."
|
||||
step5_4: "Si l’autre utilisateur possède une icône sous forme d’un cadenas à côté\
|
||||
\ de son nom, il devra accepter votre demande d’abonnement manuellement."
|
||||
step6_1: "Maintenant, vous êtes en mesure de voir s’afficher les notes des autres\
|
||||
\ utilisateur·rice·s sur votre propre fil."
|
||||
step6_2: "Vous avez également la possibilité d’intéragir rapidement avec les notes\
|
||||
\ des autres utilisateur·rice·s en ajoutant des « réactions »."
|
||||
step6_3: "Pour ajouter une réaction à une note, cliquez sur le signe « + » de celle-ci\
|
||||
\ et sélectionnez l’émoji souhaité."
|
||||
step7_1: "Félicitations ! Vous avez atteint la fin du tutoriel de base pour l’utilisation\
|
||||
\ de FoundKey."
|
||||
step7_2: "Si vous désirez en savoir plus sur FoundKey, jetez un œil sur la section\
|
||||
\ {help}."
|
||||
step7_3: "Bon courage et amusez-vous bien sur FoundKey ! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Configuration déjà achevée."
|
||||
registerDevice: "Ajouter un nouvel appareil"
|
||||
|
|
|
@ -235,7 +235,6 @@ uploadFromUrl: "Unggah dari URL"
|
|||
uploadFromUrlDescription: "URL berkas yang ingin kamu unggah"
|
||||
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
||||
uploadFromUrlMayTakeTime: "Mungkin diperlukan waktu hingga unggahan selesai."
|
||||
explore: "Jelajahi"
|
||||
messageRead: "Telah dibaca"
|
||||
noMoreHistory: "Tidak ada sejarah lagi"
|
||||
startMessaging: "Mulai mengirim pesan"
|
||||
|
@ -317,9 +316,6 @@ inMb: "dalam Megabytes"
|
|||
iconUrl: "URL Gambar ikon"
|
||||
bannerUrl: "URL Banner"
|
||||
backgroundImageUrl: "URL Gambar latar"
|
||||
pinnedUsers: "Pengguna yang disematkan"
|
||||
pinnedUsersDescription: "Tuliskan satu nama pengguna dalam satu baris. Pengguna yang\
|
||||
\ dituliskan disini akan disematkan dalam bilah \"Jelajahi\"."
|
||||
hcaptchaSiteKey: "Site Key"
|
||||
hcaptchaSecretKey: "Secret Key"
|
||||
recaptchaSiteKey: "Site key"
|
||||
|
@ -344,11 +340,6 @@ silence: "Bungkam"
|
|||
silenceConfirm: "Apakah kamu yakin ingin membungkam pengguna ini?"
|
||||
unsilence: "Hapus bungkam"
|
||||
unsilenceConfirm: "Apakah kamu ingin untuk batal membungkam pengguna ini?"
|
||||
popularUsers: "Pengguna populer"
|
||||
recentlyUpdatedUsers: "Pengguna dengan aktivitas terkini"
|
||||
recentlyRegisteredUsers: "Pengguna baru saja bergabung"
|
||||
recentlyDiscoveredUsers: "Pengguna baru saja dilihat"
|
||||
popularTags: "Tag populer"
|
||||
userList: "Daftar"
|
||||
aboutMisskey: "Tentang FoundKey"
|
||||
administrator: "Admin"
|
||||
|
@ -389,7 +380,6 @@ messagingWithGroup: "Obrolan di dalam grup"
|
|||
title: "Judul"
|
||||
text: "Teks"
|
||||
enable: "Aktifkan"
|
||||
next: "Selanjutnya"
|
||||
retype: "Masukkan ulang"
|
||||
noteOf: "Catatan milik {user}"
|
||||
inviteToGroup: "Undang ke grup"
|
||||
|
@ -585,7 +575,6 @@ abuseReports: "Laporkan"
|
|||
reportAbuse: "Laporkan"
|
||||
reportAbuseOf: "Laporkan {name}"
|
||||
fillAbuseReportDescription: "Mohon isi rincian laporan."
|
||||
abuseReported: "Laporan kamu telah dikirimkan. Terima kasih."
|
||||
reporter: "Pelapor"
|
||||
reporteeOrigin: "Yang dilaporkan"
|
||||
reporterOrigin: "Pelapor"
|
||||
|
@ -989,44 +978,6 @@ _time:
|
|||
minute: "menit"
|
||||
hour: "jam"
|
||||
day: "hari"
|
||||
_tutorial:
|
||||
title: "Cara menggunakan FoundKey"
|
||||
step1_1: "Selamat datang!"
|
||||
step1_2: "Halaman ini disebut \"linimasa\". Halaman ini menampilkan \"catatan\"\
|
||||
\ yang diurutkan secara kronologis dari orang-orang yang kamu \"ikuti\"."
|
||||
step1_3: "Linimasa kamu kosong, karena kamu belum mencatat catatan apapun atau mengikuti\
|
||||
\ siapapun."
|
||||
step2_1: "Selesaikan menyetel profilmu sebelum menulis sebuah catatan atau mengikuti\
|
||||
\ seseorang."
|
||||
step2_2: "Menyediakan beberapa informasi tentang siapa kamu akan membuat orang lain\
|
||||
\ mudah untuk mengikutimu kembali."
|
||||
step3_1: "Selesai menyetel profil kamu?"
|
||||
step3_2: "Langkah selanjutnya adalah membuat catatan. Kamu bisa lakukan ini dengan\
|
||||
\ mengklik ikon pensil pada layar kamu."
|
||||
step3_3: "Isilah di dalam modal dan tekan tombol pada atas kanan untuk memcatat\
|
||||
\ catatan kamu."
|
||||
step3_4: "Bingung tidak berpikiran untuk mengatakan sesuatu? Coba saja \"baru aja\
|
||||
\ ikutan bikin akun misskey punyaku\"!"
|
||||
step4_1: "Selesai mencatat catatan pertamamu?"
|
||||
step4_2: "Horee! Sekarang catatan pertamamu sudah ditampilkan di linimasa milikmu."
|
||||
step5_1: "Sekarang, mari mencoba untuk membuat linimasamu lebih hidup dengan mengikuti\
|
||||
\ orang lain."
|
||||
step5_2: "{featured} akan memperlihatkan catatan yang sedang tren saat ini untuk\
|
||||
\ kamu. {explore} akan membantumu untuk mencari pengguna yang sedang tren juga\
|
||||
\ saat ini. Coba ikuti seseorang yang kamu suka!"
|
||||
step5_3: "Untuk mengikuti pengguna lain, klik pada ikon mereka dan tekan tombol\
|
||||
\ follow pada profil mereka."
|
||||
step5_4: "Jika pengguna lain memiliki ikon gembok di sebelah nama mereka, maka pengguna\
|
||||
\ rersebut harus menyetujui permintaan mengikuti dari kamu secara manual."
|
||||
step6_1: "Sekarang kamu dapat melihat catatan pengguna lain pada linimasamu."
|
||||
step6_2: "Kamu juga bisa memberikan \"reaksi\" ke catatan orang lain untuk merespon\
|
||||
\ dengan cepat."
|
||||
step6_3: "Untuk memberikan \"reaksi\", tekan tanda \"+\" pada catatan pengguna lain\
|
||||
\ dan pilih emoji yang kamu suka untuk memberikan reaksimu kepada mereka."
|
||||
step7_1: "Yay, Selamat! Kamu sudah menyelesaikan tutorial dasar FoundKey."
|
||||
step7_2: "Jika kamu ingin mempelajari lebih lanjut tentang FoundKey, cobalah berkunjung\
|
||||
\ ke bagian {help}."
|
||||
step7_3: "Semoga berhasil dan bersenang-senanglah! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Kamu telah mendaftarkan perangkat otentikasi dua faktor."
|
||||
registerDevice: "Daftarkan perangkat baru"
|
||||
|
|
|
@ -27,7 +27,6 @@ const languages = [
|
|||
'id-ID',
|
||||
'it-IT',
|
||||
'ja-JP',
|
||||
'ja-KS',
|
||||
'kab-KAB',
|
||||
'kn-IN',
|
||||
'ko-KR',
|
||||
|
|
|
@ -229,7 +229,6 @@ uploadFromUrl: "Incolla URL immagine"
|
|||
uploadFromUrlDescription: "URL del file che vuoi caricare"
|
||||
uploadFromUrlRequested: "Caricamento richiesto"
|
||||
uploadFromUrlMayTakeTime: "Il caricamento del file può richiedere tempo."
|
||||
explore: "Esplora"
|
||||
messageRead: "Visualizzato"
|
||||
noMoreHistory: "Non c'è più cronologia da visualizzare"
|
||||
startMessaging: "Nuovo messaggio"
|
||||
|
@ -311,9 +310,6 @@ inMb: "in Megabytes"
|
|||
iconUrl: "URL di icona (favicon, ecc.)"
|
||||
bannerUrl: "URL dell'immagine d'intestazione"
|
||||
backgroundImageUrl: "URL dello sfondo"
|
||||
pinnedUsers: "Utenti in evidenza"
|
||||
pinnedUsersDescription: "Elenca gli/le utenti che vuoi fissare in cima alla pagina\
|
||||
\ \"Esplora\", un@ per riga."
|
||||
hcaptchaSiteKey: "Chiave del sito"
|
||||
hcaptchaSecretKey: "Chiave segreta"
|
||||
recaptchaSiteKey: "Chiave del sito"
|
||||
|
@ -338,11 +334,6 @@ silence: "Silenzia"
|
|||
silenceConfirm: "Vuoi davvero silenziare l'utente?"
|
||||
unsilence: "Riattiva"
|
||||
unsilenceConfirm: "Vuoi davvero riattivare l'utente?"
|
||||
popularUsers: "Utenti popolari"
|
||||
recentlyUpdatedUsers: "Utenti attivi di recente"
|
||||
recentlyRegisteredUsers: "Utenti registrati di recente"
|
||||
recentlyDiscoveredUsers: "Utenti scoperti di recente"
|
||||
popularTags: "Tag di tendenza"
|
||||
userList: "Liste"
|
||||
aboutMisskey: "Informazioni di FoundKey"
|
||||
administrator: "Amministratore"
|
||||
|
@ -383,7 +374,6 @@ messagingWithGroup: "Chattare in gruppo"
|
|||
title: "Titolo"
|
||||
text: "Testo"
|
||||
enable: "Abilita"
|
||||
next: "Avanti"
|
||||
retype: "Conferma"
|
||||
noteOf: "Note di {user}"
|
||||
inviteToGroup: "Invitare al gruppo"
|
||||
|
@ -574,7 +564,6 @@ abuseReports: "Segnalazioni"
|
|||
reportAbuse: "Segnalazioni"
|
||||
reportAbuseOf: "Segnala {name}"
|
||||
fillAbuseReportDescription: "Si prega di spiegare il motivo della segnalazione."
|
||||
abuseReported: "La segnalazione è stata inviata. Grazie."
|
||||
reporter: "il corrispondente"
|
||||
reporteeOrigin: "Origine del segnalato"
|
||||
reporterOrigin: "Origine del segnalatore"
|
||||
|
@ -901,45 +890,6 @@ _time:
|
|||
minute: "min"
|
||||
hour: "ore"
|
||||
day: "giorni"
|
||||
_tutorial:
|
||||
title: "Come usare FoundKey"
|
||||
step1_1: "Benvenuto/a!"
|
||||
step1_2: "Questa pagina si chiama una \" Timeline \". Mostra in ordine cronologico\
|
||||
\ le \" note \" delle persone che segui."
|
||||
step1_3: "Attualmente la tua Timeline è vuota perché non segui alcun account e non\
|
||||
\ hai pubblicato alcuna nota ancora."
|
||||
step2_1: "Prima di scrivere una nota o di seguire un account, imposta il tuo profilo!"
|
||||
step2_2: "Aggiungere qualche informazione su di te aumenterà le tue possibilità\
|
||||
\ di essere seguit@ da altre persone."
|
||||
step3_1: "Hai finito di impostare il tuo profilo?"
|
||||
step3_2: "Ora, puoi pubblicare una nota. Facciamo una prova! Premi il pulsante a\
|
||||
\ forma di penna in cima allo schermo per aprire una finestra di dialogo."
|
||||
step3_3: "Scritto il testo della nota, puoi pubblicarla premendo il pulsante nella\
|
||||
\ parte superiore destra della finestra di dialogo."
|
||||
step3_4: "Non ti viene niente in mente? Perché non scrivi semplicemente \"Ho appena\
|
||||
\ cominciato a usare FoundKey\"?"
|
||||
step4_1: "Hai pubblicato qualcosa?"
|
||||
step4_2: "Se puoi visualizzare la tua nota sulla timeline, ce l'hai fatta!"
|
||||
step5_1: "Adesso, cerca di seguire altre persone per vivacizzare la tua timeline."
|
||||
step5_2: "La pagina {featured} mostra le note di tendenza su questa istanza, e magari\
|
||||
\ ti aiuterà a trovare account che ti piacciono e che vorrai seguire. Oppure,\
|
||||
\ potrai trovare utenti popolari usando {explore}."
|
||||
step5_3: "Per seguire altrə utenti, clicca sul loro avatar per aprire la pagina\
|
||||
\ di profilo dove puoi premere il pulsante \"Seguire\"."
|
||||
step5_4: "Alcunə utenti scelgono di confermare manualmente le richieste di follow\
|
||||
\ che ricevono, quindi a seconda delle persone potrebbe volerci un pò prima che\
|
||||
\ la tua richiesta sia accolta."
|
||||
step6_1: "Ora, se puoi visualizzare le note di altrə utenti sulla tua timeline,\
|
||||
\ ce l'hai fatta!"
|
||||
step6_2: "Puoi inviare una risposta rapida alle note di altrə utenti mandando loro\
|
||||
\ \"reazioni\"."
|
||||
step6_3: "Per inviare una reazione, premi l'icona + della nota e scegli l'emoji\
|
||||
\ che vuoi mandare."
|
||||
step7_1: "Complimenti! Sei arrivat@ alla fine dell'esercitazione di base su come\
|
||||
\ usare FoundKey."
|
||||
step7_2: "Se vuoi saperne di più su FoundKey, puoi dare un'occhiata alla sezione\
|
||||
\ {help}."
|
||||
step7_3: "Da ultimo, buon divertimento su FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
registerDevice: "Aggiungi dispositivo"
|
||||
_permissions:
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
_lang_: "日本語"
|
||||
|
||||
headlineMisskey: "ノートでつながるネットワーク"
|
||||
introMisskey: "ようこそ!FoundKeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう\U0001F4E1\
|
||||
\n「リアクション」機能で、皆のノートに素早く反応を追加することもできます\U0001F44D\n新しい世界を探検しよう\U0001F680"
|
||||
introMisskey: "ようこそ!FoundKeyは、オープンソースの分散型マイクロブログサービスです。\n「ノート」を作成して、いま起こっていることを共有したり、あなたについて皆に発信しよう📡\n\
|
||||
「リアクション」機能で、皆のノートに素早く反応を追加することもできます👍\n新しい世界を探検しよう🚀"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "検索"
|
||||
notifications: "通知"
|
||||
|
@ -213,7 +213,6 @@ uploadFromUrl: "URLアップロード"
|
|||
uploadFromUrlDescription: "アップロードしたいファイルのURL"
|
||||
uploadFromUrlRequested: "アップロードをリクエストしました"
|
||||
uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。"
|
||||
explore: "みつける"
|
||||
messageRead: "既読"
|
||||
noMoreHistory: "これより過去の履歴はありません"
|
||||
startMessaging: "チャットを開始"
|
||||
|
@ -292,8 +291,6 @@ inMb: "メガバイト単位"
|
|||
iconUrl: "アイコン画像のURL (faviconなど)"
|
||||
bannerUrl: "バナー画像のURL"
|
||||
backgroundImageUrl: "背景画像のURL"
|
||||
pinnedUsers: "ピン留めユーザー"
|
||||
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
|
||||
hcaptchaSiteKey: "サイトキー"
|
||||
hcaptchaSecretKey: "シークレットキー"
|
||||
recaptchaSiteKey: "サイトキー"
|
||||
|
@ -317,11 +314,6 @@ silence: "サイレンス"
|
|||
silenceConfirm: "サイレンスしますか?"
|
||||
unsilence: "サイレンス解除"
|
||||
unsilenceConfirm: "サイレンス解除しますか?"
|
||||
popularUsers: "人気のユーザー"
|
||||
recentlyUpdatedUsers: "最近投稿したユーザー"
|
||||
recentlyRegisteredUsers: "最近登録したユーザー"
|
||||
recentlyDiscoveredUsers: "最近発見されたユーザー"
|
||||
popularTags: "人気のタグ"
|
||||
userList: "リスト"
|
||||
aboutMisskey: "FoundKeyについて"
|
||||
administrator: "管理者"
|
||||
|
@ -362,7 +354,6 @@ messagingWithGroup: "グループでチャット"
|
|||
title: "タイトル"
|
||||
text: "テキスト"
|
||||
enable: "有効にする"
|
||||
next: "次"
|
||||
retype: "再入力"
|
||||
noteOf: "{user}のノート"
|
||||
inviteToGroup: "グループに招待"
|
||||
|
@ -414,8 +405,8 @@ showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを
|
|||
objectStorage: "オブジェクトストレージ"
|
||||
useObjectStorage: "オブジェクトストレージを使用"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL。\nS3: 'https://<bucket>.s3.amazonaws.com'、GCS等:\
|
||||
\ 'https://storage.googleapis.com/<bucket>'。"
|
||||
objectStorageBaseUrlDesc: "参照に使用するURL。CDNやProxyを使用している場合はそのURL。\nS3: 'https://<bucket>.s3.amazonaws.com'、GCS等:
|
||||
'https://storage.googleapis.com/<bucket>'。"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "使用サービスのbucket名を指定してください。"
|
||||
objectStoragePrefix: "Prefix"
|
||||
|
@ -537,7 +528,6 @@ abuseReports: "通報"
|
|||
reportAbuse: "通報"
|
||||
reportAbuseOf: "{name}を通報する"
|
||||
fillAbuseReportDescription: "通報理由の詳細を記入してください。"
|
||||
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
|
||||
reporter: "通報者"
|
||||
reporteeOrigin: "通報先"
|
||||
reporterOrigin: "通報元"
|
||||
|
@ -938,30 +928,6 @@ _time:
|
|||
hour: "時間"
|
||||
day: "日"
|
||||
|
||||
_tutorial:
|
||||
title: "FoundKeyの使い方"
|
||||
step1_1: "ようこそ!"
|
||||
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
|
||||
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
|
||||
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
|
||||
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
|
||||
step3_1: "プロフィール設定はうまくできましたか?"
|
||||
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
|
||||
step3_4: "内容が思いつかない?「FoundKey始めました」というのはいかがでしょう!"
|
||||
step4_1: "投稿できましたか?"
|
||||
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
|
||||
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます!"
|
||||
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
|
||||
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
|
||||
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
|
||||
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
|
||||
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
|
||||
step7_1: "これで、FoundKeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
|
||||
step7_2: "もっとFoundKeyについて知りたいときは、{help}を見てみてください。"
|
||||
step7_3: "では、FoundKeyをお楽しみください\U0001F680"
|
||||
|
||||
_2fa:
|
||||
alreadyRegistered: "既に設定は完了しています。"
|
||||
registerDevice: "デバイスを登録"
|
||||
|
@ -1157,6 +1123,7 @@ _timelines:
|
|||
social: "ソーシャル"
|
||||
global: "グローバル"
|
||||
|
||||
shuffled: シャッフル
|
||||
_pages:
|
||||
newPage: "ページの作成"
|
||||
editPage: "ページの編集"
|
||||
|
@ -1223,6 +1190,7 @@ _notification:
|
|||
app: "連携アプリからの通知"
|
||||
|
||||
move: 自分以外のアカウントの引っ越し
|
||||
update: ウォッチ中のノートが更新された
|
||||
_actions:
|
||||
followBack: "フォローバック"
|
||||
reply: "返信"
|
||||
|
|
|
@ -1,801 +0,0 @@
|
|||
_lang_: "日本語 (関西弁)"
|
||||
headlineMisskey: "ノートでつながるネットワーク"
|
||||
introMisskey: "ようお越し!FoundKeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ノート」を作って、いま起こっとることを共有したり、あんたについて皆に発信しよう\U0001F4E1\
|
||||
\n「リアクション」機能で、皆のノートに素早く反応を追加したりもできるで✌\nほな新しい世界を探検しよか\U0001F680"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "探す"
|
||||
notifications: "通知"
|
||||
username: "ユーザー名"
|
||||
password: "パスワード"
|
||||
forgotPassword: "パスワード忘れてん"
|
||||
fetchingAsApObject: "今ちと連合に照会しとるで"
|
||||
ok: "OKや"
|
||||
gotIt: "ほい"
|
||||
cancel: "やめとく"
|
||||
renotedBy: "{user}がRenote"
|
||||
noNotes: "ノートはあらへん"
|
||||
noNotifications: "通知はあらへん"
|
||||
instance: "インスタンス"
|
||||
settings: "設定"
|
||||
basicSettings: "基本設定"
|
||||
otherSettings: "その他の設定"
|
||||
openInWindow: "ウィンドウで開くで"
|
||||
profile: "プロフィール"
|
||||
timeline: "タイムライン"
|
||||
noAccountDescription: "自己紹介食ってもた"
|
||||
login: "ログイン"
|
||||
loggingIn: "ログインしよるで"
|
||||
logout: "ログアウト"
|
||||
signup: "新規登録"
|
||||
save: "保存"
|
||||
users: "ユーザー"
|
||||
addUser: "ユーザーを追加や"
|
||||
pin: "ピン留めしとく"
|
||||
unpin: "やっぱピン留めせん"
|
||||
copyContent: "内容をコピー"
|
||||
copyLink: "リンクをコピー"
|
||||
delete: "ほかす"
|
||||
deleteAndEdit: "ほかして直す"
|
||||
deleteAndEditConfirm: "このノートをほかして書き直すんか?このノートへのリアクション、Renote、返信も全部消えてまうで。"
|
||||
addToList: "リストに入れたる"
|
||||
sendMessage: "メッセージを送る"
|
||||
copyUsername: "ユーザー名をコピー"
|
||||
reply: "返事"
|
||||
loadMore: "まだまだあるで!"
|
||||
showMore: "まだまだあるで!"
|
||||
youGotNewFollower: "フォローされたで"
|
||||
receiveFollowRequest: "フォローリクエストされたで"
|
||||
followRequestAccepted: "フォローが承認されたで"
|
||||
mention: "メンション"
|
||||
mentions: "うち宛て"
|
||||
directNotes: "ダイレクト投稿"
|
||||
importAndExport: "インポートとエクスポート"
|
||||
import: "インポート"
|
||||
export: "エクスポート"
|
||||
files: "ファイル"
|
||||
download: "ダウンロード"
|
||||
driveFileDeleteConfirm: "ファイル「{name}」を消してしもうてええか?このファイルを添付したノートも消えてまうで。"
|
||||
unfollowConfirm: "{name}のフォローを解除してもええんか?"
|
||||
exportRequested: "エクスポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。エクスポート終わったら「ドライブ」に突っ込んどくで。"
|
||||
importRequested: "インポートしてな、ってリクエストしたけど、これ多分めっちゃ時間かかるで。"
|
||||
lists: "リスト"
|
||||
note: "ノート"
|
||||
notes: "ノート"
|
||||
following: "フォロー"
|
||||
followers: "フォロワー"
|
||||
followsYou: "フォローされとるで"
|
||||
createList: "リスト作る"
|
||||
manageLists: "リストの管理"
|
||||
error: "エラー"
|
||||
somethingHappened: "なんかアカンことが起こったで"
|
||||
retry: "もっぺんやる?"
|
||||
pageLoadError: "ページの読み込みに失敗してしもうたで…"
|
||||
pageLoadErrorDescription: "これは普通、ネットワークかブラウザキャッシュが原因やからね。キャッシュをクリアするか、もうちっとだけ待ってくれへんか?"
|
||||
enterListName: "リスト名を入れてや"
|
||||
privacy: "プライバシー"
|
||||
makeFollowManuallyApprove: "自分が認めた人だけがこのアカウントをフォローできるようにする"
|
||||
defaultNoteVisibility: "もとからの公開範囲"
|
||||
follow: "フォロー"
|
||||
followRequest: "フォローを頼む"
|
||||
followRequests: "フォロー申請"
|
||||
unfollow: "フォローやめる"
|
||||
followRequestPending: "フォロー許してくれるん待っとる"
|
||||
renote: "Renote"
|
||||
unrenote: "Renoteやめる"
|
||||
quote: "引用"
|
||||
pinnedNote: "ピン留めされとるノート"
|
||||
you: "あんた"
|
||||
clickToShow: "押したら見えるで"
|
||||
sensitive: "ちょっとアカンやつやで"
|
||||
add: "増やす"
|
||||
reaction: "リアクション"
|
||||
reactionSettingDescription2: "ドラッグで並び替え、クリックで削除、+を押して追加やで。"
|
||||
attachCancel: "のっけるのやめる"
|
||||
markAsSensitive: "ちょっとこれはアカン"
|
||||
unmarkAsSensitive: "そこまでアカンことないやろ"
|
||||
enterFileName: "ファイル名を入れてや"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュートやめたる"
|
||||
block: "ブロック"
|
||||
unblock: "ブロックやめたる"
|
||||
suspend: "凍結"
|
||||
unsuspend: "溶かす"
|
||||
blockConfirm: "ブロックしてもええんか?"
|
||||
unblockConfirm: "ブロックやめたるってほんまか?"
|
||||
suspendConfirm: "凍結してしもうてええか?"
|
||||
unsuspendConfirm: "解凍するけどええか?"
|
||||
selectList: "リストを選ぶ"
|
||||
selectAntenna: "アンテナを選ぶ"
|
||||
selectWidget: "ウィジェットを選ぶ"
|
||||
editWidgets: "ウィジェットをいじる"
|
||||
editWidgetsExit: "編集終ったで"
|
||||
customEmojis: "カスタム絵文字"
|
||||
emoji: "絵文字"
|
||||
emojis: "絵文字"
|
||||
addEmoji: "絵文字を追加"
|
||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
|
||||
flagAsBot: "Botやで"
|
||||
flagAsBotDescription: "もしこのアカウントがプログラムによって運用されるんやったら、このフラグをオンにしてたのむで。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、FoundKeyのシステム上での扱いがBotに合ったもんになるんやで。"
|
||||
flagAsCat: "Catやで"
|
||||
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||
autoAcceptFollowed: "フォローしとるユーザーからのフォローリクエストを勝手に許可しとく"
|
||||
addAccount: "アカウントを追加"
|
||||
loginFailed: "ログインに失敗してしもうた…"
|
||||
showOnRemote: "リモートで見る"
|
||||
general: "全般"
|
||||
setWallpaper: "壁紙を設定"
|
||||
removeWallpaper: "壁紙を削除"
|
||||
youHaveNoLists: "リストがあらへんで?"
|
||||
followConfirm: "{name}をフォローしてええか?"
|
||||
proxyAccount: "プロキシアカウント"
|
||||
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
|
||||
host: "ホスト"
|
||||
selectUser: "ユーザーを選ぶ"
|
||||
recipient: "宛先"
|
||||
annotation: "注釈"
|
||||
federation: "連合"
|
||||
registeredAt: "初観測"
|
||||
latestRequestSentAt: "ちょっと前のリクエスト送信"
|
||||
latestRequestReceivedAt: "ちょっと前のリクエスト受信"
|
||||
latestStatus: "ちょっと前のステータス"
|
||||
charts: "チャート"
|
||||
perHour: "1時間ごと"
|
||||
perDay: "1日ごと"
|
||||
stopActivityDelivery: "アクティビティの配送をやめる"
|
||||
blockThisInstance: "このインスタンスをブロック"
|
||||
software: "ソフトウェア"
|
||||
version: "バージョン"
|
||||
withNFiles: "{n}個のファイル"
|
||||
jobQueue: "ジョブキュー"
|
||||
instanceInfo: "インスタンス情報"
|
||||
statistics: "統計"
|
||||
clearQueue: "キューにさいなら"
|
||||
clearQueueConfirmTitle: "キューをクリアしまっか?"
|
||||
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
|
||||
clearCachedFiles: "キャッシュにさいなら"
|
||||
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
||||
blockedInstances: "インスタンスブロック"
|
||||
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定してな。ブロックされてもうたインスタンスとはもう金輪際やり取りできひんくなるで。"
|
||||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
noUsers: "ユーザーはおらへん"
|
||||
editProfile: "プロフィールをいじる"
|
||||
noteDeleteConfirm: "このノートを削除しまっか?"
|
||||
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||
intro: "FoundKeyのインストールが完了してん!管理者アカウントを作ってや。"
|
||||
done: "でけた"
|
||||
processing: "処理しとる"
|
||||
preview: "プレビュー"
|
||||
default: "デフォルト"
|
||||
noCustomEmojis: "絵文字はあらへん"
|
||||
noJobs: "ジョブはあらへん"
|
||||
federating: "連合しとる"
|
||||
blocked: "ブロックしとる"
|
||||
suspended: "配信せぇへん"
|
||||
all: "みんな"
|
||||
subscribing: "購読しとる"
|
||||
publishing: "配信しとる"
|
||||
notResponding: "応答してへんで"
|
||||
changePassword: "パスワード変える"
|
||||
security: "セキュリティ"
|
||||
retypedNotMatch: "そやないねん。"
|
||||
currentPassword: "今のパスワード"
|
||||
newPassword: "今度のパスワード"
|
||||
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
||||
attachFile: "ファイルのっける"
|
||||
more: "他のやつ!"
|
||||
featured: "ハイライト"
|
||||
usernameOrUserId: "ユーザー名かユーザーID"
|
||||
noSuchUser: "ユーザーが見つからへんで"
|
||||
lookup: "見てきて"
|
||||
announcements: "お知らせ"
|
||||
imageUrl: "画像URL"
|
||||
remove: "ほかす"
|
||||
removeAreYouSure: "「{x}」はほかしてええか?"
|
||||
deleteAreYouSure: "「{x}」はほかしてええか?"
|
||||
resetAreYouSure: "リセットしてええん?"
|
||||
saved: "保存したで!"
|
||||
messaging: "チャット"
|
||||
upload: "アップロード"
|
||||
fromDrive: "ドライブから"
|
||||
fromUrl: "URLから"
|
||||
uploadFromUrl: "URLアップロード"
|
||||
uploadFromUrlDescription: "このURLのファイルをアップロードしたいねん"
|
||||
uploadFromUrlRequested: "アップロードしたい言うといたで"
|
||||
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
|
||||
explore: "みつける"
|
||||
messageRead: "もう読んだ"
|
||||
noMoreHistory: "これより過去の履歴はあらへんで"
|
||||
startMessaging: "チャットやるで"
|
||||
nUsersRead: "{n}人が読んでもうた"
|
||||
agreeTo: "{0}に同意したで"
|
||||
tos: "利用規約"
|
||||
start: "始める"
|
||||
home: "ホーム"
|
||||
remoteUserCaution: "リモートユーザーやから、足りひん情報あるかもしれへん。"
|
||||
activity: "アクティビティ"
|
||||
images: "画像"
|
||||
birthday: "生まれた日"
|
||||
yearsOld: "{age}歳"
|
||||
registeredDate: "始めた日"
|
||||
location: "場所"
|
||||
theme: "テーマ"
|
||||
themeForLightMode: "ライトモードではこのテーマつこて"
|
||||
themeForDarkMode: "ダークモードではこのテーマつこて"
|
||||
light: "ライト"
|
||||
dark: "ダーク"
|
||||
lightThemes: "デイゲーム"
|
||||
darkThemes: "ナイトゲーム"
|
||||
syncDeviceDarkMode: "デバイスのダークモードと一緒にする"
|
||||
drive: "ドライブ"
|
||||
selectFile: "ファイル選んでや"
|
||||
selectFiles: "ファイル選んでや"
|
||||
selectFolder: "フォルダ選んでや"
|
||||
selectFolders: "フォルダ選んでや"
|
||||
renameFile: "ファイル名をいらう"
|
||||
folderName: "フォルダー名"
|
||||
createFolder: "フォルダー作る"
|
||||
renameFolder: "フォルダー名を変える"
|
||||
deleteFolder: "フォルダーを消してまう"
|
||||
addFile: "ファイルを追加"
|
||||
unableToDelete: "消そうおもってんけどな、あかんかったわ"
|
||||
inputNewFileName: "今度のファイル名は何にするん?"
|
||||
inputNewDescription: "新しいキャプションを入力しましょ"
|
||||
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
||||
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
||||
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
|
||||
copyUrl: "URLをコピー"
|
||||
rename: "名前を変えるで"
|
||||
avatar: "アイコン"
|
||||
banner: "バナー"
|
||||
nsfw: "閲覧注意"
|
||||
whenServerDisconnected: "サーバーとの接続が切れたとき"
|
||||
disconnectedFromServer: "サーバーとの通信が切れたで"
|
||||
reload: "リロード"
|
||||
doNothing: "何もせんとく"
|
||||
reloadConfirm: "リロードしてええか?"
|
||||
watch: "ウォッチ"
|
||||
unwatch: "ウォッチやめる"
|
||||
accept: "ええで"
|
||||
reject: "あかん"
|
||||
normal: "ええ感じ"
|
||||
instanceName: "インスタンス名"
|
||||
instanceDescription: "インスタンスの紹介"
|
||||
maintainerName: "管理者の名前"
|
||||
maintainerEmail: "管理者のメールアドレス"
|
||||
tosUrl: "利用規約のURL"
|
||||
thisYear: "今年"
|
||||
thisMonth: "今月"
|
||||
today: "今日"
|
||||
dayX: "{day}日"
|
||||
monthX: "{month}月"
|
||||
yearX: "{year}年"
|
||||
pages: "ページ"
|
||||
enableLocalTimeline: "ローカルタイムラインを使えるようにする"
|
||||
enableGlobalTimeline: "グローバルタイムラインを使えるようにする"
|
||||
disablingTimelinesInfo: "ここらへんのタイムラインを使えんようにしてしもても、管理者とモデレーターは使えるままになってるで、そうやなかったら不便やからな。"
|
||||
enableRegistration: "一見さんでも誰でもいらっしゃ~い"
|
||||
invite: "来てや"
|
||||
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
|
||||
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
|
||||
inMb: "メガバイト単位"
|
||||
iconUrl: "アイコン画像のURL"
|
||||
bannerUrl: "バナー画像のURL"
|
||||
pinnedUsers: "ピン留めしたユーザー"
|
||||
pinnedUsersDescription: "「みつける」ページとかにピン留めしたいユーザーをここに書けばええんやで。他ん人との名前は改行で区切ればええんやで。"
|
||||
hcaptchaSiteKey: "サイトキー"
|
||||
hcaptchaSecretKey: "シークレットキー"
|
||||
recaptchaSiteKey: "サイトキー"
|
||||
recaptchaSecretKey: "シークレットキー"
|
||||
antennas: "アンテナ"
|
||||
manageAntennas: "アンテナいじる"
|
||||
name: "名前"
|
||||
antennaSource: "受信ソース(このソースは食われへん)"
|
||||
antennaKeywords: "受信キーワード"
|
||||
antennaExcludeKeywords: "除外キーワード"
|
||||
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
|
||||
notifyAntenna: "新しいノートを通知すんで"
|
||||
withFileAntenna: "なんか添付されたノートだけ"
|
||||
antennaUsersDescription: "ユーザー名を改行で区切ったってな"
|
||||
caseSensitive: "大文字と小文字は別もんや"
|
||||
withReplies: "返信も入れたって"
|
||||
connectedTo: "次のアカウントに繋がっとるで"
|
||||
notesAndReplies: "投稿と返信"
|
||||
withFiles: "ファイル付いとる"
|
||||
silence: "サイレンス"
|
||||
silenceConfirm: "サイレンスしよか?"
|
||||
unsilence: "サイレンスやめるで"
|
||||
unsilenceConfirm: "サイレンスやめよか?"
|
||||
popularUsers: "人気のユーザー"
|
||||
recentlyUpdatedUsers: "ちょっと前に投稿したばっかりのユーザー"
|
||||
recentlyRegisteredUsers: "ちょっと前に始めたばっかりのユーザー"
|
||||
recentlyDiscoveredUsers: "最近見っけたユーザー"
|
||||
popularTags: "人気のタグ"
|
||||
userList: "リスト"
|
||||
aboutMisskey: "FoundKeyってなんや?"
|
||||
administrator: "管理者"
|
||||
token: "トークン"
|
||||
twoStepAuthentication: "二段階認証"
|
||||
moderator: "モデレーター"
|
||||
nUsersMentioned: "{n}人が投稿"
|
||||
securityKey: "セキュリティキー"
|
||||
securityKeyName: "キーの名前"
|
||||
registerSecurityKey: "セキュリティキーを登録するで"
|
||||
lastUsed: "最後につこうた日"
|
||||
unregister: "登録やめる"
|
||||
passwordLessLogin: "パスワード無くてもログインできるようにする"
|
||||
resetPassword: "パスワードをリセット"
|
||||
newPasswordIs: "今度のパスワードは「{password}」や"
|
||||
reduceUiAnimation: "UIの動きやアニメーションを減らす"
|
||||
share: "わけわけ"
|
||||
notFound: "見つからへんね"
|
||||
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
|
||||
uploadFolder: "とりあえずアップロードしたやつ置いとく所"
|
||||
markAsReadAllNotifications: "通知はもう全て読んだわっ"
|
||||
markAsReadAllUnreadNotes: "投稿は全て読んだわっ"
|
||||
markAsReadAllTalkMessages: "チャットはもうぜんぶ読んだわっ"
|
||||
help: "ヘルプ"
|
||||
inputMessageHere: "ここにメッセージ書いてや"
|
||||
close: "閉じる"
|
||||
group: "グループ"
|
||||
groups: "グループ"
|
||||
createGroup: "グループを作るで"
|
||||
ownedGroups: "所有しとるグループ"
|
||||
joinedGroups: "参加しとるグループ"
|
||||
invites: "来てや"
|
||||
groupName: "グループ名"
|
||||
members: "メンバー"
|
||||
transfer: "譲渡"
|
||||
messagingWithUser: "ユーザーとチャット"
|
||||
messagingWithGroup: "グループでチャット"
|
||||
title: "タイトル"
|
||||
text: "テキスト"
|
||||
enable: "有効にするで"
|
||||
next: "次"
|
||||
retype: "もっかい入力"
|
||||
noteOf: "{user}のノート"
|
||||
inviteToGroup: "グループに招く"
|
||||
quoteAttached: "引用付いとるで"
|
||||
quoteQuestion: "引用として添付してもええか?"
|
||||
noMessagesYet: "まだチャットはあらへんで"
|
||||
newMessageExists: "新しいメッセージがきたで"
|
||||
onlyOneFileCanBeAttached: "すまん、メッセージに添付できるファイルはひとつだけなんや。"
|
||||
signinRequired: "ログインしてくれへん?"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しとるで"
|
||||
available: "利用できる"
|
||||
unavailable: "利用できん"
|
||||
usernameInvalidFormat: "a~z、A~Z、0~9、_が使えるで"
|
||||
tooShort: "短すぎやろ!"
|
||||
tooLong: "長すぎやろ!"
|
||||
weakPassword: "へぼいパスワード"
|
||||
normalPassword: "普通のパスワード"
|
||||
strongPassword: "ええ感じのパスワード"
|
||||
passwordMatched: "よし!一致や!"
|
||||
passwordNotMatched: "一致しとらんで?"
|
||||
or: "それか"
|
||||
language: "言語"
|
||||
uiLanguage: "UIの表示言語"
|
||||
groupInvited: "グループに招待されとるで"
|
||||
useOsNativeEmojis: "OSネイティブの絵文字を使う"
|
||||
youHaveNoGroups: "グループがあらへんねぇ。"
|
||||
noHistory: "履歴はあらへんねぇ。"
|
||||
signinHistory: "ログイン履歴"
|
||||
disableAnimatedMfm: "動きがやかましいMFMを止める"
|
||||
category: "カテゴリ"
|
||||
tags: "タグ"
|
||||
createAccount: "アカウントを作成"
|
||||
fontSize: "フォントサイズ"
|
||||
noFollowRequests: "フォロー申請はあらへんで"
|
||||
openImageInNewTab: "画像を新しいタブで開く"
|
||||
dashboard: "ダッシュボード"
|
||||
local: "ローカル"
|
||||
remote: "リモート"
|
||||
dayOverDayChanges: "前日比"
|
||||
appearance: "見た目"
|
||||
clientSettings: "クライアントの設定"
|
||||
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示してや"
|
||||
objectStorage: "オブジェクトストレージ"
|
||||
useObjectStorage: "オブジェクトストレージを使う"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
objectStorageBaseUrlDesc: "参照に使うにURLやで。CDNやProxyを使用してるんならそのURL、S3: 'https://<bucket>.s3.amazonaws.com'、GCSとかなら:\
|
||||
\ 'https://storage.googleapis.com/<bucket>'。"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageUseSSL: "SSLを使う"
|
||||
objectStorageUseProxy: "Proxyを使う"
|
||||
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
||||
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
||||
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
||||
newNoteRecived: "新しいノートがあるで"
|
||||
sounds: "サウンド"
|
||||
listen: "聴く"
|
||||
none: "なし"
|
||||
showInPage: "ページで表示"
|
||||
popout: "ポップアウト"
|
||||
volume: "音量"
|
||||
masterVolume: "全体の音量"
|
||||
details: "もっと"
|
||||
unableToProcess: "なんか作業が止まってしまったようやね"
|
||||
recentUsed: "最近使ったやつ"
|
||||
install: "インストール"
|
||||
uninstall: "アンインストール"
|
||||
installedApps: "インストールされとるアプリ"
|
||||
nothing: "あらへん"
|
||||
installedDate: "インストールした日時"
|
||||
lastUsedDate: "最後に使った日時"
|
||||
state: "状態"
|
||||
sort: "仕分ける"
|
||||
ascendingOrder: "小さい順"
|
||||
descendingOrder: "大きい順"
|
||||
scratchpad: "スクラッチパッド"
|
||||
scratchpadDescription: "スクラッチパッドではAiScriptを色々試すことができるんや。FoundKeyに対して色々できるコードを書いて動かしてみたり、結果を見たりできるで。"
|
||||
output: "出力"
|
||||
updateRemoteUser: "リモートユーザー情報の更新してくれん?"
|
||||
deleteAllFilesConfirm: "ホンマにすべてのファイルを削除するん?消したもんはもう戻ってこんのやで?"
|
||||
removeAllFollowing: "フォローを全解除"
|
||||
removeAllFollowingDescription: "{host}からのフォローをすべて解除するで。そのインスタンスが消えて無くなった時とかには便利な機能やで。"
|
||||
userSuspended: "このユーザーは...凍結されとる。"
|
||||
userSilenced: "このユーザーは...サイレンスされとる。"
|
||||
divider: "分割線"
|
||||
relays: "リレー"
|
||||
addRelay: "リレーの追加"
|
||||
inboxUrl: "inboxのURL"
|
||||
poll: "アンケート"
|
||||
enablePlayer: "プレイヤーを開く"
|
||||
disablePlayer: "プレイヤーを閉じる"
|
||||
themeEditor: "テーマエディター"
|
||||
description: "説明"
|
||||
author: "作者"
|
||||
leaveConfirm: "未保存の変更があるで!ほかしてええか?"
|
||||
manage: "管理"
|
||||
plugins: "プラグイン"
|
||||
deck: "デッキ"
|
||||
width: "幅"
|
||||
height: "高さ"
|
||||
large: "大"
|
||||
medium: "中"
|
||||
small: "小"
|
||||
edit: "編集"
|
||||
enableEmail: "メール配信を受け取る"
|
||||
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||
email: "メール"
|
||||
emailAddress: "メールアドレス"
|
||||
smtpConfig: "SMTP サーバーの設定"
|
||||
smtpHost: "ホスト"
|
||||
smtpPort: "ポート"
|
||||
smtpUser: "ユーザー名"
|
||||
smtpPass: "パスワード"
|
||||
emptyToDisableSmtpAuth: "ユーザー名とパスワードになんも入れんかったら、SMTP認証を無効化するで"
|
||||
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||
testEmail: "配信テスト"
|
||||
wordMute: "ワードミュート"
|
||||
userSaysSomething: "{name}が何か言ったようやで"
|
||||
makeActive: "使うで"
|
||||
display: "表示"
|
||||
copy: "コピー"
|
||||
overview: "概要"
|
||||
database: "データベース"
|
||||
channel: "チャンネル"
|
||||
create: "作成"
|
||||
notificationSetting: "通知設定"
|
||||
notificationSettingDesc: "表示する通知の種類えらんでや。"
|
||||
useGlobalSetting: "グローバル設定を使ってや"
|
||||
other: "その他"
|
||||
regenerateLoginToken: "ログイントークンを再生成"
|
||||
behavior: "動作"
|
||||
abuseReports: "通報"
|
||||
reportAbuse: "通報"
|
||||
reportAbuseOf: "{name}を通報する"
|
||||
send: "送信"
|
||||
abuseMarkAsResolved: "対応したで"
|
||||
openInNewTab: "新しいタブで開く"
|
||||
defaultNavigationBehaviour: "デフォルトのナビゲーション"
|
||||
instanceTicker: "ノートのインスタンス情報"
|
||||
system: "システム"
|
||||
switchUi: "UI切り替え"
|
||||
desktop: "デスクトップ"
|
||||
clip: "クリップ"
|
||||
receivedReactionsCount: "リアクションされた数"
|
||||
pollVotesCount: "アンケートに投票した数"
|
||||
pollVotedCount: "アンケートに投票された数"
|
||||
yes: "はい"
|
||||
no: "いいえ"
|
||||
driveFilesCount: "ドライブのファイル数"
|
||||
emailVerified: "メールアドレスは確認されたで"
|
||||
pageLikesCount: "Pageにええやんと思った数"
|
||||
pageLikedCount: "Pageにええやんと思ってくれた数"
|
||||
clips: "クリップ"
|
||||
duplicate: "複製"
|
||||
left: "左"
|
||||
center: "中央"
|
||||
wide: "広い"
|
||||
narrow: "狭い"
|
||||
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
|
||||
clearCache: "キャッシュをほかす"
|
||||
onlineUsersCount: "{n}人が起きとるで"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "アクセント"
|
||||
textColor: "文字"
|
||||
saveAs: "名前を付けて保存"
|
||||
createdAt: "作成した日"
|
||||
updatedAt: "更新日時"
|
||||
deleteConfirm: "ホンマに削除するで?"
|
||||
closeAccount: "アカウントを閉鎖する"
|
||||
newVersionOfClientAvailable: "新しいバージョンのクライアントが使えるで。"
|
||||
usageAmount: "使用量"
|
||||
capacity: "容量"
|
||||
inUse: "使用中"
|
||||
editCode: "コードを編集"
|
||||
apply: "適用"
|
||||
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
||||
emailNotification: "メール通知"
|
||||
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開くようにする"
|
||||
typingUsers: "{users}が今書きよるで"
|
||||
jumpToSpecifiedDate: "特定の日付にジャンプ"
|
||||
clear: "クリア"
|
||||
markAllAsRead: "もうみな読んでもうたわ"
|
||||
goBack: "戻る"
|
||||
info: "情報"
|
||||
user: "ユーザー"
|
||||
administration: "管理"
|
||||
hashtags: "ハッシュタグ"
|
||||
hide: "隠す"
|
||||
indefinitely: "無期限"
|
||||
_email:
|
||||
_follow:
|
||||
title: "フォローされたで"
|
||||
_receiveFollowRequest:
|
||||
title: "フォローリクエストを受け取ったで"
|
||||
_plugin:
|
||||
install: "プラグインのインストール"
|
||||
installWarn: "信頼できへんプラグインはインストールせんとってな"
|
||||
_registry:
|
||||
scope: "スコープ"
|
||||
key: "キー"
|
||||
keys: "キー"
|
||||
domain: "ドメイン"
|
||||
createKey: "キーを作る"
|
||||
_aboutMisskey:
|
||||
about: "FoundKeyはsyuiloが2014年からずっと作ってはる、オープンソースなソフトウェアや。"
|
||||
allContributors: "全ての貢献者"
|
||||
source: "ソースコード"
|
||||
_mfm:
|
||||
cheatSheet: "MFMチートシート"
|
||||
mention: "メンション"
|
||||
hashtag: "ハッシュタグ"
|
||||
url: "URL"
|
||||
link: "リンク"
|
||||
bold: "太字"
|
||||
center: "中央寄せ"
|
||||
inlineCode: "コード(インライン)"
|
||||
blockCode: "コード(ブロック)"
|
||||
inlineMath: "数式(インライン)"
|
||||
quote: "引用"
|
||||
emoji: "カスタム絵文字"
|
||||
search: "探す"
|
||||
shake: "アニメーション(ぶるぶる)"
|
||||
twitch: "アニメーション(ブレ)"
|
||||
spin: "アニメーション(回転)"
|
||||
blur: "ぼかし"
|
||||
font: "フォント"
|
||||
rotate: "回転"
|
||||
_instanceTicker:
|
||||
none: "表示せん"
|
||||
remote: "リモートユーザーに表示"
|
||||
always: "常に表示"
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動でリロード"
|
||||
dialog: "ダイアログで警告"
|
||||
_channel:
|
||||
create: "チャンネルを作る"
|
||||
edit: "チャンネルを編集"
|
||||
setBanner: "バナーを設定"
|
||||
removeBanner: "バナーを削除"
|
||||
featured: "トレンド"
|
||||
notesCount: "{n}こ投稿があるで"
|
||||
_menuDisplay:
|
||||
hide: "隠す"
|
||||
_wordMute:
|
||||
soft: "ソフト"
|
||||
hard: "ハード"
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
install: "テーマのインストール"
|
||||
manage: "テーマの管理"
|
||||
code: "テーマコード"
|
||||
description: "説明"
|
||||
installed: "{name}をインストールしたで。"
|
||||
installedThemes: "インストールされとるテーマ"
|
||||
builtinThemes: "標準のテーマ"
|
||||
alreadyInstalled: "そのテーマはもうインストールされとるで?"
|
||||
make: "テーマを作る"
|
||||
_sfx:
|
||||
note: "ノート"
|
||||
noteMy: "ノート(自分)"
|
||||
notification: "通知"
|
||||
chat: "チャット"
|
||||
_ago:
|
||||
future: "未来"
|
||||
justNow: "たった今"
|
||||
secondsAgo: "{n}秒前"
|
||||
minutesAgo: "{n}分前"
|
||||
hoursAgo: "{n}時間前"
|
||||
daysAgo: "{n}日前"
|
||||
weeksAgo: "{n}週間前"
|
||||
monthsAgo: "{n}ヶ月前"
|
||||
yearsAgo: "{n}年前"
|
||||
_time:
|
||||
second: "秒"
|
||||
minute: "分"
|
||||
hour: "時間"
|
||||
day: "日"
|
||||
_tutorial:
|
||||
step3_1: "プロフィール設定はええ感じにできたか?"
|
||||
_2fa:
|
||||
alreadyRegistered: "もう設定終わっとるわ。"
|
||||
_permissions:
|
||||
"write:votes": "投票する"
|
||||
"read:pages": "ページを見る"
|
||||
"read:page-likes": "ページのええやんを見る"
|
||||
"write:page-likes": "ページのええやんを操作する"
|
||||
"read:user-groups": "ユーザーグループを見る"
|
||||
"read:channels": "チャンネルを見る"
|
||||
_auth:
|
||||
permissionAsk: "このアプリは次の権限を要求しとるで"
|
||||
_antennaSources:
|
||||
all: "みんなのノート"
|
||||
homeTimeline: "フォローしとるユーザーのノート"
|
||||
_weekday:
|
||||
sunday: "日曜日"
|
||||
monday: "月曜日"
|
||||
tuesday: "火曜日"
|
||||
wednesday: "水曜日"
|
||||
thursday: "木曜日"
|
||||
friday: "金曜日"
|
||||
saturday: "土曜日"
|
||||
_widgets:
|
||||
memo: "付箋"
|
||||
notifications: "通知"
|
||||
timeline: "タイムライン"
|
||||
calendar: "カレンダー"
|
||||
trends: "トレンド"
|
||||
clock: "時計"
|
||||
rss: "RSSリーダー"
|
||||
activity: "アクティビティ"
|
||||
photos: "フォト"
|
||||
digitalClock: "デジタル時計"
|
||||
federation: "連合"
|
||||
postForm: "投稿フォーム"
|
||||
slideshow: "スライドショー"
|
||||
button: "ボタン"
|
||||
onlineUsers: "オンラインユーザー"
|
||||
jobQueue: "ジョブキュー"
|
||||
serverMetric: "サーバーメトリクス"
|
||||
aiscript: "AiScriptコンソール"
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
show: "続き見して"
|
||||
chars: "{count}文字"
|
||||
files: "{count}ファイル"
|
||||
_poll:
|
||||
choiceN: "選択肢{n}"
|
||||
noMore: "これ以上追加でけへん"
|
||||
canMultipleVote: "複数回答可"
|
||||
expiration: "期限"
|
||||
infinite: "無期限"
|
||||
at: "日時指定"
|
||||
after: "経過指定"
|
||||
deadlineDate: "期日"
|
||||
deadlineTime: "時間"
|
||||
duration: "期間"
|
||||
votesCount: "{n}票"
|
||||
vote: "投票する"
|
||||
_visibility:
|
||||
publicDescription: "みんなに公開"
|
||||
home: "ホーム"
|
||||
followers: "フォロワー"
|
||||
_profile:
|
||||
name: "名前"
|
||||
username: "ユーザー名"
|
||||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
followingList: "フォロー"
|
||||
muteList: "ミュート"
|
||||
blockingList: "ブロック"
|
||||
userLists: "リスト"
|
||||
_charts:
|
||||
federation: "連合"
|
||||
apRequest: "リクエスト"
|
||||
usersTotal: "ユーザーの合計"
|
||||
activeUsers: "アクティブユーザー数"
|
||||
notesIncDec: "ノートの増減"
|
||||
localNotesIncDec: "ローカルのノートの増減"
|
||||
remoteNotesIncDec: "リモートのノートの増減"
|
||||
notesTotal: "ノートの合計"
|
||||
filesIncDec: "ファイルの増減"
|
||||
filesTotal: "ファイルの合計"
|
||||
storageUsageIncDec: "ストレージ使用量の増減"
|
||||
storageUsageTotal: "ストレージ使用量の合計"
|
||||
_instanceCharts:
|
||||
requests: "リクエスト"
|
||||
users: "ユーザーの増減"
|
||||
usersTotal: "ユーザーの累積"
|
||||
notes: "ノートの増減"
|
||||
notesTotal: "ノートの累積"
|
||||
ff: "フォロー/フォロワーの増減"
|
||||
ffTotal: "フォロー/フォロワーの累積"
|
||||
cacheSize: "キャッシュサイズの増減"
|
||||
cacheSizeTotal: "キャッシュサイズの累積"
|
||||
files: "ファイル数の増減"
|
||||
filesTotal: "ファイル数の累積"
|
||||
_timelines:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
social: "ソーシャル"
|
||||
global: "グローバル"
|
||||
_pages:
|
||||
newPage: "ページを作る"
|
||||
editPage: "ページの編集"
|
||||
readPage: "ソースを表示中"
|
||||
created: "ページを作成したで"
|
||||
updated: "ページを更新したで"
|
||||
deleted: "ページを削除したで"
|
||||
pageSetting: "ページ設定"
|
||||
viewPage: "ページを見る"
|
||||
like: "ええやん"
|
||||
unlike: "良くないわ"
|
||||
liked: "ええと思ったページ"
|
||||
contents: "コンテンツ"
|
||||
summary: "ページの要約"
|
||||
alignCenter: "中央寄せ"
|
||||
font: "フォント"
|
||||
fontSerif: "セリフ"
|
||||
fontSansSerif: "サンセリフ"
|
||||
eyeCatchingImageSet: "アイキャッチ画像を設定"
|
||||
eyeCatchingImageRemove: "アイキャッチ画像を削除"
|
||||
_notification:
|
||||
youGotMention: "{name}からのメンション"
|
||||
youGotReply: "{name}からのリプライ"
|
||||
youWereFollowed: "フォローされたで"
|
||||
youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
|
||||
yourFollowRequestAccepted: "フォローさせてもろたで"
|
||||
youWereInvitedToGroup: "グループに招待されとるで"
|
||||
_types:
|
||||
follow: "フォロー"
|
||||
mention: "メンション"
|
||||
renote: "Renote"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
receiveFollowRequest: "フォロー許可してほしいみたいやで"
|
||||
followRequestAccepted: "フォローが受理されたで"
|
||||
_actions:
|
||||
reply: "返事"
|
||||
renote: "Renote"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "いつもメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
columnMargin: "カラム間のマージン"
|
||||
columnHeaderHeight: "カラムのヘッダー幅"
|
||||
addColumn: "カラムを追加"
|
||||
swapLeft: "左に移動"
|
||||
swapRight: "右に移動"
|
||||
swapUp: "上に移動"
|
||||
swapDown: "下に移動"
|
||||
stackLeft: "左に重ねる"
|
||||
popRight: "右に出す"
|
||||
profile: "プロファイル"
|
||||
_columns:
|
||||
main: "メイン"
|
||||
widgets: "ウィジェット"
|
||||
notifications: "通知"
|
||||
tl: "タイムライン"
|
||||
antenna: "アンテナ"
|
||||
list: "リスト"
|
||||
mentions: "あんた宛て"
|
||||
direct: "ダイレクト"
|
||||
_services: {}
|
|
@ -219,7 +219,6 @@ uploadFromUrl: "URL 업로드"
|
|||
uploadFromUrlDescription: "업로드하려는 파일의 URL"
|
||||
uploadFromUrlRequested: "업로드를 요청했습니다"
|
||||
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
|
||||
explore: "발견하기"
|
||||
messageRead: "읽음"
|
||||
noMoreHistory: "이것보다 과거의 기록이 없습니다"
|
||||
startMessaging: "대화 시작하기"
|
||||
|
@ -298,8 +297,6 @@ inMb: "메가바이트 단위"
|
|||
iconUrl: "아이콘 URL"
|
||||
bannerUrl: "배너 이미지 URL"
|
||||
backgroundImageUrl: "배경 이미지 URL"
|
||||
pinnedUsers: "고정된 유저"
|
||||
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
|
||||
hcaptchaSiteKey: "사이트 키"
|
||||
hcaptchaSecretKey: "시크릿 키"
|
||||
recaptchaSiteKey: "사이트 키"
|
||||
|
@ -323,11 +320,6 @@ silence: "사일런스"
|
|||
silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?"
|
||||
unsilence: "사일런스 해제"
|
||||
unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?"
|
||||
popularUsers: "인기 유저"
|
||||
recentlyUpdatedUsers: "최근 활동한 유저"
|
||||
recentlyRegisteredUsers: "최근 가입한 유저"
|
||||
recentlyDiscoveredUsers: "최근 발견한 유저"
|
||||
popularTags: "인기 태그"
|
||||
userList: "리스트"
|
||||
aboutMisskey: "FoundKey에 대하여"
|
||||
administrator: "관리자"
|
||||
|
@ -368,7 +360,6 @@ messagingWithGroup: "그룹끼리 대화하기"
|
|||
title: "제목"
|
||||
text: "텍스트"
|
||||
enable: "사용"
|
||||
next: "다음"
|
||||
retype: "다시 입력"
|
||||
noteOf: "{user}의 노트"
|
||||
inviteToGroup: "그룹에 초대하기"
|
||||
|
@ -552,7 +543,6 @@ abuseReports: "신고"
|
|||
reportAbuse: "신고"
|
||||
reportAbuseOf: "{name}을 신고하기"
|
||||
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요."
|
||||
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
|
||||
reporter: "신고자"
|
||||
reporteeOrigin: "피신고자"
|
||||
reporterOrigin: "신고자"
|
||||
|
@ -921,30 +911,6 @@ _time:
|
|||
minute: "분"
|
||||
hour: "시간"
|
||||
day: "일"
|
||||
_tutorial:
|
||||
title: "FoundKey의 사용 방법"
|
||||
step1_1: "환영합니다!"
|
||||
step1_2: "이 페이지는 \"타임라인\"이라고 불립니다. 당신이 \"팔로우\"하고 있는 사람들의 \"노트\"가 시간순으로 나타납니다."
|
||||
step1_3: "아직 아무 유저도 팔로우하고 있지 않기에 타임라인은 비어 있을 것입니다."
|
||||
step2_1: "새 노트를 작성하거나 다른 사람을 팔로우하기 전에, 먼저 프로필을 완성해보도록 합시다."
|
||||
step2_2: "당신이 어떤 사람인지를 알린다면, 다른 사람들이 당신을 팔로우할 확률이 올라갈 것입니다."
|
||||
step3_1: "프로필 설정은 잘 끝내셨나요?"
|
||||
step3_2: "그럼 시험삼아 노트를 작성해 보세요. 화면에 있는 연필 버튼을 누르면 작성 폼이 열립니다."
|
||||
step3_3: "내용을 작성한 후, 폼 오른쪽 상단의 버튼을 눌러 노트를 올릴 수 있습니다."
|
||||
step3_4: "쓸 말이 없나요? \"Misskey 시작했어요!\" 같은 건 어떨까요? :>"
|
||||
step4_1: "노트 작성을 끝내셨나요?"
|
||||
step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
|
||||
step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다."
|
||||
step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요.\
|
||||
\ 마음에 드는 사람을 골라 팔로우해 보세요!"
|
||||
step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요."
|
||||
step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다."
|
||||
step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다."
|
||||
step6_2: "다른 유저의 노트에 \"리액션\"을 붙여 간단하게 당신의 반응을 전달할 수도 있습니다."
|
||||
step6_3: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다."
|
||||
step7_1: "이것으로 FoundKey의 기본 튜토리얼을 마치겠습니다. 수고하셨습니다!"
|
||||
step7_2: "FoundKey에 대해 더 알고 싶으시다면 {help}를 참고해 주세요."
|
||||
step7_3: "그럼 FoundKey를 즐기세요! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "이미 설정이 완료되었습니다."
|
||||
registerDevice: "디바이스 등록"
|
||||
|
|
|
@ -224,7 +224,6 @@ uploadFromUrl: "Uploaden vanaf een URL"
|
|||
uploadFromUrlDescription: "URL van het bestand dat je wil uploaden"
|
||||
uploadFromUrlRequested: "Uploadverzoek"
|
||||
uploadFromUrlMayTakeTime: "Het kan even duren voordat het uploaden voltooid is."
|
||||
explore: "Verkennen"
|
||||
messageRead: "Lezen"
|
||||
noMoreHistory: "Er is geen verdere geschiedenis"
|
||||
startMessaging: "Start een gesprek"
|
||||
|
|
|
@ -225,7 +225,6 @@ uploadFromUrl: "Wyślij z adresu URL"
|
|||
uploadFromUrlDescription: "Adres URL pliku, który chcesz wysłać"
|
||||
uploadFromUrlRequested: "Zażądano wysłania"
|
||||
uploadFromUrlMayTakeTime: "Wysyłanie może chwilę potrwać."
|
||||
explore: "Eksploruj"
|
||||
messageRead: "Przeczytano"
|
||||
noMoreHistory: "Nie ma dalszej historii"
|
||||
startMessaging: "Rozpocznij czat"
|
||||
|
@ -307,9 +306,6 @@ inMb: "W megabajtach"
|
|||
iconUrl: "Adres URL ikony"
|
||||
bannerUrl: "Adres URL banera"
|
||||
backgroundImageUrl: "Adres URL tła"
|
||||
pinnedUsers: "Przypięty użytkownik"
|
||||
pinnedUsersDescription: "Wypisz po jednej nazwie użytkownika w wierszu. Podani użytkownicy\
|
||||
\ zostaną przypięci pod kartą „Eksploruj”."
|
||||
hcaptchaSiteKey: "Klucz strony"
|
||||
hcaptchaSecretKey: "Tajny klucz"
|
||||
recaptchaSiteKey: "Klucz strony"
|
||||
|
@ -334,11 +330,6 @@ silence: "Wycisz"
|
|||
silenceConfirm: "Czy na pewno chcesz wyciszyć tego użytkownika?"
|
||||
unsilence: "Cofnij wyciszenie"
|
||||
unsilenceConfirm: "Czy na pewno chcesz cofnąć wyciszenie tego użytkownika?"
|
||||
popularUsers: "Popularni użytkownicy"
|
||||
recentlyUpdatedUsers: "Ostatnio aktywni użytkownicy"
|
||||
recentlyRegisteredUsers: "Ostatnio zarejestrowani użytkownicy"
|
||||
recentlyDiscoveredUsers: "Ostatnio odkryci użytkownicy"
|
||||
popularTags: "Tagi na czasie"
|
||||
userList: "Listy"
|
||||
aboutMisskey: "O Foundkey"
|
||||
administrator: "Admin"
|
||||
|
@ -379,7 +370,6 @@ messagingWithGroup: "Rozmowy wewnątrz grupy"
|
|||
title: "Tytuł"
|
||||
text: "Tekst"
|
||||
enable: "Włącz"
|
||||
next: "Dalej"
|
||||
retype: "Wprowadź ponownie"
|
||||
noteOf: "Wpisy {user}"
|
||||
inviteToGroup: "Zaproś do grupy"
|
||||
|
@ -556,7 +546,6 @@ abuseReports: "Zgłoszenia"
|
|||
reportAbuse: "Zgłoś"
|
||||
reportAbuseOf: "Zgłoś {name}"
|
||||
fillAbuseReportDescription: "Wypełnij szczegóły zgłoszenia."
|
||||
abuseReported: "Twoje zgłoszenie zostało wysłane. Dziękujemy."
|
||||
reporteeOrigin: "Pochodzenie osoby zgłoszonej"
|
||||
reporterOrigin: "Pochodzenie osoby zgłaszającej"
|
||||
send: "Wyślij"
|
||||
|
@ -857,40 +846,6 @@ _time:
|
|||
minute: "minuta"
|
||||
hour: "godz."
|
||||
day: "dzień"
|
||||
_tutorial:
|
||||
title: "Jak korzystać z Foundkey"
|
||||
step1_1: "Witaj!"
|
||||
step1_3: "Twoja oś czasu jest jeszcze pusta, ponieważ nie opublikował*ś jeszcze\
|
||||
\ żadnych wpisów i nie obserwujesz jeszcze nikogo."
|
||||
step2_1: "Ukończmy konfigurację profilu zanim utworzymy wpis lub zaczniemy kogoś\
|
||||
\ obserwować."
|
||||
step3_1: "Zakończył*ś konfigurację profilu?"
|
||||
step3_3: "Wypełnij pole i kliknij przycisk w prawym górnym rogu by wysłać wpis."
|
||||
step4_1: Skończył*ś publikować swój pierwszy wpis?
|
||||
step6_2: Możesz też dodawać reakcję do wpisów innych ludzi, żeby szybko odpowiedzieć
|
||||
na nie.
|
||||
step4_2: Hura! Teraz Twój wpis powinien być widoczny na Twojej osi czasu.
|
||||
step5_2: '{featured} pokaże CI popularne wpisy z tej instancji. {explore} pozwoli
|
||||
ci znaleźć popularnych użytkowników. Spróbuj tam znaleźć ludzi których chciał*byś
|
||||
zaobserwować!'
|
||||
step7_1: Gratulacje! Właśnie ukończył*ś podstawowy samouczek Foundkey.
|
||||
step6_3: W celu dodania reakcji, kliknij na "+" przy poście innego użytkownika i
|
||||
wybierz jakieś emoji jako reakcję na niego.
|
||||
step6_1: Wpisy innych ludzi powinny już się pojawiać na Twojej osi czasu.
|
||||
step7_2: Jeśli chcesz dowiedzieć się więcej o Foundkey, przejdź do sekcji {help}.
|
||||
step7_3: A teraz, powodzenia i miłej zabawy z Foundkey! 🚀
|
||||
step5_1: Teraz spróbujmy ożywić oś czasu poprzez zaobserwowanie innych ludzi.
|
||||
step1_2: Ta strona jest nazywana "osią czasu". Pokazuje ona chronologicznie ułożone
|
||||
wpisy ludzi których obserwujesz.
|
||||
step5_4: Jeśli inny użytkownik ma kłódkę przy nazwie, ręczne zatwierdzenie Twojej
|
||||
prośby o obserwację przez tego użytkownika może zająć trochę czasu.
|
||||
step3_2: Spróbujmy opublikować wpis. Możesz to zrobić klikając przycisk z ikoną
|
||||
ołówka.
|
||||
step2_2: Dodanie informacji o sobie pomoże innym w decyzji czy chcą widzieć Twoje
|
||||
wpisy, lub Ciebie obserwować.
|
||||
step5_3: By zaobserwować innych użytkowników, kliknij na ich ikonę i wciśnij przycisk
|
||||
"Obserwuj" na ich profilu.
|
||||
step3_4: Nic nie przychodzi na myśl? Spróbuj "Właśnie wrócił*m z kościoła"!
|
||||
_2fa:
|
||||
registerDevice: "Zarejestruj nowe urządzenie"
|
||||
step1: "Najpierw, zainstaluj aplikację uwierzytelniającą (taką jak {a} lub {b})\
|
||||
|
|
|
@ -236,7 +236,6 @@ uploadFromUrl: "Încarcă dintr-un URL"
|
|||
uploadFromUrlDescription: "URL-ul fișierului pe care dorești să îl încarci"
|
||||
uploadFromUrlRequested: "Încărcare solicitată"
|
||||
uploadFromUrlMayTakeTime: "S-ar putea să ia puțin până se finalizează încărcarea."
|
||||
explore: "Explorează"
|
||||
messageRead: "Citit"
|
||||
noMoreHistory: "Nu există mai mult istoric"
|
||||
startMessaging: "Începe un chat nou"
|
||||
|
@ -318,9 +317,6 @@ inMb: "În megabytes"
|
|||
iconUrl: "URL-ul iconiței"
|
||||
bannerUrl: "URL-ul imaginii de banner"
|
||||
backgroundImageUrl: "URL-ul imaginii de fundal"
|
||||
pinnedUsers: "Utilizatori fixați"
|
||||
pinnedUsersDescription: "Scrie utilizatorii, separați prin pauză de rând, care vor\
|
||||
\ fi fixați pe pagina \"Explorează\"."
|
||||
hcaptchaSiteKey: "Site key"
|
||||
hcaptchaSecretKey: "Secret key"
|
||||
recaptchaSiteKey: "Site key"
|
||||
|
@ -345,11 +341,6 @@ silence: "Amuțește"
|
|||
silenceConfirm: "Ești sigur că vrei să amuțești acest utilizator?"
|
||||
unsilence: "Anulează amuțirea"
|
||||
unsilenceConfirm: "Ești sigur că vrei să anulezi amuțirea acestui utilizator?"
|
||||
popularUsers: "Utilizatori populari"
|
||||
recentlyUpdatedUsers: "Utilizatori activi recent"
|
||||
recentlyRegisteredUsers: "Utilizatori ce s-au alăturat recent"
|
||||
recentlyDiscoveredUsers: "Utilizatori descoperiți recent"
|
||||
popularTags: "Taguri populare"
|
||||
userList: "Liste"
|
||||
aboutMisskey: "Despre FoundKey"
|
||||
administrator: "Administrator"
|
||||
|
@ -390,7 +381,6 @@ messagingWithGroup: "Chat de grup"
|
|||
title: "Titlu"
|
||||
text: "Text"
|
||||
enable: "Activează"
|
||||
next: "Următorul"
|
||||
retype: "Introdu din nou"
|
||||
noteOf: "Notă de {user}"
|
||||
inviteToGroup: "Invită în grup"
|
||||
|
@ -584,7 +574,6 @@ abuseReports: "Rapoarte"
|
|||
reportAbuse: "Raportează"
|
||||
reportAbuseOf: "Raportează {name}"
|
||||
fillAbuseReportDescription: "Te rog scrie detaliile legate de acest raport."
|
||||
abuseReported: "Raportul tău a fost trimis. Mulțumim."
|
||||
reporter: "Raportorul"
|
||||
reporteeOrigin: "Originea raportatului"
|
||||
reporterOrigin: "Originea raportorului"
|
||||
|
|
|
@ -231,7 +231,6 @@ uploadFromUrl: "Загрузить по ссылке"
|
|||
uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить"
|
||||
uploadFromUrlRequested: "Загрузка выбранного"
|
||||
uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время."
|
||||
explore: "Обзор"
|
||||
messageRead: "Прочитали"
|
||||
noMoreHistory: "История закончилась"
|
||||
startMessaging: "Начать общение"
|
||||
|
@ -312,9 +311,6 @@ inMb: "В мегабайтах"
|
|||
iconUrl: "Ссылка на аватар"
|
||||
bannerUrl: "Ссылка на изображение в шапке"
|
||||
backgroundImageUrl: "Ссылка на фоновое изображение"
|
||||
pinnedUsers: "Прикреплённый пользователь"
|
||||
pinnedUsersDescription: "Перечислите по одному имени пользователя в строке. Пользователи,\
|
||||
\ перечисленные здесь, будут привязаны к закладке \"Изучение\"."
|
||||
hcaptchaSiteKey: "Ключ сайта"
|
||||
hcaptchaSecretKey: "Секретный ключ"
|
||||
recaptchaSiteKey: "Ключ сайта"
|
||||
|
@ -340,11 +336,6 @@ silence: "Заглушить"
|
|||
silenceConfirm: "Заглушить этого пользователя? Уверены?"
|
||||
unsilence: "Снять глушение"
|
||||
unsilenceConfirm: "Снять глушение с этого пользователя? Уверены?"
|
||||
popularUsers: "Популярные пользователи"
|
||||
recentlyUpdatedUsers: "Активные последнее время"
|
||||
recentlyRegisteredUsers: "Недавно зарегистрированные пользователи"
|
||||
recentlyDiscoveredUsers: "Недавно обнаруженные пользователи"
|
||||
popularTags: "Популярные теги"
|
||||
userList: "Списки"
|
||||
aboutMisskey: "О FoundKey"
|
||||
administrator: "Администратор"
|
||||
|
@ -385,7 +376,6 @@ messagingWithGroup: "Общение в группе"
|
|||
title: "Заголовок"
|
||||
text: "Текст"
|
||||
enable: "Включить"
|
||||
next: "Дальше"
|
||||
retype: "Введите ещё раз"
|
||||
noteOf: "Что пишет {user}"
|
||||
inviteToGroup: "Пригласить в группу"
|
||||
|
@ -579,7 +569,6 @@ abuseReports: "Жалобы"
|
|||
reportAbuse: "Жалоба"
|
||||
reportAbuseOf: "Пожаловаться на пользователя {name}"
|
||||
fillAbuseReportDescription: "Опишите, пожалуйста, причину жалобы подробнее."
|
||||
abuseReported: "Жалоба отправлена. Большое спасибо за информацию."
|
||||
reporteeOrigin: "О ком сообщено"
|
||||
reporterOrigin: "Кто сообщил"
|
||||
forwardReport: "Перенаправление отчета на инстант."
|
||||
|
@ -963,43 +952,6 @@ _time:
|
|||
minute: "мин"
|
||||
hour: "ч"
|
||||
day: "сут"
|
||||
_tutorial:
|
||||
title: "Как пользоваться FoundKey"
|
||||
step1_1: "Добро пожаловать!"
|
||||
step1_2: "Эта страница называется «лента». Здесь будут появляться «заметки»: ваши\
|
||||
\ личные и тех, на кого вы «подписаны». Они будут располагаться в порядке времени\
|
||||
\ их появления."
|
||||
step1_3: "Правда, ваша лента пока пуста. Она начнёт заполняться, когда вы будете\
|
||||
\ писать свои заметки и подписываться на других."
|
||||
step2_1: "Давайте, заполним профиль, прежде чем начать писать заметки и подписываться\
|
||||
\ на других."
|
||||
step2_2: "То, что вы расскажете в профиле, поможет лучше вас узнать, а значит, многим\
|
||||
\ будет легче присоединиться — вы скорее получите новых подписчиков и читателей."
|
||||
step3_1: "Успешно заполнили профиль?"
|
||||
step3_2: "Что ж, теперь самое время опубликуовать заметку. Если нажать вверху страницы\
|
||||
\ на изображение карандаша, появится форма для текста."
|
||||
step3_3: "Напишите в неё, что хотите, и нажмите на кнопку в правом верхнем углу."
|
||||
step3_4: "Ничего не приходит в голову? Как насчёт: «Я новенький, пока осваиваюсь\
|
||||
\ в FoundKey»?"
|
||||
step4_1: "С написанием первой заметки покончено?"
|
||||
step4_2: "Отлично, теперь она должна появиться в вашей ленте."
|
||||
step5_1: "А теперь самое время немного оживить ленту, подписавшись на других."
|
||||
step5_2: "На странице «{featured}» собраны популярные сегодня заметки, читая которые,\
|
||||
\ вы можете найти кого-то вам интересного, а на странице «{explore}» можно посмотреть,\
|
||||
\ кто популярен у остальных."
|
||||
step5_3: "Чтобы подписаться на кого-нибудь, щёлкните по его аватару и в открывшемся\
|
||||
\ профиле нажмите кнопку «Подписаться»."
|
||||
step5_4: "Некоторые пользователи (около их имени «висит замок») вручную подтверждают\
|
||||
\ чужие подписки. Так что иногда подписка начинает работать не сразу."
|
||||
step6_1: "Если теперь в ленте видны и чужие заметки, значит у вас получилось."
|
||||
step6_2: "Здесь можно непринуждённо выразить свои чувства к чьей-то заметке, отметив\
|
||||
\ «реакцию» под ней."
|
||||
step6_3: "Отмечайте реакции, нажмая на символ «+» под заметкой и выбирая значок\
|
||||
\ по душе."
|
||||
step7_1: "На этом вводный урок по использованию FoundKey закончен. Спасибо, что\
|
||||
\ прошли его до конца!"
|
||||
step7_2: "Хотите изучить FoundKey глубже — добро пожаловать в раздел «{help}»."
|
||||
step7_3: "Приятно вам провести время с FoundKey\U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Двухфакторная аутентификация уже настроена."
|
||||
registerDevice: "Зарегистрируйте ваше устройство"
|
||||
|
|
|
@ -230,7 +230,6 @@ uploadFromUrl: "Nahrať z URL adresy"
|
|||
uploadFromUrlDescription: "URL adresa nahrávaného súboru"
|
||||
uploadFromUrlRequested: "Upload vyžiadaný"
|
||||
uploadFromUrlMayTakeTime: "Nahrávanie môže nejaký čas trvať."
|
||||
explore: "Objavovať"
|
||||
messageRead: "Prečítané"
|
||||
noMoreHistory: "To je všetko"
|
||||
startMessaging: "Začať chat"
|
||||
|
@ -312,9 +311,6 @@ inMb: "V megabajtoch"
|
|||
iconUrl: "Favicon URL"
|
||||
bannerUrl: "URL obrázku bannera"
|
||||
backgroundImageUrl: "URL obrázku pozadia"
|
||||
pinnedUsers: "Pripnutí používatelia"
|
||||
pinnedUsersDescription: "Zoznam mien používateľov oddelených riadkami, ktorý budú\
|
||||
\ pripnutí v záložke \"Objavovať\"."
|
||||
hcaptchaSiteKey: "Site key"
|
||||
hcaptchaSecretKey: "Secret key"
|
||||
recaptchaSiteKey: "Site key"
|
||||
|
@ -339,11 +335,6 @@ silence: "Ticho"
|
|||
silenceConfirm: "Naozaj chcete utíšiť tohoto používateľa?"
|
||||
unsilence: "Vrátiť utíšenie"
|
||||
unsilenceConfirm: "Naozaj chcete vrátiť utíšenie tohoto používateľa?"
|
||||
popularUsers: "Populárni používatelia"
|
||||
recentlyUpdatedUsers: "Používatelia s najnovšou aktivitou"
|
||||
recentlyRegisteredUsers: "Najnovší používatelia"
|
||||
recentlyDiscoveredUsers: "Naposledy objavení používatelia"
|
||||
popularTags: "Populárne značky"
|
||||
userList: "Zoznamy"
|
||||
aboutMisskey: "O FoundKey"
|
||||
administrator: "Administrátor"
|
||||
|
@ -384,7 +375,6 @@ messagingWithGroup: "Skupinový chat"
|
|||
title: "Nadpis"
|
||||
text: "Text"
|
||||
enable: "Povoliť"
|
||||
next: "Ďalší"
|
||||
retype: "Zadajte znovu"
|
||||
noteOf: "Poznámky používateľa {user}"
|
||||
inviteToGroup: "Pozvať do skupiny"
|
||||
|
@ -570,7 +560,6 @@ abuseReports: "Nahlásenia"
|
|||
reportAbuse: "Nahlásiť"
|
||||
reportAbuseOf: "Nahlásiť {name}"
|
||||
fillAbuseReportDescription: "Prosím vyplňte podrobnosti nahlásenia."
|
||||
abuseReported: "Vaše nahlásenie je odoslané. Veľmi pekne ďakujeme."
|
||||
reporter: "Nahlásil"
|
||||
reporteeOrigin: "Pôvod nahláseného"
|
||||
reporterOrigin: "Pôvod nahlasovača"
|
||||
|
@ -959,40 +948,6 @@ _time:
|
|||
minute: "min"
|
||||
hour: "hod"
|
||||
day: "dní"
|
||||
_tutorial:
|
||||
title: "Ako používať FoundKey"
|
||||
step1_1: "Vitajte!"
|
||||
step1_2: "Táto stránka sa volá \"časová os\". Zobrazuje chronologicky zoradené \"\
|
||||
poznámky\" od ľudí, ktorých sledujete."
|
||||
step1_3: "Vaša časová os je teraz prázdna pretože ste nepridali žiadne poznámky\
|
||||
\ ani nikoho zatiaľ nesledujete."
|
||||
step2_1: "Podˇme dokončiť nastavenia vášho profilu pred napísaním poznámky alebo\
|
||||
\ sledovaním niekoho."
|
||||
step2_2: "Poskytnutím informácií o vás uľahčíte ostatným, či chcú vidieť alebo sledovať\
|
||||
\ vaše poznámky."
|
||||
step3_1: "Dokončili ste nastavovanie svojho profilu?"
|
||||
step3_2: "Poďme vyskúšať napísať poznámku. Môžete to spraviť stlačením ikony ceruzky\
|
||||
\ na vrchu obrazovky."
|
||||
step3_3: "Vyplňte polia a stlačte tlačítko vpravo hore."
|
||||
step3_4: "Nemáte čo povedať? Skúste \"len si nastavujem môj msky\"!"
|
||||
step4_1: "Napísali ste svoju prvú poznámku?"
|
||||
step4_2: "Hurá! Teraz by vaša prvá poznámka mala byť na vašej časovej osi."
|
||||
step5_1: "Teraz skúsme oživiť časovú os sledovaním nejakých ľudí."
|
||||
step5_2: "{featured} zobrazí populárne poznámku na tomto serveri. {explore} môžete\
|
||||
\ objavovať populárnych používateľov. Skúste tam nájsť ľudí, ktorých by ste radi\
|
||||
\ sledovali!"
|
||||
step5_3: "Ak chcete sledovať ďalších používateľov, kliknite na ich ikonu a stlačte\
|
||||
\ tlačidlo \"Sledovať\" na ich profile."
|
||||
step5_4: "Ak má niektorý používateľ ikonu zámku vedľa svojho mena, znamená to, že\
|
||||
\ môže trvať určitý čas, kým daný používateľ schváli vašu žiadosť o sledovanie."
|
||||
step6_1: "Teraz by ste mali vidieť poznámky ďalších používateľov na svojej časovej\
|
||||
\ osi."
|
||||
step6_2: "Môžete dať \"reakcie\" na poznámky ďalších ľudí ako rýchlu odpoveď."
|
||||
step6_3: "Reakciu pridáte kliknutím na \"+\" niekoho poznámke a vybratím emoji,\
|
||||
\ ktorou chcete reagovať."
|
||||
step7_1: "Gralujeme! Dokončili ste základného sprievodcu FoundKey."
|
||||
step7_2: "Ak sa chcete naučiť viac o FoundKey, skúste sekciu {help}."
|
||||
step7_3: "A teraz, veľa šťastia, bavte sa s FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Už ste zaregistrovali 2-faktorové autentifikačné zariadenie."
|
||||
registerDevice: "Registrovať nové zariadenie"
|
||||
|
|
|
@ -308,7 +308,6 @@ uploadFromUrl: Ladda upp via en URL
|
|||
uploadFromUrlDescription: URL till filen som du vill ladda upp
|
||||
uploadFromUrlRequested: Förfrågade uppladningar
|
||||
uploadFromUrlMayTakeTime: Det tar kanske ett tag innan uppladningen är färdig.
|
||||
explore: Upptäck
|
||||
messageRead: Läs
|
||||
noMoreHistory: Det finns ingen mer historik
|
||||
startMessaging: Inled en ny chatt
|
||||
|
|
|
@ -234,7 +234,6 @@ uploadFromUrl: "Завантажити з посилання"
|
|||
uploadFromUrlDescription: "Посилання на файл для завантаження"
|
||||
uploadFromUrlRequested: "Завантаження розпочалось"
|
||||
uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
|
||||
explore: "Огляд"
|
||||
messageRead: "Прочитано"
|
||||
noMoreHistory: "Подальшої історії немає"
|
||||
startMessaging: "Розпочати діалог"
|
||||
|
@ -314,9 +313,6 @@ inMb: "В мегабайтах"
|
|||
iconUrl: "URL аватара"
|
||||
bannerUrl: "URL банера"
|
||||
backgroundImageUrl: "URL-адреса фонового зображення"
|
||||
pinnedUsers: "Закріплені користувачі"
|
||||
pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці\
|
||||
\ \"Знайти\", ім'я в стовпчик."
|
||||
hcaptchaSiteKey: "Ключ сайту"
|
||||
hcaptchaSecretKey: "Секретний ключ"
|
||||
recaptchaSiteKey: "Ключ сайту"
|
||||
|
@ -341,11 +337,6 @@ silence: "Заглушити"
|
|||
silenceConfirm: "Ви впевнені, що хочете заглушити цього користувача?"
|
||||
unsilence: "Не глушити"
|
||||
unsilenceConfirm: "Ви впевнені, що хочете скасувати глушіння цього користувача?"
|
||||
popularUsers: "Популярні користувачі"
|
||||
recentlyUpdatedUsers: "Нещодавно активні користувачі"
|
||||
recentlyRegisteredUsers: "Нещодавно зареєстровані користувачі"
|
||||
recentlyDiscoveredUsers: "Нещодавно знайдені користувачі"
|
||||
popularTags: "Популярні теги"
|
||||
userList: "Списки"
|
||||
aboutMisskey: "Про FoundKey"
|
||||
administrator: "Адмін"
|
||||
|
@ -386,7 +377,6 @@ messagingWithGroup: "Чат з групою"
|
|||
title: "Тема"
|
||||
text: "Текст"
|
||||
enable: "Увімкнути"
|
||||
next: "Далі"
|
||||
retype: "Введіть ще раз"
|
||||
noteOf: "Нотатка {user}"
|
||||
inviteToGroup: "Запрошення до групи"
|
||||
|
@ -577,7 +567,6 @@ abuseReports: "Скарги"
|
|||
reportAbuse: "Поскаржитись"
|
||||
reportAbuseOf: "Поскаржитись на {name}"
|
||||
fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги."
|
||||
abuseReported: "Дякуємо, вашу скаргу було відправлено."
|
||||
reporter: "Репортер"
|
||||
reporteeOrigin: "Про кого повідомлено"
|
||||
reporterOrigin: "Хто повідомив"
|
||||
|
@ -815,40 +804,6 @@ _time:
|
|||
minute: "х"
|
||||
hour: "г"
|
||||
day: "д"
|
||||
_tutorial:
|
||||
title: "Як користуватись FoundKey"
|
||||
step1_1: "Ласкаво просимо!"
|
||||
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів\
|
||||
\ на яких ви підписані."
|
||||
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки\
|
||||
\ і не підписані на інших."
|
||||
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій\
|
||||
\ обліковий запис."
|
||||
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись\
|
||||
\ на вас."
|
||||
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення\
|
||||
\ олівця на екрані."
|
||||
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку\
|
||||
\ у верхньому правому куті форми."
|
||||
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
|
||||
step4_1: "Ви розмістили свій перший запис?"
|
||||
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
||||
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
||||
step5_2: "{featured} показує популярні записи , а {explore} популярних користувачів\
|
||||
\ з цього інстансу. Спробуйте підписатись на користувача, який вам сподобався!"
|
||||
step5_3: "Щоб підписатись на інших користувачів, нажміть на їхнє зображення, а потім\
|
||||
\ на кнопку \"підписатись\"."
|
||||
step5_4: "Якщо користувач має замок при імені, то йому потрібно буде вручну підтвердити\
|
||||
\ вашу заявку на підписку."
|
||||
step6_1: "Тепер ви повинні бачити записи інших користувачів на вашій стрічці подій."
|
||||
step6_2: "Також ви можете швидко відповісти, або \"відреагувати\" на записи інших\
|
||||
\ користувачів."
|
||||
step6_3: "Щоб \"відреагувати\", нажміть на знак плюс \"+\" на записі і виберіть\
|
||||
\ емоджі яким ви хочете \"відреагувати\"."
|
||||
step7_1: "Вітаю! Ви пройшли ознайомлення з FoundKey."
|
||||
step7_2: "Якщо ви хочете більше дізнатись про FoundKey, зайдіть в розділ {help}."
|
||||
step7_3: "Насолоджуйтесь FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
registerKey: "Зареєструвати новий ключ безпеки"
|
||||
_permissions:
|
||||
|
|
|
@ -230,7 +230,6 @@ uploadFromUrl: "Tải lên bằng một URL"
|
|||
uploadFromUrlDescription: "URL của tập tin bạn muốn tải lên"
|
||||
uploadFromUrlRequested: "Đã yêu cầu tải lên"
|
||||
uploadFromUrlMayTakeTime: "Sẽ mất một khoảng thời gian để tải lên xong."
|
||||
explore: "Khám phá"
|
||||
messageRead: "Đã đọc"
|
||||
noMoreHistory: "Không còn gì để đọc"
|
||||
startMessaging: "Bắt đầu trò chuyện"
|
||||
|
@ -312,9 +311,6 @@ inMb: "Tính bằng MB"
|
|||
iconUrl: "URL Icon"
|
||||
bannerUrl: "URL Ảnh bìa"
|
||||
backgroundImageUrl: "URL Ảnh nền"
|
||||
pinnedUsers: "Những người thú vị"
|
||||
pinnedUsersDescription: "Liệt kê mỗi hàng một tên người dùng xuống dòng để ghim trên\
|
||||
\ tab \"Khám phá\"."
|
||||
hcaptchaSiteKey: "Khóa của trang"
|
||||
hcaptchaSecretKey: "Khóa bí mật"
|
||||
recaptchaSiteKey: "Khóa của trang"
|
||||
|
@ -339,11 +335,6 @@ silence: "Ẩn"
|
|||
silenceConfirm: "Bạn có chắc muốn ẩn người này?"
|
||||
unsilence: "Bỏ ẩn"
|
||||
unsilenceConfirm: "Bạn có chắc muốn bỏ ẩn người này?"
|
||||
popularUsers: "Những người nổi tiếng"
|
||||
recentlyUpdatedUsers: "Hoạt động gần đây"
|
||||
recentlyRegisteredUsers: "Mới tham gia"
|
||||
recentlyDiscoveredUsers: "Mới khám phá"
|
||||
popularTags: "Hashtag thông dụng"
|
||||
userList: "Danh sách"
|
||||
aboutMisskey: "Về FoundKey"
|
||||
administrator: "Quản trị viên"
|
||||
|
@ -384,7 +375,6 @@ messagingWithGroup: "Chat nhóm"
|
|||
title: "Tựa đề"
|
||||
text: "Nội dung"
|
||||
enable: "Bật"
|
||||
next: "Kế tiếp"
|
||||
retype: "Nhập lại"
|
||||
noteOf: "Tút của {user}"
|
||||
inviteToGroup: "Mời vào nhóm"
|
||||
|
@ -575,7 +565,6 @@ abuseReports: "Lượt báo cáo"
|
|||
reportAbuse: "Báo cáo"
|
||||
reportAbuseOf: "Báo cáo {name}"
|
||||
fillAbuseReportDescription: "Vui lòng điền thông tin chi tiết về báo cáo này."
|
||||
abuseReported: "Báo cáo đã được gửi. Cảm ơn bạn nhiều."
|
||||
reporter: "Người báo cáo"
|
||||
reporteeOrigin: "Bị báo cáo"
|
||||
reporterOrigin: "Máy chủ người báo cáo"
|
||||
|
@ -966,43 +955,6 @@ _time:
|
|||
minute: "phút"
|
||||
hour: "giờ"
|
||||
day: "ngày"
|
||||
_tutorial:
|
||||
title: "Cách dùng FoundKey"
|
||||
step1_1: "Xin chào!"
|
||||
step1_2: "Trang này gọi là \"bảng tin\". Nó hiện \"tút\" từ những người mà bạn \"\
|
||||
theo dõi\" theo thứ tự thời gian."
|
||||
step1_3: "Bảng tin của bạn đang trống, bởi vì bạn chưa đăng tút nào hoặc chưa theo\
|
||||
\ dõi ai."
|
||||
step2_1: "Hãy hoàn thành việc thiết lập hồ sơ của bạn trước khi viết tút hoặc theo\
|
||||
\ dõi bất kỳ ai."
|
||||
step2_2: "Cung cấp một số thông tin giới thiệu bạn là ai sẽ giúp người khác dễ dàng\
|
||||
\ biết được họ muốn đọc tút hay theo dõi bạn."
|
||||
step3_1: "Hoàn thành thiết lập hồ sơ của bạn?"
|
||||
step3_2: "Sau đó, hãy thử đăng một tút tiếp theo. Bạn có thể làm như vậy bằng cách\
|
||||
\ nhấn vào nút có biểu tượng bút chì trên màn hình."
|
||||
step3_3: "Nhập nội dung vào khung soạn thảo và nhấn nút đăng ở góc trên."
|
||||
step3_4: "Chưa biết nói gì? Thử \"Tôi mới tham gia FoundKey\"!"
|
||||
step4_1: "Đăng xong tút đầu tiên của bạn?"
|
||||
step4_2: "De! Tút đầu tiên của bạn đã hiện trên bảng tin."
|
||||
step5_1: "Bây giờ, hãy thử làm cho bảng tin của bạn sinh động hơn bằng cách theo\
|
||||
\ dõi những người khác."
|
||||
step5_2: "{feature} sẽ hiển thị cho bạn các tút nổi bật trên máy chủ này. {explore}\
|
||||
\ sẽ cho phép bạn tìm thấy những người dùng thú vị. Hãy thử tìm những người bạn\
|
||||
\ muốn theo dõi ở đó!"
|
||||
step5_3: "Để theo dõi những người dùng khác, hãy nhấn vào ảnh đại diện của họ và\
|
||||
\ nhấn nút \"Theo dõi\" trên hồ sơ của họ."
|
||||
step5_4: "Nếu người dùng khác có biểu tượng ổ khóa bên cạnh tên của họ, có thể mất\
|
||||
\ một khoảng thời gian để người dùng đó phê duyệt yêu cầu theo dõi của bạn theo\
|
||||
\ cách thủ công."
|
||||
step6_1: "Bạn sẽ có thể xem tút của những người dùng khác trên bảng tin của mình\
|
||||
\ ngay bây giờ."
|
||||
step6_2: "Bạn cũng có thể đặt \"biểu cảm\" trên tút của người khác để phản hồi nhanh\
|
||||
\ chúng."
|
||||
step6_3: "Để đính kèm \"biểu cảm\", hãy nhấn vào dấu \"+\" trên tút của người dùng\
|
||||
\ khác rồi chọn biểu tượng cảm xúc mà bạn muốn dùng."
|
||||
step7_1: "Xin chúc mừng! Bây giờ bạn đã hoàn thành phần hướng dẫn cơ bản của FoundKey."
|
||||
step7_2: "Nếu bạn muốn tìm hiểu thêm về FoundKey, hãy thử phần {help}."
|
||||
step7_3: "Bây giờ, chúc may mắn và vui vẻ với FoundKey! \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "Bạn đã đăng ký thiết bị xác minh 2 bước."
|
||||
registerDevice: "Đăng ký một thiết bị"
|
||||
|
|
|
@ -211,7 +211,6 @@ uploadFromUrl: "从网址上传"
|
|||
uploadFromUrlDescription: "输入文件的URL"
|
||||
uploadFromUrlRequested: "请求上传"
|
||||
uploadFromUrlMayTakeTime: "上传可能需要一些时间完成。"
|
||||
explore: "发现"
|
||||
messageRead: "已读"
|
||||
noMoreHistory: "没有更多的历史记录"
|
||||
startMessaging: "添加聊天"
|
||||
|
@ -290,8 +289,6 @@ inMb: "以兆字节(MegaByte)为单位"
|
|||
iconUrl: "图标URL"
|
||||
bannerUrl: "横幅URL"
|
||||
backgroundImageUrl: "背景图URL"
|
||||
pinnedUsers: "置顶用户"
|
||||
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
|
||||
hcaptchaSiteKey: "网站密钥"
|
||||
hcaptchaSecretKey: "密钥"
|
||||
recaptchaSiteKey: "网站密钥"
|
||||
|
@ -315,11 +312,6 @@ silence: "禁言"
|
|||
silenceConfirm: "确认要禁言吗?"
|
||||
unsilence: "解除禁言"
|
||||
unsilenceConfirm: "要解除禁言吗?"
|
||||
popularUsers: "热门用户"
|
||||
recentlyUpdatedUsers: "最近投稿的用户"
|
||||
recentlyRegisteredUsers: "最近登录的用户"
|
||||
recentlyDiscoveredUsers: "最近发现的用户"
|
||||
popularTags: "热门标签"
|
||||
userList: "列表"
|
||||
aboutMisskey: "关于 FoundKey"
|
||||
administrator: "管理员"
|
||||
|
@ -360,7 +352,6 @@ messagingWithGroup: "与群组聊天"
|
|||
title: "标题"
|
||||
text: "文本"
|
||||
enable: "启用"
|
||||
next: "下一个"
|
||||
retype: "重新输入"
|
||||
noteOf: "{user}的帖子"
|
||||
inviteToGroup: "群组邀请"
|
||||
|
@ -535,7 +526,6 @@ abuseReports: "举报"
|
|||
reportAbuse: "举报"
|
||||
reportAbuseOf: "举报{name}"
|
||||
fillAbuseReportDescription: "请填写举报的详细原因。"
|
||||
abuseReported: "内容已发送。感谢您提交信息。"
|
||||
reporter: "举报者"
|
||||
reporteeOrigin: "举报来源"
|
||||
reporterOrigin: "举报者来源"
|
||||
|
@ -897,29 +887,6 @@ _time:
|
|||
minute: "分"
|
||||
hour: "小时"
|
||||
day: "日"
|
||||
_tutorial:
|
||||
title: "FoundKey的使用方法"
|
||||
step1_1: "欢迎!"
|
||||
step1_2: "这个页面叫做「时间线」,它会按照时间顺序显示所有你「关注」的人所发的「帖子」。"
|
||||
step1_3: "如果你并没有发布任何帖子,也没有关注其他的人,你的时间线页面应当什么都没有显示。"
|
||||
step2_1: "在您想要发帖或关注其他人之前,请先设置一下个人资料吧。"
|
||||
step2_2: "如果别人能够更加的了解你,关注你的概率也会得到提升。"
|
||||
step3_1: "已经设置完个人资料了吗?"
|
||||
step3_2: "那么接下来,试着写一些什么东西来发布吧。你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
|
||||
step3_3: "写完内容后,点击窗口右上方的按钮就可以投稿。"
|
||||
step3_4: "不知道说些什么好吗?那就写下「FoundKey我来啦!」这样的话吧。"
|
||||
step4_1: "将你的话语发布出去了吗?"
|
||||
step4_2: "太棒了!现在你可以在你的时间线中看到你刚刚发布的帖子了。"
|
||||
step5_1: "接下来,关注其他人来使时间线更生动吧。"
|
||||
step5_2: "{featured}将向您展示热门趋势的帖子。 {explore}将让您找到热门用户。 尝试关注您喜欢的人!"
|
||||
step5_3: "要关注其他用户,请单击他的头像,然后在他的个人资料上按下“关注”按钮。"
|
||||
step5_4: "如果用户的名称旁边有锁定图标,则该用户需要手动批准您的关注请求。"
|
||||
step6_1: "现在,您将可以在时间线上看到其他用户的帖子。"
|
||||
step6_2: "您还可以在其他人的帖子上进行「回应」,以快速做出简单回复。"
|
||||
step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「回应」。"
|
||||
step7_1: "对FoundKey基本操作的简单介绍,就到此结束了。 辛苦了!"
|
||||
step7_2: "如果你想了解更多有关FoundKey的信息,请参见{help}。"
|
||||
step7_3: "接下来,享受FoundKey带来的乐趣吧\U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
registerDevice: "注册设备"
|
||||
|
|
|
@ -211,7 +211,6 @@ uploadFromUrl: "從網址上傳"
|
|||
uploadFromUrlDescription: "您要上傳的文件的URL"
|
||||
uploadFromUrlRequested: "已請求上傳"
|
||||
uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
|
||||
explore: "探索"
|
||||
messageRead: "已讀"
|
||||
noMoreHistory: "沒有更多歷史紀錄"
|
||||
startMessaging: "開始傳送訊息"
|
||||
|
@ -290,8 +289,6 @@ inMb: "以Mbps為單位"
|
|||
iconUrl: "圖像URL"
|
||||
bannerUrl: "橫幅圖像URL"
|
||||
backgroundImageUrl: "背景圖片的來源網址"
|
||||
pinnedUsers: "置頂用戶"
|
||||
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
|
||||
hcaptchaSiteKey: "網站金鑰"
|
||||
hcaptchaSecretKey: "金鑰"
|
||||
recaptchaSiteKey: "網站金鑰"
|
||||
|
@ -315,11 +312,6 @@ silence: "禁言"
|
|||
silenceConfirm: "確定要禁言此用戶嗎?"
|
||||
unsilence: "解除禁言"
|
||||
unsilenceConfirm: "確定要解除禁言嗎?"
|
||||
popularUsers: "熱門使用者"
|
||||
recentlyUpdatedUsers: "最近發文的使用者"
|
||||
recentlyRegisteredUsers: "新加入使用者"
|
||||
recentlyDiscoveredUsers: "最近發現的使用者"
|
||||
popularTags: "熱門標籤"
|
||||
userList: "清單"
|
||||
aboutMisskey: "關於 FoundKey"
|
||||
administrator: "管理員"
|
||||
|
@ -360,7 +352,6 @@ messagingWithGroup: "發送訊息至群組"
|
|||
title: "標題"
|
||||
text: "文字"
|
||||
enable: "啟用"
|
||||
next: "下一步"
|
||||
retype: "重新輸入"
|
||||
noteOf: "{user}的貼文"
|
||||
inviteToGroup: "邀請至群組"
|
||||
|
@ -534,7 +525,6 @@ abuseReports: "檢舉"
|
|||
reportAbuse: "檢舉"
|
||||
reportAbuseOf: "檢舉{name}"
|
||||
fillAbuseReportDescription: "請填寫檢舉的詳細理由。"
|
||||
abuseReported: "回報已送出。感謝您的報告。"
|
||||
reporter: "檢舉者"
|
||||
reporteeOrigin: "檢舉來源"
|
||||
reporterOrigin: "檢舉者來源"
|
||||
|
@ -896,29 +886,6 @@ _time:
|
|||
minute: "分鐘"
|
||||
hour: "小時"
|
||||
day: "日"
|
||||
_tutorial:
|
||||
title: "FoundKey使用方法"
|
||||
step1_1: "歡迎!"
|
||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
|
||||
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
||||
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
||||
step3_1: "個人資料都設定好了嗎?"
|
||||
step3_2: "接下來,讓我們來試試看發個文,按一下畫面上的鉛筆圖示來開始"
|
||||
step3_3: "輸入完內容後,按視窗右上角的按鈕來發文"
|
||||
step3_4: "不知道該寫什麼內容嗎?試試看「開始使用FoundKey了」如何。"
|
||||
step4_1: "貼文發出去了嗎?"
|
||||
step4_2: "如果你的貼文出現在時間軸上,就代表發文成功。"
|
||||
step5_1: "現在試試看追隨其他人來讓你的時間軸變得更生動吧。"
|
||||
step5_2: "你會在{featured}上看到受歡迎的貼文,你也可以從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
|
||||
step5_3: "想要追隨其他人,只要點擊他們的大頭貼並按「追隨」即可。"
|
||||
step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。"
|
||||
step6_1: "現在你可以在時間軸上看到其他用戶的貼文。"
|
||||
step6_2: "你也可以對別人的貼文作出「情感」,作出簡單的回覆。"
|
||||
step6_3: "在他人的貼文按下\"+\"圖標,即可選擇喜好的表情符號進行回應。"
|
||||
step7_1: "以上為FoundKey的基本操作說明,教學在此告一段落。辛苦了。"
|
||||
step7_2: "歡迎到{help}來瞭解更多FoundKey相關介紹。"
|
||||
step7_3: "那麼,祝您在FoundKey玩的開心~ \U0001F680"
|
||||
_2fa:
|
||||
alreadyRegistered: "此設備已經被註冊過了"
|
||||
registerDevice: "註冊裝置"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "foundkey",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||
|
@ -36,7 +36,6 @@
|
|||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"argon2": "^0.30.2",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
|
|
|
@ -7,6 +7,14 @@ export class removeMentionedRemoteUsersColumn1661376843000 {
|
|||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`);
|
||||
await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`);
|
||||
await queryRunner.query(`CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS
|
||||
SELECT "id", "url", "uri", "username", "host"
|
||||
FROM "user"
|
||||
JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")`);
|
||||
await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (
|
||||
SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data"
|
||||
WHERE "data"."id" = ANY("note"."mentions")
|
||||
)`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
export class noteEditing1685997617959 {
|
||||
name = 'noteEditing1685997617959';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."updatedAt" IS 'The updated date of the Note.'`);
|
||||
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||
}
|
||||
}
|
43
packages/backend/migration/1689005520053-syncDatabase.js
Normal file
43
packages/backend/migration/1689005520053-syncDatabase.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
export class syncDatabase1689005520053 {
|
||||
name = 'syncDatabase1689005520053';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'How many delivery jobs are outstanding before the deletion is completed.'`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" RENAME TO "note_thread_muting_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update')`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."note_thread_muting_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'update', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."note_thread_muting_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "note_thread_muting" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."note_thread_muting_mutingnotificationtypes_enum_old" RENAME TO "note_thread_muting_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS NULL`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
|
||||
export class removePinnedUsers1704234742539 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedUsers"`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export class removeNoteVisibility1704236065406 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP COLUMN "noteVisibility"`);
|
||||
await queryRunner.query(`DROP TYPE "poll_notevisibility_enum"`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "poll_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD "noteVisibility" "poll_notevisibility_enum" NOT NULL`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export class removeHashtagChart1710687673333 {
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE "__chart__hashtag"`);
|
||||
await queryRunner.query(`DROP TABLE "__chart_day__hashtag"`);
|
||||
}
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE public.__chart__hashtag ("id" SERIAL NOT NULL CONSTRAINT "PK_c32f1ea2b44a5d2f7881e37f8f9" PRIMARY KEY,"date" integer NOT NULL,"group" character varying(128) NOT NULL,"___local_users" integer DEFAULT 0 NOT NULL,"___remote_users" integer DEFAULT 0 NOT NULL,"unique_temp___local_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,"unique_temp___remote_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,CONSTRAINT "UQ_25a97c02003338124b2b75fdbc8" UNIQUE ("date", "group"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON public.__chart__hashtag USING btree (date, "group")`);
|
||||
await queryRunner.query(`CREATE TABLE public.__chart_day__hashtag ("id" SERIAL NOT NULL CONSTRAINT CONSTRAINT "PK_13d5a3b089344e5557f8e0980b4" PRIMARY KEY,"date" integer NOT NULL,"group" character varying(128) NOT NULL,"___local_users" integer DEFAULT 0 NOT NULL,"___remote_users" integer DEFAULT 0 NOT NULL,"unique_temp___local_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,"unique_temp___remote_users" character varying[] DEFAULT '{}'::character varying[] NOT NULL,CONSTRAINT "UQ_8f589cf056ff51f09d6096f6450" UNIQUE ("date", "group"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8f589cf056ff51f09d6096f645" ON public.__chart_day__hashtag USING btree (date, "group")`);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"main": "./index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -9,7 +9,7 @@
|
|||
"watch": "node watch.mjs",
|
||||
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||
"mocha": "NODE_ENV=test mocha",
|
||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||
"migrate": "yarn exec typeorm migration:run -d ormconfig.js",
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"test": "npm run mocha"
|
||||
|
@ -28,6 +28,7 @@
|
|||
"abort-controller": "3.0.0",
|
||||
"ajv": "8.11.0",
|
||||
"archiver": "5.3.1",
|
||||
"argon2": "^0.30.2",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"aws-sdk": "2.1165.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
|
@ -35,12 +36,11 @@
|
|||
"bull": "4.8.4",
|
||||
"cacheable-lookup": "6.0.4",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.0.1",
|
||||
"chalk-template": "0.4.0",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.28.0",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
|
@ -51,6 +51,7 @@
|
|||
"hpagent": "0.1.2",
|
||||
"ioredis": "4.28.5",
|
||||
"ip-cidr": "3.0.10",
|
||||
"ipaddr.js": "2.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "20.0.0",
|
||||
|
@ -58,6 +59,7 @@
|
|||
"json5-loader": "4.0.1",
|
||||
"jsonld": "6.0.0",
|
||||
"jsrsasign": "10.5.25",
|
||||
"katex": "^0.16.0",
|
||||
"koa": "2.13.4",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-favicon": "2.1.0",
|
||||
|
@ -77,7 +79,6 @@
|
|||
"os-utils": "0.0.14",
|
||||
"parse5": "7.0.0",
|
||||
"pg": "8.7.3",
|
||||
"private-ip": "2.3.3",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
|
@ -109,7 +110,6 @@
|
|||
"tsconfig-paths": "4.1.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.7",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.5.0",
|
||||
"ws": "8.8.0",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import Xev from 'xev';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
|
@ -10,8 +9,8 @@ import 'reflect-metadata';
|
|||
import { masterMain } from './master.js';
|
||||
import { workerMain } from './worker.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||
const logger = new Logger('core');
|
||||
const clusterLogger = logger.createSubLogger('cluster');
|
||||
const ev = new Xev();
|
||||
|
||||
/**
|
||||
|
@ -57,14 +56,6 @@ cluster.on('online', worker => {
|
|||
clusterLogger.debug(`Process is now online: [${worker.id}]`);
|
||||
});
|
||||
|
||||
// Listen for dying workers
|
||||
cluster.on('exit', worker => {
|
||||
// Replace the dead worker,
|
||||
// we're not sentimental
|
||||
clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
|
||||
cluster.fork();
|
||||
});
|
||||
|
||||
// Display detail of unhandled promise rejection
|
||||
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
||||
process.on('unhandledRejection', console.dir);
|
||||
|
|
|
@ -3,8 +3,6 @@ import { fileURLToPath } from 'node:url';
|
|||
import { dirname } from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import semver from 'semver';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
|
@ -19,29 +17,27 @@ const _dirname = dirname(_filename);
|
|||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
|
||||
const themeColor = chalk.hex('#86b300');
|
||||
const logger = new Logger('core');
|
||||
const bootLogger = logger.createSubLogger('boot');
|
||||
|
||||
function greet(): void {
|
||||
if (envOption.logLevel !== LOG_LEVELS.quiet) {
|
||||
//#region FoundKey logo
|
||||
console.log(themeColor(' ___ _ _ __ '));
|
||||
console.log(themeColor(' | __|__ _ _ _ _ __| | |/ /___ _ _ '));
|
||||
console.log(themeColor(' | _/ _ \\ || | \' \\/ _` | \' </ -_) || |'));
|
||||
console.log(themeColor(' |_|\\___/\\_,_|_||_\\__,_|_|\\_\\___|\\_, |'));
|
||||
console.log(themeColor(' |__/ '));
|
||||
console.log(' ___ _ _ __ ');
|
||||
console.log(' | __|__ _ _ _ _ __| | |/ /___ _ _ ');
|
||||
console.log(' | _/ _ \\ || | \' \\/ _` | \' </ -_) || |');
|
||||
console.log(' |_|\\___/\\_,_|_||_\\__,_|_|\\_\\___|\\_, |');
|
||||
console.log(' |__/ ');
|
||||
//#endregion
|
||||
|
||||
console.log(' FoundKey is an open-source decentralized microblogging platform.');
|
||||
|
||||
console.log('');
|
||||
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
|
||||
console.log(`--- ${os.hostname()} (PID: ${process.pid.toString()}) ---`);
|
||||
}
|
||||
|
||||
bootLogger.info('Welcome to FoundKey!');
|
||||
bootLogger.info(`FoundKey v${meta.version}`, true);
|
||||
bootLogger.info(`FoundKey v${meta.version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +55,7 @@ export async function masterMain(): Promise<void> {
|
|||
config = loadConfigBoot();
|
||||
await connectDb();
|
||||
} catch (e) {
|
||||
bootLogger.error('Fatal error occurred during initialization', true);
|
||||
bootLogger.error('Fatal error occurred during initialization');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@ -69,7 +65,7 @@ export async function masterMain(): Promise<void> {
|
|||
await spawnWorkers(config.clusterLimits);
|
||||
}
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, true);
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`);
|
||||
|
||||
if (!envOption.noDaemons) {
|
||||
import('../daemons/server-stats.js').then(x => x.serverStats());
|
||||
|
@ -84,7 +80,7 @@ function showEnvironment(): void {
|
|||
|
||||
if (env !== 'production') {
|
||||
logger.warn('The environment is not in production mode.');
|
||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', true);
|
||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +105,7 @@ function loadConfigBoot(): Config {
|
|||
} catch (exception) {
|
||||
const e = exception as Partial<NodeJS.ErrnoException> | Error;
|
||||
if ('code' in e && e.code === 'ENOENT') {
|
||||
configLogger.error('Configuration file not found', true);
|
||||
configLogger.error('Configuration file not found');
|
||||
process.exit(1);
|
||||
} else if (e instanceof Error) {
|
||||
configLogger.error(e.message);
|
||||
|
@ -133,7 +129,7 @@ async function connectDb(): Promise<void> {
|
|||
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
|
||||
dbLogger.succ(`Connected: v${v}`);
|
||||
} catch (e) {
|
||||
dbLogger.error('Cannot connect', true);
|
||||
dbLogger.error('Cannot connect');
|
||||
dbLogger.error(e as Error | string);
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -168,6 +164,10 @@ async function spawnWorkers(clusterLimits: Required<Config['clusterLimits']>): P
|
|||
function spawnWorker(mode: 'web' | 'queue'): Promise<void> {
|
||||
return new Promise(res => {
|
||||
const worker = cluster.fork({ mode });
|
||||
worker.on('exit', async (code, signal) => {
|
||||
logger.error(mode + ' worker died, restarting...');
|
||||
await spawnWorker(mode);
|
||||
});
|
||||
worker.on('message', message => {
|
||||
switch (message) {
|
||||
case 'listenFailed':
|
||||
|
|
|
@ -61,6 +61,7 @@ export function loadConfig(): Config {
|
|||
proxyRemoteFiles: false,
|
||||
maxFileSize: 262144000, // 250 MiB
|
||||
maxNoteTextLength: 3000,
|
||||
allowUnsignedFetches: false,
|
||||
}, config);
|
||||
|
||||
mixin.version = meta.version;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Logger from '@/services/logger.js';
|
||||
import config from './index.js';
|
||||
|
||||
const logger = new Logger('config:redis', 'gray', false);
|
||||
const logger = new Logger('config:redis');
|
||||
|
||||
function getRedisFamily(family?: string | number): number {
|
||||
const familyMap = {
|
||||
|
|
|
@ -68,6 +68,8 @@ export type Source = {
|
|||
notFound?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
allowUnsignedFetches?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -72,7 +72,7 @@ import { getRedisOptions } from '@/config/redis.js';
|
|||
import { dbLogger } from './logger.js';
|
||||
import { redisClient } from './redis.js';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
const sqlLogger = dbLogger.createSubLogger('sql');
|
||||
|
||||
class MyCustomLogger implements Logger {
|
||||
private highlight(sql: string): string {
|
||||
|
|
|
@ -24,7 +24,9 @@ for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
|
|||
if (value.toLowerCase() in LOG_LEVELS) {
|
||||
envOption.logLevel = LOG_LEVELS[value.toLowerCase()];
|
||||
}
|
||||
console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"');
|
||||
else {
|
||||
console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"');
|
||||
}
|
||||
} else {
|
||||
envOption[key] = true;
|
||||
}
|
||||
|
|
|
@ -68,14 +68,16 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
|||
|
||||
case 'a':
|
||||
{
|
||||
const txt = getText(node);
|
||||
// trim spaces away, because some AP servers (app.wafrn.net) send strange
|
||||
// zero width non-break space in strange places and things like that
|
||||
const txt = getText(node).trim();
|
||||
const href = getAttr(node, 'href');
|
||||
|
||||
// hashtags
|
||||
if (txt.startsWith('#') && href && (attrHas(node, 'rel', 'tag') || attrHas(node, 'class', 'hashtag'))) {
|
||||
text += txt;
|
||||
// mentions
|
||||
} else if (txt.startsWith('@') && !attrHas(node, 'rel', 'me')) {
|
||||
// mentions: a link that starts with `@` and does not include space
|
||||
} else if (txt.startsWith('@') && txt.match(/\s/) == null && !attrHas(node, 'rel', 'me')) {
|
||||
const part = txt.split('@');
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
|
@ -176,6 +178,53 @@ export function fromHtml(html: string, quoteUri?: string | null): string {
|
|||
break;
|
||||
}
|
||||
|
||||
// inline or block KaTeX
|
||||
case 'math': {
|
||||
// This node should contain <semantics>[...]<annotation/>[...]</semantics> tag with the "source code".
|
||||
if (node.childNodes.length !== 1 || node.childNodes[0].nodeName !== 'semantics')
|
||||
break;
|
||||
const semantics = node.childNodes[0];
|
||||
|
||||
// only select well formed annotations
|
||||
const annotations = semantics.childNodes
|
||||
.filter(node =>
|
||||
node.nodeName === 'annotation'
|
||||
&& node.childNodes.length === 1
|
||||
&& node.childNodes[0].nodeName === '#text'
|
||||
);
|
||||
if (annotations.length === 0)
|
||||
break;
|
||||
|
||||
let annotation = annotations[0];
|
||||
// try to prefer a TeX annotation if there are multiple annotations
|
||||
const filteredAnnotations = annotations.filter(node => node.attrs.some(attribute => attribute.name === 'encoding' && attribute.value === 'application/x-tex'));
|
||||
if (filteredAnnotations.length > 0) {
|
||||
annotation = filteredAnnotations[0];
|
||||
}
|
||||
|
||||
const formula = annotation.childNodes[0].value;
|
||||
if (annotation.attrs.some(attribute => attribute.name === 'encoding' && attribute.value === 'application/x-tex')) {
|
||||
// can be rendered as KaTeX, now decide if it is possible to render as inline or not
|
||||
if (/[\r\n]/.test(formula)) {
|
||||
// line break, this must be rendered as a block
|
||||
text += '\n\\[' + formula + '\\]\n';
|
||||
} else {
|
||||
// render as inline
|
||||
text += '\\(' + formula + '\\)';
|
||||
}
|
||||
} else {
|
||||
// not KaTeX, but if there is a plaintext annotation it can still be rendered as code
|
||||
if (/[\r\n]/.test(formula)) {
|
||||
// line break, this must be rendered as a block
|
||||
text += '\n```\n' + formula + '\n```\n';
|
||||
} else {
|
||||
// render as inline
|
||||
text += '`' + formula + '`';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blockquote': {
|
||||
const t = getText(node);
|
||||
if (t) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { JSDOM } from 'jsdom';
|
||||
import katex from 'katex';
|
||||
import * as mfm from 'mfm-js';
|
||||
import config from '@/config/index.js';
|
||||
import { UserProfiles } from '@/models/index.js';
|
||||
|
@ -6,6 +7,14 @@ import { extractMentions } from '@/misc/extract-mentions.js';
|
|||
import { intersperse } from '@/prelude/array.js';
|
||||
import { toPunyNullable } from '@/misc/convert-host.js';
|
||||
|
||||
function toMathMl(code: string): HTMLElement {
|
||||
const rendered = katex.renderToString(code, {
|
||||
throwOnError: false,
|
||||
output: 'mathml',
|
||||
});
|
||||
return JSDOM.fragment(rendered).querySelector('math');
|
||||
}
|
||||
|
||||
// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
|
||||
// mentioned in the text. If the list of mentions is not given, all mentions
|
||||
// from the text will be extracted.
|
||||
|
@ -98,15 +107,11 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise<stri
|
|||
},
|
||||
|
||||
async mathInline(node) {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
return toMathMl(node.props.formula);
|
||||
},
|
||||
|
||||
async mathBlock(node) {
|
||||
const el = doc.createElement('code');
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
return toMathMl(node.props.formula);
|
||||
},
|
||||
|
||||
async link(node) {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
export const kinds = [
|
||||
'read:account',
|
||||
'write:account',
|
||||
'read:blocks',
|
||||
'write:blocks',
|
||||
'read:drive',
|
||||
'write:drive',
|
||||
'read:following',
|
||||
'write:following',
|
||||
'read:messaging',
|
||||
'write:messaging',
|
||||
'read:mutes',
|
||||
'write:mutes',
|
||||
'write:notes',
|
||||
'read:notifications',
|
||||
'write:notifications',
|
||||
'read:reactions',
|
||||
'write:reactions',
|
||||
'write:votes',
|
||||
'read:pages',
|
||||
'write:pages',
|
||||
'write:page-likes',
|
||||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
];
|
||||
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
|
|
@ -1,12 +1,23 @@
|
|||
export class Cache<T> {
|
||||
// The actual "database" that holds the cache entries, along with their
|
||||
// insertion time.
|
||||
// Insertion order is the same as the order of elements expiring. This is
|
||||
// important because the expiration logic relies on the insertion order.
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
// The lifetime of each cache member.
|
||||
//
|
||||
// This must not be changed after setup because it may upset
|
||||
// the expiration logic.
|
||||
private lifetime: number;
|
||||
// Function of which the results should be cached.
|
||||
public fetcher: (key: string) => Promise<T | undefined>;
|
||||
private timeoutScheduled: boolean;
|
||||
|
||||
constructor(lifetime: number, fetcher: Cache<T>['fetcher']) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
this.fetcher = fetcher;
|
||||
this.timeoutScheduled = false;
|
||||
}
|
||||
|
||||
public set(key: string, value: T): void {
|
||||
|
@ -14,38 +25,36 @@ export class Cache<T> {
|
|||
date: Date.now(),
|
||||
value,
|
||||
});
|
||||
|
||||
// make sure the expiration timeout is in place
|
||||
this.expire();
|
||||
}
|
||||
|
||||
public get(key: string): T | undefined {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached == null) return undefined;
|
||||
|
||||
// discard if past the cache lifetime
|
||||
if ((Date.now() - cached.date) > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cached.value;
|
||||
else return cached.value;
|
||||
}
|
||||
|
||||
public delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the value is cached, it is returned. Otherwise the fetcher is
|
||||
* run to get the value. If the fetcher returns undefined, it is
|
||||
* returned but not cached.
|
||||
*/
|
||||
// If the value is cached, it is returned. Otherwise the fetcher is
|
||||
// run to get the value. If the fetcher returns undefined, it is
|
||||
// returned but not cached.
|
||||
public async fetch(key: string): Promise<T | undefined> {
|
||||
// Check if this value is cached
|
||||
const cached = this.get(key);
|
||||
if (cached !== undefined) {
|
||||
// The value was cached, return it.
|
||||
return cached;
|
||||
} else {
|
||||
// The value was not cached, need to call the original function
|
||||
// to get its result and then cache it.
|
||||
const value = await this.fetcher(key);
|
||||
|
||||
// don't cache undefined
|
||||
// `undefined` is not cached
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
}
|
||||
|
@ -53,4 +62,43 @@ export class Cache<T> {
|
|||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the expiration of cached values.
|
||||
// This is done using a timeout.
|
||||
private expire(): void {
|
||||
// If there already is a timeout scheduled, it will be appropriate
|
||||
// for the first inserted element of the cache.
|
||||
// If the first element of the cache was removed, it will reschedule
|
||||
// to the appropriate time when it runs out.
|
||||
//
|
||||
// If the cache is empty, there is nothing to expire either.
|
||||
if (this.timeoutScheduled) return;
|
||||
// Otherwise, this must mean this is the previously scheduled timeout.
|
||||
// Since it is running now, it is no longer scheduled.
|
||||
this.timeoutScheduled = false;
|
||||
|
||||
// Check if the first element is actually due for expiration.
|
||||
//
|
||||
// Items may have been removed in the meantime or this may be
|
||||
// the initial call for the first key inserted into the cache.
|
||||
const [expiredKey, expiredValue] = this.cache.entries().next().value;
|
||||
if (expiredValue.date + this.lifetime <= Date.now()) {
|
||||
// This item is due for expiration, so remove it.
|
||||
this.cache.delete(expiredKey);
|
||||
}
|
||||
|
||||
// If there are no further elements in the cache, there is nothing to
|
||||
// expire at a later time. The timeout will be set up again later by
|
||||
// a call from `this.set`.
|
||||
if (this.cache.size === 0) return;
|
||||
|
||||
// Check when the next key is due for removal and schedule
|
||||
// an appropriate timeout.
|
||||
const [nextKey, nextValue] = this.cache.entries().next().value;
|
||||
setTimeout(
|
||||
() => this.expire(),
|
||||
nextValue.date + this.lifetime - Date.now()
|
||||
);
|
||||
this.timeoutScheduled = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { URLSearchParams } from 'node:url';
|
||||
import fetch from 'node-fetch';
|
||||
import { getResponse } from '@/misc/fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import { getAgentByUrl } from './fetch.js';
|
||||
|
||||
export async function verifyRecaptcha(secret: string, response: string): Promise<void> {
|
||||
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
|
||||
|
@ -36,15 +35,10 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
|
|||
response,
|
||||
});
|
||||
|
||||
const res = await fetch(url, {
|
||||
const res = await getResponse({
|
||||
url,
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'User-Agent': config.userAgent,
|
||||
},
|
||||
// TODO
|
||||
//timeout: 10 * 1000,
|
||||
agent: getAgentByUrl,
|
||||
}).catch(e => {
|
||||
throw new Error(`${e.message || e}`);
|
||||
});
|
||||
|
|
|
@ -13,11 +13,24 @@ type UserLike = {
|
|||
};
|
||||
|
||||
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
|
||||
// 自分自身
|
||||
// own posts
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
const text = [
|
||||
note.cw,
|
||||
note.text,
|
||||
...note.files.map(file => file.comment)
|
||||
]
|
||||
.map(x => {
|
||||
if (x == null || x.trim() == '') {
|
||||
return null;
|
||||
} else {
|
||||
return x.trim();
|
||||
}
|
||||
})
|
||||
.filter(x => x != null)
|
||||
.join('\n');
|
||||
|
||||
if (text === '') return false;
|
||||
const textLower = text.toLowerCase();
|
||||
|
|
|
@ -11,7 +11,7 @@ export function isSelfHost(host: string | null): boolean {
|
|||
return toPuny(config.host) === toPuny(host);
|
||||
}
|
||||
|
||||
export function extractDbHost(uri: string): string {
|
||||
export function extractPunyHost(uri: string): string {
|
||||
const url = new URL(uri);
|
||||
return toPuny(url.hostname);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream';
|
||||
import * as util from 'node:util';
|
||||
import chalk from 'chalk';
|
||||
import got, * as Got from 'got';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import PrivateIp from 'private-ip';
|
||||
import { SECOND, MINUTE } from '@/const.js';
|
||||
import config from '@/config/index.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
|
@ -15,7 +12,7 @@ const pipeline = util.promisify(stream.pipeline);
|
|||
export async function downloadUrl(url: string, path: string): Promise<void> {
|
||||
const logger = new Logger('download');
|
||||
|
||||
logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||
logger.info(`Downloading ${url} ...`);
|
||||
|
||||
const timeout = 30 * SECOND;
|
||||
const operationTimeout = MINUTE;
|
||||
|
@ -42,13 +39,6 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
limit: 0,
|
||||
},
|
||||
}).on('response', (res: Got.Response) => {
|
||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
|
||||
if (isPrivateIp(res.ip)) {
|
||||
logger.warn(`Blocked address: ${res.ip}`);
|
||||
req.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const contentLength = res.headers['content-length'];
|
||||
if (contentLength != null) {
|
||||
const size = Number(contentLength);
|
||||
|
@ -74,16 +64,5 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||
}
|
||||
|
||||
function isPrivateIp(ip: string): boolean {
|
||||
for (const net of config.allowedPrivateNetworks || []) {
|
||||
const cidr = new IPCIDR(net);
|
||||
if (cidr.contains(ip)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return PrivateIp(ip);
|
||||
logger.succ(`Download finished: ${url}`);
|
||||
}
|
||||
|
|
|
@ -1,57 +1,67 @@
|
|||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { URL } from 'node:url';
|
||||
import * as dns from 'node:dns';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import { SECOND } from '@/const.js';
|
||||
import config from '@/config/index.js';
|
||||
|
||||
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10 * SECOND, headers?: Record<string, string>) {
|
||||
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10 * SECOND, headers: Record<string, string> = {}) {
|
||||
const res = await getResponse({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers || {}),
|
||||
}, headers),
|
||||
timeout,
|
||||
});
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10 * SECOND, headers?: Record<string, string>) {
|
||||
export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10 * SECOND, headers: Record<string, string> = {}) {
|
||||
const res = await getResponse({
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: accept,
|
||||
}, headers || {}),
|
||||
}, headers),
|
||||
timeout,
|
||||
});
|
||||
|
||||
return await res.text();
|
||||
}
|
||||
|
||||
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number, redirect: 'follow' | 'manual' | 'error' = 'follow' }) {
|
||||
const timeout = args.timeout || 10 * SECOND;
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeout * 6);
|
||||
export async function getResponse(_args: {
|
||||
url: string,
|
||||
method: string,
|
||||
body?: string,
|
||||
headers: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
redirect?: 'follow' | 'manual' | 'error',
|
||||
}) {
|
||||
const args = {
|
||||
timeout: 10 * SECOND,
|
||||
size: 10 * 1024 * 1024, // 10 MiB
|
||||
redirect: 'follow',
|
||||
..._args,
|
||||
};
|
||||
|
||||
const res = await fetch(args.url, {
|
||||
method: args.method,
|
||||
headers: args.headers,
|
||||
headers: Object.assign({
|
||||
'User-Agent': config.userAgent,
|
||||
}, args.headers),
|
||||
body: args.body,
|
||||
redirect: args.redirect,
|
||||
timeout,
|
||||
size: args.size || 10 * 1024 * 1024, // 10 MiB
|
||||
size: args.size,
|
||||
agent: getAgentByUrl,
|
||||
signal: controller.signal,
|
||||
signal: AbortSignal.timeout(args.timeout),
|
||||
});
|
||||
|
||||
if (
|
||||
|
@ -71,13 +81,63 @@ const cache = new CacheableLookup({
|
|||
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||
});
|
||||
|
||||
// because `cacheable-lookup` is annoying and prefers IPv4 by default,
|
||||
// we need a little wrapper setting some extra options
|
||||
const cacheLookup = (host, _2, _3) => {
|
||||
// do all the weird parameter shenanigans that nodejs's dns.lookup does.
|
||||
let options = {}, callback;
|
||||
if (_3) {
|
||||
options = _2;
|
||||
if (typeof options === 'number') {
|
||||
options = { family: options };
|
||||
}
|
||||
callback = _3;
|
||||
} else {
|
||||
callback = _2;
|
||||
}
|
||||
|
||||
// here come the shenanigans, trying to be careful not to mess up
|
||||
// intentionally different behaviour
|
||||
if (options.family == null && (options.hints ?? 0) === 0) {
|
||||
options.family = 6;
|
||||
options.hints = dns.V4MAPPED;
|
||||
}
|
||||
|
||||
// callback wrapper that checks whether an IP is private.
|
||||
// Private IPs will not be returned in the first place.
|
||||
const wrapper = (err, address, family) => {
|
||||
// mimics dns.lookup behaviour as if no result was found
|
||||
const fakeErr = new Error("private IP");
|
||||
fakeErr.code = 'ENOTFOUND';
|
||||
|
||||
if (err != null) {
|
||||
return callback(err, address, family);
|
||||
} else if (options.all) {
|
||||
const results = address.filter(({ address, family }) => isPublicIp(address));
|
||||
if (results.length === 0) {
|
||||
return callback(fakeErr);
|
||||
} else {
|
||||
return callback(err, results);
|
||||
}
|
||||
} else {
|
||||
if (isPublicIp(address)) {
|
||||
return callback(err, address, family);
|
||||
} else {
|
||||
return callback(fakeErr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return cache.lookup(host, options, wrapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
const _http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * SECOND,
|
||||
lookup: cache.lookup,
|
||||
lookup: cacheLookup,
|
||||
} as http.AgentOptions);
|
||||
|
||||
/**
|
||||
|
@ -86,7 +146,7 @@ const _http = new http.Agent({
|
|||
const _https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * SECOND,
|
||||
lookup: cache.lookup,
|
||||
lookup: cacheLookup,
|
||||
} as https.AgentOptions);
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency);
|
||||
|
@ -145,3 +205,27 @@ export class StatusError extends Error {
|
|||
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
||||
}
|
||||
}
|
||||
|
||||
function isPublicIp(ip: string): boolean {
|
||||
for (const net of config.allowedPrivateNetworks || []) {
|
||||
const cidr = new IPCIDR(net);
|
||||
if (cidr.contains(ip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround because ipaddr.js in the installed version cannot cope
|
||||
// with v4mapped addresses. a fix has been submitted and merged, but a new
|
||||
// version was not yet released.
|
||||
// FIXME: update ipaddr.js and remove workaround
|
||||
ip = ip.replace(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i, '$1');
|
||||
|
||||
try {
|
||||
let range = ipaddr.process(ip).range();
|
||||
// only unicast or multicast addresses are allowed by default
|
||||
// this does not include e.g. loopback addresses
|
||||
return ['unicast', 'multicast'].includes(range);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,14 +37,5 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
|||
}
|
||||
}
|
||||
|
||||
// Renoteのとき
|
||||
if (note.renoteId) {
|
||||
if (note.renote) {
|
||||
summary += `\n\nRN: ${getNoteSummary(note.renote)}`;
|
||||
} else {
|
||||
summary += '\n\nRN: ...';
|
||||
}
|
||||
}
|
||||
|
||||
return summary.trim();
|
||||
};
|
||||
|
|
|
@ -2,9 +2,10 @@ import { UserKeypairs } from '@/models/index.js';
|
|||
import { User } from '@/models/entities/user.js';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||
import { Cache } from './cache.js';
|
||||
import { MINUTE } from '@/const.js';
|
||||
|
||||
const cache = new Cache<UserKeypair>(
|
||||
Infinity,
|
||||
15 * MINUTE,
|
||||
(userId) => UserKeypairs.findOneByOrFail({ userId }),
|
||||
);
|
||||
|
||||
|
|
|
@ -44,12 +44,10 @@ function normalizeHost(src: string | undefined, noteUserHost: string | null): st
|
|||
}
|
||||
|
||||
function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
const name = match[1];
|
||||
|
||||
const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
|
||||
// emojiName may be of the form `emoji@host`, turn it into a suitable form
|
||||
const match = emojiName.split("@");
|
||||
const name = match[0];
|
||||
const host = toPunyNullable(normalizeHost(match[1], noteUserHost));
|
||||
|
||||
return { name, host };
|
||||
}
|
||||
|
|
|
@ -67,11 +67,6 @@ export class Meta {
|
|||
})
|
||||
public langs: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
public pinnedUsers: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
|
|
|
@ -19,6 +19,12 @@ export class Note {
|
|||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The updated date of the Note.',
|
||||
})
|
||||
public updatedAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -34,12 +34,6 @@ export class Poll {
|
|||
public votes: number[];
|
||||
|
||||
//#region Denormalized fields
|
||||
@Column('enum', {
|
||||
enum: noteVisibilities,
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteVisibility: typeof noteVisibilities[number];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -169,6 +169,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
updatedAt: note.updatedAt?.toISOString() ?? null,
|
||||
userId: note.userId,
|
||||
user: Users.pack(note.user ?? note.userId, me, {
|
||||
detail: false,
|
||||
|
|
|
@ -8,14 +8,10 @@ import { populateEmojis } from '@/misc/populate-emojis.js';
|
|||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import { Instance } from '../entities/instance.js';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||
|
||||
const userInstanceCache = new Cache<Instance | null>(
|
||||
3 * HOUR,
|
||||
(host) => Instances.findOneBy({ host }).then(x => x ?? undefined),
|
||||
);
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||
Detailed extends true ?
|
||||
|
@ -319,7 +315,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isModerator: user.isModerator,
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
instance: !user.host ? undefined : userInstanceCache.fetch(user.host)
|
||||
instance: !user.host ? undefined : registerOrFetchInstanceDoc(user.host)
|
||||
.then(instance => !instance ? undefined : {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
|
|
|
@ -12,6 +12,11 @@ export const packedNoteSchema = {
|
|||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -58,8 +58,16 @@ deliverQueue
|
|||
await deletionRefCount(job);
|
||||
})
|
||||
.on('failed', async (job, err) => {
|
||||
deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
||||
await deletionRefCount(job);
|
||||
if (err.type === 'aborted') {
|
||||
deliverLogger.debug(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
||||
} else {
|
||||
deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`);
|
||||
}
|
||||
|
||||
if (job.attemptsMade >= (job.opts?.attempts ?? 1)) {
|
||||
// this was the last attempt
|
||||
await deletionRefCount(job);
|
||||
}
|
||||
})
|
||||
.on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`))
|
||||
.on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
|
|
@ -23,6 +23,19 @@ export function initialize<T>(name: string, limitPerSec = -1): Bull.Queue<T> {
|
|||
function apBackoff(attemptsMade: number /*, err: Error */): number {
|
||||
const baseDelay = MINUTE;
|
||||
const maxBackoff = 8 * HOUR;
|
||||
/*
|
||||
attempt | average seconds + up to 2% random offset
|
||||
0 | 0
|
||||
1 | 60 = 1min
|
||||
2 | 180 = 3min
|
||||
3 | 420 = 7min
|
||||
4 | 900 = 15min
|
||||
5 | 1860 = 31min
|
||||
6 | 3780 = 63min
|
||||
7 | 7620 = 127min ~= 2.1h
|
||||
8 | 15300 = 4.25h
|
||||
>8 | 28800 = 8h
|
||||
*/
|
||||
let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay;
|
||||
backoff = Math.min(backoff, maxBackoff);
|
||||
backoff += Math.round(backoff * Math.random() * 0.2);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Logger from '@/services/logger.js';
|
||||
|
||||
export const queueLogger = new Logger('queue', 'orange');
|
||||
export const queueLogger = new Logger('queue');
|
||||
|
|
|
@ -58,12 +58,10 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
|||
});
|
||||
|
||||
for (const emoji of customEmojis) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
|
||||
this.logger.error(`invalid emoji name: ${emoji.name}, skipping in emoji export`);
|
||||
continue;
|
||||
}
|
||||
const ext = mime.extension(emoji.type);
|
||||
const fileName = emoji.name + (ext ? '.' + ext : '');
|
||||
// there are some restrictions on file names, so to be safe the files are
|
||||
// named after their database id instead of the actual emoji name
|
||||
const fileName = emoji.id + (ext ? '.' + ext : '');
|
||||
const emojiPath = path + '/' + fileName;
|
||||
fs.writeFileSync(emojiPath, '', 'binary');
|
||||
let downloaded = false;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as fs from 'node:fs';
|
||||
import Bull from 'bull';
|
||||
import unzipper from 'unzipper';
|
||||
import decompress from 'decompress';
|
||||
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { DriveFiles, Emojis } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { copyFileTo } from '@/services/drive/read-file.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-custom-emojis');
|
||||
|
||||
|
@ -33,7 +33,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
|
||||
try {
|
||||
fs.writeFileSync(destPath, '', 'binary');
|
||||
await downloadUrl(file.url, destPath);
|
||||
await copyFileTo(file, destPath);
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
if (e instanceof Error || typeof e === 'string') {
|
||||
logger.error(e);
|
||||
|
@ -42,44 +42,41 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
}
|
||||
|
||||
const outputPath = path + '/emojis';
|
||||
const unzipStream = fs.createReadStream(destPath);
|
||||
const extractor = unzipper.Extract({ path: outputPath });
|
||||
extractor.on('close', async () => {
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
});
|
||||
unzipStream.pipe(extractor);
|
||||
logger.succ(`Unzipping to ${outputPath}`);
|
||||
await decompress(destPath, outputPath);
|
||||
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -2,29 +2,40 @@ import { URL } from 'node:url';
|
|||
import Bull from 'bull';
|
||||
import { request } from '@/remote/activitypub/request.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
import { toPuny } from '@/misc/convert-host.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import { shouldSkipInstance } from '@/misc/skipped-instances.js';
|
||||
import { DeliverJobData } from '@/queue/types.js';
|
||||
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||
const { host } = new URL(job.data.to);
|
||||
const puny = toPuny(host);
|
||||
|
||||
if (await shouldSkipInstance(puny)) return 'skip';
|
||||
// for the first few tries (where most attempts will be made)
|
||||
// we assume that inserting deliver jobs took care of this check
|
||||
// only on later attempts do we actually do it, to ease database
|
||||
// performance. this might cause a slight delay of a few minutes
|
||||
// for instance blocks being applied
|
||||
//
|
||||
// with apBackoff, attempt 2 happens ~4min after the initial try, while
|
||||
// attempt 3 happens ~11 min after the initial try, which seems like a
|
||||
// good tradeoff between database and blocks being applied reasonably quick
|
||||
if (job.attemptsMade >= 3 && await shouldSkipInstance(puny)) {
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
const keypair = await getUserKeypair(job.data.user.id);
|
||||
|
||||
try {
|
||||
if (Array.isArray(job.data.content)) {
|
||||
await Promise.all(
|
||||
job.data.content.map(x => request(job.data.user, job.data.to, x))
|
||||
job.data.content.map(x => request(job.data.user, job.data.to, x, keypair))
|
||||
);
|
||||
} else {
|
||||
await request(job.data.user, job.data.to, job.data.content);
|
||||
await request(job.data.user, job.data.to, job.data.content, keypair);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { perform } from '@/remote/activitypub/perform.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||
import { toPuny, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
|
||||
import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { AuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { InboxJobData } from '@/queue/types.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
||||
// Processing when an activity arrives in the user's inbox
|
||||
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
const resolver = new Resolver();
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity) as any;
|
||||
|
@ -29,93 +28,29 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
logger.debug(JSON.stringify(info, null, 2));
|
||||
//#endregion
|
||||
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||
}
|
||||
|
||||
const host = toPuny(new URL(keyIdLower).hostname);
|
||||
|
||||
// Stop if the host is blocked.
|
||||
if (await shouldBlockInstance(host)) {
|
||||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let authUser;
|
||||
try {
|
||||
authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver);
|
||||
} catch (e) {
|
||||
if (e instanceof StatusError) {
|
||||
if (e.isClientError) {
|
||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||
} else {
|
||||
throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
// Key not found? Unacceptable!
|
||||
return 'skip: failed to resolve user';
|
||||
} else {
|
||||
// Found key!
|
||||
}
|
||||
|
||||
// verify the HTTP Signature
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
const validated = await verifyHttpSignature(signature, resolver, getApId(activity.actor));
|
||||
let authUser = validated.authUser;
|
||||
|
||||
// The signature must be valid.
|
||||
// The signature must also match the actor otherwise anyone could sign any activity.
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
// Last resort: LD-Signature
|
||||
if (activity.signature) {
|
||||
if (activity.signature.type !== 'RsaSignature2017') {
|
||||
return `skip: unsupported LD-signature type ${activity.signature.type}`;
|
||||
}
|
||||
|
||||
// get user based on LD-Signature key id.
|
||||
// lets assume that the creator has this common form:
|
||||
// <https://example.com/users/user#main-key>
|
||||
// Then we can use it as the key id and (without fragment part) user id.
|
||||
authUser = await getAuthUser(activity.signature.creator, activity.signature.creator.replace(/#.*$/, ''), resolver);
|
||||
|
||||
if (authUser == null) {
|
||||
return 'skip: failed to resolve LD-Signature user';
|
||||
}
|
||||
|
||||
// LD-Signature verification
|
||||
const ldSignature = new LdSignature();
|
||||
const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||
if (!verified) {
|
||||
return 'skip: LD-Signatureの検証に失敗しました';
|
||||
}
|
||||
|
||||
// Again, the actor must match.
|
||||
if (authUser.user.uri !== activity.actor) {
|
||||
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
|
||||
}
|
||||
|
||||
// Stop if the host is blocked.
|
||||
const ldHost = extractDbHost(authUser.user.uri);
|
||||
if (await shouldBlockInstance(ldHost)) {
|
||||
return `Blocked request: ${ldHost}`;
|
||||
}
|
||||
} else {
|
||||
return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`;
|
||||
}
|
||||
if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) {
|
||||
return `skip: http-signature verification failed. keyId=${signature.keyId}`;
|
||||
}
|
||||
|
||||
// authUser cannot be null at this point:
|
||||
// either it was already not null because the HTTP signature was valid
|
||||
// or, if the LD signature was not verified, this function will already have returned.
|
||||
authUser = authUser as AuthUser;
|
||||
|
||||
// Verify that the actor's host is not blocked
|
||||
const signerHost = extractDbHost(authUser.user.uri!);
|
||||
const signerHost = extractPunyHost(authUser.user.uri!);
|
||||
if (await shouldBlockInstance(signerHost)) {
|
||||
return `Blocked request: ${signerHost}`;
|
||||
}
|
||||
|
||||
if (typeof activity.id === 'string') {
|
||||
// Verify that activity and actor are from the same host.
|
||||
const activityIdHost = extractDbHost(activity.id);
|
||||
const activityIdHost = extractPunyHost(activity.id);
|
||||
if (signerHost !== activityIdHost) {
|
||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
||||
}
|
||||
|
|
|
@ -45,6 +45,6 @@ export default async function cleanRemoteFiles(job: Bull.Job<Record<string, unkn
|
|||
job.progress(deletedCount / total);
|
||||
}
|
||||
|
||||
logger.succ('All cahced remote files has been deleted.');
|
||||
logger.succ('All cached remote files have been deleted.');
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Bull from 'bull';
|
||||
|
||||
import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||
import { activeUsersChart, driveChart, federationChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('clean-charts');
|
||||
|
@ -17,7 +17,6 @@ export async function cleanCharts(job: Bull.Job<Record<string, unknown>>, done:
|
|||
perUserNotesChart.clean(),
|
||||
driveChart.clean(),
|
||||
perUserReactionsChart.clean(),
|
||||
hashtagChart.clean(),
|
||||
perUserFollowingChart.clean(),
|
||||
perUserDriveChart.clean(),
|
||||
apRequestChart.clean(),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Bull from 'bull';
|
||||
|
||||
import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||
import { activeUsersChart, driveChart, federationChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('tick-charts');
|
||||
|
@ -17,7 +17,6 @@ export async function tickCharts(job: Bull.Job<Record<string, unknown>>, done: a
|
|||
perUserNotesChart.tick(false),
|
||||
driveChart.tick(false),
|
||||
perUserReactionsChart.tick(false),
|
||||
hashtagChart.tick(false),
|
||||
perUserFollowingChart.tick(false),
|
||||
perUserDriveChart.tick(false),
|
||||
apRequestChart.tick(false),
|
||||
|
|
|
@ -80,7 +80,7 @@ function groupingAudience(ids: string[], actor: IRemoteUser) {
|
|||
function isPublic(id: string) {
|
||||
return [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
'as#Public',
|
||||
'as:Public',
|
||||
'Public',
|
||||
].includes(id);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
|
@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
|||
const uri = getApId(activity);
|
||||
|
||||
// Cancel if the announced from host is blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) return;
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) return;
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { createNote, fetchNote } from '@/remote/activitypub/models/note.js';
|
||||
|
@ -18,7 +18,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
|
|||
}
|
||||
|
||||
if (typeof note.id === 'string') {
|
||||
if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
|
||||
if (extractPunyHost(actor.uri) !== extractPunyHost(note.id)) {
|
||||
return 'skip: host in actor.uri !== note.id';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export default async function(actor: IRemoteUser, uri: string): Promise<string>
|
|||
return 'skip: cant delete other actors note';
|
||||
}
|
||||
|
||||
await deleteNotes([note], actor);
|
||||
await deleteNotes([note]);
|
||||
return 'ok: note deleted';
|
||||
}
|
||||
} finally {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isFlag, isMove, getApId } from '../type.js';
|
||||
import create from './create/index.js';
|
||||
import performDeleteActivity from './delete/index.js';
|
||||
import performUpdateActivity from './update/index.js';
|
||||
|
@ -22,27 +21,10 @@ import flag from './flag/index.js';
|
|||
import { move } from './move/index.js';
|
||||
|
||||
export async function performActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
const act = await resolver.resolve(item);
|
||||
try {
|
||||
await performOneActivity(actor, act, resolver);
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
apLogger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await performOneActivity(actor, activity, resolver);
|
||||
}
|
||||
}
|
||||
|
||||
async function performOneActivity(actor: IRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (typeof activity.id !== 'undefined') {
|
||||
const host = extractDbHost(getApId(activity));
|
||||
const host = extractPunyHost(getApId(activity));
|
||||
if (await shouldBlockInstance(host)) return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { isSelfHost, extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { MessagingMessages } from '@/models/index.js';
|
||||
import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js';
|
||||
import { IRead, getApId } from '../type.js';
|
||||
|
@ -7,7 +7,7 @@ import { IRead, getApId } from '../type.js';
|
|||
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
|
||||
const id = await getApId(activity.object);
|
||||
|
||||
if (!isSelfHost(extractDbHost(id))) {
|
||||
if (!isSelfHost(extractPunyHost(id))) {
|
||||
return `skip: Read to foreign host (${id})`;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,6 @@ export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Pro
|
|||
|
||||
if (!note) return 'skip: no such Announce';
|
||||
|
||||
await deleteNotes([note], actor);
|
||||
await deleteNotes([note]);
|
||||
return 'ok: deleted';
|
||||
};
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js';
|
||||
import { getApId, getOneApId, getApType, IUpdate, isActor, isPost } from '@/remote/activitypub/type.js';
|
||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||
import { updateQuestion } from '@/remote/activitypub/models/question.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||
import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js';
|
||||
|
||||
/**
|
||||
* Updateアクティビティを捌きます
|
||||
* Handle Update activity
|
||||
*/
|
||||
export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver): Promise<string> => {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
|
@ -30,6 +31,8 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver)
|
|||
} else if (getApType(object) === 'Question') {
|
||||
await updateQuestion(object, resolver).catch(e => console.log(e));
|
||||
return 'ok: Question updated';
|
||||
} else if (isPost(object)) {
|
||||
return await updateNote(actor, object, resolver);
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import createNote from '@/remote/activitypub/kernel/create/note.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { updateNote } from '@/remote/activitypub/models/note.js';
|
||||
|
||||
export async function update(actor: IRemoteUser, note: IObject, resolver: Resolver): Promise<string> {
|
||||
// check whether note exists
|
||||
const uri = getApId(note);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
|
||||
if (exists == null) {
|
||||
// does not yet exist, handle as if this was a create activity
|
||||
// and since this is not a direct creation, handle it silently
|
||||
createNote(resolver, actor, note, true);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
// if creating was successful...
|
||||
const existsNow = await Notes.findOneByOrFail({ uri });
|
||||
return 'ok: unknown note created and marked as updated';
|
||||
} catch (e) {
|
||||
return `skip: updated note unknown and creating rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
} else {
|
||||
// check that actor is authorized to update this note
|
||||
if (actor.id !== exists.userId) {
|
||||
return 'skip: actor not authorized to update Note';
|
||||
}
|
||||
// this does not redo the checks from the Create Note kernel
|
||||
// since if the note made it into the database, we assume
|
||||
// those checks must have been passed before.
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
await updateNote(note, actor, resolver);
|
||||
return 'ok: note updated';
|
||||
} catch (e) {
|
||||
return `skip: update note rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
import { remoteLogger } from '../logger.js';
|
||||
|
||||
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
|
||||
export const apLogger = remoteLogger.createSubLogger('ap');
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
import { Cache } from '@/misc/cache.js';
|
||||
import { UserPublickeys } from '@/models/index.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||
import { uriPersonCache, userByIdCache, publicKeyCache, publicKeyByUserIdCache } from '@/services/user-cache.js';
|
||||
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
|
||||
export type AuthUser = {
|
||||
user: IRemoteUser;
|
||||
key: UserPublickey;
|
||||
};
|
||||
|
||||
const publicKeyCache = new Cache<UserPublickey>(
|
||||
Infinity,
|
||||
(keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined),
|
||||
);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey>(
|
||||
Infinity,
|
||||
(userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined),
|
||||
);
|
||||
|
||||
function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
||||
return uriPersonCache.fetch(uri)
|
||||
.then(async user => {
|
||||
|
@ -29,15 +20,18 @@ function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await publicKeyCache.fetch(keyId)
|
||||
export async function authUserFromKeyId(keyId: string): Promise<AuthUser | null> {
|
||||
return await publicKeyCache.fetch(keyId)
|
||||
.then(async key => {
|
||||
if (!key) return null;
|
||||
else return {
|
||||
user: await userByIdCache.fetch(key.userId),
|
||||
key,
|
||||
};
|
||||
const user = await userByIdCache.fetch(key.userId);
|
||||
if (!user) return null;
|
||||
return { user, key };
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await authUserFromKeyId(keyId);
|
||||
if (authUser != null) return authUser;
|
||||
|
||||
authUser = await authUserFromApId(actorUri);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import config from '@/config/index.js';
|
||||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
||||
import { vote } from '@/services/note/polls/vote.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages } from '@/models/index.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages, Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
|
@ -27,6 +27,8 @@ import { resolveImage } from './image.js';
|
|||
import { extractApHashtags, extractQuoteUrl, extractEmojis } from './tag.js';
|
||||
import { extractPollFromQuestion } from './question.js';
|
||||
import { extractApMentions } from './mention.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { sideEffects } from '@/services/note/side-effects.js';
|
||||
|
||||
export function validateNote(object: IObject): Error | null {
|
||||
if (object == null) {
|
||||
|
@ -45,9 +47,9 @@ export function validateNote(object: IObject): Error | null {
|
|||
}
|
||||
|
||||
// Check that the server is authorized to act on behalf of this author.
|
||||
const expectHost = extractDbHost(id);
|
||||
const expectHost = extractPunyHost(id);
|
||||
const attributedToHost = object.attributedTo
|
||||
? extractDbHost(getOneApId(object.attributedTo))
|
||||
? extractPunyHost(getOneApId(object.attributedTo))
|
||||
: null;
|
||||
if (attributedToHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
|
||||
|
@ -93,7 +95,7 @@ async function processContent(actor: IRemoteUser, note: IPost, quoteUri: string
|
|||
text = fromHtml(note.content, quoteUri);
|
||||
}
|
||||
|
||||
const emojis = await extractEmojis(note.tag || [], extractDbHost(getApId(note))).catch(e => {
|
||||
const emojis = await extractEmojis(note.tag || [], extractPunyHost(getApId(note))).catch(e => {
|
||||
apLogger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
|
@ -276,6 +278,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
return await post(actor, {
|
||||
...processedContent,
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
updatedAt: note.updated,
|
||||
reply,
|
||||
renote: quote,
|
||||
localOnly: false,
|
||||
|
@ -299,7 +302,7 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
if (uri == null) throw new Error('missing uri');
|
||||
|
||||
// Interrupt if blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) throw new StatusError('host blocked', 451, `host ${extractDbHost(uri)} is blocked`);
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) throw new StatusError('host blocked', 451, `host ${extractPunyHost(uri)} is blocked`);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
@ -324,3 +327,45 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a note.
|
||||
*
|
||||
* If the target Note is not registered, it will be ignored.
|
||||
*/
|
||||
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
|
||||
const err = validateNote(value);
|
||||
if (err) {
|
||||
apLogger.error(`${err.message}`);
|
||||
throw new Error('invalid updated note');
|
||||
}
|
||||
|
||||
const uri = getApId(value);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
if (exists == null) return null;
|
||||
|
||||
let quoteUri = null;
|
||||
if (exists.renoteId && !foundkey.entities.isPureRenote(exists)) {
|
||||
const quote = await Notes.findOneBy({ id: exists.renoteId });
|
||||
quoteUri = quote.uri;
|
||||
}
|
||||
|
||||
// process content and update attached files (e.g. also image descriptions)
|
||||
const processedContent = await processContent(actor, value, quoteUri, resolver);
|
||||
|
||||
// update note content itself
|
||||
await Notes.update(exists.id, {
|
||||
updatedAt: value.updated ?? new Date(),
|
||||
|
||||
cw: processedContent.cw,
|
||||
fileIds: processedContent.files.map(file => file.id),
|
||||
attachedFileTypes: processedContent.files.map(file => file.type),
|
||||
text: processedContent.text,
|
||||
emojis: processedContent.apEmoji,
|
||||
tags: processedContent.apHashtags.map(tag => normalizeForSearch(tag)),
|
||||
url: processedContent.url,
|
||||
name: processedContent.name,
|
||||
});
|
||||
|
||||
await sideEffects(actor, await Notes.findOneByOrFail({ id: exists.id }), false, false);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { URL } from 'node:url';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
|
||||
|
@ -13,7 +14,7 @@ import { genId } from '@/misc/gen-id.js';
|
|||
import { instanceChart, usersChart } from '@/services/chart/index.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
|
@ -57,7 +58,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This check is security critical.
|
||||
// Without this check, an entry could be inserted into UserPublickey for a local user.
|
||||
if (extractDbHost(uri) === extractDbHost(config.url)) {
|
||||
if (extractPunyHost(uri) === extractPunyHost(config.url)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
|
@ -77,8 +78,23 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
});
|
||||
}
|
||||
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong inbox');
|
||||
// check that inbox is a valid and absolute URL
|
||||
// in NodeJS, the first parameter must be an absolute URL or the base URL is required
|
||||
try {
|
||||
new URL(x.inbox)
|
||||
} catch (err) {
|
||||
throw new Error('invalid Actor: wrong inbox', { cause: err });
|
||||
}
|
||||
|
||||
// unify different sharedInbox places
|
||||
x.sharedInbox = x.sharedInbox ?? x.endpoints?.sharedInbox ?? null;
|
||||
if (x.sharedInbox != null) {
|
||||
// check that sharedInbox is a valid and absolute URL
|
||||
try {
|
||||
new URL(x.sharedInbox);
|
||||
} catch (err) {
|
||||
delete x.sharedInbox;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||
|
@ -108,7 +124,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This is a security critical check to not insert or change an entry of
|
||||
// UserPublickey to point to a local key id.
|
||||
if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) {
|
||||
if (extractPunyHost(uri) !== extractPunyHost(x.publicKey.id)) {
|
||||
throw new Error('invalid Actor: publicKey.id has different host');
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +173,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
|||
|
||||
apLogger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
const host = extractDbHost(object.id);
|
||||
const host = extractPunyHost(object.id);
|
||||
|
||||
const { fields } = analyzeAttachments(person.attachment || []);
|
||||
|
||||
|
@ -185,7 +201,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
|||
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||
host,
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
sharedInbox: person.sharedInbox,
|
||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||
featured: person.featured ? getApId(person.featured) : undefined,
|
||||
uri: person.id,
|
||||
|
@ -335,7 +351,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
sharedInbox: person.sharedInbox,
|
||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||
featured: person.featured,
|
||||
emojis: emojiNames,
|
||||
|
@ -382,7 +398,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
await Followings.update({
|
||||
followerId: exist.id,
|
||||
}, {
|
||||
followerSharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined),
|
||||
followerSharedInbox: person.sharedInbox,
|
||||
});
|
||||
|
||||
await updateFeatured(exist.id, resolver).catch(err => apLogger.error(err));
|
||||
|
|
|
@ -46,13 +46,8 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
},
|
||||
// schema
|
||||
schema: 'http://schema.org/',
|
||||
PropertyValue: {
|
||||
'@id': 'schema:PropertyValue',
|
||||
'@context': {
|
||||
'value': 'schema:value',
|
||||
'name': 'schema:name',
|
||||
},
|
||||
},
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_quote': {
|
||||
|
|
|
@ -13,7 +13,9 @@ export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
|
|||
id: `${config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
||||
content: reaction,
|
||||
... (reaction !== '\u2b50' ? {
|
||||
content: reaction,
|
||||
} : {}),
|
||||
} as any;
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
|
|
|
@ -24,6 +24,7 @@ export async function renderPerson(user: ILocalUser) {
|
|||
const attachment: {
|
||||
type: 'PropertyValue',
|
||||
name: string,
|
||||
'schema:name': string,
|
||||
value: string,
|
||||
identifier?: IIdentifier
|
||||
}[] = [];
|
||||
|
@ -44,6 +45,15 @@ export async function renderPerson(user: ILocalUser) {
|
|||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
// uses "name" for backward compatibility, but JSON-LD-expanding
|
||||
// it ends up being the ActivityPub name property, which is not
|
||||
// correct for this type. which is why we also send the "correct"
|
||||
// schema.org name property field, which would not be recognized
|
||||
// by people just processing JSON-LD as JSON as allowed by the spec.
|
||||
// it would be nicer to do this with JSON-LD context, but this is
|
||||
// only possible when using JSON-LD 1.1 with nested contexts, but
|
||||
// activitystreams 2.0 requires the JSON-LD 1.0 expansion algorithm.
|
||||
'schema:name': field.name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import { URL } from 'node:url';
|
|||
import config from '@/config/index.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||
import { getResponse } from '@/misc/fetch.js';
|
||||
import { getApId } from './type.js';
|
||||
import { createSignedPost, createSignedGet } from './ap-request.js';
|
||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||
|
||||
|
@ -14,11 +16,9 @@ import { apRequestChart, federationChart, instanceChart } from '@/services/chart
|
|||
* @param url The URL of the inbox.
|
||||
* @param object The Activity or other object to be posted to the inbox.
|
||||
*/
|
||||
export async function request(user: { id: User['id'] }, url: string, object: any): Promise<void> {
|
||||
export async function request(user: { id: User['id'] }, url: string, object: any, keypair: UserKeypair): Promise<void> {
|
||||
const body = JSON.stringify(object);
|
||||
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const req = createSignedPost({
|
||||
key: {
|
||||
privateKeyPem: keypair.privateKey,
|
||||
|
@ -55,6 +55,39 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
|||
}
|
||||
}
|
||||
|
||||
// Determine that the content type actually is an activitypub object.
|
||||
//
|
||||
// This defends against the possibility of a user of a remote instance uploading
|
||||
// something that looks like an ActivityPub object and thus masquerading as any
|
||||
// other user on that same instance. It in turn depends on the server not
|
||||
// returning that content as an ActivityPub MIME type.
|
||||
//
|
||||
// Ref: GHSA-jhrq-qvrm-qr36
|
||||
function isActivitypub(_contentType: string): boolean {
|
||||
const contentType = _contentType.toLowerCase()
|
||||
if (contentType.startsWith('application/activity+json')) {
|
||||
return true;
|
||||
}
|
||||
if (contentType.startsWith('application/ld+json')) {
|
||||
// oh lord, actually parsing the MIME type
|
||||
// Ref: <urn:ietf:rfc:2045> § 5.1
|
||||
// Ref: <https://www.iana.org/assignments/media-types/application/ld+json>
|
||||
let start = contentType.indexOf('profile="');
|
||||
if (start === -1) return false; // profile is required for our purposes
|
||||
|
||||
start += 'profile="'.length;
|
||||
let end = contentType.indexOf('"', start);
|
||||
if (end === -1) return false; // malformed MIME type
|
||||
|
||||
let profiles = contentType.substring(start, end).split(/\s+/);
|
||||
if (profiles.includes('https://www.w3.org/ns/activitystreams')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get AP object with http-signature
|
||||
* @param user http-signature user
|
||||
|
@ -64,7 +97,7 @@ export async function signedGet(_url: string, user: { id: User['id'] }): Promise
|
|||
let url = _url;
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
for (let redirects = 0; redirects < 3; redirects++) {
|
||||
for (let redirects = 3; redirects > 0; redirects--) {
|
||||
const req = createSignedGet({
|
||||
key: {
|
||||
privateKeyPem: keypair.privateKey,
|
||||
|
@ -86,9 +119,35 @@ export async function signedGet(_url: string, user: { id: User['id'] }): Promise
|
|||
if (res.status >= 300 && res.status < 400) {
|
||||
// Have been redirected, need to make a new signature.
|
||||
// Use Location header and fetched URL as the base URL.
|
||||
url = new URL(res.headers.get('Location'), url).href;
|
||||
let newUrl = new URL(res.headers.get('Location'), url);
|
||||
// Check that we have not been redirected to a different host.
|
||||
if (newUrl.host !== new URL(url).host) {
|
||||
throw new Error('cross-origin redirect not allowed');
|
||||
}
|
||||
url = newUrl.href;
|
||||
} else {
|
||||
return await res.json();
|
||||
if (!isActivitypub(res.headers.get('Content-Type'))) {
|
||||
throw new Error('invalid response content type');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
// In theory, activitypub allows for `id` to be null for ephemeral
|
||||
// objects, but we wouldn't be fetching those with signed get, since
|
||||
// they are... ephemeral.
|
||||
const id = new URL(getApId(data));
|
||||
if (id.href !== url.href) {
|
||||
// if the id and fetched url mismatch, treat it as if it was a redirect
|
||||
// SECURITY: this is to prevent impersonation via improper media files
|
||||
url = id;
|
||||
// if this kind of "redirect" happens, there should be at most one more
|
||||
// redirect since we now have the canonical url. setting to 2 because it
|
||||
// will be decremented to 1 right away by the for loop.
|
||||
if (redirects > 2) {
|
||||
redirects = 2;
|
||||
}
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost, isSelfHost } from '@/misc/convert-host.js';
|
||||
import { Notes, NoteReactions, Polls, Users } from '@/models/index.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
|
@ -50,7 +50,7 @@ export class Resolver {
|
|||
|
||||
if (typeof value !== 'string') {
|
||||
if (typeof value.id !== 'undefined') {
|
||||
const host = extractDbHost(getApId(value));
|
||||
const host = extractPunyHost(getApId(value));
|
||||
if (await shouldBlockInstance(host)) {
|
||||
throw new Error('instance is blocked');
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ export class Resolver {
|
|||
}
|
||||
this.history.add(value);
|
||||
|
||||
const host = extractDbHost(value);
|
||||
const host = extractPunyHost(value);
|
||||
if (isSelfHost(host)) {
|
||||
return await this.resolveLocal(value);
|
||||
}
|
||||
|
|
95
packages/backend/src/remote/http-signature.ts
Normal file
95
packages/backend/src/remote/http-signature.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { URL } from 'node:url';
|
||||
import { extractPunyHost } from "@/misc/convert-host.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import httpSignature from "@peertube/http-signature";
|
||||
import { Resolver } from "./activitypub/resolver.js";
|
||||
import { StatusError } from "@/misc/fetch.js";
|
||||
import { AuthUser, authUserFromKeyId, getAuthUser } from "./activitypub/misc/auth-user.js";
|
||||
import { ApObject, getApId, isActor } from "./activitypub/type.js";
|
||||
import { createPerson } from "./activitypub/models/person.js";
|
||||
|
||||
async function resolveKeyId(keyId: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
// Do we already know that keyId?
|
||||
const authUser = await authUserFromKeyId(keyId);
|
||||
if (authUser != null) return authUser;
|
||||
|
||||
// If not, discover it.
|
||||
const keyUrl = new URL(keyId);
|
||||
keyUrl.hash = ''; // Fragment should not be part of the request.
|
||||
|
||||
const keyObject = await resolver.resolve(keyUrl.toString());
|
||||
|
||||
// Does the keyId end up resolving to an Actor?
|
||||
if (isActor(keyObject)) {
|
||||
await createPerson(keyObject, resolver);
|
||||
return await getAuthUser(keyId, getApId(keyObject), resolver);
|
||||
}
|
||||
|
||||
// Does the keyId end up resolving to a Key-like?
|
||||
const keyData = keyObject as any;
|
||||
if (keyData.owner != null && keyData.publicKeyPem != null) {
|
||||
await createPerson(keyData.owner, resolver);
|
||||
return await getAuthUser(keyId, getApId(keyData.owner), resolver);
|
||||
}
|
||||
|
||||
// Cannot be resolved.
|
||||
return null;
|
||||
}
|
||||
|
||||
export type SignatureValidationResult = {
|
||||
status: 'missing' | 'invalid' | 'rejected';
|
||||
authUser: AuthUser | null;
|
||||
} | {
|
||||
status: 'valid';
|
||||
authUser: AuthUser;
|
||||
};
|
||||
|
||||
export async function verifyHttpSignature(signature: httpSignature.IParsedSignature, resolver: Resolver, actor?: ApObject): Promise<SignatureValidationResult> {
|
||||
// This old `keyId` format is no longer supported.
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) return { status: 'invalid', authUser: null };
|
||||
|
||||
const host = extractPunyHost(keyIdLower);
|
||||
|
||||
// Reject if the host is blocked.
|
||||
if (await shouldBlockInstance(host)) return { status: 'rejected', authUser: null };
|
||||
|
||||
let authUser = null;
|
||||
try {
|
||||
if (actor != null) {
|
||||
authUser = await getAuthUser(signature.keyId, getApId(actor), resolver);
|
||||
} else {
|
||||
authUser = await resolveKeyId(signature.keyId, resolver);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof StatusError) {
|
||||
if (e.isClientError) {
|
||||
// Actor is deleted.
|
||||
return { status: 'rejected', authUser };
|
||||
} else {
|
||||
throw new Error(`Error in signature ${signature} - ${e.statusCode || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
// Key not found? Unacceptable!
|
||||
return { status: 'invalid', authUser };
|
||||
} else {
|
||||
// Found key!
|
||||
}
|
||||
|
||||
// Make sure the resolved user matches the keyId host.
|
||||
if (authUser.user.host !== host) return { status: 'rejected', authUser };
|
||||
|
||||
// Verify the HTTP Signature
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
if (httpSignatureValidated === true)
|
||||
return {
|
||||
status: 'valid',
|
||||
authUser,
|
||||
};
|
||||
|
||||
// Otherwise, fail.
|
||||
return { status: 'invalid', authUser };
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
import Logger from '@/services/logger.js';
|
||||
|
||||
export const remoteLogger = new Logger('remote', 'cyan');
|
||||
export const remoteLogger = new Logger('remote');
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { URL } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DAY } from '@/const.js';
|
||||
import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
||||
|
@ -46,7 +45,7 @@ export async function resolveUser(username: string, idnHost: string | null, reso
|
|||
if (user == null) {
|
||||
const self = await resolveSelf(acctLower);
|
||||
|
||||
logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
|
||||
logger.succ(`return new remote user: ${acctLower}`);
|
||||
return await createPerson(self, resolver);
|
||||
}
|
||||
|
||||
|
@ -101,16 +100,16 @@ export async function resolveUser(username: string, idnHost: string | null, reso
|
|||
* Gets the Webfinger href matching rel="self".
|
||||
*/
|
||||
async function resolveSelf(acctLower: string): string {
|
||||
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
||||
logger.info(`WebFinger for ${acctLower}`);
|
||||
// get webfinger response for user
|
||||
const finger = await webFinger(acctLower).catch(e => {
|
||||
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
|
||||
logger.error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
|
||||
throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
|
||||
});
|
||||
// try to find the rel="self" link
|
||||
const self = finger.links.find(link => link.rel?.toLowerCase() === 'self');
|
||||
if (!self?.href) {
|
||||
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
|
||||
logger.error(`Failed to WebFinger for ${acctLower}: self link not found`);
|
||||
throw new Error('self link not found');
|
||||
}
|
||||
return self.href;
|
||||
|
|
|
@ -20,6 +20,11 @@ import Outbox from './activitypub/outbox.js';
|
|||
import Followers from './activitypub/followers.js';
|
||||
import Following from './activitypub/following.js';
|
||||
import Featured from './activitypub/featured.js';
|
||||
import { isInstanceActor } from '@/services/instance-actor.js';
|
||||
import { getUser } from './api/common/getters.js';
|
||||
import config from '@/config/index.js';
|
||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
@ -50,6 +55,18 @@ function isActivityPubReq(ctx: Router.RouterContext): boolean {
|
|||
return typeof accepted === 'string' && !accepted.match(/html/);
|
||||
}
|
||||
|
||||
export function denyActivityPub() {
|
||||
return async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
// Clients are required to set the `Accept` header to an Activitypub content type.
|
||||
// If such a content type negotiation header is received on an unexpected route,
|
||||
// something seems to be fishy and we should not respond with content. A user
|
||||
// might have e.g. uploaded a malicious file that looks like an activity.
|
||||
ctx.status = 406;
|
||||
};
|
||||
}
|
||||
|
||||
export function setResponseType(ctx: Router.RouterContext): void {
|
||||
const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
|
||||
if (accept === LD_JSON) {
|
||||
|
@ -59,6 +76,36 @@ export function setResponseType(ctx: Router.RouterContext): void {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleSignature(ctx: Router.RouterContext): Promise<boolean> {
|
||||
if (config.allowUnsignedFetches) {
|
||||
// Fetch signature verification is disabled.
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
return true;
|
||||
} else {
|
||||
let verified;
|
||||
try {
|
||||
let signature = httpSignature.parseRequest(ctx.req);
|
||||
verified = await verifyHttpSignature(signature, new Resolver());
|
||||
} catch (e) {
|
||||
verified = { status: 'missing' };
|
||||
}
|
||||
|
||||
switch (verified.status) {
|
||||
// Fetch signature verification succeeded.
|
||||
case 'valid':
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
return true;
|
||||
case 'missing':
|
||||
case 'invalid':
|
||||
case 'rejected':
|
||||
default:
|
||||
ctx.status = 403;
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inbox
|
||||
router.post('/inbox', json(), inbox);
|
||||
router.post('/users/:user/inbox', json(), inbox);
|
||||
|
@ -66,6 +113,7 @@ router.post('/users/:user/inbox', json(), inbox);
|
|||
// note
|
||||
router.get('/notes/:note', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
|
@ -89,7 +137,6 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNote(note, false));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
@ -103,6 +150,7 @@ router.get('/notes/:note/activity', async ctx => {
|
|||
ctx.redirect(`/notes/${ctx.params.note}`);
|
||||
return;
|
||||
}
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
|
@ -117,23 +165,32 @@ router.get('/notes/:note/activity', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNoteOrRenoteActivity(note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
async function requireHttpSignature(ctx: Router.Context, next: () => Promise<void>) {
|
||||
if (!(await handleSignature(ctx))) {
|
||||
return;
|
||||
} else {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
// outbox
|
||||
router.get('/users/:user/outbox', Outbox);
|
||||
router.get('/users/:user/outbox', requireHttpSignature, Outbox);
|
||||
|
||||
// followers
|
||||
router.get('/users/:user/followers', Followers);
|
||||
router.get('/users/:user/followers', requireHttpSignature, Followers);
|
||||
|
||||
// following
|
||||
router.get('/users/:user/following', Following);
|
||||
router.get('/users/:user/following', requireHttpSignature, Following);
|
||||
|
||||
// featured
|
||||
router.get('/users/:user/collections/featured', Featured);
|
||||
router.get('/users/:user/collections/featured', requireHttpSignature, Featured);
|
||||
|
||||
// publickey
|
||||
// This does not require HTTP signatures in order for other instances
|
||||
// to be able to verify our own signatures.
|
||||
router.get('/users/:user/publickey', async ctx => {
|
||||
const userId = ctx.params.user;
|
||||
|
||||
|
@ -166,7 +223,6 @@ async function userInfo(ctx: Router.RouterContext, user: User | null): Promise<v
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
|
||||
|
@ -181,11 +237,26 @@ router.get('/users/:user', async (ctx, next) => {
|
|||
isSuspended: false,
|
||||
});
|
||||
|
||||
// Allow fetching the instance actor without any HTTP signature.
|
||||
// Only on this route, as it is the canonical route.
|
||||
// If the user could not be resolved, or is not the instance actor,
|
||||
// validate and enforce signatures.
|
||||
if (user == null || !isInstanceActor(user))
|
||||
{
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
}
|
||||
else if (isInstanceActor(user))
|
||||
{
|
||||
// Set cache at all times for instance actors.
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
|
||||
await userInfo(ctx, user);
|
||||
});
|
||||
|
||||
router.get('/@:user', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: ctx.params.user.toLowerCase(),
|
||||
|
@ -198,6 +269,9 @@ router.get('/@:user', async (ctx, next) => {
|
|||
|
||||
// emoji
|
||||
router.get('/emojis/:emoji', async ctx => {
|
||||
// Enforcing HTTP signatures on Emoji objects could cause problems for
|
||||
// other software that might use those objects for copying custom emoji.
|
||||
|
||||
const emoji = await Emojis.findOneBy({
|
||||
host: IsNull(),
|
||||
name: ctx.params.emoji,
|
||||
|
@ -215,6 +289,7 @@ router.get('/emojis/:emoji', async ctx => {
|
|||
|
||||
// like
|
||||
router.get('/likes/:like', async ctx => {
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
const reaction = await NoteReactions.findOneBy({ id: ctx.params.like });
|
||||
|
||||
if (reaction == null) {
|
||||
|
@ -233,12 +308,12 @@ router.get('/likes/:like', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderLike(reaction, note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// follow
|
||||
router.get('/follows/:follower/:followee', async ctx => {
|
||||
if (!(await handleSignature(ctx))) return;
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists.
|
||||
|
||||
|
@ -259,7 +334,6 @@ router.get('/follows/:follower/:followee', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
|
|
@ -36,6 +36,5 @@ export default async (ctx: Router.RouterContext) => {
|
|||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
};
|
||||
|
|
|
@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -82,7 +82,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -90,7 +90,6 @@ export default async (ctx: Router.RouterContext) => {
|
|||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue