mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-23 20:53:09 +02:00
Merge branch 'develop'
This commit is contained in:
commit
2375359d12
452 changed files with 4971 additions and 3460 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -10,6 +10,27 @@
|
|||
You should also include the user name that made the change.
|
||||
-->
|
||||
|
||||
## 12.109.0 (2022/04/02)
|
||||
|
||||
### Improvements
|
||||
- Webhooks @syuilo
|
||||
- Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo
|
||||
- Bull Dashboardを開くには、最初だけ一旦ログアウトしてから再度管理者権限を持つアカウントでログインする必要があります
|
||||
- Check that installed Node.js version fulfills version requirement @ThatOneCalculator
|
||||
- Server: overall performance improvements @syuilo
|
||||
- Federation: avoid duplicate activity delivery @Johann150
|
||||
- Federation: limit federation of reactions on direct notes @Johann150
|
||||
- Client: タッチパッド・タッチスクリーンでのデッキの操作性を向上 @tamaina
|
||||
|
||||
### Bugfixes
|
||||
- email address validation was not working @ybw2016v
|
||||
- API: fix endpoint endpoint @Johann150
|
||||
- API: fix admin/meta endpoint @syuilo
|
||||
- API: improved validation and documentation for endpoints that accept different variants of input @Johann150
|
||||
- API: `notes/create`: The `mediaIds` property is now deprecated. @Johann150
|
||||
- Use `fileIds` instead, it has the same behaviour.
|
||||
- Client: URIエンコーディングが異常でdecodeURIComponentが失敗するとURLが表示できなくなる問題を修正 @tamaina
|
||||
|
||||
## 12.108.1 (2022/03/12)
|
||||
|
||||
### Bugfixes
|
||||
|
|
|
@ -6,6 +6,9 @@ Also, you might receive comments on your Issue/PR in Japanese, but you do not ne
|
|||
The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language.
|
||||
It will also allow the reader to use the translation tool of their preference if necessary.
|
||||
|
||||
## Roadmap
|
||||
See [ROADMAP.md](./ROADMAP.md)
|
||||
|
||||
## Issues
|
||||
Before creating an issue, please check the following:
|
||||
- To avoid duplication, please search for similar issues before creating a new issue.
|
||||
|
@ -198,11 +201,13 @@ MongoDBの時とは違い、findOneでレコードを取得する時に対象レ
|
|||
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
|
||||
|
||||
### Migration作成方法
|
||||
```
|
||||
npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前 -o
|
||||
packages/backendで:
|
||||
```sh
|
||||
npx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
```
|
||||
|
||||
作成されたスクリプトは不必要な変更を含むため除去してください。
|
||||
- 生成後、ファイルをmigration下に移してください
|
||||
- 作成されたスクリプトは不必要な変更を含むため除去してください
|
||||
|
||||
### コネクションには`markRaw`せよ
|
||||
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
||||
|
|
36
ROADMAP.md
Normal file
36
ROADMAP.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Roadmap
|
||||
The order of individual tasks is a guide only and is subject to change depending on the situation.
|
||||
Also, the later tasks are more indefinite and are subject to change as development progresses.
|
||||
|
||||
## (1) Improve maintainability \<current phase\>
|
||||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
|
||||
|
||||
- Make the number of type errors zero (backend)
|
||||
- Probably need to switch some libraries to others that make it difficult to reduce type errors
|
||||
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
|
||||
- Improve CI
|
||||
- Fix tests
|
||||
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
|
||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
||||
- Add more tests
|
||||
- May need to implement a mechanism that allows for DI
|
||||
- Improve documentation
|
||||
|
||||
## (2) Improve functionality
|
||||
Once Phase 1 is complete and an environment conducive to the development of a stable system is in place, the implementation of new functions can begin gradually.
|
||||
|
||||
- OAuth2 support https://github.com/misskey-dev/misskey/issues/8262
|
||||
- GraphQL support?
|
||||
|
||||
## (3) Improve scalability
|
||||
Once the development of the feature has settled down, this may be an opportunity to make larger modifications.
|
||||
|
||||
- Rewriting in Rust?
|
||||
|
||||
## (4) Change the world
|
||||
It is time to promote Misskey and change the world.
|
||||
|
||||
- Become more major than services such as Twitter and become critical infrastructure for the world
|
||||
- MiOS will be developed and integrated into various systems - What is MiOS?
|
||||
- Letting Ai-chan interfere with the real world
|
||||
- Make Misskey a member of GAFA; Misskey's office must be a reinforced concrete brutalist building with a courtyard.
|
|
@ -189,7 +189,7 @@ clearCachedFiles: "امسح التخزين المؤقت"
|
|||
clearCachedFilesConfirm: "أتريد حذف التخزين المؤقت للملفات البعيدة؟"
|
||||
blockedInstances: "المثلاء المحجوبون"
|
||||
blockedInstancesDescription: "قائمة بالمثلاء التي تريد حظرها بحيث كل نطاق في سطر لوحده. بعد إدراجهم لن يتمكنوا من التفاعل مع هذا المثيل."
|
||||
muteAndBlock: "تم كتمها / تم حجبها"
|
||||
muteAndBlock: "المكتومون والمحجوبون"
|
||||
mutedUsers: "الحسابات المكتومة"
|
||||
blockedUsers: "الحسابات المحجوبة"
|
||||
noUsers: "ليس هناك مستخدمون"
|
||||
|
@ -490,7 +490,7 @@ none: "لا شيء"
|
|||
showInPage: "اعرض في الصفحة"
|
||||
popout: "منبثقة"
|
||||
volume: "مستوى الصوت"
|
||||
masterVolume: "القرص الرئيسي"
|
||||
masterVolume: "حجم الصوت الرئيس"
|
||||
details: "التفاصيل"
|
||||
chooseEmoji: "اختر إيموجي"
|
||||
unableToProcess: "يتعذر إكمال العملية"
|
||||
|
@ -521,6 +521,7 @@ divider: "فاصل"
|
|||
addItem: "إضافة عنصر"
|
||||
relays: "المُرَحلات"
|
||||
addRelay: "إضافة مُرحّل"
|
||||
inboxUrl: "رابط صندوق الوارد"
|
||||
addedRelays: "المرحلات المضافة"
|
||||
serviceworkerInfo: "يجب أن يفعل لإرسال الإشعارات."
|
||||
deletedNote: "ملاحظة محذوفة"
|
||||
|
@ -533,6 +534,8 @@ enablePlayer: "افتح مشغل الفيديو"
|
|||
disablePlayer: "أغلق مشغل الفيديو"
|
||||
themeEditor: "مصمم القوالب"
|
||||
description: "الوصف"
|
||||
describeFile: "أضف تعليقًا توضيحيًا"
|
||||
enterFileDescription: "أدخل تعليقًا توضيحيًا"
|
||||
author: "الكاتب"
|
||||
leaveConfirm: "لديك تغييرات غير محفوظة. أتريد المتابعة دون حفظها؟"
|
||||
manage: "إدارة "
|
||||
|
@ -564,6 +567,9 @@ smtpPass: "الكلمة السرية"
|
|||
emptyToDisableSmtpAuth: "اترك اسم المستخدم وكلمة المرور فارغين لتعطيل التحقق من SMTP"
|
||||
smtpSecureInfo: "عطل هذا الخيار عند استخدام STARTTLS"
|
||||
wordMute: "حظر الكلمات"
|
||||
regexpError: "خطأ في التعبير النمطي"
|
||||
instanceMute: "المثلاء المكتومون"
|
||||
userSaysSomething: "كتب {name} شيءً"
|
||||
makeActive: "تفعيل"
|
||||
display: "المظهر"
|
||||
copy: "نسخ"
|
||||
|
@ -590,10 +596,16 @@ reportAbuse: "أبلغ"
|
|||
reportAbuseOf: "أبلغ عن {name}"
|
||||
fillAbuseReportDescription: "أكتب بالتفصيل سبب البلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
|
||||
abuseReported: "أُرسل البلاغ، شكرًا لك"
|
||||
reporter: "المُبلّغ"
|
||||
reporteeOrigin: "أصل البلاغ"
|
||||
reporterOrigin: "أصل المُبلّغ"
|
||||
forwardReport: "وجّه البلاغ إلى المثيل البعيد"
|
||||
forwardReportIsAnonymous: "في المثيل البعيد سيظهر المبلّغ كحساب مجهول."
|
||||
send: "أرسل"
|
||||
abuseMarkAsResolved: "علّم البلاغ كمحلول"
|
||||
openInNewTab: "افتح في لسان جديد"
|
||||
defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
|
||||
editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا لحسابك"
|
||||
instanceTicker: "معلومات المثيل الأصلي للملاحظات"
|
||||
waitingFor: "في انتظار {x}"
|
||||
random: "عشوائي"
|
||||
|
@ -624,10 +636,15 @@ no: "لا"
|
|||
driveFilesCount: "عدد الملفات في قرص التخزين"
|
||||
driveUsage: "المستغل من قرص التخزين"
|
||||
noCrawleDescription: "يطلب من محركات البحث ألّا يُفهرسوا ملفك الشخصي وملاحظات وصفحاتك وما شابه."
|
||||
alwaysMarkSensitive: "علّم افتراضيًا جميع ملاحظاتي كذات محتوى حساس"
|
||||
loadRawImages: "حمّل الصور الأصلية بدلًا من المصغرات"
|
||||
disableShowingAnimatedImages: "لا تشغّل الصور المتحركة"
|
||||
verificationEmailSent: "أُرسل بريد التحقق. أنقر على الرابط المضمن لإكمال التحقق."
|
||||
notSet: "لم يعيّن"
|
||||
emailVerified: "تُحقّق من بريدك الإلكتروني"
|
||||
noteFavoritesCount: "عدد الملاحظات المفضلة"
|
||||
pageLikesCount: "عدد الصفحات التي أعجبت بها"
|
||||
pageLikedCount: "عدد صفحاتك المُعجب بها"
|
||||
contact: "التواصل"
|
||||
useSystemFont: "استخدم الخط الافتراضية للنظام"
|
||||
clips: "مشابك"
|
||||
|
@ -635,6 +652,7 @@ experimentalFeatures: "ميّزات اختبارية"
|
|||
developer: "المطور"
|
||||
makeExplorable: "أظهر الحساب في صفحة \"استكشاف\""
|
||||
makeExplorableDescription: "بتعطيل هذا الخيار لن يظهر حسابك في صفحة \"استكشاف\""
|
||||
showGapBetweenNotesInTimeline: "أظهر فجوات بين المشاركات في الخيط الزمني"
|
||||
wide: "عريض"
|
||||
narrow: "رفيع"
|
||||
reloadToApplySetting: "سيُطبق هذا الإعداد بعد إعادة تحميل الصفحة، أتريد إعادة تحميلها الآن؟"
|
||||
|
@ -782,6 +800,7 @@ tenMinutes: "10 دقائق"
|
|||
oneHour: "ساعة"
|
||||
oneDay: "يوم"
|
||||
oneWeek: "أسبوع"
|
||||
failedToFetchAccountInformation: "تعذر جلب معلومات الحساب"
|
||||
_emailUnavailable:
|
||||
used: "هذا البريد الإلكتروني مستخدم"
|
||||
format: "صيغة البريد الإلكتروني غير صالحة"
|
||||
|
@ -860,6 +879,7 @@ _mfm:
|
|||
centerDescription: "يمركز المحتوى في الوَسَط."
|
||||
quote: "اقتبس"
|
||||
emoji: "إيموجي مخصص"
|
||||
emojiDescription: "إحاطة اسم الإيموجي بنقطتي تفسير سيستبدله بصورة الإيموجي."
|
||||
search: "البحث"
|
||||
flip: "اقلب"
|
||||
flipDescription: "يقلب المحتوى عموديًا أو أفقيًا"
|
||||
|
@ -871,15 +891,27 @@ _mfm:
|
|||
jumpDescription: "يمنح للمحتوى حركة قفز."
|
||||
bounce: "تأثير (ارتداد)"
|
||||
bounceDescription: "يمنح للمحتوى حركة ارتدادية"
|
||||
shake: "تأثير (اهتزاز)"
|
||||
shakeDescription: "يمنح المحتوى حركة اهتزازية."
|
||||
spin: "تأثير (دوران)"
|
||||
spinDescription: "يمنح المحتوى حركة دورانية."
|
||||
x2: "كبير"
|
||||
x2Description: "يُكبر المحتوى"
|
||||
x3: "كبير جداً"
|
||||
x3Description: "يُضخم المحتوى"
|
||||
x4: "هائل"
|
||||
x4Description: "يُضخم المحتوى أكثر مما سبق."
|
||||
blur: "طمس"
|
||||
blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح."
|
||||
font: "الخط"
|
||||
fontDescription: "الخط المستخدم لعرض المحتوى."
|
||||
rainbow: "قوس قزح"
|
||||
rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
|
||||
rotate: "تدوير"
|
||||
_instanceTicker:
|
||||
none: "لا تظهره بتاتًا"
|
||||
remote: "أظهر للمستخدمين البِعاد"
|
||||
always: "أظهره دائمًا"
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "إعادة تحميل تلقائية"
|
||||
dialog: "أظهر مربع حوار التحذيرات"
|
||||
|
@ -899,12 +931,18 @@ _menuDisplay:
|
|||
hide: "إخفاء"
|
||||
_wordMute:
|
||||
muteWords: "الكلمات المحظورة"
|
||||
muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"."
|
||||
muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية"
|
||||
softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني."
|
||||
hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط."
|
||||
soft: "لينة"
|
||||
hard: "قاسية"
|
||||
mutedNotes: "الملاحظات المكتومة"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب."
|
||||
instanceMuteDescription2: "مدخلة لكل سطر"
|
||||
title: "يخفي ملاحظات الخوادم المسرودة."
|
||||
heading: "قائمة الخوادم المحجوبة"
|
||||
_theme:
|
||||
explore: "استكشف قوالب المظهر"
|
||||
install: "تنصيب قالب"
|
||||
|
|
|
@ -478,8 +478,8 @@ promote: "Werbung schalten"
|
|||
numberOfDays: "Anzahl der Tage"
|
||||
hideThisNote: "Diese Notiz verstecken"
|
||||
showFeaturedNotesInTimeline: "Beliebte Notizen in der Chronik anzeigen"
|
||||
objectStorage: "Objektspeicher"
|
||||
useObjectStorage: "Objektspeicher verwenden"
|
||||
objectStorage: "Object Storage"
|
||||
useObjectStorage: "Object Storage verwenden"
|
||||
objectStorageBaseUrl: "Basis-URL"
|
||||
objectStorageBaseUrlDesc: "Die als Referenz verwendete URL. Verwendest du einen CDN oder Proxy, gib dessen URL an. Für S3 verwende 'https://<bucket>.s3.amazonaws.com'. Für GCS o.ä. verwende 'https://storage.googleapis.com/<bucket>'."
|
||||
objectStorageBucket: "Bucket"
|
||||
|
@ -827,7 +827,7 @@ overridedDeviceKind: "Gerätetyp"
|
|||
smartphone: "Smartphone"
|
||||
tablet: "Tablet"
|
||||
auto: "Automatisch"
|
||||
themeColor: "Instanzfarbe"
|
||||
themeColor: "Farbe der Instanz-Information"
|
||||
size: "Größe"
|
||||
numberOfColumn: "Spaltenanzahl"
|
||||
searchByGoogle: "Googlen"
|
||||
|
@ -840,6 +840,8 @@ tenMinutes: "10 Minuten"
|
|||
oneHour: "Eine Stunde"
|
||||
oneDay: "Einen Tag"
|
||||
oneWeek: "Eine Woche"
|
||||
reflectMayTakeTime: "Es kann etwas dauern, bis sich dies widerspiegelt."
|
||||
failedToFetchAccountInformation: "Benutzerkontoinformationen konnten nicht abgefragt werden"
|
||||
_emailUnavailable:
|
||||
used: "Diese Email-Adresse wird bereits verwendet"
|
||||
format: "Das Format dieser Email-Adresse ist ungültig"
|
||||
|
|
|
@ -827,7 +827,7 @@ overridedDeviceKind: "Device type"
|
|||
smartphone: "Smartphone"
|
||||
tablet: "Tablet"
|
||||
auto: "Auto"
|
||||
themeColor: "Theme Color"
|
||||
themeColor: "Instance Ticker Color"
|
||||
size: "Size"
|
||||
numberOfColumn: "Number of columns"
|
||||
searchByGoogle: "Google"
|
||||
|
@ -840,6 +840,8 @@ tenMinutes: "10 minutes"
|
|||
oneHour: "One hour"
|
||||
oneDay: "One day"
|
||||
oneWeek: "One week"
|
||||
reflectMayTakeTime: "It may take some time for this to be reflected."
|
||||
failedToFetchAccountInformation: "Could not fetch account information"
|
||||
_emailUnavailable:
|
||||
used: "This email address is already being used"
|
||||
format: "The format of this email address is invalid"
|
||||
|
|
|
@ -7,7 +7,7 @@ search: "Serĉi"
|
|||
notifications: "Sciigoj"
|
||||
username: "Uzantnomo"
|
||||
password: "Pasvorto"
|
||||
forgotPassword: "Ĉu vi forgesis pasvorton?"
|
||||
forgotPassword: "Ĉu vi forgesis vian pasvorton?"
|
||||
fetchingAsApObject: "Informpetado de la Fediverso…"
|
||||
ok: "Okej"
|
||||
gotIt: "Kompreni"
|
||||
|
@ -71,7 +71,7 @@ lists: "Listoj"
|
|||
noLists: "Neniu listo"
|
||||
note: "Noti"
|
||||
notes: "Notoj"
|
||||
following: "Sekvatoj"
|
||||
following: "Sekvi"
|
||||
followers: "Sekvantoj"
|
||||
followsYou: "Sekvas vin"
|
||||
createList: "Krei liston"
|
||||
|
@ -138,7 +138,7 @@ cacheRemoteFiles: "Stapli forajn dosierojn"
|
|||
flagAsBot: "Marki kiel esti uzanto de roboto"
|
||||
flagAsCat: "Marki kiel esti kato"
|
||||
flagAsCatDescription: "Flagu por montri ke la konton havas kato."
|
||||
flagShowTimelineReplies: "Montri respondon de notoj en templinio."
|
||||
flagShowTimelineReplies: "Montri la respondojn en la templinio"
|
||||
autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas"
|
||||
addAccount: "Aldoni konton"
|
||||
loginFailed: "Saluto malsukcesis"
|
||||
|
@ -239,7 +239,7 @@ agreeTo: "Mi akceptas {0}"
|
|||
tos: "Kondiĉoj de uzado"
|
||||
start: "Komenciĝi"
|
||||
home: "Hejma"
|
||||
remoteUserCaution: "Pro fora uzanto, la infomoj ne estas tuto."
|
||||
remoteUserCaution: "La informoj eblas nekompletaj ĉar estas fora uzanto."
|
||||
activity: "Aktiveco"
|
||||
images: "Bildoj"
|
||||
birthday: "Naskiĝdato"
|
||||
|
@ -412,7 +412,7 @@ usernameInvalidFormat: "La uzantnomo povas enhavi minusklajn kaj majusklajn lite
|
|||
tooShort: "Tro mallonga"
|
||||
tooLong: "Tro longa"
|
||||
weakPassword: "Malforta pasvorto"
|
||||
normalPassword: "Normala pasvorto"
|
||||
normalPassword: "Meza pasvorto"
|
||||
strongPassword: "Forta pasvorto"
|
||||
passwordMatched: "Konforma"
|
||||
passwordNotMatched: "Nekonforma"
|
||||
|
@ -493,11 +493,11 @@ deletedNote: "Forviŝita noto"
|
|||
invisibleNote: "Malpublikigita noto"
|
||||
enableInfiniteScroll: "Ebligi infinitan rulumon"
|
||||
visibility: "Videbleco"
|
||||
poll: "Enketo"
|
||||
poll: "Balot-enketo"
|
||||
useCw: "Kaŝi enhavo"
|
||||
enablePlayer: "Vidigi la filmeton"
|
||||
disablePlayer: "Malfermi la filmeton"
|
||||
expandTweet: "Disvolvi pepon"
|
||||
expandTweet: "Disvolvi la pepon"
|
||||
themeEditor: "Redaktilo de koloraroj"
|
||||
description: "Priskribo"
|
||||
describeFile: "Priskribi la bildon"
|
||||
|
@ -506,7 +506,7 @@ author: "Aŭtoro"
|
|||
manage: "Bonteni"
|
||||
plugins: "Kromaĵoj"
|
||||
deck: "Kartaro"
|
||||
useFullReactionPicker: "Uzi la tuton de la elektilon de reagoj"
|
||||
useFullReactionPicker: "Uzi la tuton de la elektilo de reagoj"
|
||||
width: "Larĝeco"
|
||||
height: "Alteco"
|
||||
large: "Granda"
|
||||
|
@ -530,6 +530,7 @@ smtpPort: "Pordo"
|
|||
smtpUser: "Uzantnomo"
|
||||
smtpPass: "Pasvorto"
|
||||
wordMute: "Silentigi specifajn vortojn"
|
||||
instanceMute: "Nodoj silentigitaj"
|
||||
userSaysSomething: "{name} diras ion"
|
||||
makeActive: "Aktivigi"
|
||||
display: "Vidi"
|
||||
|
@ -548,7 +549,11 @@ regenerateLoginToken: "Regeneri la aŭtentikigan pecon"
|
|||
fileIdOrUrl: "Dosiera identigilo aŭ URL"
|
||||
behavior: "Konduto"
|
||||
sample: "Ekzemplo"
|
||||
abuseReports: "Raportoj"
|
||||
reportAbuse: "Raportoj"
|
||||
reportAbuseOf: "raporti {name}n"
|
||||
reporter: "Informanto"
|
||||
reporterOrigin: "Raportanto"
|
||||
send: "Sendi"
|
||||
openInNewTab: "Malfermi en nova langeto"
|
||||
editTheseSettingsMayBreakAccount: "Redakti tiujn agordojn povas damaĝi vian konton."
|
||||
|
@ -634,6 +639,7 @@ offline: "Forkonektita"
|
|||
notRecommended: "Evitindaj"
|
||||
instanceBlocking: "Bloki specifajn nodojn"
|
||||
selectAccount: "Elekti konton"
|
||||
switchAccount: "Ŝanĝi konton"
|
||||
user: "Uzantoj"
|
||||
administration: "Bontenado"
|
||||
accounts: "Kontoj"
|
||||
|
@ -644,6 +650,7 @@ shareWithNote: "Kundividi en noto"
|
|||
ads: "Reklamaĵo"
|
||||
expiration: "Limtempo"
|
||||
memo: "Memorigilo"
|
||||
priority: "Prioritato"
|
||||
high: "Alta"
|
||||
middle: "Meza"
|
||||
low: "Malalta"
|
||||
|
@ -677,6 +684,7 @@ unmuteThread: "Malsilentigi la mesaĝaron"
|
|||
ffVisibility: "Videbleco de viaj sekvatoj/sekvantoj"
|
||||
ffVisibilityDescription: "Oni permesas agordi tiuln kiuj povas vidi la homojn kiujn vi sekvas, kaj la homojn kiuj sekvas vin."
|
||||
continueThread: "Pli vidi la mesaĝaron"
|
||||
deleteAccountConfirm: "La konto estos forviŝita. Ĉu vi daŭrigas fari?"
|
||||
incorrectPassword: "Nevalida pasvorto"
|
||||
voteConfirm: "Ĉu vi voĉdonas {choice}n?"
|
||||
hide: "Kaŝi"
|
||||
|
@ -684,14 +692,19 @@ leaveGroup: "Eliĝi el la grupo"
|
|||
leaveGroupConfirm: "Ĉu vi certas ke vi volas eliĝi el la grupo {name}?"
|
||||
welcomeBackWithName: "Bonrevenon, {name}!"
|
||||
clickToFinishEmailVerification: "Volu klaki [{ok}] por fini konfirmon de via retadreso."
|
||||
overridedDeviceKind: "tipo de aparato"
|
||||
smartphone: "Saĝtelefono"
|
||||
tablet: "Platkomputilo"
|
||||
auto: "Aŭtomate"
|
||||
size: "Grandeco"
|
||||
searchByGoogle: "Serĉi en Google-Serĉo"
|
||||
tenMinutes: "10 minutoj"
|
||||
oneHour: "1 horo"
|
||||
oneDay: "1 tago"
|
||||
oneWeek: "1 semajno"
|
||||
mutePeriod: "Daŭro de silentigo"
|
||||
indefinitely: "Sen limdato"
|
||||
tenMinutes: "Je 10 minutoj"
|
||||
oneHour: "Je 1 horo"
|
||||
oneDay: "Je 1 tago"
|
||||
oneWeek: "Je 1 semajno"
|
||||
failedToFetchAccountInformation: "Malsukcesas akiri informon de konto"
|
||||
_emailUnavailable:
|
||||
used: "La retpoŝto jam estas uzita."
|
||||
format: "Nevalida formato."
|
||||
|
@ -834,7 +847,6 @@ _ago:
|
|||
_time:
|
||||
second: "sek"
|
||||
minute: "min"
|
||||
hour: "hor"
|
||||
day: "Tago"
|
||||
_tutorial:
|
||||
title: "Uzado de Misskey"
|
||||
|
@ -893,16 +905,15 @@ _cw:
|
|||
chars: "{count} literoj"
|
||||
files: "{count} dosiero(j)"
|
||||
_poll:
|
||||
choiceN: "Ebla voĉdono {n}"
|
||||
choiceN: "Balotilo {n}"
|
||||
noMore: "Oni ne povas aldoni pli"
|
||||
canMultipleVote: "Permesi plurelekton"
|
||||
expiration: "Limtempo"
|
||||
deadlineTime: "hor"
|
||||
duration: "Daŭro"
|
||||
infinite: "Por ĉiam"
|
||||
votesCount: "{n} voĉoj"
|
||||
totalVotes: "Sume {n} voĉoj"
|
||||
vote: "Voĉdoni"
|
||||
showResult: "Vidi la rezultojn"
|
||||
showResult: "Vidi rezultojn"
|
||||
voted: "Voĉdonita"
|
||||
closed: "Finita"
|
||||
_visibility:
|
||||
|
@ -923,7 +934,7 @@ _postForm:
|
|||
_placeholders:
|
||||
a: "Kiel vi fartas?"
|
||||
b: "Kio okazis ĉirkaŭ vi?"
|
||||
c: "Kio estas sur via penso?"
|
||||
c: "Kion vi pensas?"
|
||||
d: "Kion vi volas diri?"
|
||||
e: "Komencu skribi tie"
|
||||
_profile:
|
||||
|
@ -1115,6 +1126,7 @@ _notification:
|
|||
youReceivedFollowRequest: "Vi ricevis peton de sekvado"
|
||||
yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
|
||||
youWereInvitedToGroup: "Invitita al grupo"
|
||||
pollEnded: "La rezulto de la balot-enketo estas disponebla"
|
||||
_types:
|
||||
all: "Ĉio"
|
||||
follow: "Novaj sekvantoj"
|
||||
|
@ -1123,7 +1135,8 @@ _notification:
|
|||
renote: "Plusendoj"
|
||||
quote: "Citi"
|
||||
reaction: "Reagoj"
|
||||
pollVote: "Voĉdonoj en balotoj"
|
||||
pollVote: "Voĉdonoj en balot-enketo"
|
||||
pollEnded: "Enketo finiĝis"
|
||||
receiveFollowRequest: "Ricevi peton de sekvado"
|
||||
followRequestAccepted: "Akceptita peto de sekvado"
|
||||
groupInvited: "Invitita al grupo"
|
||||
|
|
|
@ -1216,7 +1216,7 @@ _poll:
|
|||
votesCount: "{n} votes"
|
||||
totalVotes: "{n} votes au total"
|
||||
vote: "Voter"
|
||||
showResult: "Voir les résultats"
|
||||
showResult: "Voir résultats"
|
||||
voted: "Déjà voté"
|
||||
closed: "Terminé"
|
||||
remainingDays: "{d} jours, {h} heures restantes"
|
||||
|
|
|
@ -592,6 +592,7 @@ smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP"
|
|||
smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS"
|
||||
testEmail: "Tes pengiriman surel"
|
||||
wordMute: "Bisukan kata"
|
||||
regexpError: "Kesalahan ekspresi reguler"
|
||||
instanceMute: "Bisuka instansi"
|
||||
userSaysSomething: "{name} mengatakan sesuatu"
|
||||
makeActive: "Aktifkan"
|
||||
|
@ -825,8 +826,20 @@ overridedDeviceKind: "Tipe perangkat"
|
|||
smartphone: "Ponsel"
|
||||
tablet: "Tablet"
|
||||
auto: "Otomatis"
|
||||
themeColor: "Warna Tema"
|
||||
size: "Ukuran"
|
||||
numberOfColumn: "Jumlah per kolom"
|
||||
searchByGoogle: "Penelusuran"
|
||||
instanceDefaultLightTheme: "Bawaan instan tema terang"
|
||||
instanceDefaultDarkTheme: "Bawaan instan tema gelap"
|
||||
instanceDefaultThemeDescription: "Masukkan kode tema di format obyek."
|
||||
mutePeriod: "Batas waktu bisu"
|
||||
indefinitely: "Selamanya"
|
||||
tenMinutes: "10 Menit"
|
||||
oneHour: "1 Jam"
|
||||
oneDay: "1 Hari"
|
||||
oneWeek: "1 Bulan"
|
||||
failedToFetchAccountInformation: "Gagal untuk mendapatkan informasi akun"
|
||||
_emailUnavailable:
|
||||
used: "Alamat surel ini telah digunakan"
|
||||
format: "Format tidak valid."
|
||||
|
@ -1599,6 +1612,7 @@ _notification:
|
|||
youReceivedFollowRequest: "Kamu menerima permintaan mengikuti"
|
||||
yourFollowRequestAccepted: "Permintaan mengikuti kamu telah diterima"
|
||||
youWereInvitedToGroup: "Telah diundang ke grup"
|
||||
pollEnded: "Hasil Kuesioner telah keluar"
|
||||
_types:
|
||||
all: "Semua"
|
||||
follow: "Ikuti"
|
||||
|
@ -1608,6 +1622,7 @@ _notification:
|
|||
quote: "Kutip"
|
||||
reaction: "Reaksi"
|
||||
pollVote: "Memilih di angket"
|
||||
pollEnded: "Jajak pendapat berakhir"
|
||||
receiveFollowRequest: "Permintaan mengikuti diterima"
|
||||
followRequestAccepted: "Permintaan mengikuti disetujui"
|
||||
groupInvited: "Diundang ke grup"
|
||||
|
|
|
@ -840,6 +840,8 @@ tenMinutes: "10分"
|
|||
oneHour: "1時間"
|
||||
oneDay: "1日"
|
||||
oneWeek: "1週間"
|
||||
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
|
||||
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
|
||||
|
||||
_emailUnavailable:
|
||||
used: "既に使用されています"
|
||||
|
|
|
@ -119,6 +119,23 @@ unblock: "Deblokkeren"
|
|||
suspend: "Opschorten"
|
||||
unsuspend: "Heractiveren"
|
||||
blockConfirm: "Weet je zeker dat je dit account wil blokkeren?"
|
||||
unblockConfirm: "Ben je zeker dat je deze account wil blokkeren?"
|
||||
suspendConfirm: "Ben je zeker dat je deze account wil suspenderen?"
|
||||
unsuspendConfirm: "Ben je zeker dat je deze account wil opnieuw aanstellen?"
|
||||
flagAsBot: "Markeer dit account als een robot."
|
||||
flagAsBotDescription: "Als dit account van een programma wordt beheerd, zet deze vlag aan. Het aanzetten helpt andere ontwikkelaars om bijvoorbeeld onbedoelde feedback loops te doorbreken of om Misskey meer geschikt te maken."
|
||||
flagAsCat: "Markeer dit account als een kat."
|
||||
flagAsCatDescription: "Zet deze vlag aan als je wilt aangeven dat dit account een kat is."
|
||||
flagShowTimelineReplies: "Toon antwoorden op de tijdlijn."
|
||||
flagShowTimelineRepliesDescription: "Als je dit vlag aanzet, toont de tijdlijn ook antwoorden op andere en niet alleen jouw eigen notities."
|
||||
autoAcceptFollowed: "Accepteer verzoeken om jezelf te volgen vanzelf als je de verzoeker al volgt."
|
||||
addAccount: "Account toevoegen"
|
||||
loginFailed: "Aanmelding mislukt."
|
||||
showOnRemote: "Toon op de externe instantie."
|
||||
general: "Algemeen"
|
||||
wallpaper: "Achtergrond"
|
||||
setWallpaper: "Achtergrond instellen"
|
||||
removeWallpaper: "Achtergrond verwijderen"
|
||||
searchWith: "Zoeken: {q}"
|
||||
youHaveNoLists: "Je hebt geen lijsten"
|
||||
followConfirm: "Weet je zeker dat je {name} wilt volgen?"
|
||||
|
@ -205,6 +222,8 @@ resetAreYouSure: "Resetten?"
|
|||
saved: "Opgeslagen"
|
||||
messaging: "Chat"
|
||||
upload: "Uploaden"
|
||||
keepOriginalUploading: "Origineel beeld behouden."
|
||||
keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert."
|
||||
fromDrive: "Van schijf"
|
||||
fromUrl: "Van URL"
|
||||
uploadFromUrl: "Uploaden vanaf een URL"
|
||||
|
@ -245,9 +264,36 @@ renameFile: "Wijzig bestandsnaam"
|
|||
folderName: "Mapnaam"
|
||||
createFolder: "Map aanmaken"
|
||||
renameFolder: "Map hernoemen"
|
||||
deleteFolder: "Map verwijderen"
|
||||
addFile: "Bestand toevoegen"
|
||||
emptyDrive: "Jouw Drive is leeg."
|
||||
emptyFolder: "Deze map is leeg"
|
||||
unableToDelete: "Kan niet worden verwijderd"
|
||||
inputNewFileName: "Voer een nieuwe naam in"
|
||||
copyUrl: "URL kopiëren"
|
||||
rename: "Hernoemen"
|
||||
avatar: "Avatar"
|
||||
banner: "Banner"
|
||||
nsfw: "NSFW"
|
||||
whenServerDisconnected: "Wanneer de verbinding met de server wordt onderbroken"
|
||||
disconnectedFromServer: "Verbinding met de server onderbroken."
|
||||
inMb: "in megabytes"
|
||||
pinnedNotes: "Vastgemaakte notitie"
|
||||
userList: "Lijsten"
|
||||
aboutMisskey: "Over Misskey"
|
||||
administrator: "Beheerder"
|
||||
token: "Token"
|
||||
securityKeyName: "Sleutelnaam"
|
||||
registerSecurityKey: "Zekerheids-Sleutel registreren"
|
||||
lastUsed: "Laatst gebruikt"
|
||||
unregister: "Uitschrijven"
|
||||
passwordLessLogin: "Inloggen zonder wachtwoord"
|
||||
resetPassword: "Wachtwoord terugzetten"
|
||||
newPasswordIs: "Het nieuwe wachtwoord is „{password}”."
|
||||
reduceUiAnimation: "Verminder beweging in de UI"
|
||||
share: "Delen"
|
||||
notFound: "Niet gevonden"
|
||||
cacheClear: "Cache verwijderen"
|
||||
smtpHost: "Server"
|
||||
smtpUser: "Gebruikersnaam"
|
||||
smtpPass: "Wachtwoord"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
_lang_: "Português"
|
||||
headlineMisskey: "Rede conectada por notas"
|
||||
introMisskey: "Bem-vindo! Misskey é um serviço de microblogue descentralizado de código aberto.\nCria \"notas\" e partilha o que te ocorre com todos à tua volta. 📡\nCom \"reações\" podes também expressar logo o que sentes às notas de todos. 👍\nExploremos um novo mundo! 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Pesquisar"
|
||||
notifications: "Notificações"
|
||||
|
@ -22,6 +23,7 @@ otherSettings: "Outras configurações"
|
|||
openInWindow: "Abrir numa janela"
|
||||
profile: "Perfil"
|
||||
timeline: "Timeline"
|
||||
noAccountDescription: "Este usuário não tem uma descrição."
|
||||
login: "Iniciar sessão"
|
||||
loggingIn: "Iniciando sessão…"
|
||||
logout: "Sair"
|
||||
|
@ -29,8 +31,12 @@ signup: "Registrar-se"
|
|||
uploading: "Enviando…"
|
||||
save: "Guardar"
|
||||
users: "Usuários"
|
||||
addUser: "Adicionar usuário"
|
||||
favorite: "Favoritar"
|
||||
favorites: "Favoritar"
|
||||
unfavorite: "Remover dos favoritos"
|
||||
favorited: "Adicionado aos favoritos."
|
||||
alreadyFavorited: "Já adicionado aos favoritos."
|
||||
showMore: "Ver mais"
|
||||
youGotNewFollower: "Você tem um novo seguidor"
|
||||
followRequestAccepted: "Pedido de seguir aceito"
|
||||
|
|
|
@ -449,6 +449,45 @@ groupInvited: "Ai fost invitat într-un grup"
|
|||
aboutX: "Despre {x}"
|
||||
useOsNativeEmojis: "Folosește emojiuri native OS-ului"
|
||||
disableDrawer: "Nu folosi meniuri în stil sertar"
|
||||
youHaveNoGroups: "Nu ai niciun grup"
|
||||
joinOrCreateGroup: "Primește o invitație într-un grup sau creează unul nou."
|
||||
noHistory: "Nu există istoric"
|
||||
signinHistory: "Istoric autentificări"
|
||||
disableAnimatedMfm: "Dezactivează MFM cu animații"
|
||||
doing: "Se procesează..."
|
||||
category: "Categorie"
|
||||
tags: "Etichete"
|
||||
docSource: "Sursa acestui document"
|
||||
createAccount: "Creează un cont"
|
||||
existingAccount: "Cont existent"
|
||||
regenerate: "Regenerează"
|
||||
fontSize: "Mărimea fontului"
|
||||
noFollowRequests: "Nu ai nicio cerere de urmărire în așteptare"
|
||||
openImageInNewTab: "Deschide imaginile în taburi noi"
|
||||
dashboard: "Panou de control"
|
||||
local: "Local"
|
||||
remote: "Extern"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Schimbări până săptămâna trecută"
|
||||
dayOverDayChanges: "Schimbări până ieri"
|
||||
appearance: "Aspect"
|
||||
clientSettings: "Setări client"
|
||||
accountSettings: "Setări cont"
|
||||
promotion: "Promovat"
|
||||
promote: "Promovează"
|
||||
numberOfDays: "Numărul zilelor"
|
||||
hideThisNote: "Ascunde această notă"
|
||||
showFeaturedNotesInTimeline: "Arată notele recomandate în cronologii"
|
||||
objectStorage: "Object Storage"
|
||||
useObjectStorage: "Folosește Object Storage"
|
||||
objectStorageBaseUrl: "URL de bază"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "Te rog specifică numele bucket-ului furnizorului tău."
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStoragePrefixDesc: "Fișierele vor fi stocate sub directoare cu acest prefix."
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageRegion: "Regiune"
|
||||
objectStorageUseSSL: "Folosește SSl"
|
||||
sounds: "Sunete"
|
||||
listen: "Ascultă"
|
||||
none: "Nimic"
|
||||
|
@ -471,6 +510,18 @@ sort: "Sortează"
|
|||
ascendingOrder: "Crescător"
|
||||
descendingOrder: "Descrescător"
|
||||
scratchpad: "Scratchpad"
|
||||
scratchpadDescription: "Scratchpad-ul oferă un mediu de experimentare în AiScript. Poți scrie, executa și verifica rezultatele acestuia interacționând cu Misskey în el."
|
||||
output: "Ieșire"
|
||||
script: "Script"
|
||||
disablePagesScript: "Dezactivează AiScript în Pagini"
|
||||
updateRemoteUser: "Actualizează informațiile utilizatorului extern"
|
||||
deleteAllFiles: "Șterge toate fișierele"
|
||||
deleteAllFilesConfirm: "Ești sigur că vrei să ștergi toate fișierele?"
|
||||
removeAllFollowing: "Dezurmărește toți utilizatorii urmăriți"
|
||||
removeAllFollowingDescription: "Asta va dez-urmări toate conturile din {host}. Te rog execută asta numai dacă instanța, de ex., nu mai există."
|
||||
userSuspended: "Acest utilizator a fost suspendat."
|
||||
userSilenced: "Acest utilizator a fost setat silențios."
|
||||
yourAccountSuspendedTitle: "Acest cont a fost suspendat"
|
||||
smtpHost: "Gazdă"
|
||||
smtpUser: "Nume de utilizator"
|
||||
smtpPass: "Parolă"
|
||||
|
|
|
@ -839,6 +839,8 @@ tenMinutes: "10 minút"
|
|||
oneHour: "1 hodina"
|
||||
oneDay: "1 deň"
|
||||
oneWeek: "1 týždeň"
|
||||
reflectMayTakeTime: "Zmeny môžu chvíľu trvať kým sa prejavia."
|
||||
failedToFetchAccountInformation: "Nepodarilo sa načítať informácie o účte."
|
||||
_emailUnavailable:
|
||||
used: "Táto emailová adresa sa už používa"
|
||||
format: "Formát emailovej adresy je nesprávny"
|
||||
|
|
|
@ -8,12 +8,12 @@ notifications: "通知"
|
|||
username: "用户名"
|
||||
password: "密码"
|
||||
forgotPassword: "忘记密码"
|
||||
fetchingAsApObject: "联合查询"
|
||||
fetchingAsApObject: "在联邦宇宙查询中..."
|
||||
ok: "OK"
|
||||
gotIt: "我明白了"
|
||||
cancel: "取消"
|
||||
enterUsername: "输入用户名"
|
||||
renotedBy: "由 {user} 转推"
|
||||
renotedBy: "由 {user} 转贴"
|
||||
noNotes: "没有帖文"
|
||||
noNotifications: "无通知"
|
||||
instance: "实例"
|
||||
|
@ -69,7 +69,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导
|
|||
importRequested: "导入请求已提交,这可能需要花一点时间。"
|
||||
lists: "列表"
|
||||
noLists: "列表为空"
|
||||
note: "帖子"
|
||||
note: "发帖"
|
||||
notes: "帖子"
|
||||
following: "关注中"
|
||||
followers: "关注者"
|
||||
|
@ -85,7 +85,7 @@ serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
|
|||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
makeFollowManuallyApprove: "关注者的关注请求需要批准"
|
||||
makeFollowManuallyApprove: "关注请求需要批准"
|
||||
defaultNoteVisibility: "默认可见性"
|
||||
follow: "关注"
|
||||
followRequest: "关注申请"
|
||||
|
@ -143,7 +143,7 @@ flagAsCat: "将这个账户设定为一只猫"
|
|||
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。"
|
||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||
autoAcceptFollowed: "自动允许关注"
|
||||
autoAcceptFollowed: "自动允许关注者的关注"
|
||||
addAccount: "添加账户"
|
||||
loginFailed: "登录失败"
|
||||
showOnRemote: "转到所在实例显示"
|
||||
|
@ -162,7 +162,7 @@ recipient: "收件人"
|
|||
annotation: "注解"
|
||||
federation: "联合"
|
||||
instances: "实例"
|
||||
registeredAt: "初次观察"
|
||||
registeredAt: "初次观测"
|
||||
latestRequestSentAt: "上次发送的请求"
|
||||
latestRequestReceivedAt: "上次收到的请求"
|
||||
latestStatus: "最后状态"
|
||||
|
@ -254,7 +254,7 @@ agreeTo: "{0}人同意"
|
|||
tos: "服务条款"
|
||||
start: "开始"
|
||||
home: "首页"
|
||||
remoteUserCaution: "由于是远程用户,信息不完整。"
|
||||
remoteUserCaution: "由于此用户来自其它实例,显示的信息可能不完整。"
|
||||
activity: "活动"
|
||||
images: "图片"
|
||||
birthday: "生日"
|
||||
|
@ -372,7 +372,7 @@ recentlyUpdatedUsers: "最近投稿的用户"
|
|||
recentlyRegisteredUsers: "最近登录的用户"
|
||||
recentlyDiscoveredUsers: "最近发现的用户"
|
||||
exploreUsersCount: "有{count}个用户"
|
||||
exploreFediverse: "探索Fediverse"
|
||||
exploreFediverse: "探索联邦宇宙"
|
||||
popularTags: "热门标签"
|
||||
userList: "列表"
|
||||
about: "关于"
|
||||
|
@ -561,7 +561,7 @@ manage: "管理"
|
|||
plugins: "插件"
|
||||
deck: "Deck"
|
||||
undeck: "取消Deck"
|
||||
useBlurEffectForModal: "模态框使用模糊效果"
|
||||
useBlurEffectForModal: "对话框使用模糊效果"
|
||||
useFullReactionPicker: "使用全功能的回应工具栏"
|
||||
width: "宽度"
|
||||
height: "高度"
|
||||
|
@ -840,6 +840,8 @@ tenMinutes: "10分钟"
|
|||
oneHour: "1小时"
|
||||
oneDay: "1天"
|
||||
oneWeek: "1周"
|
||||
reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
|
||||
failedToFetchAccountInformation: "获取账户信息失败"
|
||||
_emailUnavailable:
|
||||
used: "已经被使用过"
|
||||
format: "无效的格式"
|
||||
|
@ -904,7 +906,7 @@ _nsfw:
|
|||
_mfm:
|
||||
cheatSheet: "MFM代码速查表"
|
||||
intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。"
|
||||
dummy: "通过Misskey扩展Fediverse的世界"
|
||||
dummy: "通过Misskey扩展联邦宇宙的世界"
|
||||
mention: "提及"
|
||||
mentionDescription: "可以使用 @+用户名 来指示特定用户"
|
||||
hashtag: "话题标签"
|
||||
|
@ -967,7 +969,7 @@ _mfm:
|
|||
rotateDescription: "旋转指定的角度。"
|
||||
_instanceTicker:
|
||||
none: "不显示"
|
||||
remote: "仅显示远程用户的"
|
||||
remote: "仅远程用户"
|
||||
always: "始终显示"
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自动重载"
|
||||
|
@ -1051,7 +1053,7 @@ _theme:
|
|||
mention: "提及"
|
||||
mentionMe: "提及"
|
||||
renote: "转发"
|
||||
modalBg: "模态框背景"
|
||||
modalBg: "对话框背景"
|
||||
divider: "分割线"
|
||||
scrollbarHandle: "滚动条"
|
||||
scrollbarHandleHover: "滚动条(悬停)"
|
||||
|
@ -1238,7 +1240,7 @@ _visibility:
|
|||
publicDescription: "您的帖子将出现在全局时间线上"
|
||||
home: "首页"
|
||||
homeDescription: "仅发送至首页的时间线"
|
||||
followers: "关注者"
|
||||
followers: "仅关注者"
|
||||
followersDescription: "仅发送至关注者"
|
||||
specified: "指定用户"
|
||||
specifiedDescription: "仅发送至指定用户"
|
||||
|
|
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "12.108.1",
|
||||
"version": "12.109.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -13,8 +13,7 @@
|
|||
"start": "cd packages/backend && node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"init": "npm run migrate",
|
||||
"ormconfig": "node ./packages/backend/ormconfig.js",
|
||||
"migrate": "cd packages/backend && npx typeorm migration:run",
|
||||
"migrate": "cd packages/backend && npx typeorm migration:run -d ormconfig.js",
|
||||
"migrateandstart": "npm run migrate && npm run start",
|
||||
"gulp": "gulp build",
|
||||
"watch": "npm run dev",
|
||||
|
@ -42,10 +41,10 @@
|
|||
"js-yaml": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.14.0",
|
||||
"@typescript-eslint/parser": "5.17.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "9.5.0",
|
||||
"cypress": "9.5.3",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"typescript": "4.6.2"
|
||||
"typescript": "4.6.3"
|
||||
}
|
||||
}
|
||||
|
|
19
packages/backend/migration/1648548247382-webhook.js
Normal file
19
packages/backend/migration/1648548247382-webhook.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
export class webhook1648548247382 {
|
||||
name = 'webhook1648548247382'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "webhook" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "on" character varying(128) array NOT NULL DEFAULT '{}', "url" character varying(1024) NOT NULL, "secret" character varying(1024) NOT NULL, "active" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6765510c2d078db49632b59020" PRIMARY KEY ("id")); COMMENT ON COLUMN "webhook"."createdAt" IS 'The created date of the Antenna.'; COMMENT ON COLUMN "webhook"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "webhook"."name" IS 'The name of the Antenna.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f272c8c8805969e6a6449c77b3" ON "webhook" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8063a0586ed1dfbe86e982d961" ON "webhook" ("on") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5a056076f76b2efe08216ba655" ON "webhook" ("active") `);
|
||||
await queryRunner.query(`ALTER TABLE "webhook" ADD CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "webhook" DROP CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_5a056076f76b2efe08216ba655"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8063a0586ed1dfbe86e982d961"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f272c8c8805969e6a6449c77b3"`);
|
||||
await queryRunner.query(`DROP TABLE "webhook"`);
|
||||
}
|
||||
}
|
14
packages/backend/migration/1648816172177-webhook-2.js
Normal file
14
packages/backend/migration/1648816172177-webhook-2.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
export class webhook21648816172177 {
|
||||
name = 'webhook21648816172177'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestSentAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestStatus" integer`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestStatus"`);
|
||||
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestSentAt"`);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import config from './built/config/index.js';
|
||||
import { entities } from './built/db/postgre.js';
|
||||
|
||||
export default {
|
||||
export default new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
|
@ -11,7 +12,4 @@ export default {
|
|||
extra: config.db.extra,
|
||||
entities: entities,
|
||||
migrations: ['migration/*.js'],
|
||||
cli: {
|
||||
migrationsDir: 'migration'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"init": "npm run migrate",
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "eslint --quiet src/**/*.ts",
|
||||
|
@ -15,7 +14,7 @@
|
|||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "13.1.0",
|
||||
"@discordapp/twemoji": "13.1.1",
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@koa/cors": "3.1.0",
|
||||
"@koa/multer": "3.0.0",
|
||||
|
@ -31,7 +30,7 @@
|
|||
"@types/jsdom": "16.2.14",
|
||||
"@types/jsonld": "1.5.6",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa-bodyparser": "4.3.6",
|
||||
"@types/koa-bodyparser": "4.3.7",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
"@types/koa-logger": "3.1.2",
|
||||
|
@ -42,7 +41,7 @@
|
|||
"@types/koa__multer": "2.0.4",
|
||||
"@types/koa__router": "8.0.11",
|
||||
"@types/mocha": "9.1.0",
|
||||
"@types/node": "17.0.21",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.4",
|
||||
"@types/oauth": "0.9.1",
|
||||
|
@ -56,8 +55,8 @@
|
|||
"@types/redis": "4.0.11",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.6.2",
|
||||
"@types/sharp": "0.29.5",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
"@types/sharp": "0.30.0",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/speakeasy": "2.0.7",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
|
@ -65,19 +64,20 @@
|
|||
"@types/uuid": "8.3.4",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.14.0",
|
||||
"@typescript-eslint/parser": "5.14.0",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.17.0",
|
||||
"@typescript-eslint/parser": "5.17.0",
|
||||
"@bull-board/koa": "3.10.2",
|
||||
"abort-controller": "3.0.0",
|
||||
"ajv": "8.10.0",
|
||||
"ajv": "8.11.0",
|
||||
"archiver": "5.3.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1079.0",
|
||||
"aws-sdk": "2.1105.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.10.0",
|
||||
"bull": "4.7.0",
|
||||
"bull": "4.8.1",
|
||||
"cacheable-lookup": "6.0.4",
|
||||
"cafy": "15.2.1",
|
||||
"cbor": "8.1.0",
|
||||
|
@ -89,19 +89,19 @@
|
|||
"date-fns": "2.28.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "8.10.0",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "17.1.1",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"got": "12.0.1",
|
||||
"got": "12.0.3",
|
||||
"hpagent": "0.1.2",
|
||||
"http-signature": "1.3.6",
|
||||
"ip-cidr": "3.0.4",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "19.0.0",
|
||||
"json5": "2.2.0",
|
||||
"json5": "2.2.1",
|
||||
"json5-loader": "4.0.1",
|
||||
"jsonld": "5.2.0",
|
||||
"jsrsasign": "8.0.20",
|
||||
|
@ -115,14 +115,14 @@
|
|||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"mfm-js": "0.21.0",
|
||||
"mime-types": "2.1.34",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.14",
|
||||
"mocha": "9.2.1",
|
||||
"mocha": "9.2.2",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"multer": "1.4.4",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.2.2",
|
||||
"nodemailer": "6.7.2",
|
||||
"node-fetch": "3.2.3",
|
||||
"nodemailer": "6.7.3",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "6.0.1",
|
||||
"pg": "8.7.3",
|
||||
|
@ -145,24 +145,25 @@
|
|||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.7.0",
|
||||
"sharp": "0.30.2",
|
||||
"semver": "7.3.5",
|
||||
"sharp": "0.30.3",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "3.3.1",
|
||||
"summaly": "2.5.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.11.6",
|
||||
"systeminformation": "5.11.9",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.2.7",
|
||||
"ts-loader": "9.2.8",
|
||||
"ts-node": "10.7.0",
|
||||
"tsc-alias": "1.4.1",
|
||||
"tsconfig-paths": "3.13.0",
|
||||
"twemoji-parser": "13.1.0",
|
||||
"typeorm": "0.2.45",
|
||||
"typescript": "4.6.2",
|
||||
"tsconfig-paths": "3.14.1",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.4",
|
||||
"typescript": "4.6.3",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "8.3.2",
|
||||
|
@ -172,7 +173,7 @@
|
|||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.83",
|
||||
"@redocly/openapi-core": "1.0.0-beta.91",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"cross-env": "7.0.3",
|
||||
"execa": "6.1.0"
|
||||
|
|
|
@ -6,7 +6,7 @@ import cluster from 'node:cluster';
|
|||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as portscanner from 'portscanner';
|
||||
import { getConnection } from 'typeorm';
|
||||
import semver from 'semver';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
import loadConfig from '@/config/load.js';
|
||||
|
@ -14,7 +14,7 @@ import { Config } from '@/config/types.js';
|
|||
import { lessThan } from '@/prelude/array.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { initDb } from '../db/postgre.js';
|
||||
import { db, initDb } from '../db/postgre.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
@ -88,10 +88,6 @@ export async function masterMain() {
|
|||
}
|
||||
}
|
||||
|
||||
const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10));
|
||||
const requiredNodejsVersion = [11, 7, 0];
|
||||
const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion);
|
||||
|
||||
function showEnvironment(): void {
|
||||
const env = process.env.NODE_ENV;
|
||||
const logger = bootLogger.createSubLogger('env');
|
||||
|
@ -108,10 +104,11 @@ function showEnvironment(): void {
|
|||
function showNodejsVersion(): void {
|
||||
const nodejsLogger = bootLogger.createSubLogger('nodejs');
|
||||
|
||||
nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`);
|
||||
nodejsLogger.info(`Version ${process.version} detected.`);
|
||||
|
||||
if (!satisfyNodejsVersion) {
|
||||
nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true);
|
||||
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
|
||||
if (semver.lt(process.version, minVersion)) {
|
||||
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +143,7 @@ async function connectDb(): Promise<void> {
|
|||
try {
|
||||
dbLogger.info('Connecting...');
|
||||
await initDb();
|
||||
const v = await getConnection().query('SHOW server_version').then(x => x[0].server_version);
|
||||
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', null, true);
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
import pg from 'pg';
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
import { createConnection, Logger, getConnection } from 'typeorm';
|
||||
import { Logger, DataSource } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import config from '@/config/index.js';
|
||||
import { envOption } from '../env.js';
|
||||
|
||||
import { dbLogger } from './logger.js';
|
||||
|
||||
|
@ -61,7 +62,6 @@ import { Antenna } from '@/models/entities/antenna.js';
|
|||
import { AntennaNote } from '@/models/entities/antenna-note.js';
|
||||
import { PromoNote } from '@/models/entities/promo-note.js';
|
||||
import { PromoRead } from '@/models/entities/promo-read.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { Relay } from '@/models/entities/relay.js';
|
||||
import { MutedNote } from '@/models/entities/muted-note.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
|
@ -73,8 +73,9 @@ import { PasswordResetRequest } from '@/models/entities/password-reset-request.j
|
|||
import { UserPending } from '@/models/entities/user-pending.js';
|
||||
|
||||
import { entities as charts } from '@/services/chart/entities.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
|
||||
class MyCustomLogger implements Logger {
|
||||
private highlight(sql: string) {
|
||||
|
@ -84,9 +85,7 @@ class MyCustomLogger implements Logger {
|
|||
}
|
||||
|
||||
public logQuery(query: string, parameters?: any[]) {
|
||||
if (envOption.verbose) {
|
||||
sqlLogger.info(this.highlight(query));
|
||||
}
|
||||
sqlLogger.info(this.highlight(query).substring(0, 100));
|
||||
}
|
||||
|
||||
public logQueryError(error: string, query: string, parameters?: any[]) {
|
||||
|
@ -173,58 +172,55 @@ export const entities = [
|
|||
Ad,
|
||||
PasswordResetRequest,
|
||||
UserPending,
|
||||
Webhook,
|
||||
...charts,
|
||||
];
|
||||
|
||||
export function initDb(justBorrow = false, sync = false, forceRecreate = false) {
|
||||
if (!forceRecreate) {
|
||||
try {
|
||||
const conn = getConnection();
|
||||
return Promise.resolve(conn);
|
||||
} catch (e) {}
|
||||
}
|
||||
const log = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const log = process.env.NODE_ENV !== 'production';
|
||||
|
||||
return createConnection({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: {
|
||||
statement_timeout: 1000 * 10,
|
||||
...config.db.extra,
|
||||
export const db = new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: {
|
||||
statement_timeout: 1000 * 10,
|
||||
...config.db.extra,
|
||||
},
|
||||
synchronize: process.env.NODE_ENV === 'test',
|
||||
dropSchema: process.env.NODE_ENV === 'test',
|
||||
cache: !config.db.disableCache ? {
|
||||
type: 'redis',
|
||||
options: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.pass,
|
||||
prefix: `${config.redis.prefix}:query:`,
|
||||
db: config.redis.db || 0,
|
||||
},
|
||||
synchronize: process.env.NODE_ENV === 'test' || sync,
|
||||
dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
|
||||
cache: !config.db.disableCache ? {
|
||||
type: 'redis',
|
||||
options: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
password: config.redis.pass,
|
||||
prefix: `${config.redis.prefix}:query:`,
|
||||
db: config.redis.db || 0,
|
||||
},
|
||||
} : false,
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
entities: entities,
|
||||
});
|
||||
} : false,
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
maxQueryExecutionTime: 300,
|
||||
entities: entities,
|
||||
migrations: ['../../migration/*.js'],
|
||||
});
|
||||
|
||||
export async function initDb() {
|
||||
await db.connect();
|
||||
}
|
||||
|
||||
export async function resetDb() {
|
||||
const reset = async () => {
|
||||
const conn = await getConnection();
|
||||
const tables = await conn.query(`SELECT relname AS "table"
|
||||
const tables = await db.query(`SELECT relname AS "table"
|
||||
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||
AND C.relkind = 'r'
|
||||
AND nspname !~ '^pg_toast';`);
|
||||
for (const table of tables) {
|
||||
await conn.query(`DELETE FROM "${table.table}" CASCADE`);
|
||||
await db.query(`DELETE FROM "${table.table}" CASCADE`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export class Cache<T> {
|
||||
private cache: Map<string | null, { date: number; value: T; }>;
|
||||
public cache: Map<string | null, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
|
||||
constructor(lifetime: Cache<never>['lifetime']) {
|
||||
|
@ -28,16 +28,52 @@ export class Cache<T> {
|
|||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
if (validator) {
|
||||
if (validator(cachedValue)) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
} else {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
if (validator(cachedValue)) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
} else {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
import { Antenna } from '@/models/entities/antenna.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserListJoinings, UserGroupJoinings } from '@/models/index.js';
|
||||
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
|
||||
import { getFullApAccount } from './convert-host.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { Packed } from './schema.js';
|
||||
import { Cache } from './cache.js';
|
||||
|
||||
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
||||
/**
|
||||
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
||||
*/
|
||||
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
|
||||
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
||||
|
@ -23,15 +32,15 @@ export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'No
|
|||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await UserListJoinings.find({
|
||||
const listUsers = (await UserListJoinings.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'group') {
|
||||
const joining = await UserGroupJoinings.findOneOrFail(antenna.userGroupJoiningId!);
|
||||
const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! });
|
||||
|
||||
const groupUsers = (await UserGroupJoinings.find({
|
||||
const groupUsers = (await UserGroupJoinings.findBy({
|
||||
userGroupId: joining.userGroupId,
|
||||
})).map(x => x.userId);
|
||||
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { db } from '@/db/postgre.js';
|
||||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { getConnection } from 'typeorm';
|
||||
|
||||
let cache: Meta;
|
||||
|
||||
export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||
if (!noCache && cache) return cache;
|
||||
|
||||
return await getConnection().transaction(async transactionalEntityManager => {
|
||||
return await db.transaction(async transactionalEntityManager => {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
const meta = metas[0];
|
||||
|
||||
if (meta) {
|
||||
cache = meta;
|
||||
return meta;
|
||||
|
|
|
@ -5,5 +5,5 @@ import { Users } from '@/models/index.js';
|
|||
export async function fetchProxyAccount(): Promise<ILocalUser | null> {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.proxyAccountId == null) return null;
|
||||
return await Users.findOneOrFail(meta.proxyAccountId) as ILocalUser;
|
||||
return await Users.findOneByOrFail({ id: meta.proxyAccountId }) as ILocalUser;
|
||||
}
|
||||
|
|
|
@ -6,5 +6,5 @@ import { Cache } from './cache.js';
|
|||
const cache = new Cache<UserKeypair>(Infinity);
|
||||
|
||||
export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||
return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId));
|
||||
return await cache.fetch(userId, () => UserKeypairs.findOneByOrFail({ userId: userId }));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { In } from 'typeorm';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { Emojis } from '@/models/index.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
|
@ -52,9 +52,9 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu
|
|||
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
|
||||
if (name == null) return null;
|
||||
|
||||
const queryOrNull = async () => (await Emojis.findOne({
|
||||
const queryOrNull = async () => (await Emojis.findOneBy({
|
||||
name,
|
||||
host,
|
||||
host: host ?? IsNull(),
|
||||
})) || null;
|
||||
|
||||
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
|
@ -112,7 +112,7 @@ export async function prefetchEmojis(emojis: { name: string; host: string | null
|
|||
for (const host of hosts) {
|
||||
emojisQuery.push({
|
||||
name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
|
||||
host: host,
|
||||
host: host ?? IsNull(),
|
||||
});
|
||||
}
|
||||
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
|
||||
|
|
|
@ -3,6 +3,7 @@ import { emojiRegex } from './emoji-regex.js';
|
|||
import { fetchMeta } from './fetch-meta.js';
|
||||
import { Emojis } from '@/models/index.js';
|
||||
import { toPunyNullable } from './convert-host.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
|
@ -74,8 +75,8 @@ export async function toDbReaction(reaction?: string | null, reacterHost?: strin
|
|||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await Emojis.findOne({
|
||||
host: reacterHost || null,
|
||||
const emoji = await Emojis.findOneBy({
|
||||
host: reacterHost ?? IsNull(),
|
||||
name,
|
||||
});
|
||||
|
||||
|
|
49
packages/backend/src/misc/webhook-cache.ts
Normal file
49
packages/backend/src/misc/webhook-cache.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { Webhooks } from '@/models/index.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { subsdcriber } from '../db/redis.js';
|
||||
|
||||
let webhooksFetched = false;
|
||||
let webhooks: Webhook[] = [];
|
||||
|
||||
export async function getActiveWebhooks() {
|
||||
if (!webhooksFetched) {
|
||||
webhooks = await Webhooks.findBy({
|
||||
active: true,
|
||||
});
|
||||
webhooksFetched = true;
|
||||
}
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
subsdcriber.on('message', async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
webhooks.push(body);
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
webhooks[i] = body;
|
||||
} else {
|
||||
webhooks.push(body);
|
||||
}
|
||||
} else {
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
case 'webhookDeleted':
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -234,3 +234,9 @@ export interface ILocalUser extends User {
|
|||
export interface IRemoteUser extends User {
|
||||
host: string;
|
||||
}
|
||||
|
||||
export type CacheableLocalUser = ILocalUser;
|
||||
|
||||
export type CacheableRemoteUser = IRemoteUser;
|
||||
|
||||
export type CacheableUser = CacheableLocalUser | CacheableRemoteUser;
|
||||
|
|
73
packages/backend/src/models/entities/webhook.ts
Normal file
73
packages/backend/src/models/entities/webhook.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user.js';
|
||||
import { id } from '../id.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
|
||||
@Entity()
|
||||
export class Webhook {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Antenna.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
comment: 'The name of the Antenna.',
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
public on: (typeof webhookEventTypes)[number][];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public url: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public secret: string;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信日時
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestSentAt: Date | null;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信時のHTTPステータスコード
|
||||
*/
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestStatus: number | null;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { getRepository, getCustomRepository } from 'typeorm';
|
||||
import { } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
import { Announcement } from './entities/announcement.js';
|
||||
import { AnnouncementRead } from './entities/announcement-read.js';
|
||||
import { Instance } from './entities/instance.js';
|
||||
|
@ -62,66 +64,68 @@ import { Ad } from './entities/ad.js';
|
|||
import { PasswordResetRequest } from './entities/password-reset-request.js';
|
||||
import { UserPending } from './entities/user-pending.js';
|
||||
import { InstanceRepository } from './repositories/instance.js';
|
||||
import { Webhook } from './entities/webhook.js';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
export const Apps = getCustomRepository(AppRepository);
|
||||
export const Notes = getCustomRepository(NoteRepository);
|
||||
export const NoteFavorites = getCustomRepository(NoteFavoriteRepository);
|
||||
export const NoteWatchings = getRepository(NoteWatching);
|
||||
export const NoteThreadMutings = getRepository(NoteThreadMuting);
|
||||
export const NoteReactions = getCustomRepository(NoteReactionRepository);
|
||||
export const NoteUnreads = getRepository(NoteUnread);
|
||||
export const Polls = getRepository(Poll);
|
||||
export const PollVotes = getRepository(PollVote);
|
||||
export const Users = getCustomRepository(UserRepository);
|
||||
export const UserProfiles = getRepository(UserProfile);
|
||||
export const UserKeypairs = getRepository(UserKeypair);
|
||||
export const UserPendings = getRepository(UserPending);
|
||||
export const AttestationChallenges = getRepository(AttestationChallenge);
|
||||
export const UserSecurityKeys = getRepository(UserSecurityKey);
|
||||
export const UserPublickeys = getRepository(UserPublickey);
|
||||
export const UserLists = getCustomRepository(UserListRepository);
|
||||
export const UserListJoinings = getRepository(UserListJoining);
|
||||
export const UserGroups = getCustomRepository(UserGroupRepository);
|
||||
export const UserGroupJoinings = getRepository(UserGroupJoining);
|
||||
export const UserGroupInvitations = getCustomRepository(UserGroupInvitationRepository);
|
||||
export const UserNotePinings = getRepository(UserNotePining);
|
||||
export const UsedUsernames = getRepository(UsedUsername);
|
||||
export const Followings = getCustomRepository(FollowingRepository);
|
||||
export const FollowRequests = getCustomRepository(FollowRequestRepository);
|
||||
export const Instances = getCustomRepository(InstanceRepository);
|
||||
export const Emojis = getCustomRepository(EmojiRepository);
|
||||
export const DriveFiles = getCustomRepository(DriveFileRepository);
|
||||
export const DriveFolders = getCustomRepository(DriveFolderRepository);
|
||||
export const Notifications = getCustomRepository(NotificationRepository);
|
||||
export const Metas = getRepository(Meta);
|
||||
export const Mutings = getCustomRepository(MutingRepository);
|
||||
export const Blockings = getCustomRepository(BlockingRepository);
|
||||
export const SwSubscriptions = getRepository(SwSubscription);
|
||||
export const Hashtags = getCustomRepository(HashtagRepository);
|
||||
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
|
||||
export const RegistrationTickets = getRepository(RegistrationTicket);
|
||||
export const AuthSessions = getCustomRepository(AuthSessionRepository);
|
||||
export const AccessTokens = getRepository(AccessToken);
|
||||
export const Signins = getCustomRepository(SigninRepository);
|
||||
export const MessagingMessages = getCustomRepository(MessagingMessageRepository);
|
||||
export const Pages = getCustomRepository(PageRepository);
|
||||
export const PageLikes = getCustomRepository(PageLikeRepository);
|
||||
export const GalleryPosts = getCustomRepository(GalleryPostRepository);
|
||||
export const GalleryLikes = getCustomRepository(GalleryLikeRepository);
|
||||
export const ModerationLogs = getCustomRepository(ModerationLogRepository);
|
||||
export const Clips = getCustomRepository(ClipRepository);
|
||||
export const ClipNotes = getRepository(ClipNote);
|
||||
export const Antennas = getCustomRepository(AntennaRepository);
|
||||
export const AntennaNotes = getRepository(AntennaNote);
|
||||
export const PromoNotes = getRepository(PromoNote);
|
||||
export const PromoReads = getRepository(PromoRead);
|
||||
export const Relays = getCustomRepository(RelayRepository);
|
||||
export const MutedNotes = getRepository(MutedNote);
|
||||
export const Channels = getCustomRepository(ChannelRepository);
|
||||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
export const RegistryItems = getRepository(RegistryItem);
|
||||
export const Ads = getRepository(Ad);
|
||||
export const PasswordResetRequests = getRepository(PasswordResetRequest);
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
export const Apps = (AppRepository);
|
||||
export const Notes = (NoteRepository);
|
||||
export const NoteFavorites = (NoteFavoriteRepository);
|
||||
export const NoteWatchings = db.getRepository(NoteWatching);
|
||||
export const NoteThreadMutings = db.getRepository(NoteThreadMuting);
|
||||
export const NoteReactions = (NoteReactionRepository);
|
||||
export const NoteUnreads = db.getRepository(NoteUnread);
|
||||
export const Polls = db.getRepository(Poll);
|
||||
export const PollVotes = db.getRepository(PollVote);
|
||||
export const Users = (UserRepository);
|
||||
export const UserProfiles = db.getRepository(UserProfile);
|
||||
export const UserKeypairs = db.getRepository(UserKeypair);
|
||||
export const UserPendings = db.getRepository(UserPending);
|
||||
export const AttestationChallenges = db.getRepository(AttestationChallenge);
|
||||
export const UserSecurityKeys = db.getRepository(UserSecurityKey);
|
||||
export const UserPublickeys = db.getRepository(UserPublickey);
|
||||
export const UserLists = (UserListRepository);
|
||||
export const UserListJoinings = db.getRepository(UserListJoining);
|
||||
export const UserGroups = (UserGroupRepository);
|
||||
export const UserGroupJoinings = db.getRepository(UserGroupJoining);
|
||||
export const UserGroupInvitations = (UserGroupInvitationRepository);
|
||||
export const UserNotePinings = db.getRepository(UserNotePining);
|
||||
export const UsedUsernames = db.getRepository(UsedUsername);
|
||||
export const Followings = (FollowingRepository);
|
||||
export const FollowRequests = (FollowRequestRepository);
|
||||
export const Instances = (InstanceRepository);
|
||||
export const Emojis = (EmojiRepository);
|
||||
export const DriveFiles = (DriveFileRepository);
|
||||
export const DriveFolders = (DriveFolderRepository);
|
||||
export const Notifications = (NotificationRepository);
|
||||
export const Metas = db.getRepository(Meta);
|
||||
export const Mutings = (MutingRepository);
|
||||
export const Blockings = (BlockingRepository);
|
||||
export const SwSubscriptions = db.getRepository(SwSubscription);
|
||||
export const Hashtags = (HashtagRepository);
|
||||
export const AbuseUserReports = (AbuseUserReportRepository);
|
||||
export const RegistrationTickets = db.getRepository(RegistrationTicket);
|
||||
export const AuthSessions = (AuthSessionRepository);
|
||||
export const AccessTokens = db.getRepository(AccessToken);
|
||||
export const Signins = (SigninRepository);
|
||||
export const MessagingMessages = (MessagingMessageRepository);
|
||||
export const Pages = (PageRepository);
|
||||
export const PageLikes = (PageLikeRepository);
|
||||
export const GalleryPosts = (GalleryPostRepository);
|
||||
export const GalleryLikes = (GalleryLikeRepository);
|
||||
export const ModerationLogs = (ModerationLogRepository);
|
||||
export const Clips = (ClipRepository);
|
||||
export const ClipNotes = db.getRepository(ClipNote);
|
||||
export const Antennas = (AntennaRepository);
|
||||
export const AntennaNotes = db.getRepository(AntennaNote);
|
||||
export const PromoNotes = db.getRepository(PromoNote);
|
||||
export const PromoReads = db.getRepository(PromoRead);
|
||||
export const Relays = (RelayRepository);
|
||||
export const MutedNotes = db.getRepository(MutedNote);
|
||||
export const Channels = (ChannelRepository);
|
||||
export const ChannelFollowings = db.getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = db.getRepository(ChannelNotePining);
|
||||
export const RegistryItems = db.getRepository(RegistryItem);
|
||||
export const Webhooks = db.getRepository(Webhook);
|
||||
export const Ads = db.getRepository(Ad);
|
||||
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users } from '../index.js';
|
||||
import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
||||
@EntityRepository(AbuseUserReport)
|
||||
export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
|
||||
public async pack(
|
||||
export const AbuseUserReportRepository = db.getRepository(AbuseUserReport).extend({
|
||||
async pack(
|
||||
src: AbuseUserReport['id'] | AbuseUserReport,
|
||||
) {
|
||||
const report = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const report = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: report.id,
|
||||
|
@ -29,11 +28,11 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
|
|||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
reports: any[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Antenna } from '@/models/entities/antenna.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { AntennaNotes, UserGroupJoinings } from '../index.js';
|
||||
|
||||
@EntityRepository(Antenna)
|
||||
export class AntennaRepository extends Repository<Antenna> {
|
||||
public async pack(
|
||||
export const AntennaRepository = db.getRepository(Antenna).extend({
|
||||
async pack(
|
||||
src: Antenna['id'] | Antenna,
|
||||
): Promise<Packed<'Antenna'>> {
|
||||
const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const antenna = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null;
|
||||
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOne(antenna.userGroupJoiningId) : null;
|
||||
const hasUnreadNote = (await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false })) != null;
|
||||
const userGroupJoining = antenna.userGroupJoiningId ? await UserGroupJoinings.findOneBy({ id: antenna.userGroupJoiningId }) : null;
|
||||
|
||||
return {
|
||||
id: antenna.id,
|
||||
|
@ -29,5 +28,5 @@ export class AntennaRepository extends Repository<Antenna> {
|
|||
withFile: antenna.withFile,
|
||||
hasUnreadNote,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { App } from '@/models/entities/app.js';
|
||||
import { AccessTokens } from '../index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { User } from '../entities/user.js';
|
||||
|
||||
@EntityRepository(App)
|
||||
export class AppRepository extends Repository<App> {
|
||||
public async pack(
|
||||
export const AppRepository = db.getRepository(App).extend({
|
||||
async pack(
|
||||
src: App['id'] | App,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -21,7 +20,7 @@ export class AppRepository extends Repository<App> {
|
|||
includeProfileImageIds: false,
|
||||
}, options);
|
||||
|
||||
const app = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const app = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: app.id,
|
||||
|
@ -30,11 +29,11 @@ export class AppRepository extends Repository<App> {
|
|||
permission: app.permission,
|
||||
...(opts.includeSecret ? { secret: app.secret } : {}),
|
||||
...(me ? {
|
||||
isAuthorized: await AccessTokens.count({
|
||||
isAuthorized: await AccessTokens.countBy({
|
||||
appId: app.id,
|
||||
userId: me.id,
|
||||
}).then(count => count > 0),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Apps } from '../index.js';
|
||||
import { AuthSession } from '@/models/entities/auth-session.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(AuthSession)
|
||||
export class AuthSessionRepository extends Repository<AuthSession> {
|
||||
public async pack(
|
||||
export const AuthSessionRepository = db.getRepository(AuthSession).extend({
|
||||
async pack(
|
||||
src: AuthSession['id'] | AuthSession,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
) {
|
||||
const session = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const session = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: session.id,
|
||||
app: Apps.pack(session.appId, me),
|
||||
token: session.token,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users } from '../index.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(Blocking)
|
||||
export class BlockingRepository extends Repository<Blocking> {
|
||||
public async pack(
|
||||
export const BlockingRepository = db.getRepository(Blocking).extend({
|
||||
async pack(
|
||||
src: Blocking['id'] | Blocking,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
): Promise<Packed<'Blocking'>> {
|
||||
const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const blocking = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: blocking.id,
|
||||
|
@ -21,12 +20,12 @@ export class BlockingRepository extends Repository<Blocking> {
|
|||
detail: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
blockings: any[],
|
||||
me: { id: User['id'] }
|
||||
) {
|
||||
return Promise.all(blockings.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(Channel)
|
||||
export class ChannelRepository extends Repository<Channel> {
|
||||
public async pack(
|
||||
export const ChannelRepository = db.getRepository(Channel).extend({
|
||||
async pack(
|
||||
src: Channel['id'] | Channel,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'Channel'>> {
|
||||
const channel = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const channel = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
const meId = me ? me.id : null;
|
||||
|
||||
const banner = channel.bannerId ? await DriveFiles.findOne(channel.bannerId) : null;
|
||||
const banner = channel.bannerId ? await DriveFiles.findOneBy({ id: channel.bannerId }) : null;
|
||||
|
||||
const hasUnreadNote = meId ? (await NoteUnreads.findOne({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
||||
const hasUnreadNote = meId ? (await NoteUnreads.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
||||
|
||||
const following = meId ? await ChannelFollowings.findOne({
|
||||
const following = meId ? await ChannelFollowings.findOneBy({
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
}) : null;
|
||||
|
@ -38,5 +37,5 @@ export class ChannelRepository extends Repository<Channel> {
|
|||
hasUnreadNote,
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Clip } from '@/models/entities/clip.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Users } from '../index.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
||||
@EntityRepository(Clip)
|
||||
export class ClipRepository extends Repository<Clip> {
|
||||
public async pack(
|
||||
export const ClipRepository = db.getRepository(Clip).extend({
|
||||
async pack(
|
||||
src: Clip['id'] | Clip,
|
||||
): Promise<Packed<'Clip'>> {
|
||||
const clip = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const clip = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: clip.id,
|
||||
|
@ -20,12 +19,12 @@ export class ClipRepository extends Repository<Clip> {
|
|||
description: clip.description,
|
||||
isPublic: clip.isPublic,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
clips: Clip[],
|
||||
) {
|
||||
return Promise.all(clips.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Users, DriveFolders } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
@ -16,9 +16,8 @@ type PackOptions = {
|
|||
withUser?: boolean,
|
||||
};
|
||||
|
||||
@EntityRepository(DriveFile)
|
||||
export class DriveFileRepository extends Repository<DriveFile> {
|
||||
public validateFileName(name: string): boolean {
|
||||
export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||
validateFileName(name: string): boolean {
|
||||
return (
|
||||
(name.trim().length > 0) &&
|
||||
(name.length <= 200) &&
|
||||
|
@ -26,9 +25,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
(name.indexOf('/') === -1) &&
|
||||
(name.indexOf('..') === -1)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||
getPublicProperties(file: DriveFile): DriveFile['properties'] {
|
||||
if (file.properties.orientation != null) {
|
||||
const properties = JSON.parse(JSON.stringify(file.properties));
|
||||
if (file.properties.orientation >= 5) {
|
||||
|
@ -39,9 +38,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
}
|
||||
|
||||
return file.properties;
|
||||
}
|
||||
},
|
||||
|
||||
public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
|
||||
getPublicUrl(file: DriveFile, thumbnail = false): string | null {
|
||||
// リモートかつメディアプロキシ
|
||||
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
|
||||
return appendQuery(config.mediaProxy, query({
|
||||
|
@ -62,9 +61,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/svg+xml'].includes(file.type);
|
||||
|
||||
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
|
||||
}
|
||||
},
|
||||
|
||||
public async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
|
||||
async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
|
||||
const id = typeof user === 'object' ? user.id : user;
|
||||
|
||||
const { sum } = await this
|
||||
|
@ -75,9 +74,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
.getRawOne();
|
||||
|
||||
return parseInt(sum, 10) || 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async calcDriveUsageOfHost(host: string): Promise<number> {
|
||||
async calcDriveUsageOfHost(host: string): Promise<number> {
|
||||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost = :host', { host: toPuny(host) })
|
||||
|
@ -86,9 +85,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
.getRawOne();
|
||||
|
||||
return parseInt(sum, 10) || 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async calcDriveUsageOfLocal(): Promise<number> {
|
||||
async calcDriveUsageOfLocal(): Promise<number> {
|
||||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost IS NULL')
|
||||
|
@ -97,9 +96,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
.getRawOne();
|
||||
|
||||
return parseInt(sum, 10) || 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async calcDriveUsageOfRemote(): Promise<number> {
|
||||
async calcDriveUsageOfRemote(): Promise<number> {
|
||||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost IS NOT NULL')
|
||||
|
@ -108,11 +107,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
.getRawOne();
|
||||
|
||||
return parseInt(sum, 10) || 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async pack(src: DriveFile['id'], options?: PackOptions): Promise<Packed<'DriveFile'> | null>;
|
||||
public async pack(src: DriveFile, options?: PackOptions): Promise<Packed<'DriveFile'>>;
|
||||
public async pack(
|
||||
async pack(
|
||||
src: DriveFile['id'] | DriveFile,
|
||||
options?: PackOptions
|
||||
): Promise<Packed<'DriveFile'> | null> {
|
||||
|
@ -121,11 +118,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
self: false,
|
||||
}, options);
|
||||
|
||||
const file = typeof src === 'object' ? src : await this.findOne(src);
|
||||
const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
|
||||
if (file == null) return null;
|
||||
|
||||
const meta = await fetchMeta();
|
||||
|
||||
return await awaitAll<Packed<'DriveFile'>>({
|
||||
id: file.id,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
|
@ -146,13 +141,13 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
userId: opts.withUser ? file.userId : null,
|
||||
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public async packMany(
|
||||
async packMany(
|
||||
files: (DriveFile['id'] | DriveFile)[],
|
||||
options?: PackOptions
|
||||
) {
|
||||
const items = await Promise.all(files.map(f => this.pack(f, options)));
|
||||
return items.filter(x => x != null);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { DriveFolders, DriveFiles } from '../index.js';
|
||||
import { DriveFolder } from '@/models/entities/drive-folder.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(DriveFolder)
|
||||
export class DriveFolderRepository extends Repository<DriveFolder> {
|
||||
public async pack(
|
||||
export const DriveFolderRepository = db.getRepository(DriveFolder).extend({
|
||||
async pack(
|
||||
src: DriveFolder['id'] | DriveFolder,
|
||||
options?: {
|
||||
detail: boolean
|
||||
|
@ -16,7 +15,7 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
|||
detail: false,
|
||||
}, options);
|
||||
|
||||
const folder = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const folder = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: folder.id,
|
||||
|
@ -25,10 +24,10 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
|||
parentId: folder.parentId,
|
||||
|
||||
...(opts.detail ? {
|
||||
foldersCount: DriveFolders.count({
|
||||
foldersCount: DriveFolders.countBy({
|
||||
parentId: folder.id,
|
||||
}),
|
||||
filesCount: DriveFiles.count({
|
||||
filesCount: DriveFiles.countBy({
|
||||
folderId: folder.id,
|
||||
}),
|
||||
|
||||
|
@ -39,5 +38,5 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
|||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(Emoji)
|
||||
export class EmojiRepository extends Repository<Emoji> {
|
||||
public async pack(
|
||||
export const EmojiRepository = db.getRepository(Emoji).extend({
|
||||
async pack(
|
||||
src: Emoji['id'] | Emoji,
|
||||
): Promise<Packed<'Emoji'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const emoji = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
|
@ -18,11 +17,11 @@ export class EmojiRepository extends Repository<Emoji> {
|
|||
// || emoji.originalUrl してるのは後方互換性のため
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
emojis: any[],
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { FollowRequest } from '@/models/entities/follow-request.js';
|
||||
import { Users } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(FollowRequest)
|
||||
export class FollowRequestRepository extends Repository<FollowRequest> {
|
||||
public async pack(
|
||||
export const FollowRequestRepository = db.getRepository(FollowRequest).extend({
|
||||
async pack(
|
||||
src: FollowRequest['id'] | FollowRequest,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
) {
|
||||
const request = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const request = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: request.id,
|
||||
follower: await Users.pack(request.followerId, me),
|
||||
followee: await Users.pack(request.followeeId, me),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users } from '../index.js';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
@ -29,25 +29,24 @@ type RemoteFolloweeFollowing = Following & {
|
|||
followeeSharedInbox: string;
|
||||
};
|
||||
|
||||
@EntityRepository(Following)
|
||||
export class FollowingRepository extends Repository<Following> {
|
||||
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
|
||||
export const FollowingRepository = db.getRepository(Following).extend({
|
||||
isLocalFollower(following: Following): following is LocalFollowerFollowing {
|
||||
return following.followerHost == null;
|
||||
}
|
||||
},
|
||||
|
||||
public isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
|
||||
isRemoteFollower(following: Following): following is RemoteFollowerFollowing {
|
||||
return following.followerHost != null;
|
||||
}
|
||||
},
|
||||
|
||||
public isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
|
||||
isLocalFollowee(following: Following): following is LocalFolloweeFollowing {
|
||||
return following.followeeHost == null;
|
||||
}
|
||||
},
|
||||
|
||||
public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
|
||||
isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing {
|
||||
return following.followeeHost != null;
|
||||
}
|
||||
},
|
||||
|
||||
public async pack(
|
||||
async pack(
|
||||
src: Following['id'] | Following,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
opts?: {
|
||||
|
@ -55,7 +54,7 @@ export class FollowingRepository extends Repository<Following> {
|
|||
populateFollower?: boolean;
|
||||
}
|
||||
): Promise<Packed<'Following'>> {
|
||||
const following = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const following = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
if (opts == null) opts = {};
|
||||
|
||||
|
@ -71,9 +70,9 @@ export class FollowingRepository extends Repository<Following> {
|
|||
detail: true,
|
||||
}) : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
followings: any[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
opts?: {
|
||||
|
@ -82,5 +81,5 @@ export class FollowingRepository extends Repository<Following> {
|
|||
}
|
||||
) {
|
||||
return Promise.all(followings.map(x => this.pack(x, me, opts)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { GalleryLike } from '@/models/entities/gallery-like.js';
|
||||
import { GalleryPosts } from '../index.js';
|
||||
|
||||
@EntityRepository(GalleryLike)
|
||||
export class GalleryLikeRepository extends Repository<GalleryLike> {
|
||||
public async pack(
|
||||
export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({
|
||||
async pack(
|
||||
src: GalleryLike['id'] | GalleryLike,
|
||||
me?: any
|
||||
) {
|
||||
const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: like.id,
|
||||
post: await GalleryPosts.pack(like.post || like.postId, me),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
likes: any[],
|
||||
me: any
|
||||
) {
|
||||
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Users, DriveFiles, GalleryLikes } from '../index.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(GalleryPost)
|
||||
export class GalleryPostRepository extends Repository<GalleryPost> {
|
||||
public async pack(
|
||||
export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
|
||||
async pack(
|
||||
src: GalleryPost['id'] | GalleryPost,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'GalleryPost'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const post = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const post = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: post.id,
|
||||
|
@ -27,14 +26,14 @@ export class GalleryPostRepository extends Repository<GalleryPost> {
|
|||
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||
isSensitive: post.isSensitive,
|
||||
likedCount: post.likedCount,
|
||||
isLiked: meId ? await GalleryLikes.findOne({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isLiked: meId ? await GalleryLikes.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
posts: GalleryPost[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(posts.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Hashtag } from '@/models/entities/hashtag.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(Hashtag)
|
||||
export class HashtagRepository extends Repository<Hashtag> {
|
||||
public async pack(
|
||||
export const HashtagRepository = db.getRepository(Hashtag).extend({
|
||||
async pack(
|
||||
src: Hashtag,
|
||||
): Promise<Packed<'Hashtag'>> {
|
||||
return {
|
||||
|
@ -16,11 +15,11 @@ export class HashtagRepository extends Repository<Hashtag> {
|
|||
attachedLocalUsersCount: src.attachedLocalUsersCount,
|
||||
attachedRemoteUsersCount: src.attachedRemoteUsersCount,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
hashtags: Hashtag[],
|
||||
) {
|
||||
return Promise.all(hashtags.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Instance } from '@/models/entities/instance.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(Instance)
|
||||
export class InstanceRepository extends Repository<Instance> {
|
||||
public async pack(
|
||||
export const InstanceRepository = db.getRepository(Instance).extend({
|
||||
async pack(
|
||||
instance: Instance,
|
||||
): Promise<Packed<'FederationInstance'>> {
|
||||
return {
|
||||
|
@ -29,11 +28,11 @@ export class InstanceRepository extends Repository<Instance> {
|
|||
iconUrl: instance.iconUrl,
|
||||
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
instances: Instance[],
|
||||
) {
|
||||
return Promise.all(instances.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
||||
import { Users, DriveFiles, UserGroups } from '../index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(MessagingMessage)
|
||||
export class MessagingMessageRepository extends Repository<MessagingMessage> {
|
||||
public async pack(
|
||||
export const MessagingMessageRepository = db.getRepository(MessagingMessage).extend({
|
||||
async pack(
|
||||
src: MessagingMessage['id'] | MessagingMessage,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -19,7 +18,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
|
|||
populateGroup: true,
|
||||
};
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const message = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
|
@ -36,5 +35,5 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
|
|||
isRead: message.isRead,
|
||||
reads: message.reads,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users } from '../index.js';
|
||||
import { ModerationLog } from '@/models/entities/moderation-log.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
||||
@EntityRepository(ModerationLog)
|
||||
export class ModerationLogRepository extends Repository<ModerationLog> {
|
||||
public async pack(
|
||||
export const ModerationLogRepository = db.getRepository(ModerationLog).extend({
|
||||
async pack(
|
||||
src: ModerationLog['id'] | ModerationLog,
|
||||
) {
|
||||
const log = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const log = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: log.id,
|
||||
|
@ -20,11 +19,11 @@ export class ModerationLogRepository extends Repository<ModerationLog> {
|
|||
detail: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
reports: any[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Users } from '../index.js';
|
||||
import { Muting } from '@/models/entities/muting.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(Muting)
|
||||
export class MutingRepository extends Repository<Muting> {
|
||||
public async pack(
|
||||
export const MutingRepository = db.getRepository(Muting).extend({
|
||||
async pack(
|
||||
src: Muting['id'] | Muting,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
): Promise<Packed<'Muting'>> {
|
||||
const muting = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const muting = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: muting.id,
|
||||
|
@ -22,12 +21,12 @@ export class MutingRepository extends Repository<Muting> {
|
|||
detail: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
mutings: any[],
|
||||
me: { id: User['id'] }
|
||||
) {
|
||||
return Promise.all(mutings.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { NoteFavorite } from '@/models/entities/note-favorite.js';
|
||||
import { Notes } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(NoteFavorite)
|
||||
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
|
||||
public async pack(
|
||||
export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({
|
||||
async pack(
|
||||
src: NoteFavorite['id'] | NoteFavorite,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
) {
|
||||
const favorite = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const favorite = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: favorite.id,
|
||||
|
@ -17,12 +16,12 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
|
|||
noteId: favorite.noteId,
|
||||
note: await Notes.pack(favorite.note || favorite.noteId, me),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
favorites: any[],
|
||||
me: { id: User['id'] }
|
||||
) {
|
||||
return Promise.all(favorites.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import { Notes, Users } from '../index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { convertLegacyReaction } from '@/misc/reaction-lib.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(NoteReaction)
|
||||
export class NoteReactionRepository extends Repository<NoteReaction> {
|
||||
public async pack(
|
||||
export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
|
||||
async pack(
|
||||
src: NoteReaction['id'] | NoteReaction,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -18,7 +17,7 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
|
|||
withNote: false,
|
||||
}, options);
|
||||
|
||||
const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const reaction = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: reaction.id,
|
||||
|
@ -29,5 +28,5 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
|
|||
note: await Notes.pack(reaction.note ?? reaction.noteId, me),
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
@ -9,10 +9,133 @@ import { awaitAll } from '@/prelude/await-all.js';
|
|||
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
@EntityRepository(Note)
|
||||
export class NoteRepository extends Repository<Note> {
|
||||
public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
|
||||
async function hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
|
||||
if (specified) {
|
||||
hide = false;
|
||||
} else {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await Followings.findOneBy({
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
});
|
||||
|
||||
if (following == null) {
|
||||
hide = true;
|
||||
} else {
|
||||
hide = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
packedNote.visibleUserIds = undefined;
|
||||
packedNote.fileIds = [];
|
||||
packedNote.files = [];
|
||||
packedNote.text = null;
|
||||
packedNote.poll = undefined;
|
||||
packedNote.cw = null;
|
||||
packedNote.isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function populatePoll(note: Note, meId: User['id'] | null) {
|
||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
const choices = poll.choices.map(c => ({
|
||||
text: c,
|
||||
votes: poll.votes[poll.choices.indexOf(c)],
|
||||
isVoted: false,
|
||||
}));
|
||||
|
||||
if (meId) {
|
||||
if (poll.multiple) {
|
||||
const votes = await PollVotes.findBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
const myChoices = votes.map(v => v.choice);
|
||||
for (const myChoice of myChoices) {
|
||||
choices[myChoice].isVoted = true;
|
||||
}
|
||||
} else {
|
||||
const vote = await PollVotes.findOneBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
if (vote) {
|
||||
choices[vote.choice].isVoted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
multiple: poll.multiple,
|
||||
expiresAt: poll.expiresAt,
|
||||
choices,
|
||||
};
|
||||
}
|
||||
|
||||
async function populateMyReaction(note: Note, meId: User['id'], _hint_?: {
|
||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||
}) {
|
||||
if (_hint_?.myReactions) {
|
||||
const reaction = _hint_.myReactions.get(note.id);
|
||||
if (reaction) {
|
||||
return convertLegacyReaction(reaction.reaction);
|
||||
} else if (reaction === null) {
|
||||
return undefined;
|
||||
}
|
||||
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
|
||||
}
|
||||
|
||||
const reaction = await NoteReactions.findOneBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
if (reaction) {
|
||||
return convertLegacyReaction(reaction.reaction);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const NoteRepository = db.getRepository(Note).extend({
|
||||
async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (note.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
|
@ -45,7 +168,7 @@ export class NoteRepository extends Repository<Note> {
|
|||
return true;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await Followings.findOne({
|
||||
const following = await Followings.findOneBy({
|
||||
followeeId: note.userId,
|
||||
followerId: meId,
|
||||
});
|
||||
|
@ -59,69 +182,9 @@ export class NoteRepository extends Repository<Note> {
|
|||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) {
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
|
||||
if (specified) {
|
||||
hide = false;
|
||||
} else {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await Followings.findOne({
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
});
|
||||
|
||||
if (following == null) {
|
||||
hide = true;
|
||||
} else {
|
||||
hide = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
packedNote.visibleUserIds = undefined;
|
||||
packedNote.fileIds = [];
|
||||
packedNote.files = [];
|
||||
packedNote.text = null;
|
||||
packedNote.poll = undefined;
|
||||
packedNote.cw = null;
|
||||
packedNote.isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async pack(
|
||||
async pack(
|
||||
src: Note['id'] | Note,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -138,68 +201,9 @@ export class NoteRepository extends Repository<Note> {
|
|||
}, options);
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const note = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const note = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
const host = note.userHost;
|
||||
|
||||
async function populatePoll() {
|
||||
const poll = await Polls.findOneOrFail(note.id);
|
||||
const choices = poll.choices.map(c => ({
|
||||
text: c,
|
||||
votes: poll.votes[poll.choices.indexOf(c)],
|
||||
isVoted: false,
|
||||
}));
|
||||
|
||||
if (poll.multiple) {
|
||||
const votes = await PollVotes.find({
|
||||
userId: meId!,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
const myChoices = votes.map(v => v.choice);
|
||||
for (const myChoice of myChoices) {
|
||||
choices[myChoice].isVoted = true;
|
||||
}
|
||||
} else {
|
||||
const vote = await PollVotes.findOne({
|
||||
userId: meId!,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
if (vote) {
|
||||
choices[vote.choice].isVoted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
multiple: poll.multiple,
|
||||
expiresAt: poll.expiresAt,
|
||||
choices,
|
||||
};
|
||||
}
|
||||
|
||||
async function populateMyReaction() {
|
||||
if (options?._hint_?.myReactions) {
|
||||
const reaction = options._hint_.myReactions.get(note.id);
|
||||
if (reaction) {
|
||||
return convertLegacyReaction(reaction.reaction);
|
||||
} else if (reaction === null) {
|
||||
return undefined;
|
||||
}
|
||||
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
|
||||
}
|
||||
|
||||
const reaction = await NoteReactions.findOne({
|
||||
userId: meId!,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
if (reaction) {
|
||||
return convertLegacyReaction(reaction.reaction);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let text = note.text;
|
||||
|
||||
if (note.name && (note.url ?? note.uri)) {
|
||||
|
@ -209,7 +213,7 @@ export class NoteRepository extends Repository<Note> {
|
|||
const channel = note.channelId
|
||||
? note.channel
|
||||
? note.channel
|
||||
: await Channels.findOne(note.channelId)
|
||||
: await Channels.findOneBy({ id: note.channelId })
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
|
||||
|
@ -255,10 +259,10 @@ export class NoteRepository extends Repository<Note> {
|
|||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
|
||||
poll: note.hasPoll ? populatePoll() : undefined,
|
||||
poll: note.hasPoll ? populatePoll(note, meId) : undefined,
|
||||
|
||||
...(meId ? {
|
||||
myReaction: populateMyReaction(),
|
||||
myReaction: populateMyReaction(note, meId, options?._hint_),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
|
@ -275,13 +279,13 @@ export class NoteRepository extends Repository<Note> {
|
|||
}
|
||||
|
||||
if (!opts.skipHide) {
|
||||
await this.hideNote(packed, meId);
|
||||
await hideNote(packed, meId);
|
||||
}
|
||||
|
||||
return packed;
|
||||
}
|
||||
},
|
||||
|
||||
public async packMany(
|
||||
async packMany(
|
||||
notes: Note[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -296,7 +300,7 @@ export class NoteRepository extends Repository<Note> {
|
|||
if (meId) {
|
||||
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
|
||||
const targets = [...notes.map(n => n.id), ...renoteIds];
|
||||
const myReactions = await NoteReactions.find({
|
||||
const myReactions = await NoteReactions.findBy({
|
||||
userId: meId,
|
||||
noteId: In(targets),
|
||||
});
|
||||
|
@ -314,5 +318,5 @@ export class NoteRepository extends Repository<Note> {
|
|||
myReactions: myReactionsMap,
|
||||
},
|
||||
})));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityRepository, In, Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
|
||||
import { Notification } from '@/models/entities/notification.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
|
@ -8,10 +8,10 @@ import { NoteReaction } from '@/models/entities/note-reaction.js';
|
|||
import { User } from '@/models/entities/user.js';
|
||||
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
|
||||
import { notificationTypes } from '@/types.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
@EntityRepository(Notification)
|
||||
export class NotificationRepository extends Repository<Notification> {
|
||||
public async pack(
|
||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
async pack(
|
||||
src: Notification['id'] | Notification,
|
||||
options: {
|
||||
_hintForEachNotes_?: {
|
||||
|
@ -19,8 +19,8 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
};
|
||||
}
|
||||
): Promise<Packed<'Notification'>> {
|
||||
const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
|
||||
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||
|
||||
return await awaitAll({
|
||||
id: notification.id,
|
||||
|
@ -82,9 +82,9 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
icon: notification.customIcon || token?.iconUrl,
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public async packMany(
|
||||
async packMany(
|
||||
notifications: Notification[],
|
||||
meId: User['id']
|
||||
) {
|
||||
|
@ -95,7 +95,7 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
|
||||
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
|
||||
const targets = [...noteIds, ...renoteIds];
|
||||
const myReactions = await NoteReactions.find({
|
||||
const myReactions = await NoteReactions.findBy({
|
||||
userId: meId,
|
||||
noteId: In(targets),
|
||||
});
|
||||
|
@ -111,5 +111,5 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
myReactions: myReactionsMap,
|
||||
},
|
||||
})));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { PageLike } from '@/models/entities/page-like.js';
|
||||
import { Pages } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(PageLike)
|
||||
export class PageLikeRepository extends Repository<PageLike> {
|
||||
public async pack(
|
||||
export const PageLikeRepository = db.getRepository(PageLike).extend({
|
||||
async pack(
|
||||
src: PageLike['id'] | PageLike,
|
||||
me?: { id: User['id'] } | null | undefined
|
||||
) {
|
||||
const like = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: like.id,
|
||||
page: await Pages.pack(like.page || like.pageId, me),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
likes: any[],
|
||||
me: { id: User['id'] }
|
||||
) {
|
||||
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Users, DriveFiles, PageLikes } from '../index.js';
|
||||
|
@ -6,20 +6,19 @@ import { awaitAll } from '@/prelude/await-all.js';
|
|||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
@EntityRepository(Page)
|
||||
export class PageRepository extends Repository<Page> {
|
||||
public async pack(
|
||||
export const PageRepository = db.getRepository(Page).extend({
|
||||
async pack(
|
||||
src: Page['id'] | Page,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'Page'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const page = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFiles: Promise<DriveFile | undefined>[] = [];
|
||||
const collectFile = (xs: any[]) => {
|
||||
for (const x of xs) {
|
||||
if (x.type === 'image') {
|
||||
attachedFiles.push(DriveFiles.findOne({
|
||||
attachedFiles.push(DriveFiles.findOneBy({
|
||||
id: x.fileId,
|
||||
userId: page.userId,
|
||||
}));
|
||||
|
@ -76,14 +75,14 @@ export class PageRepository extends Repository<Page> {
|
|||
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
|
||||
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await PageLikes.findOne({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
pages: Page[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(pages.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Relay } from '@/models/entities/relay.js';
|
||||
|
||||
@EntityRepository(Relay)
|
||||
export class RelayRepository extends Repository<Relay> {
|
||||
}
|
||||
export const RelayRepository = db.getRepository(Relay).extend({
|
||||
});
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Signin } from '@/models/entities/signin.js';
|
||||
|
||||
@EntityRepository(Signin)
|
||||
export class SigninRepository extends Repository<Signin> {
|
||||
public async pack(
|
||||
export const SigninRepository = db.getRepository(Signin).extend({
|
||||
async pack(
|
||||
src: Signin,
|
||||
) {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js';
|
||||
import { UserGroups } from '../index.js';
|
||||
|
||||
@EntityRepository(UserGroupInvitation)
|
||||
export class UserGroupInvitationRepository extends Repository<UserGroupInvitation> {
|
||||
public async pack(
|
||||
export const UserGroupInvitationRepository = db.getRepository(UserGroupInvitation).extend({
|
||||
async pack(
|
||||
src: UserGroupInvitation['id'] | UserGroupInvitation,
|
||||
) {
|
||||
const invitation = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const invitation = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: invitation.id,
|
||||
group: await UserGroups.pack(invitation.userGroup || invitation.userGroupId),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public packMany(
|
||||
packMany(
|
||||
invitations: any[],
|
||||
) {
|
||||
return Promise.all(invitations.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { UserGroup } from '@/models/entities/user-group.js';
|
||||
import { UserGroupJoinings } from '../index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(UserGroup)
|
||||
export class UserGroupRepository extends Repository<UserGroup> {
|
||||
public async pack(
|
||||
export const UserGroupRepository = db.getRepository(UserGroup).extend({
|
||||
async pack(
|
||||
src: UserGroup['id'] | UserGroup,
|
||||
): Promise<Packed<'UserGroup'>> {
|
||||
const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const userGroup = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await UserGroupJoinings.find({
|
||||
const users = await UserGroupJoinings.findBy({
|
||||
userGroupId: userGroup.id,
|
||||
});
|
||||
|
||||
|
@ -21,5 +20,5 @@ export class UserGroupRepository extends Repository<UserGroup> {
|
|||
ownerId: userGroup.userId,
|
||||
userIds: users.map(x => x.userId),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { UserList } from '@/models/entities/user-list.js';
|
||||
import { UserListJoinings } from '../index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
|
||||
@EntityRepository(UserList)
|
||||
export class UserListRepository extends Repository<UserList> {
|
||||
public async pack(
|
||||
export const UserListRepository = db.getRepository(UserList).extend({
|
||||
async pack(
|
||||
src: UserList['id'] | UserList,
|
||||
): Promise<Packed<'UserList'>> {
|
||||
const userList = typeof src === 'object' ? src : await this.findOneOrFail(src);
|
||||
const userList = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await UserListJoinings.find({
|
||||
const users = await UserListJoinings.findBy({
|
||||
userListId: userList.id,
|
||||
});
|
||||
|
||||
|
@ -20,5 +19,5 @@ export class UserListRepository extends Repository<UserList> {
|
|||
name: userList.name,
|
||||
userIds: users.map(x => x.userId),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,6 +8,11 @@ import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
|||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { Instance } from '../entities/instance.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||
|
@ -19,51 +24,69 @@ type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends bo
|
|||
|
||||
const ajv = new Ajv();
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
public localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||
public passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||
public nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
public descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
|
||||
public locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
public birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||
const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||
const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||
const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
|
||||
const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||
|
||||
function isLocalUser(user: User): user is ILocalUser;
|
||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
||||
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
function isRemoteUser(user: User): user is IRemoteUser;
|
||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
||||
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||
return !isLocalUser(user);
|
||||
}
|
||||
|
||||
export const UserRepository = db.getRepository(User).extend({
|
||||
localUsernameSchema,
|
||||
passwordSchema,
|
||||
nameSchema,
|
||||
descriptionSchema,
|
||||
locationSchema,
|
||||
birthdaySchema,
|
||||
|
||||
//#region Validators
|
||||
public validateLocalUsername = ajv.compile(this.localUsernameSchema);
|
||||
public validatePassword = ajv.compile(this.passwordSchema);
|
||||
public validateName = ajv.compile(this.nameSchema);
|
||||
public validateDescription = ajv.compile(this.descriptionSchema);
|
||||
public validateLocation = ajv.compile(this.locationSchema);
|
||||
public validateBirthday = ajv.compile(this.birthdaySchema);
|
||||
validateLocalUsername: ajv.compile(localUsernameSchema),
|
||||
validatePassword: ajv.compile(passwordSchema),
|
||||
validateName: ajv.compile(nameSchema),
|
||||
validateDescription: ajv.compile(descriptionSchema),
|
||||
validateLocation: ajv.compile(locationSchema),
|
||||
validateBirthday: ajv.compile(birthdaySchema),
|
||||
//#endregion
|
||||
|
||||
public async getRelation(me: User['id'], target: User['id']) {
|
||||
async getRelation(me: User['id'], target: User['id']) {
|
||||
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
|
||||
Followings.findOne({
|
||||
Followings.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
Followings.findOne({
|
||||
Followings.findOneBy({
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
}),
|
||||
FollowRequests.findOne({
|
||||
FollowRequests.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
FollowRequests.findOne({
|
||||
FollowRequests.findOneBy({
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
}),
|
||||
Blockings.findOne({
|
||||
Blockings.findOneBy({
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
}),
|
||||
Blockings.findOne({
|
||||
Blockings.findOneBy({
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
}),
|
||||
Mutings.findOne({
|
||||
Mutings.findOneBy({
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
}),
|
||||
|
@ -79,14 +102,14 @@ export class UserRepository extends Repository<User> {
|
|||
isBlocked: fromBlocked != null,
|
||||
isMuted: mute != null,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.find({
|
||||
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
|
||||
const joinings = await UserGroupJoinings.find({ userId: userId });
|
||||
const joinings = await UserGroupJoinings.findBy({ userId: userId });
|
||||
|
||||
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
|
||||
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
|
||||
|
@ -108,44 +131,44 @@ export class UserRepository extends Repository<User> {
|
|||
]);
|
||||
|
||||
return withUser || withGroups.some(x => x);
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
||||
const reads = await AnnouncementReads.find({
|
||||
async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
||||
const reads = await AnnouncementReads.findBy({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
const count = await Announcements.count(reads.length > 0 ? {
|
||||
const count = await Announcements.countBy(reads.length > 0 ? {
|
||||
id: Not(In(reads.map(read => read.announcementId))),
|
||||
} : {});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
||||
|
||||
const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({
|
||||
const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.find({ followerId: userId });
|
||||
async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.findBy({ followerId: userId });
|
||||
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOne({
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
|
||||
userId: userId,
|
||||
noteChannelId: In(channels.map(x => x.followeeId)),
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.find({
|
||||
async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.muteeId);
|
||||
|
@ -160,17 +183,17 @@ export class UserRepository extends Repository<User> {
|
|||
});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
},
|
||||
|
||||
public async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
||||
const count = await FollowRequests.count({
|
||||
async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
||||
const count = await FollowRequests.countBy({
|
||||
followeeId: userId,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
},
|
||||
|
||||
public getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
||||
getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
||||
if (user.hideOnlineStatus) return 'unknown';
|
||||
if (user.lastActiveDate == null) return 'unknown';
|
||||
const elapsed = Date.now() - user.lastActiveDate.getTime();
|
||||
|
@ -179,22 +202,22 @@ export class UserRepository extends Repository<User> {
|
|||
elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
|
||||
'offline'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
public getAvatarUrl(user: User): string {
|
||||
getAvatarUrl(user: User): string {
|
||||
// TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング
|
||||
if (user.avatar) {
|
||||
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
public getIdenticonUrl(userId: User['id']): string {
|
||||
getIdenticonUrl(userId: User['id']): string {
|
||||
return `${config.url}/identicon/${userId}`;
|
||||
}
|
||||
},
|
||||
|
||||
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
|
||||
async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
|
||||
src: User['id'] | User,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -211,11 +234,15 @@ export class UserRepository extends Repository<User> {
|
|||
|
||||
if (typeof src === 'object') {
|
||||
user = src;
|
||||
if (src.avatar === undefined && src.avatarId) src.avatar = await DriveFiles.findOne(src.avatarId) ?? null;
|
||||
if (src.banner === undefined && src.bannerId) src.banner = await DriveFiles.findOne(src.bannerId) ?? null;
|
||||
if (src.avatar === undefined && src.avatarId) src.avatar = await DriveFiles.findOneBy({ id: src.avatarId }) ?? null;
|
||||
if (src.banner === undefined && src.bannerId) src.banner = await DriveFiles.findOneBy({ id: src.bannerId }) ?? null;
|
||||
} else {
|
||||
user = await this.findOneOrFail(src, {
|
||||
relations: ['avatar', 'banner'],
|
||||
user = await this.findOneOrFail({
|
||||
where: { id: src },
|
||||
relations: {
|
||||
avatar: true,
|
||||
banner: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -228,7 +255,7 @@ export class UserRepository extends Repository<User> {
|
|||
.innerJoinAndSelect('pin.note', 'note')
|
||||
.orderBy('pin.id', 'DESC')
|
||||
.getMany() : [];
|
||||
const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
|
||||
const profile = opts.detail ? await UserProfiles.findOneByOrFail({ userId: user.id }) : null;
|
||||
|
||||
const followingCount = profile == null ? null :
|
||||
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
|
||||
|
@ -254,8 +281,10 @@ export class UserRepository extends Repository<User> {
|
|||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
isCat: user.isCat || falsy,
|
||||
showTimelineReplies: user.showTimelineReplies || falsy,
|
||||
instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? {
|
||||
instance: user.host ? userInstanceCache.fetch(user.host,
|
||||
() => Instances.findOneBy({ host: user.host! }),
|
||||
v => v != null
|
||||
).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
|
@ -297,7 +326,7 @@ export class UserRepository extends Repository<User> {
|
|||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.count({
|
||||
? UserSecurityKeys.countBy({
|
||||
userId: user.id,
|
||||
}).then(result => result >= 1)
|
||||
: false,
|
||||
|
@ -334,6 +363,7 @@ export class UserRepository extends Repository<User> {
|
|||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||
showTimelineReplies: user.showTimelineReplies || falsy,
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
|
@ -344,7 +374,11 @@ export class UserRepository extends Repository<User> {
|
|||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
select: ['id', 'name', 'lastUsed'],
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
lastUsed: true,
|
||||
},
|
||||
})
|
||||
: [],
|
||||
} : {}),
|
||||
|
@ -361,9 +395,9 @@ export class UserRepository extends Repository<User> {
|
|||
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
|
||||
|
||||
return await awaitAll(packed);
|
||||
}
|
||||
},
|
||||
|
||||
public packMany<D extends boolean = false>(
|
||||
packMany<D extends boolean = false>(
|
||||
users: (User['id'] | User)[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
options?: {
|
||||
|
@ -372,17 +406,8 @@ export class UserRepository extends Repository<User> {
|
|||
}
|
||||
): Promise<IsUserDetailed<D>[]> {
|
||||
return Promise.all(users.map(u => this.pack(u, me, options)));
|
||||
}
|
||||
},
|
||||
|
||||
public isLocalUser(user: User): user is ILocalUser;
|
||||
public isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
||||
public isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
public isRemoteUser(user: User): user is IRemoteUser;
|
||||
public isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
||||
public isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||
return !this.isLocalUser(user);
|
||||
}
|
||||
}
|
||||
isLocalUser,
|
||||
isRemoteUser,
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export const packedEmojiSchema = {
|
|||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
|
|
|
@ -21,6 +21,7 @@ export const packedUserLiteSchema = {
|
|||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
example: 'misskey.example.com',
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
avatarUrl: {
|
||||
type: 'string',
|
||||
|
|
|
@ -8,13 +8,15 @@ import processInbox from './processors/inbox.js';
|
|||
import processDb from './processors/db/index.js';
|
||||
import processObjectStorage from './processors/object-storage/index.js';
|
||||
import processSystemQueue from './processors/system/index.js';
|
||||
import processWebhookDeliver from './processors/webhook-deliver.js';
|
||||
import { endedPollNotification } from './processors/ended-poll-notification.js';
|
||||
import { queueLogger } from './logger.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { getJobInfo } from './get-job-info.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
|
||||
import { ThinUser } from './types.js';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
|
||||
function renderError(e: Error): any {
|
||||
return {
|
||||
|
@ -26,6 +28,7 @@ function renderError(e: Error): any {
|
|||
|
||||
const systemLogger = queueLogger.createSubLogger('system');
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const webhookLogger = queueLogger.createSubLogger('webhook');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
const dbLogger = queueLogger.createSubLogger('db');
|
||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||
|
@ -70,6 +73,14 @@ objectStorageQueue
|
|||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
export function deliver(user: ThinUser, content: unknown, to: string | null) {
|
||||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
|
@ -251,18 +262,39 @@ export function createCleanRemoteFilesJob() {
|
|||
});
|
||||
}
|
||||
|
||||
export function webhookDeliver(webhook: Webhook, content: unknown) {
|
||||
const data = {
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
};
|
||||
|
||||
return webhookDeliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (envOption.onlyServer) return;
|
||||
|
||||
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
||||
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
||||
endedPollNotificationQueue.process(endedPollNotification);
|
||||
webhookDeliverQueue.process(64, processWebhookDeliver);
|
||||
processDb(dbQueue);
|
||||
processObjectStorage(objectStorageQueue);
|
||||
|
||||
systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { cron: '55 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
systemQueue.add('resyncCharts', {
|
||||
|
@ -278,6 +310,7 @@ export default function() {
|
|||
systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { cron: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
processSystemQueue(systemQueue);
|
||||
|
|
|
@ -13,7 +13,7 @@ const logger = queueLogger.createSubLogger('delete-account');
|
|||
export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
|
||||
logger.info(`Deleting account of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise
|
|||
}
|
||||
|
||||
{ // Send email notification
|
||||
const profile = await UserProfiles.findOneOrFail(user.id);
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
if (profile.email && profile.emailVerified) {
|
||||
sendEmail(profile.email, 'Account deleted',
|
||||
`Your account has been deleted.`,
|
||||
|
|
|
@ -11,7 +11,7 @@ const logger = queueLogger.createSubLogger('delete-drive-files');
|
|||
export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -44,7 +44,7 @@ export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any):
|
|||
deletedCount++;
|
||||
}
|
||||
|
||||
const total = await DriveFiles.count({
|
||||
const total = await DriveFiles.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ const logger = queueLogger.createSubLogger('export-blocking');
|
|||
export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -56,7 +56,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
|
|||
cursor = blockings[blockings.length - 1].id;
|
||||
|
||||
for (const block of blockings) {
|
||||
const u = await Users.findOne({ id: block.blockeeId });
|
||||
const u = await Users.findOneBy({ id: block.blockeeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
|
|||
exportedCount++;
|
||||
}
|
||||
|
||||
const total = await Blockings.count({
|
||||
const total = await Blockings.countBy({
|
||||
blockerId: user.id,
|
||||
});
|
||||
|
||||
|
|
|
@ -12,13 +12,14 @@ import { Users, Emojis } from '@/models/index.js';
|
|||
import { } from '@/queue/types.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import config from '@/config/index.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-custom-emojis');
|
||||
|
||||
export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promise<void> {
|
||||
logger.info(`Exporting custom emojis ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -57,7 +58,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
|
|||
|
||||
const customEmojis = await Emojis.find({
|
||||
where: {
|
||||
host: null,
|
||||
host: IsNull(),
|
||||
},
|
||||
order: {
|
||||
id: 'ASC',
|
||||
|
|
|
@ -16,7 +16,7 @@ const logger = queueLogger.createSubLogger('export-following');
|
|||
export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> {
|
||||
logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -36,7 +36,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
|
|||
|
||||
let cursor: Following['id'] | null = null;
|
||||
|
||||
const mutings = job.data.excludeMuting ? await Mutings.find({
|
||||
const mutings = job.data.excludeMuting ? await Mutings.findBy({
|
||||
muterId: user.id,
|
||||
}) : [];
|
||||
|
||||
|
@ -60,7 +60,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
|
|||
cursor = followings[followings.length - 1].id;
|
||||
|
||||
for (const following of followings) {
|
||||
const u = await Users.findOne({ id: following.followeeId });
|
||||
const u = await Users.findOneBy({ id: following.followeeId });
|
||||
if (u == null) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ const logger = queueLogger.createSubLogger('export-mute');
|
|||
export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting mute of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -57,7 +57,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
|
|||
cursor = mutes[mutes.length - 1].id;
|
||||
|
||||
for (const mute of mutes) {
|
||||
const u = await Users.findOne({ id: mute.muteeId });
|
||||
const u = await Users.findOneBy({ id: mute.muteeId });
|
||||
if (u == null) {
|
||||
exportedCount++; continue;
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
|
|||
exportedCount++;
|
||||
}
|
||||
|
||||
const total = await Mutings.count({
|
||||
const total = await Mutings.countBy({
|
||||
muterId: user.id,
|
||||
});
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const logger = queueLogger.createSubLogger('export-notes');
|
|||
export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
|
@ -74,7 +74,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
|
|||
for (const note of notes) {
|
||||
let poll: Poll | undefined;
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneOrFail({ noteId: note.id });
|
||||
poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
}
|
||||
const content = JSON.stringify(serialize(note, poll));
|
||||
const isFirst = exportedNotesCount === 0;
|
||||
|
@ -82,7 +82,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
|
|||
exportedNotesCount++;
|
||||
}
|
||||
|
||||
const total = await Notes.count({
|
||||
const total = await Notes.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ const logger = queueLogger.createSubLogger('export-user-lists');
|
|||
export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const lists = await UserLists.find({
|
||||
const lists = await UserLists.findBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
|
@ -38,8 +38,8 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
|
|||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
for (const list of lists) {
|
||||
const joinings = await UserListJoinings.find({ userListId: list.id });
|
||||
const users = await Users.find({
|
||||
const joinings = await UserListJoinings.findBy({ userListId: list.id });
|
||||
const users = await Users.findBy({
|
||||
id: In(joinings.map(j => j.userId)),
|
||||
});
|
||||
|
||||
|
|
|
@ -8,19 +8,20 @@ import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
|||
import { Users, DriveFiles, Blockings } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import block from '@/services/blocking/create.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-blocking');
|
||||
|
||||
export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await DriveFiles.findOne({
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
|
@ -39,10 +40,10 @@ export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: a
|
|||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = isSelfHost(host!) ? await Users.findOne({
|
||||
host: null,
|
||||
let target = isSelfHost(host!) ? await Users.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await Users.findOne({
|
||||
}) : await Users.findOneBy({
|
||||
host: toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import Bull from 'bull';
|
|||
import * as tmp from 'tmp';
|
||||
import * as fs from 'node:fs';
|
||||
import unzipper from 'unzipper';
|
||||
import { getConnection } from 'typeorm';
|
||||
|
||||
import { queueLogger } from '../../logger.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
|
@ -10,6 +9,7 @@ import { DriveFiles, Emojis } from '@/models/index.js';
|
|||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-custom-emojis');
|
||||
|
||||
|
@ -17,7 +17,7 @@ const logger = queueLogger.createSubLogger('import-custom-emojis');
|
|||
export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing custom emojis ...`);
|
||||
|
||||
const file = await DriveFiles.findOne({
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
|
@ -72,10 +72,10 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
|
||||
}).then(x => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
await getConnection().queryResultCache!.remove(['meta_emojis']);
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
|
|
|
@ -8,19 +8,20 @@ import { downloadTextFile } from '@/misc/download-text-file.js';
|
|||
import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
||||
import { Users, DriveFiles } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-following');
|
||||
|
||||
export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await DriveFiles.findOne({
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
|
@ -39,10 +40,10 @@ export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done:
|
|||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = isSelfHost(host!) ? await Users.findOne({
|
||||
host: null,
|
||||
let target = isSelfHost(host!) ? await Users.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await Users.findOne({
|
||||
}) : await Users.findOneBy({
|
||||
host: toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
|
|
@ -9,19 +9,20 @@ import { Users, DriveFiles, Mutings } from '@/models/index.js';
|
|||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-muting');
|
||||
|
||||
export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing muting of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await DriveFiles.findOne({
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
|
@ -40,10 +41,10 @@ export async function importMuting(job: Bull.Job<DbUserImportJobData>, done: any
|
|||
const acct = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(acct);
|
||||
|
||||
let target = isSelfHost(host!) ? await Users.findOne({
|
||||
host: null,
|
||||
let target = isSelfHost(host!) ? await Users.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await Users.findOne({
|
||||
}) : await Users.findOneBy({
|
||||
host: toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
|
|
@ -9,19 +9,20 @@ import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
|||
import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-user-lists');
|
||||
|
||||
export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await DriveFiles.findOne({
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: job.data.fileId,
|
||||
});
|
||||
if (file == null) {
|
||||
|
@ -40,7 +41,7 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
|
|||
const listName = line.split(',')[0].trim();
|
||||
const { username, host } = Acct.parse(line.split(',')[1].trim());
|
||||
|
||||
let list = await UserLists.findOne({
|
||||
let list = await UserLists.findOneBy({
|
||||
userId: user.id,
|
||||
name: listName,
|
||||
});
|
||||
|
@ -51,13 +52,13 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
|
|||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: listName,
|
||||
}).then(x => UserLists.findOneOrFail(x.identifiers[0]));
|
||||
}).then(x => UserLists.findOneByOrFail(x.identifiers[0]));
|
||||
}
|
||||
|
||||
let target = isSelfHost(host!) ? await Users.findOne({
|
||||
host: null,
|
||||
let target = isSelfHost(host!) ? await Users.findOneBy({
|
||||
host: IsNull(),
|
||||
usernameLower: username.toLowerCase(),
|
||||
}) : await Users.findOne({
|
||||
}) : await Users.findOneBy({
|
||||
host: toPuny(host!),
|
||||
usernameLower: username.toLowerCase(),
|
||||
});
|
||||
|
@ -66,7 +67,7 @@ export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done:
|
|||
target = await resolveUser(username, host);
|
||||
}
|
||||
|
||||
if (await UserListJoinings.findOne({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||
if (await UserListJoinings.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
|
||||
|
||||
pushUserToUserList(target, list!);
|
||||
} catch (e) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { createNotification } from '@/services/create-notification.js';
|
|||
const logger = queueLogger.createSubLogger('ended-poll-notification');
|
||||
|
||||
export async function endedPollNotification(job: Bull.Job<EndedPollNotificationJobData>, done: any): Promise<void> {
|
||||
const note = await Notes.findOne(job.data.noteId);
|
||||
const note = await Notes.findOneBy({ id: job.data.noteId });
|
||||
if (note == null || !note.hasPoll) {
|
||||
done();
|
||||
return;
|
||||
|
|
|
@ -15,6 +15,8 @@ import DbResolver from '@/remote/activitypub/db-resolver.js';
|
|||
import { resolvePerson } from '@/remote/activitypub/models/person.js';
|
||||
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
|
@ -42,11 +44,13 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||
}
|
||||
|
||||
// TDOO: キャッシュ
|
||||
const dbResolver = new DbResolver();
|
||||
|
||||
// HTTP-Signature keyIdを元にDBから取得
|
||||
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||
let authUser: {
|
||||
user: CacheableRemoteUser;
|
||||
key: UserPublickey | null;
|
||||
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||
|
||||
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
|
||||
if (authUser == null) {
|
||||
|
|
|
@ -37,7 +37,7 @@ export default async function cleanRemoteFiles(job: Bull.Job<Record<string, unkn
|
|||
|
||||
deletedCount += 8;
|
||||
|
||||
const total = await DriveFiles.count({
|
||||
const total = await DriveFiles.countBy({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
});
|
||||
|
|
56
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
56
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { WebhookDeliverJobData } from '../types.js';
|
||||
import { getResponse, StatusError } from '@/misc/fetch.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import config from '@/config/index.js';
|
||||
|
||||
const logger = new Logger('webhook');
|
||||
|
||||
let latest: string | null = null;
|
||||
|
||||
export default async (job: Bull.Job<WebhookDeliverJobData>) => {
|
||||
try {
|
||||
if (latest !== (latest = JSON.stringify(job.data.content, null, 2))) {
|
||||
logger.debug(`delivering ${latest}`);
|
||||
}
|
||||
|
||||
const res = await getResponse({
|
||||
url: job.data.to,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify(job.data.content),
|
||||
});
|
||||
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res.status,
|
||||
});
|
||||
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import config from '@/config/index.js';
|
||||
import { initialize as initializeQueue } from './initialize.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js';
|
||||
|
||||
export const systemQueue = initializeQueue<Record<string, unknown>>('system');
|
||||
export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification');
|
||||
|
@ -8,3 +8,14 @@ export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.de
|
|||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
|
||||
export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>('webhookDeliver', 64);
|
||||
|
||||
export const queues = [
|
||||
systemQueue,
|
||||
endedPollNotificationQueue,
|
||||
deliverQueue,
|
||||
inboxQueue,
|
||||
dbQueue,
|
||||
objectStorageQueue,
|
||||
webhookDeliverQueue,
|
||||
];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import httpSignature from 'http-signature';
|
||||
|
||||
|
@ -46,6 +47,13 @@ export type EndedPollNotificationJobData = {
|
|||
noteId: Note['id'];
|
||||
};
|
||||
|
||||
export type WebhookDeliverJobData = {
|
||||
content: unknown;
|
||||
webhookId: Webhook['id'];
|
||||
to: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
export type ThinUser = {
|
||||
id: User['id'];
|
||||
};
|
||||
|
|
|
@ -3,26 +3,26 @@ import Resolver from './resolver.js';
|
|||
import { resolvePerson } from './models/person.js';
|
||||
import { unique, concat } from '@/prelude/array.js';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js';
|
||||
|
||||
type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
||||
|
||||
type AudienceInfo = {
|
||||
visibility: Visibility,
|
||||
mentionedUsers: User[],
|
||||
visibleUsers: User[],
|
||||
mentionedUsers: CacheableUser[],
|
||||
visibleUsers: CacheableUser[],
|
||||
};
|
||||
|
||||
export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||
export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
|
||||
const toGroups = groupingAudience(getApIds(to), actor);
|
||||
const ccGroups = groupingAudience(getApIds(cc), actor);
|
||||
|
||||
const others = unique(concat([toGroups.other, ccGroups.other]));
|
||||
|
||||
const limit = promiseLimit<User | null>(2);
|
||||
const limit = promiseLimit<CacheableUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null)))
|
||||
)).filter((x): x is User => x != null);
|
||||
)).filter((x): x is CacheableUser => x != null);
|
||||
|
||||
if (toGroups.public.length > 0) {
|
||||
return {
|
||||
|
@ -55,7 +55,7 @@ export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApOb
|
|||
};
|
||||
}
|
||||
|
||||
function groupingAudience(ids: string[], actor: IRemoteUser) {
|
||||
function groupingAudience(ids: string[], actor: CacheableRemoteUser) {
|
||||
const groups = {
|
||||
public: [] as string[],
|
||||
followers: [] as string[],
|
||||
|
@ -85,7 +85,7 @@ function isPublic(id: string) {
|
|||
].includes(id);
|
||||
}
|
||||
|
||||
function isFollowers(id: string, actor: IRemoteUser) {
|
||||
function isFollowers(id: string, actor: CacheableRemoteUser) {
|
||||
return (
|
||||
id === (actor.followersUri || `${actor.uri}/followers`)
|
||||
);
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import escapeRegexp from 'escape-regexp';
|
||||
import config from '@/config/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
||||
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
|
||||
import { IObject, getApId } from './type.js';
|
||||
import { resolvePerson } from './models/person.js';
|
||||
import escapeRegexp from 'escape-regexp';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||
|
||||
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
|
||||
export default class DbResolver {
|
||||
constructor() {
|
||||
|
@ -19,15 +24,15 @@ export default class DbResolver {
|
|||
const parsed = this.parseUri(value);
|
||||
|
||||
if (parsed.id) {
|
||||
return (await Notes.findOne({
|
||||
return await Notes.findOneBy({
|
||||
id: parsed.id,
|
||||
})) || null;
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
return (await Notes.findOne({
|
||||
return await Notes.findOneBy({
|
||||
uri: parsed.uri,
|
||||
})) || null;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -37,15 +42,15 @@ export default class DbResolver {
|
|||
const parsed = this.parseUri(value);
|
||||
|
||||
if (parsed.id) {
|
||||
return (await MessagingMessages.findOne({
|
||||
return await MessagingMessages.findOneBy({
|
||||
id: parsed.id,
|
||||
})) || null;
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
return (await MessagingMessages.findOne({
|
||||
return await MessagingMessages.findOneBy({
|
||||
uri: parsed.uri,
|
||||
})) || null;
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -54,19 +59,19 @@ export default class DbResolver {
|
|||
/**
|
||||
* AP Person => Misskey User in DB
|
||||
*/
|
||||
public async getUserFromApId(value: string | IObject): Promise<User | null> {
|
||||
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
|
||||
const parsed = this.parseUri(value);
|
||||
|
||||
if (parsed.id) {
|
||||
return (await Users.findOne({
|
||||
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
|
||||
id: parsed.id,
|
||||
})) || null;
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
}
|
||||
|
||||
if (parsed.uri) {
|
||||
return (await Users.findOne({
|
||||
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
|
||||
uri: parsed.uri,
|
||||
})) || null;
|
||||
}));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -75,17 +80,24 @@ export default class DbResolver {
|
|||
/**
|
||||
* AP KeyId => Misskey User and Key
|
||||
*/
|
||||
public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> {
|
||||
const key = await UserPublickeys.findOne({
|
||||
keyId,
|
||||
});
|
||||
public async getAuthUserFromKeyId(keyId: string): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
key: UserPublickey;
|
||||
} | null> {
|
||||
const key = await publicKeyCache.fetch(keyId, async () => {
|
||||
const key = await UserPublickeys.findOneBy({
|
||||
keyId,
|
||||
});
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
return key;
|
||||
}, key => key != null);
|
||||
|
||||
if (key == null) return null;
|
||||
|
||||
const user = await Users.findOne(key.userId) as IRemoteUser;
|
||||
|
||||
return {
|
||||
user,
|
||||
user: await userByIdCache.fetch(key.userId, () => Users.findOneByOrFail({ id: key.userId })) as CacheableRemoteUser,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
@ -93,12 +105,15 @@ export default class DbResolver {
|
|||
/**
|
||||
* AP Actor id => Misskey User and Key
|
||||
*/
|
||||
public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> {
|
||||
const user = await resolvePerson(uri) as IRemoteUser;
|
||||
public async getAuthUserFromApId(uri: string): Promise<{
|
||||
user: CacheableRemoteUser;
|
||||
key: UserPublickey | null;
|
||||
} | null> {
|
||||
const user = await resolvePerson(uri) as CacheableRemoteUser;
|
||||
|
||||
if (user == null) return null;
|
||||
|
||||
const key = await UserPublickeys.findOne(user.id);
|
||||
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOneBy({ userId: user.id }), v => v != null);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
@ -125,11 +140,6 @@ export default class DbResolver {
|
|||
}
|
||||
}
|
||||
|
||||
export type AuthUser = {
|
||||
user: IRemoteUser;
|
||||
key?: UserPublickey;
|
||||
};
|
||||
|
||||
type UriParseResult = {
|
||||
/** id in DB (local object only) */
|
||||
id?: string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Users, Followings } from '@/models/index.js';
|
||||
import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js';
|
||||
import { deliver } from '@/queue/index.js';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
|
||||
//#region types
|
||||
interface IRecipe {
|
||||
|
@ -78,27 +79,46 @@ export default class DeliverManager {
|
|||
|
||||
const inboxes = new Set<string>();
|
||||
|
||||
// build inbox list
|
||||
for (const recipe of this.recipes) {
|
||||
if (isFollowers(recipe)) {
|
||||
// followers deliver
|
||||
const followers = await Followings.find({
|
||||
followeeId: this.actor.id,
|
||||
});
|
||||
/*
|
||||
build inbox list
|
||||
|
||||
for (const following of followers) {
|
||||
if (Followings.isRemoteFollower(following)) {
|
||||
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||
inboxes.add(inbox);
|
||||
}
|
||||
}
|
||||
} else if (isDirect(recipe)) {
|
||||
// direct deliver
|
||||
const inbox = recipe.to.inbox;
|
||||
if (inbox) inboxes.add(inbox);
|
||||
Process follower recipes first to avoid duplication when processing
|
||||
direct recipes later.
|
||||
*/
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
// followers deliver
|
||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||
const followers = await Followings.find({
|
||||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
}) as {
|
||||
followerSharedInbox: string | null;
|
||||
followerInbox: string;
|
||||
}[];
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||
inboxes.add(inbox);
|
||||
}
|
||||
}
|
||||
|
||||
this.recipes.filter((recipe): recipe is IDirectRecipe => {
|
||||
// followers recipes have already been processed
|
||||
isDirect(recipe)
|
||||
// check that shared inbox has not been added yet
|
||||
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
|
||||
// check that they actually have an inbox
|
||||
&& recipe.to.inbox
|
||||
})
|
||||
.forEach(recipe => inboxes.add(recipe.to.inbox));
|
||||
|
||||
// deliver
|
||||
for (const inbox of inboxes) {
|
||||
deliver(this.actor, this.activity, inbox);
|
||||
|
@ -112,7 +132,7 @@ export default class DeliverManager {
|
|||
* @param activity Activity
|
||||
* @param from Followee
|
||||
*/
|
||||
export async function deliverToFollowers(actor: ILocalUser, activity: any) {
|
||||
export async function deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) {
|
||||
const manager = new DeliverManager(actor, activity);
|
||||
manager.addFollowersRecipe();
|
||||
await manager.execute();
|
||||
|
@ -123,7 +143,7 @@ export async function deliverToFollowers(actor: ILocalUser, activity: any) {
|
|||
* @param activity Activity
|
||||
* @param to Target user
|
||||
*/
|
||||
export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) {
|
||||
export async function deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) {
|
||||
const manager = new DeliverManager(actor, activity);
|
||||
manager.addDirectRecipe(to);
|
||||
await manager.execute();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import accept from '@/services/following/requests/accept.js';
|
||||
import { IFollow } from '../../type.js';
|
||||
import DbResolver from '../../db-resolver.js';
|
||||
import { relayAccepted } from '@/services/relay.js';
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
||||
const dbResolver = new DbResolver();
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Resolver from '../../resolver.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import acceptFollow from './follow.js';
|
||||
import { IAccept, isFollow, getApType } from '../../type.js';
|
||||
import { apLogger } from '../../logger.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => {
|
||||
const uri = activity.id || activity;
|
||||
|
||||
logger.info(`Accept: ${uri}`);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { IAdd } from '../../type.js';
|
||||
import { resolveNote } from '../../models/note.js';
|
||||
import { addPinned } from '@/services/i/pin.js';
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IAdd): Promise<void> => {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Resolver from '../../resolver.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import announceNote from './note.js';
|
||||
import { IAnnounce, getApId } from '../../type.js';
|
||||
import { apLogger } from '../../logger.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<void> => {
|
||||
const uri = getApId(activity);
|
||||
|
||||
logger.info(`Announce: ${uri}`);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Resolver from '../../resolver.js';
|
||||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { IAnnounce, getApId } from '../../type.js';
|
||||
import { fetchNote, resolveNote } from '../../models/note.js';
|
||||
import { apLogger } from '../../logger.js';
|
||||
|
@ -15,10 +15,9 @@ const logger = apLogger;
|
|||
/**
|
||||
* アナウンスアクティビティを捌きます
|
||||
*/
|
||||
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
// アナウンサーが凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { IBlock } from '../../type.js';
|
||||
import block from '@/services/blocking/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import DbResolver from '../../db-resolver.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
|
||||
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
|
||||
|
||||
const dbResolver = new DbResolver();
|
||||
|
@ -17,6 +18,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
|
|||
return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
|
||||
}
|
||||
|
||||
await block(actor, blockee);
|
||||
await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
|
||||
return `ok`;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Resolver from '../../resolver.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import createNote from './note.js';
|
||||
import { ICreate, getApId, isPost, getApType } from '../../type.js';
|
||||
import { apLogger } from '../../logger.js';
|
||||
|
@ -7,7 +7,7 @@ import { toArray, concat, unique } from '@/prelude/array.js';
|
|||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: ICreate): Promise<void> => {
|
||||
const uri = getApId(activity);
|
||||
|
||||
logger.info(`Create: ${uri}`);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Resolver from '../../resolver.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { createNote, fetchNote } from '../../models/note.js';
|
||||
import { getApId, IObject, ICreate } from '../../type.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
|
@ -9,7 +9,7 @@ import { StatusError } from '@/misc/fetch.js';
|
|||
/**
|
||||
* 投稿作成アクティビティを捌きます
|
||||
*/
|
||||
export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
|
||||
export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
|
||||
const uri = getApId(note);
|
||||
|
||||
if (typeof note === 'object') {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { apLogger } from '../../logger.js';
|
||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
|
||||
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||
logger.info(`Deleting the Actor: ${uri}`);
|
||||
|
||||
if (actor.uri !== uri) {
|
||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||
}
|
||||
|
||||
if (actor.isDeleted) {
|
||||
const user = await Users.findOneByOrFail({ id: actor.id });
|
||||
if (user.isDeleted) {
|
||||
logger.info(`skip: already deleted`);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import deleteNote from './note.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type.js';
|
||||
import { toSingle } from '@/prelude/array.js';
|
||||
import { deleteActor } from './actor.js';
|
||||
|
@ -7,7 +7,7 @@ import { deleteActor } from './actor.js';
|
|||
/**
|
||||
* 削除アクティビティを捌きます
|
||||
*/
|
||||
export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
|
||||
export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<string> => {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import deleteNode from '@/services/note/delete.js';
|
||||
import { apLogger } from '../../logger.js';
|
||||
import DbResolver from '../../db-resolver.js';
|
||||
|
@ -7,7 +7,7 @@ import { deleteMessage } from '@/services/messages/delete.js';
|
|||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async function(actor: IRemoteUser, uri: string): Promise<string> {
|
||||
export default async function(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||
logger.info(`Deleting the Note: ${uri}`);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue