diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9ba110f77..4e42fa931 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,25 +1,39 @@ -name: Lint - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - uses: actions/setup-node@v1 - with: - node-version: 12.x - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - run: yarn install - - run: yarn lint +name: Lint + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'yarn' + cache-dependency-path: | + packages/backend/yarn.lock + - run: yarn install + - run: yarn --cwd ./packages/backend lint + + client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions/setup-node@v3 + with: + node-version: 18.x + cache: 'yarn' + cache-dependency-path: | + packages/client/yarn.lock + - run: yarn install + - run: yarn --cwd ./packages/client lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e9585f96..2d858daa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [16.x] + node-version: [18.x] services: postgres: @@ -33,9 +33,13 @@ jobs: with: submodules: true - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' + cache-dependency-path: | + packages/backend/yarn.lock + packages/client/yarn.lock - name: Install dependencies run: yarn install - name: Check yarn.lock @@ -53,7 +57,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [18.x] browser: [chrome] services: @@ -80,13 +84,13 @@ jobs: #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + cache: 'yarn' + cache-dependency-path: | + packages/backend/yarn.lock + packages/client/yarn.lock - name: Install dependencies run: yarn install - name: Check yarn.lock diff --git a/.node-version b/.node-version index bf79505bb..658984787 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v16.14.0 +v18.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e32e71146..a13686b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 12.x.x (unreleased) ### Improvements -- ### Bugfixes - @@ -11,14 +10,69 @@ You should also include the user name that made the change. --> ## 12.x.x (unreleased) +### NOTE +- From this version, Node 18.0.0 or later is required. ### Improvements -- Bull Dashboardを組み込み、ジョブキューの確認や操作を行えるように @syuilo -- Check that installed Node.js version fulfills version requirement @ThatOneCalculator -- Server: performance improvements @syuilo +- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina +- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina +- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina +- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina ### Bugfixes +- Client: fix settings page @tamaina +- Client: fix profile tabs @futchitwo +- Server: await promises when following or unfollowing users @Johann150 +- Client: fix abuse reports page to be able to show all reports @Johann150 +- Federation: Add rel attribute to host-meta @mei23 + +## 12.110.1 (2022/04/23) + +### Bugfixes +- Fix GOP rendering @syuilo +- Improve performance of antenna, clip, and list @xianonn + +## 12.110.0 (2022/04/11) + +### Improvements +- Improve webhook @syuilo +- Client: Show loading icon on splash screen @syuilo + +### Bugfixes +- API: parameter validation of users/show was wrong +- Federation: リモートインスタンスへのダイレクト投稿が届かない問題を修正 @syuilo + +## 12.109.2 (2022/04/03) + +### Bugfixes +- API: admin/update-meta was not working @syuilo +- Client: テーマを切り替えたり読み込んだりするとmeta[name="theme-color"]のcontentがundefinedになる問題を修正 @tamaina + +## 12.109.1 (2022/04/02) + +### Bugfixes +- API: Renoteが行えない問題を修正 + +## 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a696bc5ce..cafca625d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,21 @@ Be willing to comment on the good points and not just the things you want fixed - Are there any omissions or gaps? - Does it check for anomalies? +## Merge +For now, basically only @syuilo has the authority to merge PRs into develop because he is most familiar with the codebase. +However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor. + +## Release +For now, basically only @syuilo has the authority to release Misskey. +However, in case of emergency, a release can be made at the discretion of a contributor. + +### Release Instructions +1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) +2. follow the `master` branch to the `develop` branch. +3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) + - The target branch must be `master` + - The tag name must be the version + ## Localization (l10n) Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. You can improve our translations with your Crowdin account. diff --git a/Dockerfile b/Dockerfile index e4959756e..174e2e9bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.14.0-alpine3.15 AS base +FROM node:18.0.0-alpine3.15 AS base ENV NODE_ENV=production @@ -11,16 +11,16 @@ FROM base AS builder COPY . ./ RUN apk add --no-cache $BUILD_DEPS && \ - git submodule update --init && \ - yarn install && \ - yarn build && \ - rm -rf .git + git submodule update --init && \ + yarn install && \ + yarn build && \ + rm -rf .git FROM base AS runner RUN apk add --no-cache \ - ffmpeg \ - tini + ffmpeg \ + tini ENTRYPOINT ["/sbin/tini", "--"] diff --git a/README.md b/README.md index c7bc9ef21..19e953aee 100644 --- a/README.md +++ b/README.md @@ -50,119 +50,3 @@
RSS3
- -## Backers - - - - - - - - - - - - - - - -
Roujo Oliver Maximilian Seidelweepjp kiritan みなしま Eduardo Quiros
Roujo Oliver Maximilian Seidelweepjp kiritan みなしま Eduardo Quiros
- - - - - - - - - - - - - - - - - - - - -
NesakkoDemogrognardLiaizon Wakestmkatze kabo2468y AureoleArk osapon 見当かなみ Wataru Manji (manji0)
NesakkoDemogrognardLiaizon Wakestmkatze kabo2468y AureoleArk osapon 見当かなみ Wataru Manji (manji0)
- - - - - - - - - - - - - - - - - - -
YuzuRyo61 mewl hayabusaS YTakumi Sugitasikyosyounin YUKIMOCHIxianontotokoro
YuzuRyo61 mewl hayabusaS YTakumi Sugitasikyosyounin YUKIMOCHIxianontotokoro
- - - - - - - - - - - - - - - - - - - - - - - - -
sheeta.s motcha axtuki1 Satsuki Yanagitakimura aqz tamainanafuchoco Atsuko TominaganatalieEBISUMEnoellabo
sheeta.s motcha axtuki1 Satsuki Yanagitakimura aqz tamainanafuchoco Atsuko TominaganatalieEBISUMEnoellabo
- - - - - - - - - - - - - - - - - - -
CG Hekovic uroco @99Chandler Nokotaro Takedanenohi Efertone Takashi Shibuya
CG Hekovic uroco @99Chandler Nokotaro Takedanenohi Efertone Takashi Shibuya
- -**Last updated:** Sun, 26 Jul 2020 07:00:10 UTC - - -[backer-url]: #backers -[backer-badge]: https://opencollective.com/misskey/backers/badge.svg -[backers-image]: https://opencollective.com/misskey/backers.svg -[sponsor-url]: #sponsors -[sponsor-badge]: https://opencollective.com/misskey/sponsors/badge.svg -[sponsors-image]: https://opencollective.com/misskey/sponsors.svg -[support-url]: https://opencollective.com/misskey#support - -[syuilo-link]: https://syuilo.com -[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70 diff --git a/ROADMAP.md b/ROADMAP.md index c12526bbc..3ccc098d3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -26,3 +26,11 @@ Once Phase 1 is complete and an environment conducive to the development of a st 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. diff --git a/gulpfile.js b/gulpfile.js index b7aa4e328..90f8ebaab 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -37,7 +37,6 @@ gulp.task('copy:client:locales', cb => { gulp.task('build:backend:script', () => { return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js']) - .pipe(replace('VERSION', JSON.stringify(meta.version))) .pipe(replace('LANGS', JSON.stringify(Object.keys(locales)))) .pipe(terser({ toplevel: true diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index def791f9d..f3f8b4577 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -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,28 @@ _mfm: jumpDescription: "يمنح للمحتوى حركة قفز." bounce: "تأثير (ارتداد)" bounceDescription: "يمنح للمحتوى حركة ارتدادية" + shake: "تأثير (اهتزاز)" + shakeDescription: "يمنح المحتوى حركة اهتزازية." + spin: "تأثير (دوران)" + spinDescription: "يمنح المحتوى حركة دورانية." x2: "كبير" + x2Description: "يُكبر المحتوى" x3: "كبير جداً" + x3Description: "يُضخم المحتوى" + x4: "هائل" + x4Description: "يُضخم المحتوى أكثر مما سبق." blur: "طمس" + blurDescription: "يطمس المحتوى، لكن بالتمرير فوقه سيظهر بوضوح." font: "الخط" + fontDescription: "الخط المستخدم لعرض المحتوى." rainbow: "قوس قزح" rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف" rotate: "تدوير" + rotateDescription: "يُدير المحتوى بزاوية معيّنة." _instanceTicker: + none: "لا تظهره بتاتًا" remote: "أظهر للمستخدمين البِعاد" + always: "أظهره دائمًا" _serverDisconnectedBehavior: reload: "إعادة تحميل تلقائية" dialog: "أظهر مربع حوار التحذيرات" @@ -899,12 +932,18 @@ _menuDisplay: hide: "إخفاء" _wordMute: muteWords: "الكلمات المحظورة" + muteWordsDescription: "افصل بينهم بمسافة لاستخدام معامل \"و\" أو بسطر لاستخدام معامل \"أو\"." muteWordsDescription2: "احصر الكلمات المفتاحية بين بين شرطتين مائلتين لاستخدامها كتعابير نمطية" softDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني." hardDescription: "اخف الملاحظات التي تستوف الشروط من الخيط الزمني.بالإضافة إلى أن هذه الملاحظات ستبقى مخفية حتى وإن تغيرت الشروط." soft: "لينة" hard: "قاسية" mutedNotes: "الملاحظات المكتومة" +_instanceMute: + instanceMuteDescription: "هذه سيحجب كل ملاحظات الخوادم المحجوبة ومشاركاتها والردود على تلك الملاحظات حتى وإن كانت من خادم غير محجوب." + instanceMuteDescription2: "مدخلة لكل سطر" + title: "يخفي ملاحظات الخوادم المسرودة." + heading: "قائمة الخوادم المحجوبة" _theme: explore: "استكشف قوالب المظهر" install: "تنصيب قالب" @@ -1198,6 +1237,8 @@ _pages: font: "الخط" fontSerif: "Serif" fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "عيّن صورة مصغّرة" + eyeCatchingImageRemove: "احذف صورة مصغّرة" chooseBlock: "إضافة كتلة" selectType: "اختر النوع" enterVariableName: "أدخل اسم المتغيّر" @@ -1458,6 +1499,7 @@ _notification: pollVote: "مصوِت شارك في الاستطلاع" receiveFollowRequest: "طلبات المتابعة المتلقاة" followRequestAccepted: "طلبات المتابعة المقبولة" + groupInvited: "دعوات الفريق" app: "إشعارات التطبيقات المرتبطة" _deck: alwaysShowMainColumn: "أظهر العمود الرئيسي دائمًا" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 870a16bc0..b2ba236fd 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -832,6 +832,10 @@ size: "আকার" numberOfColumn: "কলামের সংখ্যা" searchByGoogle: "গুগল" indefinitely: "অনির্দিষ্ট" +tenMinutes: "১০ মিনিট" +oneHour: "১ ঘণ্টা" +oneDay: "একদিন" +oneWeek: "এক সপ্তাহ" _emailUnavailable: used: "এই ইমেইল ঠিকানাটি ইতোমধ্যে ব্যবহৃত হয়েছে" format: "এই ইমেল ঠিকানাটি সঠিকভাবে লিখা হয়নি" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index e70249da1..1f558787a 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -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://.s3.amazonaws.com'. Für GCS o.ä. verwende 'https://storage.googleapis.com/'." 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" diff --git a/locales/en-US.yml b/locales/en-US.yml index 5ec97f05f..99fe05375 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -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" diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml deleted file mode 100644 index 934ffd2d4..000000000 --- a/locales/eo-UY.yml +++ /dev/null @@ -1,1138 +0,0 @@ ---- -_lang_: "Esperanto" -headlineMisskey: "Reto konektita per notoj" -introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por diskonigi nunan aferon, aŭ por paroli vian penson al ĉiuj ĉirkaŭ vi. 📡\nLa funkcio \"reago\" ebligas esprimi rapide vian senton pri la noto de la alia en la Fediverso. 👍\nBonvole esploru novan mondon. 🚀" -monthAndDay: "La {day}a de l' {month}a" -search: "Serĉi" -notifications: "Sciigoj" -username: "Uzantnomo" -password: "Pasvorto" -forgotPassword: "Ĉu vi forgesis pasvorton?" -fetchingAsApObject: "Informpetado de la Fediverso…" -ok: "Okej" -gotIt: "Kompreni" -cancel: "Nuligi" -enterUsername: "Entajpu uzantnomon" -renotedBy: "Plusendita de {user}" -noNotes: "Neniu noto!" -noNotifications: "Vi ne havas sciigojn." -instance: "Nodo" -settings: "Agordoj" -basicSettings: "Ĝeneralaj agordoj" -otherSettings: "Aliaj agordoj" -openInWindow: "Malfermi en nova fenestro" -profile: "Profilo" -timeline: "Templinio" -noAccountDescription: "La uzanto ankoraŭ ne skribis la prion de sia profilo." -login: "Saluti" -loggingIn: "Salutado…" -logout: "Adiaŭi" -signup: "Registriĝi" -uploading: "Alŝutado…" -save: "Konservi" -users: "Uzantoj" -addUser: "Aldoni uzanton" -favorite: "Preferi" -favorites: "Preferaĵoj" -unfavorite: "Malpreferi" -favorited: "Aldonita al viaj preferaĵoj." -alreadyFavorited: "Jam aldonita al viaj preferaĵoj." -cantFavorite: "Oni ne povis aldoni al viaj preferaĵoj." -pin: "Alpingli" -unpin: "Depingli" -copyContent: "Kopii enhavon" -copyLink: "Kopii la ligilon" -delete: "Forviŝi" -deleteAndEdit: "Forviŝi kaj redakti" -deleteAndEditConfirm: "Ĉu vi certas ke vi volas foriginte redakti la noton? Vi perdos ĉiujn reagojn, plusendojn, kaj respondojn je ĝi." -addToList: "Aldoni al listo" -sendMessage: "Sendi mesaĝon" -copyUsername: "Kopii uzantnomon" -searchUser: "Serĉi uzanton" -reply: "Respondi" -loadMore: "Vidi pli" -showMore: "Vidi pli" -youGotNewFollower: "eksekvis vin" -receiveFollowRequest: "Peto de sekvado estas ricevita" -followRequestAccepted: "La peto de sekvado akceptita" -mention: "Mencioj" -mentions: "Mencioj" -directNotes: "Rekte senditaj" -importAndExport: "Enporti kaj elporti" -import: "Enporti" -export: "Elporti" -files: "Dosieroj" -download: "Elŝuti" -driveFileDeleteConfirm: "Ĉu vi certas, ke vi volas forviŝi la dosieron \"{name}\"? Tio ankaŭ forviŝos la notojn kiuj citas ĝin." -unfollowConfirm: "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?" -exportRequested: "Vi petis elporton. Ĝi povas bezoni longan tempon. Elportaĵo estos aldonita al disko post elportado." -importRequested: "Vi petis enportadon. Ĝi povas bezoni longan tempon. " -lists: "Listoj" -noLists: "Neniu listo" -note: "Noti" -notes: "Notoj" -following: "Sekvatoj" -followers: "Sekvantoj" -followsYou: "Sekvas vin" -createList: "Krei liston" -manageLists: "Bonteni la listojn" -error: "Eraro" -somethingHappened: "Problemo okazis" -retry: "Provi denove" -pageLoadError: "Fuŝiĝis enlegi paĝon." -pageLoadErrorDescription: "Ordinare tio okazas pro la retkondiĉo aŭ la staplo de retumilo.\nPurigu la staplon, aŭ refaru poste. " -serverIsDead: "La servilo ne respondas. Vole atendu iom kaj penu denove." -youShouldUpgradeClient: "Por montri ĉi paĝon, bonvolu enlegi refoje kaj uzi klienton novversian." -enterListName: "Entajpu nomon de la listo" -privacy: "Privateco" -makeFollowManuallyApprove: "Eksekvi vin devas peti al vi" -defaultNoteVisibility: "Implicita videbleco de la noto" -follow: "Sekvi" -followRequest: "Peti de sekvado" -followRequests: "Petoj de sekvado" -unfollow: "Ne plu sekvi" -followRequestPending: "Atendado akcepti vian peton de eksekvado" -enterEmoji: "Entajpu emoĵion" -renote: "Plusendi la noton" -unrenote: "Malfari plusendon" -renoted: "Sukcese plusendita" -cantRenote: "Oni ne povas plusendi la noton." -cantReRenote: "Plusendo ne estas plusendebla." -quote: "Citi" -pinnedNote: "Alpinglita noto" -pinned: "Alpingli" -you: "Vi" -clickToShow: "Klaki por malkaŝi" -sensitive: "Enhavo ne estas deca por laborejo (NSFW)" -add: "Aldoni" -reaction: "Reagoj" -reactionSetting: "Reagoj aperontaj en la elektilo de reagoj" -rememberNoteVisibility: "Rememori la agordon de videbleco de la lasta afiŝado" -attachCancel: "Deigi aldonaĵon" -markAsSensitive: "Troviĝi NSFW" -unmarkAsSensitive: "Ne troviĝi NSFW" -enterFileName: "Entajpu nomon de la dosiero" -mute: "Silentigi" -unmute: "Malsilentigi" -block: "Bloki" -unblock: "Malbloki" -suspend: "Flostigi" -unsuspend: "Fandi" -blockConfirm: "Ĉu vi certas ke vi volas bloki la uzanton?" -unblockConfirm: "Ĉu vi certas ke vi volas malbloki la uzanton?" -suspendConfirm: "Ĉu vi certas ke vi volas frostigi la uzanton?" -unsuspendConfirm: "Ĉu vi certas ke vi volas fandi la uzanton?" -selectList: "Elekti liston" -selectAntenna: "Elekti antenon" -selectWidget: "Elekti enestraĵon" -editWidgets: "Redakti fenestraĵon" -editWidgetsExit: "Fini la redaktadon" -customEmojis: "Propraj emoĝioj" -emoji: "Emoĵio" -emojis: "Emoĵio" -emojiName: "Nomo de la emoĵio" -emojiUrl: "URL de la emoĵio" -addEmoji: "Aldoni emoĵion" -settingGuide: "Agordaj rekomendoj" -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." -autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas" -addAccount: "Aldoni konton" -loginFailed: "Saluto malsukcesis" -showOnRemote: "Vidi pli al la originala profilo" -general: "Ĝenerala" -wallpaper: "Ekranfonoj" -setWallpaper: "Apliki ekranfonon" -removeWallpaper: "Forviŝi ekranfonon. " -searchWith: "Serĉi: {q}" -youHaveNoLists: "Vi ne havas listojn." -followConfirm: "Ĉu vi certas ke vi volas sekvi {name}?" -proxyAccount: "Retperanta konto" -host: "Nodo" -selectUser: "Elekti uzanton" -recipient: "Ricevonton" -annotation: "Komentarioj" -federation: "Federaĵo" -instances: "Nodoj" -registeredAt: "Registrita je" -latestRequestSentAt: "La laste sendita peto" -latestRequestReceivedAt: "La laste ricevita peto " -latestStatus: "Laŭstato" -charts: "Diagramoj" -perHour: "por horo" -perDay: "por tago" -blockThisInstance: "Bloki la nodon" -operations: "Agoj" -software: "Programaro" -version: "Versio" -metadata: "Pridatumoj" -withNFiles: "{n} dosiero(j)" -monitor: "Monitoro" -network: "Reto" -disk: "Disko" -instanceInfo: "Informoj sur la nodo" -statistics: "Statistikoj" -clearCachedFiles: "Malplenigi la staplon" -clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn forajn dosierojn en la staplo?" -blockedInstances: "Blokitaj nodoj" -muteAndBlock: "Silentigi / Bloki" -mutedUsers: "Silentigitaj uzantoj" -blockedUsers: "Blokitaj uzantoj" -noUsers: "Neniu uzanto" -editProfile: "Redakti profilon" -noteDeleteConfirm: "Ĉu vi certas ke vi volas forviŝi la noton?" -pinLimitExceeded: "Vi ne povas alpingli pli" -intro: "La instalado de Misskey finiĝis! Volu krei administran konton." -done: "Fini" -processing: "Prilaborado…" -preview: "Antaŭmontro" -default: "Implicitaĵo" -noCustomEmojis: "Neniu emoĵio" -noJobs: "Neniu laboro" -federating: "Federantaj" -blocked: "Blokitaj" -suspended: "Suspenditaj" -all: "Ĉiuj" -subscribing: "Abonitaj" -publishing: "Abonintaj" -notResponding: "Alvokato ne disponeblas" -instanceFollowing: "Sekvatoj en la nodo" -instanceFollowers: "Sekvantoj el la nodo" -instanceUsers: "Uzantoj de la nodo" -changePassword: "Ŝanĝi pasvorton" -security: "Sekureco" -retypedNotMatch: "La enigitoj ne estas konformaj." -currentPassword: "Aktuala pasvorto" -newPassword: "Nova pasvorto" -newPasswordRetype: "Reentajpu la novan pasvorton" -attachFile: "Aldoni dosieron" -more: "Pli!" -featured: "Maksimumi" -usernameOrUserId: "Uzantnomo aŭ identigilo de uzanto" -noSuchUser: "Neniuj uzantoj trovitaj" -lookup: "Informpeti" -announcements: "Novaĵoj" -imageUrl: "URL de la bildo" -remove: "Forigi" -removed: "Forigita" -removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"n?" -deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'?" -resetAreYouSure: "Ĉu vi certas restarigi?" -saved: "Konservita" -messaging: "Retbabili" -upload: "Alŝuti" -keepOriginalUploading: "Konservi originalon" -fromDrive: "De la disko" -fromUrl: "De URL" -uploadFromUrl: "Alŝuti de URL" -uploadFromUrlDescription: "URL de la dosiero kiun vi volas alŝuti" -uploadFromUrlRequested: "La alŝutado estis patita" -explore: "Esplori" -messageRead: "Legita" -noMoreHistory: "Ne plu de la historio" -startMessaging: "Komenci babiladon" -nUsersRead: "Legita de {n} homoj" -agreeTo: "Mi akceptas {0}" -tos: "Kondiĉoj de uzado" -start: "Komenciĝi" -home: "Hejma" -remoteUserCaution: "Pro fora uzanto, la infomoj ne estas tuto." -activity: "Aktiveco" -images: "Bildoj" -birthday: "Naskiĝdato" -yearsOld: "{age} jaroj aĝa" -registeredDate: "Dato de registriĝo" -location: "Kie" -theme: "Koloraro" -themeForLightMode: "Koloraro uzita en la luma modo" -themeForDarkMode: "Koloraro uzita en la malluma modo" -light: "Luma" -dark: "Malluma" -lightThemes: "Luma koloraro" -darkThemes: "Malluma koloraro" -syncDeviceDarkMode: "Speguli la luman modon de via aparato" -drive: "Disko" -fileName: "Dosiernomo" -selectFile: "Elekti dosieron" -selectFiles: "Elekti dosieron" -selectFolder: "Elekti dosierujon" -selectFolders: "Elekti dosierujon" -renameFile: "Alinomi la dosieron" -folderName: "Nomo de la dosierujo" -createFolder: "Krei dosierujon" -renameFolder: "Alinomi la dosierujon" -deleteFolder: "Forviŝi dosierujon" -addFile: "Aldoni dosieron" -emptyDrive: "La disko malplenas" -emptyFolder: "La dosierujo malplenas" -unableToDelete: "Ne forviŝebla" -inputNewFileName: "Entajpu novan nomon de la dosiero" -inputNewDescription: "Entajpu novan priskribon" -inputNewFolderName: "Entajpu novan nomon de la dosierujo" -hasChildFilesOrFolders: "La dosierujo ne estas forviŝebla, ĉar ĝi ne malplenas." -copyUrl: "Kopii URL" -rename: "Alinomi" -avatar: "Bildsimbolo" -banner: "Standardo" -nsfw: "Enhavo ne estas deca por laborejo (NSFW)" -whenServerDisconnected: "Kiam vi malligiĝas de servilo" -disconnectedFromServer: "Malkonektita de servilo" -reload: "Reŝargi" -doNothing: "Ignori" -reloadConfirm: "Ĉu vi volas reŝargi?" -watch: "Observi" -unwatch: "Malobservi" -accept: "Permesi" -reject: "Malakcepti" -normal: "Normala" -instanceName: "Nomo de la nodo" -instanceDescription: "Priskribo de la nodo " -maintainerName: "Nomo de la administranto" -maintainerEmail: "Retpoŝtadreso de la administranto" -tosUrl: "URL de kondiĉoj de uzado" -thisYear: "Ĉi-jare" -thisMonth: "Ĉi-monate" -today: "Hodiaŭ" -dayX: "{day}a" -monthX: "La {month}a monato" -yearX: "La jaro {year}" -pages: "Paĝoj" -integration: "Integri" -connectService: "Konekti" -disconnectService: "Farkonektiĝi" -enableLocalTimeline: "Ebligi lokan templinion" -enableGlobalTimeline: "Ebligi mallokan templinion" -registration: "Registri" -enableRegistration: "Ebligi novan uzanton registriĝon" -invite: "Inviti" -driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto" -driveCapacityPerRemoteAccount: "Volumo de disko po unu fora uzanto" -iconUrl: "URL de la bildsimbolo (retpaĝsimbolo, ktp.)" -bannerUrl: "URL de standardo" -backgroundImageUrl: "URL de la fona bildo" -basicInfo: "Baza informo" -pinnedUsers: "Alpinglita uzanto" -pinnedUsersDescription: "Laŭlinigu uzantnomojn en ĉiu linio, por alpingli al la paĝoj ekz \"Esplori\"." -pinnedPages: "Alpinglitaj paĝoj" -pinnedPagesDescription: "Laŭlinigu dosierindikojn de paĝo en ĉiu linio, por alpingli al la ĉefpaĝo de la nodo." -pinnedNotes: "Alpinglita noto" -hcaptcha: "hCaptcha" -enableHcaptcha: "Ebligi hCaptcha" -hcaptchaSiteKey: "Reteja ŝlosilo" -hcaptchaSecretKey: "Sekreta ŝlosilo" -recaptcha: "reCAPTCHA" -enableRecaptcha: "Ebligi reCAPTCHA" -recaptchaSiteKey: "Reteja ŝlosilo" -recaptchaSecretKey: "Sekreta ŝlosilo" -antennas: "Antenoj" -manageAntennas: "Administri antenojn" -name: "Nomo" -antennaSource: "Fonto de la anteno" -antennaKeywords: "Ricevi per ĉefterminoj" -antennaExcludeKeywords: "Krom ĉefterminoj" -notifyAntenna: "Oni sciigos novajn notojn" -withFileAntenna: "Nur kun aldonaĵo" -enableServiceworker: "Aktivigi ServiceWorker" -caseSensitive: "Distingi usklecon" -withReplies: "Inkluzive respondoj" -connectedTo: "Sekva konto estas konektita" -notesAndReplies: "Kun respondoj" -withFiles: "Kun aldonaĵo" -silence: "Mutigi" -silenceConfirm: "Ĉu vi certas ke vi volas mutigi la uzanton?" -unsilence: "Malmutigi" -unsilenceConfirm: "Ĉu vi certas ke vi volas malmutigi la uzanton?" -popularUsers: "Popularaj uzantoj" -recentlyUpdatedUsers: "Lastatempe afiŝintaj uzantoj" -recentlyRegisteredUsers: "Lastatempe aniĝintaj uzantoj" -recentlyDiscoveredUsers: "Lastatempe eltrovitaj uzantoj" -exploreUsersCount: "Jen {count} uzantoj" -exploreFediverse: "Esplori la Fediverson" -popularTags: "Popularaj kradvortoj" -userList: "Listoj" -about: "Informoj" -aboutMisskey: "Pri Misskey" -administrator: "Administranto" -token: "Peco" -twoStepAuthentication: "Dua-faktora aŭtentiko" -moderator: "Kontrolisto" -nUsersMentioned: "{n} uzanto(j) menciis" -securityKey: "Sekureca ŝlosilo" -securityKeyName: "Nomo de la ŝlosilo" -registerSecurityKey: "Registri ŝlosilon de sekureco" -lastUsed: "La plej malnove uzita" -unregister: "Malregistriĝi" -passwordLessLogin: "Saluti sen pasvorto" -resetPassword: "Restarigi pasvorton" -newPasswordIs: "La nova pasvorto estas {password}." -reduceUiAnimation: "Redukti la animaciojn de la fasado" -share: "Kundividi" -notFound: "Ne trovita" -uploadFolder: "Dosierujo implicita por alŝuto" -cacheClear: "Malplenigi staplon" -markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legita" -markAsReadAllUnreadNotes: "Marki ĉiujn afiŝojn kiel legita" -markAsReadAllTalkMessages: "Marki ĉiujn retbabiladojn kiel legita" -help: "Manlibro de uzado" -inputMessageHere: "Entajpu mesaĝon tie" -close: "Fermi" -group: "Grupo" -groups: "Grupoj" -createGroup: "Krei grupon" -ownedGroups: "Administrataj grupoj" -joinedGroups: "Grupoj al kiuj vi aliĝis" -invites: "Inviti" -groupName: "Grupa nomo" -members: "Membroj" -transfer: "Movi" -messagingWithUser: "Private babili " -messagingWithGroup: "Babili grupe" -title: "Titolo" -text: "Teksto" -enable: "Ebligi" -next: "Sekve" -retype: "Retajpu" -noteOf: "Noto de {user}" -inviteToGroup: "Inviti al grupo" -quoteAttached: "Kun citaĵo" -quoteQuestion: "Ĉu vi volas aldoni citaĵon?" -noMessagesYet: "Ankoraŭ neniu mesaĝo" -newMessageExists: "Vi ricevis novan mesaĝon." -onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo." -signinRequired: "Bonvolu saluti" -invitations: "Inviti" -invitationCode: "Kodo de invito" -checking: "kontrolante..." -available: "Disposabla" -unavailable: "Ne disponebla" -usernameInvalidFormat: "La uzantnomo povas enhavi minusklajn kaj majusklajn literojn, numerojn, nur kaj '_'." -tooShort: "Tro mallonga" -tooLong: "Tro longa" -weakPassword: "Malforta pasvorto" -normalPassword: "Normala pasvorto" -strongPassword: "Forta pasvorto" -passwordMatched: "Konforma" -passwordNotMatched: "Nekonforma" -signinWith: "Saluti kun {x}" -signinFailed: "Fuŝiĝis ensaluti. Bonvolu kontroli uzantnomon kaj pasvorton." -or: "Aŭ" -language: "Lingvo" -uiLanguage: "Lingvo de la fasado" -groupInvited: "Invitita al grupo" -aboutX: "Pri {x}" -useOsNativeEmojis: "Uzi la emoĵiojn implicitan de la operaciumo" -youHaveNoGroups: "Neniuj grupoj" -noHistory: "Neniom historio" -signinHistory: "Historio de aliroj al la konto" -doing: "Traktado..." -category: "Kategorio" -tags: "Etikedoj" -docSource: "Fonto de la dokumento" -createAccount: "Krei konton" -existingAccount: "Ekzistan konton" -regenerate: "Regeneri" -fontSize: "Tipara grando" -noFollowRequests: "Vi ne havas peto de sekvado" -openImageInNewTab: "Malfermi la bildojn en nova tablo" -dashboard: "Stirpanelo" -local: "Loka" -remote: "Fora" -total: "Entute" -appearance: "Eksteraĵo" -clientSettings: "Agordoj de kliento" -accountSettings: "Agordoj de konto" -promote: "Reklamata" -numberOfDays: "Nombro de tagoj" -hideThisNote: "Kaŝi la noton" -showFeaturedNotesInTimeline: "Montri en via templinio notojn de la tendenco" -objectStorageBaseUrl: "Baza URL" -objectStoragePrefix: "Prefix" -objectStorageRegion: "Regiono" -objectStorageUseSSL: "Oni uzas SSL" -objectStorageUseProxy: "Uzi retperilon" -serverLogs: "Servila protokolo" -deleteAll: "Forviŝi ĉiujn" -newNoteRecived: "Jen novaj notoj" -sounds: "Sonoj" -listen: "Aŭdi" -none: "Neniu" -showInPage: "Vidi en paĝo" -popout: "Superigi" -volume: "Laŭteco" -masterVolume: "Baza laŭteco" -details: "Detaloj" -chooseEmoji: "Elekti emoĵion" -recentUsed: "Lastatempe uzitaj" -install: "Instali" -uninstall: "Malinstali" -installedApps: "Instalita programo" -nothing: "Neniu" -installedDate: "Dato de instalado" -lastUsedDate: "Lastfoje uzita je" -state: "Stato" -sort: "Ordigi laŭ" -ascendingOrder: "Kreska ordo" -descendingOrder: "Malkreska ordo" -scratchpad: "Malneta redaktilo" -output: "Elmeto" -script: "Skripto" -disablePagesScript: "Malebligi AiScript en la paĝoj" -updateRemoteUser: "Aktualigi informon de foraj uzantoj" -deleteAllFiles: "Forviŝi ĉiujn dosierojn" -deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?" -removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn" -userSuspended: "La uzanto estas flostigita." -userSilenced: "La uzanto estas mutigita." -yourAccountSuspendedTitle: "La konto estas frostigita." -menu: "Menuo" -addItem: "Aldoni novaĵon" -deletedNote: "Forviŝita noto" -invisibleNote: "Malpublikigita noto" -enableInfiniteScroll: "Ebligi infinitan rulumon" -visibility: "Videbleco" -poll: "Enketo" -useCw: "Kaŝi enhavo" -enablePlayer: "Vidigi la filmeton" -disablePlayer: "Malfermi la filmeton" -expandTweet: "Disvolvi pepon" -themeEditor: "Redaktilo de koloraroj" -description: "Priskribo" -describeFile: "Priskribi la bildon" -enterFileDescription: "Priskribu" -author: "Aŭtoro" -manage: "Bonteni" -plugins: "Kromaĵoj" -deck: "Kartaro" -useFullReactionPicker: "Uzi la tuton de la elektilon de reagoj" -width: "Larĝeco" -height: "Alteco" -large: "Granda" -medium: "Meza" -small: "Malgranda" -generateAccessToken: "Generi aŭtentikigan pecon" -permission: "Permesoj" -enableAll: "Ebligi ĉiujn" -disableAll: "Malebligi ĉiujn" -tokenRequested: "Alirpermeso al konto" -notificationType: "Tipo de sciigoj" -edit: "Redakti" -emailServer: "Retpoŝta servilo" -enableEmail: "Ebligi dissendon de retpoŝto" -emailConfigInfo: "Uzata por konfirmi vian retadreson kiam registri kaj por restarigi vian pasvorton" -email: "Retpoŝto" -emailAddress: "Retpoŝtadreso" -smtpConfig: "Agordoj de SMTP servilo" -smtpHost: "Transa servilo" -smtpPort: "Pordo" -smtpUser: "Uzantnomo" -smtpPass: "Pasvorto" -wordMute: "Silentigi specifajn vortojn" -userSaysSomething: "{name} diras ion" -makeActive: "Aktivigi" -display: "Vidi" -copy: "Kopii" -metrics: "mezurciferoj" -overview: "Resumo" -logs: "Protokoloj" -delayed: "Prokrasto " -database: "Datumbazo" -channel: "Kanaloj" -create: "Krei" -notificationSetting: "Agordoj de sciigoj" -useGlobalSetting: "Oni uzas malloka agordo" -other: "Aliaj" -regenerateLoginToken: "Regeneri la aŭtentikigan pecon" -fileIdOrUrl: "Dosiera identigilo aŭ URL" -behavior: "Konduto" -sample: "Ekzemplo" -reporter: "Informanto" -send: "Sendi" -openInNewTab: "Malfermi en nova langeto" -editTheseSettingsMayBreakAccount: "Redakti tiujn agordojn povas damaĝi vian konton." -instanceTicker: "Nomo de la nodo sendinta notojn" -waitingFor: "Atendado pro {x}" -random: "Hazarde" -system: "Sistemo" -switchUi: "Modifi la aspektigon" -desktop: "Labortablo" -createNew: "Krei novan" -optional: "Opciaj" -public: "Publika" -i18nInfo: "Misskey estas tradukata en diversaj lingvoj de volontuloj. Oni povas kontribui ĉe {link}." -manageAccessTokens: "Bonteni la aŭtentikigajn pecojn" -accountInfo: "Kontaj Informoj" -notesCount: "La nombro de notoj" -repliesCount: "La nombro de respondoj senditaj" -renotesCount: "La nombro de notoj plusenditaj de la uzanto" -repliedCount: "La nombro de respondoj ricevitaj" -renotedCount: "La nombro de plusendoj de la notoj skribitaj de la uzanto" -followingCount: "La nombro de sekvatoj" -followersCount: "La nombro de sekvantoj" -sentReactionsCount: "La nombro de la reagoj senditaj" -receivedReactionsCount: "La nombro de la reagoj ricevitaj" -pollVotesCount: "Nombro de voĉdonado" -pollVotedCount: "La nombro de la voĉoj ricevitaj en siaj enketoj" -yes: "Jes" -no: "Ne" -driveFilesCount: "La nombro de la dosieroj sur la disko" -notSet: "Ne elektita" -emailVerified: "Via retpoŝtadreso estis kontrolita." -noteFavoritesCount: "La nombro de notoj preferataj" -pageLikesCount: "La nombro de paĝa plaĉon" -pageLikedCount: "La nombro de la ricevita \"Mi plaĉas\"" -contact: "Kontakto" -useSystemFont: "Uzi la tiparon implicitan de la sistemo" -developer: "Evoluiganto" -makeExplorable: "La konton videbligi sur la paĝo \"Esplori\"" -makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros sur la paĝo \"Esplori\"." -duplicate: "Duobligi" -left: "Maldekstra" -center: "Centra" -wide: "Vasta" -narrow: "Malvasta" -needReloadToApply: "Tiu agordo estos aplikita nur poste reŝargi." -showTitlebar: "Videbligi titolan stangon" -clearCache: "Malplenigi staplon" -onlineUsersCount: "{n} uzantoj estas surlineaj" -nUsers: "{n} uzanto(j)" -nNotes: "{n} notoj" -myTheme: "Miaj koloraroj" -backgroundColor: "Fona koloro" -textColor: "Teksto" -saveAs: "Konservi kiel…" -advanced: "Altnivela" -value: "Valoro" -createdAt: "Kreita je" -updatedAt: "Laste ĝisdatigita" -saveConfirm: "Ĉu vi konservas la ŝanĝon?" -deleteConfirm: "Ĉu certas forviŝi?" -invalidValue: "Nevalida valoro" -closeAccount: "Forigi konton" -currentVersion: "La aktuala versio" -latestVersion: "La plej nova versio" -youAreRunningUpToDateClient: "Vi uzas la plej novan version de via kliento." -newVersionOfClientAvailable: "Nova versio de via kliento estas disponebla." -inUse: "Uzata" -editCode: "Redakti kodon" -receiveAnnouncementFromInstance: "Ricevi informojn sciigintajn de la nodo" -emailNotification: "Sciigoj per retpoŝto" -inChannelSearch: "Serĉi en la kanalo" -useReactionPickerForContextMenu: "Dekstre-klaki por malfermi la elektilon de reagoj" -typingUsers: "{users} nun skribas…" -clear: "Vakigi" -markAllAsRead: "Marki ĉiujn kiel legito" -goBack: "Reiri antaŭ" -addDescription: "Priskribi" -info: "Informoj" -userInfo: "Informoj de uzanto" -unknown: "Nekonata" -online: "Surkonektita" -offline: "Forkonektita" -notRecommended: "Evitindaj" -instanceBlocking: "Bloki specifajn nodojn" -selectAccount: "Elekti konton" -user: "Uzantoj" -administration: "Bontenado" -accounts: "Kontoj" -configure: "Agordi" -recentPosts: "Novaj afiŝoj" -popularPosts: "Plej viditaj" -shareWithNote: "Kundividi en noto" -ads: "Reklamaĵo" -expiration: "Limtempo" -memo: "Memorigilo" -high: "Alta" -middle: "Meza" -low: "Malalta" -emailNotConfiguredWarning: "Vi ne agordis retpoŝtadreso." -customCss: "Propra CSS" -global: "Malloka" -squareAvatars: "Montri bildsimbolon kiel kvadrata" -sent: "Sendi" -received: "Ricevita" -searchResult: "Serĉorezultoj" -hashtags: "Kradvorto" -troubleshooting: "Problemsolvi" -learnMore: "Lernu pli" -misskeyUpdated: "Misskey ĝisdatiĝis!" -whatIsNew: "Montri novaĵojn" -translate: "Traduki" -translatedFrom: "Tradukita el {x}" -accountDeletionInProgress: "La konto estas forviŝanta." -resolved: "Solvita" -unresolved: "Nesolvita" -breakFollow: "Ĉesigi la sekvadon al vi" -itsOn: "Ŝaltita" -emailRequiredForSignup: "Registri konton devas konformi retpoŝtadreson" -unread: "Nelegita" -filter: "Filtrilo" -controlPanel: "Ŝaltpodio" -manageAccounts: "Bonteni la kontojn" -classic: "Klasika" -muteThread: "Silentigi la mesaĝaron" -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" -incorrectPassword: "Nevalida pasvorto" -voteConfirm: "Ĉu vi voĉdonas {choice}n?" -hide: "Kaŝi" -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." -smartphone: "Saĝtelefono" -tablet: "Platkomputilo" -auto: "Aŭtomate" -searchByGoogle: "Serĉi en Google-Serĉo" -tenMinutes: "10 minutoj" -oneHour: "1 horo" -oneDay: "1 tago" -oneWeek: "1 semajno" -_emailUnavailable: - used: "La retpoŝto jam estas uzita." - format: "Nevalida formato." - disposable: "Dumtempa retpoŝto ne estas uzebla." - mx: "Ĉi retpoŝto-servilo ne estas uzebla." - smtp: "Tiu retpoŝta servilo ne respondas" -_ffVisibility: - public: "Publika" - followers: "Nur al sekvantoj" - private: "Malpublikigita" -_signup: - almostThere: "Preskaŭ plenumita" - emailAddressInfo: "Entajpu vian retpoŝton" -_accountDelete: - accountDelete: "Forigi konton" - requestAccountDelete: "Peti forviŝi konton" - started: "Forviŝado komenciĝis." -_ad: - back: "Nuligi" -_forgotPassword: - enterEmail: "Entajpu la retpoŝton kiun vi registrigis al via konto. Ligilo por restarigi pasvorton estos sendita al la retadreso." -_gallery: - my: "Miaj afiŝoj" - liked: "Ŝatitaj notoj" - like: "Ŝati" -_email: - _follow: - title: "Eksekvis vin" - _receiveFollowRequest: - title: "Vi ricevis peton de sekvado" -_plugin: - install: "Instali kromaĵon" - manage: "Bonteni kromaĵojn" -_registry: - key: "Ŝlosilo" - keys: "Ŝlosiloj" - domain: "Nomregno" - createKey: "Krei ŝlosilon" -_aboutMisskey: - about: "Misskey estas malfermitkoda programo evoluigata de syuilo ekde la 2014." - contributors: "Precipaj kontribuantoj" - allContributors: "Ĉiuj kontribuantoj" - source: "Fontkodo" - translation: "Traduki Misskey" - donate: "Mondonaci al Misskey" - patrons: "Mecenatoj" -_mfm: - dummy: "Misskey evoluigas la mondon de Fediverso" - mention: "Mencioj" - hashtag: "Kradvorto" - url: "URL" - link: "Ligilo" - bold: "Grasa" - small: "Malgrande" - center: "Centrigi" - inlineCode: "Kodo (en linio)" - blockCode: "Kodo (bloko)" - inlineMath: "Formulo (en linio)" - blockMath: "Formulo (bloko)" - quote: "Citi" - emoji: "Propraj emoĝioj" - search: "Serĉi" - flip: "Inversa" - x2: "Granda" - x3: "Grandega" - x4: "Pli grandega" - font: "Presliteraro" - rotate: "Orientiĝo" -_instanceTicker: - none: "Ne montri" - remote: "Montri al foraj uzantoj" - always: "Ĉiam montri" -_serverDisconnectedBehavior: - reload: "Aŭtomate reŝargi" -_channel: - create: "Krei kanalon" - edit: "Redakti kanalon" - setBanner: "Apliki standardan bildon" - removeBanner: "Forviŝi la standardan bildon" - owned: "Bontenitaj de vi" - following: "Sekvado" - usersCount: "{n} partoprenantoj" - notesCount: "{n} notoj" -_menuDisplay: - sideFull: "Sur la flanko" - sideIcon: "Sur la flanko (bildsimbolo)" - top: "Sur la supro" - hide: "Kaŝi" -_wordMute: - muteWords: "Silentigitaj vortoj" - soft: "Per la kliento" - hard: "Per la servilo" - mutedNotes: "Silentigitaj notoj" -_theme: - explore: "Serĉi koloraron" - install: "Instali koloraron" - manage: "Bonteni kolorarojn" - code: "Kolorara kodo" - description: "Priskribo" - installedThemes: "Instalita koloraro" - make: "Krei koloraron" - addConstant: "Aldoni konstanton" - constant: "Konstanto" - defaultValue: "Implicitaĵa valoro" - color: "Koloro" - func: "Funkcio" - darken: "Malbrileco" - lighten: "Brileco" - keys: - bg: "Fono" - shadow: "Ombro" - navBg: "Fono de flanka stango" - link: "Ligilo" - hashtag: "Kradvorto" - mention: "Mencioj" - mentionMe: "Mencio al vi" - renote: "Plusendita" - infoBg: "Fono de informo" - buttonBg: "Fono de butono" - driveFolderBg: "Fono de dosierujo de la disko" - messageBg: "Fono de la retbabilejo" -_sfx: - note: "Nova noto" - noteMy: "Mia noto" - notification: "Sciigoj" - chat: "Retbabili" - chatBg: "Retbabili (BG)" - antenna: "Ricevo de la anteno" - channel: "Sciigoj de kanalo" -_ago: - future: "Futuro" - justNow: "Ĵus" - secondsAgo: "Antaŭ {n} sekundoj" - minutesAgo: "Antaŭ {n} minutoj" - hoursAgo: "Antaŭ {n} horo(j)" - daysAgo: "Antaŭ {n} tago(j)" - weeksAgo: "Antaŭ {n} semajnoj" - monthsAgo: "Antaŭ {n} monatoj" - yearsAgo: "Antaŭ {n} jaroj" -_time: - second: "sek" - minute: "min" - hour: "hor" - day: "Tago" -_tutorial: - title: "Uzado de Misskey" - step1_1: "Bonvenon." - step7_2: "Se vi volas pli scii pri Misskey, vidu la fakon {help}." - step7_3: "Do, bonvolu amuziĝi sur Misskey🚀" -_2fa: - registerKey: "Nove registri ŝlosilon" -_permissions: - "read:account": "Vidi la informojn de via konto" - "write:account": "Redatado de la informoj de via konto" - "read:blocks": "Vidi vian liston de uzantoj blokitaj" - "write:blocks": "Redakti vian liston de blokitoj" - "read:drive": "Legi vian diskon" - "write:drive": "Ĉia operacio por skribi, forviŝi, aŭ alimaniere ŝanĝi la informon de dosiero en via disko de Misskey" - "read:favorites": "Vidi vian liston de preferaĵoj" - "write:favorites": "Redakti vian liston de preferaĵoj" - "read:following": "Vidi la informojn de sekvo" - "write:following": "Sekvi/ Ĉesi sekvi alian uzanton" - "read:messaging": "Vidi viajn retbabiladojn" - "write:messaging": "Administri viajn retbabiladojn" - "read:mutes": "Vidi vian liston de silentigitoj" - "write:mutes": "Redakti vian liston de silentigitoj" - "write:notes": "Krei / Forviŝi noton" - "read:notifications": "Vidi sciigojn" - "write:notifications": "Manipulado por viaj sciigoj" - "read:reactions": "Vidi reagojn" - "write:reactions": "Redakti viajn reagojn" - "read:page-likes": "Vidi ŝatojn de paĝo" - "read:user-groups": "Vidi viajn grupojn de uzantoj" - "read:channels": "Vidi kanalojn" -_antennaSources: - all: "Ĉiuj notoj" - homeTimeline: "Notoj de la uzantoj kiujn vi sekvas" -_weekday: - sunday: "Dimanĉo" - monday: "Lundo" - tuesday: "Mardo" - wednesday: "Merkredo" - thursday: "Ĵaŭdo" - friday: "Vendredo" - saturday: "Sabato" -_widgets: - notifications: "Sciigoj" - timeline: "Templinio" - clock: "Horloĝo" - activity: "Aktiveco" - photos: "Fotoj" - federation: "Federaĵo" - slideshow: "Bildoprezento" - button: "Butono" - onlineUsers: "Surkonektitaj uzantoj" - aichan: "Ai" -_cw: - show: "Vidi pli" - chars: "{count} literoj" - files: "{count} dosiero(j)" -_poll: - choiceN: "Ebla voĉdono {n}" - noMore: "Oni ne povas aldoni pli" - canMultipleVote: "Permesi plurelekton" - expiration: "Limtempo" - deadlineTime: "hor" - duration: "Daŭro" - votesCount: "{n} voĉoj" - totalVotes: "Sume {n} voĉoj" - vote: "Voĉdoni" - showResult: "Vidi la rezultojn" - voted: "Voĉdonita" - closed: "Finita" -_visibility: - public: "Publika" - publicDescription: "Publikigi al ĉiuj en la Fediverso" - home: "Hejma" - homeDescription: "Dissendi nur sur hejma templinio" - followers: "Nur al sekvantoj" - followersDescription: "Videbligi nur al sekvantoj" - specified: "Rekte montrita" - specifiedDescription: "Montri nur al specifaj uzantoj" - localOnly: "Nur loka" - localOnlyDescription: "Ne videbligi al foraj uzantoj" -_postForm: - replyPlaceholder: "Respondi la noton…" - quotePlaceholder: "Citi la noton…" - channelPlaceholder: "Afiŝi en la kanalo…" - _placeholders: - a: "Kiel vi fartas?" - b: "Kio okazis ĉirkaŭ vi?" - c: "Kio estas sur via penso?" - d: "Kion vi volas diri?" - e: "Komencu skribi tie" -_profile: - name: "Nomo" - username: "Uzantnomo" - description: "Sinprezento" - metadata: "Kromaj Informoj" - metadataEdit: "Redakti kromajn informojn" - changeAvatar: "Ŝanĝi profilbildon" - changeBanner: "Ŝanĝi standardon" -_exportOrImport: - allNotes: "Ĉiuj notoj" - followingList: "Sekvatoj" - muteList: "Silentigoj" - blockingList: "Blokitoj" - userLists: "Listoj" -_charts: - federation: "Federaĵo" - usersTotal: "La totala nombro de la uzantoj" - activeUsers: "La nombro de la uzantoj aktivaj" - notesTotal: "La totala nombro de notoj" - filesTotal: "La totala nombro de la dosieroj" -_timelines: - home: "Hejma" - local: "Loka" - social: "Sociala" - global: "Malloka" -_pages: - newPage: "Krei novan paĝon" - editPage: "Redakti paĝon" - deleted: "Oni forviŝis la paĝon." - editThisPage: "Redakti la paĝon" - viewPage: "Vidi paĝojn" - my: "Miaj paĝoj" - featured: "Ravaĵoj" - contents: "Enhavo" - content: "Bloko de paĝo" - title: "Temlinio" - url: "URL de la paĝo" - alignCenter: "Centrigi" - hideTitleWhenPinned: "Kaŝi la titolon de la paĝo kiam alpinglita" - chooseBlock: "Aldoni blokon" - contentBlocks: "Enhavo" - inputBlocks: "Enigo" - blocks: - text: "Teksto" - textarea: "Areo de teksto" - image: "Bildo" - button: "Butono" - _post: - canvasId: "Kanvasa identigilo" - textInput: "Enmeto el teksto" - _textInput: - text: "Titolo" - default: "Implicitaĵa valoro" - textareaInput: "Enmeto el teksto en multaj linioj" - _textareaInput: - text: "Titolo" - default: "Implicitaĵa valoro" - numberInput: "Nombra enmeto" - _numberInput: - text: "Titolo" - default: "Implicitaĵa valoro" - _canvas: - id: "Kanvasa identigilo" - width: "Larĝeco" - _note: - id: "Identigilo de noto" - _switch: - text: "Titolo" - default: "Implicitaĵa valoro" - _counter: - text: "Titolo" - _button: - text: "Titolo" - _action: - _dialog: - content: "Enhavo" - _pushEvent: - event: "Nomo de la evento" - no-variable: "Neniu" - _radioButton: - title: "Titolo" - default: "Implicitaĵa valoro" - script: - categories: - random: "Hazardo" - value: "Valoro" - fn: "Funkcio" - text: "Manipulo de teksto" - list: "Listoj" - blocks: - text: "Teksto" - multiLineText: "Teksto (multaj linioj)" - textList: "List de teksto" - _strLen: - arg1: "Teksto" - _strPick: - arg1: "Teksto" - _strReplace: - arg1: "Teksto" - _strReverse: - arg1: "Teksto" - _join: - arg1: "Listoj" - arg2: "apartigilo" - _add: - arg1: "A" - arg2: "B" - _subtract: - arg1: "A" - arg2: "B" - _multiply: - arg1: "A" - arg2: "B" - _divide: - arg1: "A" - arg2: "B" - _mod: - arg1: "A" - arg2: "B" - _eq: - arg1: "A" - arg2: "B" - _notEq: - arg1: "A" - arg2: "B" - _and: - arg1: "A" - arg2: "B" - _or: - arg1: "A" - arg2: "B" - _lt: - arg1: "A" - arg2: "B" - _gt: - arg1: "A" - arg2: "B" - _ltEq: - arg1: "A" - arg2: "B" - _gtEq: - arg1: "A" - arg2: "B" - random: "Hazardo" - _randomPick: - arg1: "Listoj" - _dailyRandomPick: - arg1: "Listoj" - _seedRandomPick: - arg2: "Listoj" - _DRPWPM: - arg1: "List de teksto" - pick: "Elekti de la listo" - _pick: - arg1: "Listoj" - _listLen: - arg1: "Listoj" - _stringToNumber: - arg1: "Teksto" - _splitStrByLine: - arg1: "Teksto" - fn: "Funkcio" - _fn: - slots: "Juntoj" - arg1: "Elmeto" - thereIsEmptySlot: "La junto {slot} estas malplena!" - types: - string: "Teksto" - array: "Listoj" - stringArray: "List de teksto" - emptySlot: "Malplena junto" - argVariables: "Eniga junto" -_relayStatus: - requesting: "Atendado de aprobon" - accepted: "Konfirmita" - rejected: "Malakceptita" -_notification: - fileUploaded: "La dosiero sukcese alŝutiĝis." - youGotMention: "{name} mencis" - youGotReply: "{name} respondis" - youGotQuote: "{name} citis" - youRenoted: "{name} plusendis" - youGotPoll: "{name} voĉdonis" - youGotMessagingMessageFromUser: "{name} sendis al vi mesaĝon" - youGotMessagingMessageFromGroup: "Oni sendis al la grupo {name} mesaĝon" - youWereFollowed: "Eksekvis vin" - youReceivedFollowRequest: "Vi ricevis peton de sekvado" - yourFollowRequestAccepted: "Via peto de sekvado estis akceptita." - youWereInvitedToGroup: "Invitita al grupo" - _types: - all: "Ĉio" - follow: "Novaj sekvantoj" - mention: "Mencioj" - reply: "Respondoj" - renote: "Plusendoj" - quote: "Citi" - reaction: "Reagoj" - pollVote: "Voĉdonoj en balotoj" - receiveFollowRequest: "Ricevi peton de sekvado" - followRequestAccepted: "Akceptita peto de sekvado" - groupInvited: "Invitita al grupo" -_deck: - profile: "Agordaro" - _columns: - notifications: "Sciigoj" - tl: "Templinio" - antenna: "Antenoj" - list: "Listoj" - mentions: "Mencioj" - direct: "Rekte" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 5ccf1b2b6..1fe74fa9a 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -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" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index cf8915867..11dff184c 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -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" diff --git a/locales/index.js b/locales/index.js index b271b79b7..98c30fe01 100644 --- a/locales/index.js +++ b/locales/index.js @@ -19,7 +19,6 @@ const languages = [ 'da-DK', 'de-DE', 'en-US', - 'eo-UY', 'es-ES', 'fr-FR', 'id-ID', diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6326094dd..4d04cd28c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -356,7 +356,7 @@ antennaExcludeKeywords: "除外キーワード" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" notifyAntenna: "新しいノートを通知する" withFileAntenna: "ファイルが添付されたノートのみ" -enableServiceworker: "ServiceWorkerを有効にする" +enableServiceworker: "ブラウザへのプッシュ通知を有効にする" antennaUsersDescription: "ユーザー名を改行で区切って指定します" caseSensitive: "大文字小文字を区別する" withReplies: "返信を含む" @@ -1668,8 +1668,9 @@ _notification: youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" - youWereInvitedToGroup: "グループに招待されました" + youWereInvitedToGroup: "{userName}があなたをグループに招待しました" pollEnded: "アンケートの結果が出ました" + emptyPushNotificationMessage: "プッシュ通知の更新をしました" _types: all: "すべて" @@ -1686,6 +1687,11 @@ _notification: groupInvited: "グループに招待された" app: "連携アプリからの通知" + _actions: + followBack: "フォローバック" + reply: "返信" + renote: "Renote" + _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index a00716fe0..f4e4a6218 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -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" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 0f12155e3..104e4ceb7 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -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" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 7f8ed82b8..6b2ff19e8 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -449,6 +449,56 @@ 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ă" +objectStorageBaseUrlDesc: "URL-ul este folosit pentru referință. Specifică URL-ul CDN-ului sau Proxy-ului tău dacă folosești unul. Pentru S3 folosește 'https://.s3.amazonaws.com' și pentru GCS sau servicii echivalente folosește 'https://storage.googleapis.com/', etc." +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" +objectStorageEndpointDesc: "Lasă acest câmp gol dacă folosești AWS S3, dacă nu specifică endpoint-ul ca '' sau ':', depinzând de ce serviciu folosești." +objectStorageRegion: "Regiune" +objectStorageRegionDesc: "Specifică o regiune precum 'xx-east-1'. Dacă serviciul tău nu face distincția între regiuni lasă acest câmp gol sau introdu 'us-east-1'." +objectStorageUseSSL: "Folosește SSl" +objectStorageUseSSLDesc: "Oprește această opțiune dacă nu vei folosi HTTPS pentru conexiunile API-ului" +objectStorageUseProxy: "Conectează-te prin Proxy" +objectStorageUseProxyDesc: "Oprește această opțiune dacă vei nu folosi un Proxy pentru conexiunile API-ului" +objectStorageSetPublicRead: "Setează \"public-read\" pentru încărcare" +serverLogs: "Loguri server" +deleteAll: "Șterge tot" +showFixedPostForm: "Arată caseta de postare în vârful cronologie" +newNoteRecived: "Sunt note noi" sounds: "Sunete" listen: "Ascultă" none: "Nimic" @@ -471,12 +521,54 @@ 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" +yourAccountSuspendedDescription: "Acest cont a fost suspendat din cauza încălcării termenilor de serviciu al serverului sau ceva similar. Contactează administratorul dacă ai dori să afli un motiv mai detaliat. Te rog nu crea un cont nou." +menu: "Meniu" +divider: "Separator" +addItem: "Adaugă element" +relays: "Relee" +addRelay: "Adaugă Releu" +inboxUrl: "URL-ul inbox-ului" +addedRelays: "Relee adăugate" +serviceworkerInfo: "Trebuie să fie activat pentru notificări push." +deletedNote: "Notă ștearsă" +invisibleNote: "Note ascunse" +enableInfiniteScroll: "Încarcă mai mult automat" +visibility: "Vizibilitate" +poll: "Sondaj" +useCw: "Ascunde conținutul" +enablePlayer: "Deschide player-ul video" +disablePlayer: "Închide player-ul video" +expandTweet: "Expandează tweet" +themeEditor: "Editor de teme" +description: "Descriere" +describeFile: "Adaugă titrări" +enterFileDescription: "Introdu titrările" +author: "Autor" +leaveConfirm: "Ai schimbări nesalvate. Vrei să renunți la ele?" +manage: "Gestionare" +plugins: "Pluginuri" +deck: "Deck" +undeck: "Părăsește Deck" +useBlurEffectForModal: "Folosește efect de blur pentru modale" smtpHost: "Gazdă" smtpUser: "Nume de utilizator" smtpPass: "Parolă" clearCache: "Golește cache-ul" info: "Despre" user: "Utilizatori" +administration: "Gestionare" searchByGoogle: "Caută" _email: _follow: @@ -487,9 +579,11 @@ _mfm: emoji: "Emoji personalizat" search: "Caută" _theme: + description: "Descriere" keys: mention: "Mențiune" renote: "Re-notează" + divider: "Separator" _sfx: note: "Note" notification: "Notificări" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index db45365a5..c6f2f59bd 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -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" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 483faba0d..f64458583 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -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: "仅发送至指定用户" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index a1e0a830c..18c6f1715 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -81,6 +81,8 @@ somethingHappened: "發生錯誤" retry: "重試" pageLoadError: "載入頁面失敗" pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取殘留的原因。請先清除瀏覽器快取,稍後再重試" +serverIsDead: "伺服器沒有回應。請稍等片刻,然後重試。" +youShouldUpgradeClient: "請重新載入以使用新版本的客戶端顯示此頁面" enterListName: "輸入清單名稱" privacy: "隱私" makeFollowManuallyApprove: "手動審核追隨請求" @@ -104,6 +106,7 @@ clickToShow: "按一下以顯示" sensitive: "敏感內容" add: "新增" reaction: "情感" +reactionSetting: "在選擇器中顯示反應" reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。" rememberNoteVisibility: "記住貼文可見性" attachCancel: "移除附件" @@ -138,6 +141,7 @@ flagAsBot: "此使用者是機器人" flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人" flagAsCat: "此使用者是貓" flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示" +flagShowTimelineReplies: "在時間軸上顯示貼文的回覆" autoAcceptFollowed: "自動追隨中使用者的追隨請求" addAccount: "添加帳戶" loginFailed: "登入失敗" @@ -599,6 +603,9 @@ reportAbuse: "檢舉" reportAbuseOf: "檢舉{name}" fillAbuseReportDescription: "請填寫檢舉的詳細理由。可以的話,請附上針對的URL網址。" abuseReported: "回報已送出。感謝您的報告。" +reporter: "檢舉者" +reporteeOrigin: "檢舉來源" +reporterOrigin: "檢舉者來源" send: "發送" abuseMarkAsResolved: "處理完畢" openInNewTab: "在新分頁中開啟" @@ -734,6 +741,7 @@ postToGallery: "發佈到相簿" gallery: "相簿" recentPosts: "最新貼文" popularPosts: "熱門的貼文" +shareWithNote: "在貼文中分享" ads: "廣告" expiration: "期限" memo: "備忘錄" @@ -743,14 +751,35 @@ middle: "中" low: "低" emailNotConfiguredWarning: "沒有設定電子郵件地址" ratio: "%" +previewNoteText: "預覽文本" +customCss: "自定義 CSS" global: "公開" sent: "發送" +received: "收取" +searchResult: "搜尋結果" hashtags: "#tag" +troubleshooting: "故障排除" +useBlurEffect: "在 UI 上使用模糊效果" +misskeyUpdated: "Misskey 更新完成!" +translate: "翻譯" +translatedFrom: "從 {x} 翻譯" +accountDeletionInProgress: "正在刪除帳戶" +pubSub: "Pub/Sub 帳戶" +resolved: "已解決" +unresolved: "未解決" +breakFollow: "移除追蹤者" hide: "隱藏" +leaveGroupConfirm: "確定離開「{name}」?" +auto: "自動" searchByGoogle: "搜尋" indefinitely: "無期限" _ffVisibility: public: "發佈" + private: "私密" +_signup: + almostThere: "即將完成" +_accountDelete: + inProgress: "正在刪除" _ad: back: "返回" reduceFrequencyOfThisAd: "降低此廣告的頻率 " diff --git a/package.json b/package.json index cc49d1ffa..9c3c39bf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "12.108.1", + "version": "12.110.1", "codename": "indigo", "repository": { "type": "git", @@ -30,8 +30,6 @@ "cleanall": "npm run clean-all" }, "dependencies": { - "@types/gulp": "4.0.9", - "@types/gulp-rename": "2.0.1", "execa": "5.1.1", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", @@ -41,9 +39,11 @@ "js-yaml": "4.1.0" }, "devDependencies": { - "@typescript-eslint/parser": "5.16.0", + "@types/gulp": "4.0.9", + "@types/gulp-rename": "2.0.1", + "@typescript-eslint/parser": "5.18.0", "cross-env": "7.0.3", - "cypress": "9.5.2", + "cypress": "9.5.3", "start-server-and-test": "1.14.0", "typescript": "4.6.3" } diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs new file mode 100644 index 000000000..dfc9d0495 --- /dev/null +++ b/packages/backend/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + extends: [ + '../shared/.eslintrc.js', + ], + rules: { + 'import/order': ['warn', { + 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'pathGroups': [ + { + 'pattern': '@/**', + 'group': 'external', + 'position': 'after' + } + ], + }] + }, +}; diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js deleted file mode 100644 index e2e31e9e3..000000000 --- a/packages/backend/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], -}; diff --git a/packages/backend/.vscode/settings.json b/packages/backend/.vscode/settings.json index df3bf0507..9fb3b29d4 100644 --- a/packages/backend/.vscode/settings.json +++ b/packages/backend/.vscode/settings.json @@ -2,5 +2,9 @@ "typescript.tsdk": "node_modules\\typescript\\lib", "path-intellisense.mappings": { "@": "${workspaceRoot}/packages/backend/src/" + }, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true } } diff --git a/packages/backend/migration/1648548247382-webhook.js b/packages/backend/migration/1648548247382-webhook.js new file mode 100644 index 000000000..aea369a5c --- /dev/null +++ b/packages/backend/migration/1648548247382-webhook.js @@ -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"`); + } +} diff --git a/packages/backend/migration/1648816172177-webhook-2.js b/packages/backend/migration/1648816172177-webhook-2.js new file mode 100644 index 000000000..2feb68d61 --- /dev/null +++ b/packages/backend/migration/1648816172177-webhook-2.js @@ -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"`); + } +} diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js new file mode 100644 index 000000000..44ba7fb6c --- /dev/null +++ b/packages/backend/migration/1651224615271-foreign-key.js @@ -0,0 +1,89 @@ +export class foreignKeyReports1651224615271 { + name = 'foreignKeyReports1651224615271' + + async up(queryRunner) { + await Promise.all([ + queryRunner.query(`ALTER INDEX "public"."IDX_seoignmeoprigmkpodgrjmkpormg" RENAME TO "IDX_c8cc87bd0f2f4487d17c651fbf"`), + queryRunner.query(`DROP INDEX "public"."IDX_note_on_channelId_and_id_desc"`), + + // remove unnecessary default null, see also down + queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "followersUri" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "session" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "name" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "iconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareName" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareVersion" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "name" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerName" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerEmail" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "iconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "faviconUrl" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "themeColor" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "clip" ALTER COLUMN "description" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "channelId" DROP DEFAULT`), + queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" DROP DEFAULT`), + + queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`), + queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`), + queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`), + + queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => { + queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + }), + + queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`), + queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`), + queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`), + + queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" TO "FK_a9ca79ad939bf06066b81c9d3aa"`), + + queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'pollEnded' AFTER 'pollVote'`), + ]); + } + + async down(queryRunner) { + await Promise.all([ + // There is no ALTER TYPE REMOVE VALUE query, so the reverse operation is a bit more complex + queryRunner.query(`UPDATE "user_profile" SET "mutingNotificationTypes" = array_remove("mutingNotificationTypes", 'pollEnded')`) + .then(() => + queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`) + ).then(() => + queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`) + ).then(() => + queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`) + ).then(() => + queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`) + ), + + queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" TO "FK_3126dd7c502c9e4d7597ef7ef10"`), + + queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`), + queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`), + queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`), + queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`), + queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`), + + queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" SET DEFAULT '{}'`), + queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`), + + queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`), + queryRunner.query(`DROP INDEX "public"."IDX_f22169eb10657bded6d875ac8f"`), + queryRunner.query(`DROP INDEX "public"."IDX_315c779174fe8247ab324f036e"`), + + /* DEFAULT's are not set again because if the column can be NULL, then DEFAULT NULL is not necessary. + see also https://github.com/typeorm/typeorm/issues/7579#issuecomment-835423615 */ + + queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("id", "channelId") `), + queryRunner.query(`ALTER INDEX "public"."IDX_c8cc87bd0f2f4487d17c651fbf" RENAME TO "IDX_seoignmeoprigmkpodgrjmkpormg"`), + ]); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 91c099e9f..5950cd23f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", - "lint": "eslint --quiet src/**/*.ts", + "lint": "eslint --quiet \"src/**/*.ts\"", "mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", "test": "npm run mocha" }, @@ -14,6 +14,7 @@ "lodash": "^4.17.21" }, "dependencies": { + "@bull-board/koa": "3.10.4", "@discordapp/twemoji": "13.1.1", "@elastic/elasticsearch": "7.11.0", "@koa/cors": "3.1.0", @@ -21,65 +22,19 @@ "@koa/router": "9.0.1", "@sinonjs/fake-timers": "9.1.1", "@syuilo/aiscript": "0.11.1", - "@types/bcryptjs": "2.4.2", - "@types/bull": "3.15.8", - "@types/cbor": "6.0.0", - "@types/escape-regexp": "0.0.1", - "@types/is-url": "1.2.30", - "@types/js-yaml": "4.0.5", - "@types/jsdom": "16.2.14", - "@types/jsonld": "1.5.6", - "@types/koa": "2.13.4", - "@types/koa-bodyparser": "4.3.7", - "@types/koa-cors": "0.0.2", - "@types/koa-favicon": "2.0.21", - "@types/koa-logger": "3.1.2", - "@types/koa-mount": "4.0.1", - "@types/koa-send": "4.1.3", - "@types/koa-views": "7.0.0", - "@types/koa__cors": "3.1.1", - "@types/koa__multer": "2.0.4", - "@types/koa__router": "8.0.11", - "@types/mocha": "9.1.0", - "@types/node": "17.0.23", - "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.4", - "@types/oauth": "0.9.1", - "@types/parse5": "6.0.3", - "@types/portscanner": "2.1.1", - "@types/pug": "2.0.6", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.4.2", - "@types/random-seed": "0.3.3", - "@types/ratelimiter": "3.4.3", - "@types/redis": "4.0.11", - "@types/rename": "1.0.4", - "@types/sanitize-html": "2.6.2", - "@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", - "@types/tmp": "0.2.3", - "@types/uuid": "8.3.4", - "@types/web-push": "3.3.2", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "5.16.0", - "@typescript-eslint/parser": "5.16.0", - "@bull-board/koa": "3.10.1", + "@typescript-eslint/eslint-plugin": "5.20.0", + "@typescript-eslint/parser": "5.20.0", "abort-controller": "3.0.0", "ajv": "8.11.0", - "archiver": "5.3.0", + "archiver": "5.3.1", "autobind-decorator": "2.4.0", "autwh": "0.1.0", - "aws-sdk": "2.1100.0", + "aws-sdk": "2.1120.0", "bcryptjs": "2.4.3", "blurhash": "1.1.5", - "broadcast-channel": "4.10.0", - "bull": "4.8.1", + "broadcast-channel": "4.11.0", + "bull": "4.8.2", "cacheable-lookup": "6.0.4", - "cafy": "15.2.1", "cbor": "8.1.0", "chalk": "5.0.1", "chalk-template": "0.4.0", @@ -89,22 +44,22 @@ "date-fns": "2.28.0", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", - "eslint": "8.12.0", - "eslint-plugin-import": "2.25.4", + "eslint": "8.14.0", + "eslint-plugin-import": "2.26.0", "feed": "4.2.2", "file-type": "17.1.1", "fluent-ffmpeg": "2.1.2", "got": "12.0.3", "hpagent": "0.1.2", "http-signature": "1.3.6", - "ip-cidr": "3.0.4", + "ip-cidr": "3.0.7", "is-svg": "4.3.2", "js-yaml": "4.1.0", "jsdom": "19.0.0", "json5": "2.2.1", "json5-loader": "4.0.1", "jsonld": "5.2.0", - "jsrsasign": "8.0.20", + "jsrsasign": "10.5.19", "koa": "2.13.4", "koa-bodyparser": "4.3.0", "koa-favicon": "2.1.0", @@ -145,16 +100,15 @@ "rndstr": "1.0.0", "s-age": "1.1.2", "sanitize-html": "2.7.0", - "semver": "7.3.5", - "sharp": "0.30.3", + "semver": "7.3.7", + "sharp": "0.30.4", "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.9", - "throttle-debounce": "3.0.1", + "systeminformation": "5.11.14", "tinycolor2": "1.4.2", "tmp": "0.2.1", "ts-loader": "9.2.8", @@ -162,7 +116,7 @@ "tsc-alias": "1.4.1", "tsconfig-paths": "3.14.1", "twemoji-parser": "14.0.0", - "typeorm": "0.3.4", + "typeorm": "0.3.6", "typescript": "4.6.3", "ulid": "2.3.0", "unzipper": "0.10.11", @@ -170,11 +124,56 @@ "web-push": "3.4.5", "websocket": "1.0.34", "ws": "8.5.0", - "xev": "2.0.1" + "xev": "3.0.2" }, "devDependencies": { - "@redocly/openapi-core": "1.0.0-beta.90", + "@redocly/openapi-core": "1.0.0-beta.93", + "@types/semver": "7.3.9", + "@types/bcryptjs": "2.4.2", + "@types/bull": "3.15.8", + "@types/cbor": "6.0.0", + "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.20", + "@types/is-url": "1.2.30", + "@types/js-yaml": "4.0.5", + "@types/jsdom": "16.2.14", + "@types/jsonld": "1.5.6", + "@types/jsrsasign": "10.2.1", + "@types/koa": "2.13.4", + "@types/koa-bodyparser": "4.3.7", + "@types/koa-cors": "0.0.2", + "@types/koa-favicon": "2.0.21", + "@types/koa-logger": "3.1.2", + "@types/koa-mount": "4.0.1", + "@types/koa-send": "4.1.3", + "@types/koa-views": "7.0.0", + "@types/koa__cors": "3.1.1", + "@types/koa__multer": "2.0.4", + "@types/koa__router": "8.0.11", + "@types/mocha": "9.1.1", + "@types/node": "17.0.25", + "@types/node-fetch": "3.0.3", + "@types/nodemailer": "6.4.4", + "@types/oauth": "0.9.1", + "@types/parse5": "6.0.3", + "@types/portscanner": "2.1.1", + "@types/pug": "2.0.6", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.4.2", + "@types/random-seed": "0.3.3", + "@types/ratelimiter": "3.4.3", + "@types/redis": "4.0.11", + "@types/rename": "1.0.4", + "@types/sanitize-html": "2.6.2", + "@types/sharp": "0.30.2", + "@types/sinonjs__fake-timers": "8.1.2", + "@types/speakeasy": "2.0.7", + "@types/tinycolor2": "1.4.3", + "@types/tmp": "0.2.3", + "@types/uuid": "8.3.4", + "@types/web-push": "3.3.2", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.3", "cross-env": "7.0.3", "execa": "6.1.0" } diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts index 8d484312d..0426cb8bc 100644 --- a/packages/backend/src/@types/http-signature.d.ts +++ b/packages/backend/src/@types/http-signature.d.ts @@ -1,5 +1,5 @@ declare module 'http-signature' { - import { IncomingMessage, ClientRequest } from 'http'; + import { IncomingMessage, ClientRequest } from 'node:http'; interface ISignature { keyId: string; diff --git a/packages/backend/src/@types/jsrsasign.d.ts b/packages/backend/src/@types/jsrsasign.d.ts deleted file mode 100644 index bb52f8f64..000000000 --- a/packages/backend/src/@types/jsrsasign.d.ts +++ /dev/null @@ -1,800 +0,0 @@ -// Attention: Partial Type Definition - -declare module 'jsrsasign' { - //// HELPER TYPES - - /** - * Attention: The value might be changed by the function. - */ - type Mutable = T; - - /** - * Deprecated: The function might be deleted in future release. - */ - type Deprecated = T; - - //// COMMON TYPES - - /** - * byte number - */ - type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255; - - /** - * hexadecimal string /[0-9A-F]/ - */ - type HexString = string; - - /** - * binary string /[01]/ - */ - type BinString = string; - - /** - * base64 string /[A-Za-z0-9+/]=+/ - */ - type Base64String = string; - - /** - * base64 URL encoded string /[A-Za-z0-9_-]/ - */ - type Base64URLString = string; - - /** - * time value (ex. "151231235959Z") - */ - type TimeValue = string; - - /** - * OID string (ex. '1.2.3.4.567') - */ - type OID = string; - - /** - * OID name - */ - type OIDName = string; - - /** - * PEM formatted string - */ - type PEM = string; - - //// ASN1 TYPES - - class ASN1Object { - public isModified: boolean; - - public hTLV: ASN1TLV; - - public hT: ASN1T; - - public hL: ASN1L; - - public hV: ASN1V; - - public getLengthHexFromValue(): HexString; - - public getEncodedHex(): ASN1TLV; - - public getValueHex(): ASN1V; - - public getFreshValueHex(): ASN1V; - } - - class DERAbstractStructured extends ASN1Object { - constructor(params?: Partial>); - - public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void; - - public appendASN1Object(asn1Object: ASN1Object): void; - } - - class DERSequence extends DERAbstractStructured { - constructor(params?: Partial>); - - public getFreshValueHex(): ASN1V; - } - - //// ASN1HEX TYPES - - /** - * ASN.1 DER encoded data (hexadecimal string) - */ - type ASN1S = HexString; - - /** - * index of something - */ - type Idx = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never; - - /** - * byte length of something - */ - type ByteLength = T['length']; - - /** - * ASN.1 L(length) (hexadecimal string) - */ - type ASN1L = HexString; - - /** - * ASN.1 T(tag) (hexadecimal string) - */ - type ASN1T = HexString; - - /** - * ASN.1 V(value) (hexadecimal string) - */ - type ASN1V = HexString; - - /** - * ASN.1 TLV (hexadecimal string) - */ - type ASN1TLV = HexString; - - /** - * ASN.1 object string - */ - type ASN1ObjectString = string; - - /** - * nth - */ - type Nth = number; - - /** - * ASN.1 DER encoded OID value (hexadecimal string) - */ - type ASN1OIDV = HexString; - - class ASN1HEX { - public static getLblen(s: ASN1S, idx: Idx): ByteLength; - - public static getL(s: ASN1S, idx: Idx): ASN1L; - - public static getVblen(s: ASN1S, idx: Idx): ByteLength; - - public static getVidx(s: ASN1S, idx: Idx): Idx; - - public static getV(s: ASN1S, idx: Idx): ASN1V; - - public static getTLV(s: ASN1S, idx: Idx): ASN1TLV; - - public static getNextSiblingIdx(s: ASN1S, idx: Idx): Idx; - - public static getChildIdx(h: ASN1S, pos: Idx): Idx[]; - - public static getNthChildIdx(h: ASN1S, idx: Idx, nth: Nth): Idx; - - public static getIdxbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): Idx>; - - public static getTLVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): ASN1TLV; - - // eslint:disable-next-line:bool-param-default - public static getVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string, removeUnusedbits?: boolean): ASN1V; - - public static hextooidstr(hex: ASN1OIDV): OID; - - public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record, idx?: Idx, indent?: string): string; - - public static isASN1HEX(hex: string): hex is HexString; - - public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName; - } - - //// BIG INTEGER TYPES (PARTIAL) - - class BigInteger { - constructor(a: null); - - constructor(a: number, b: SecureRandom); - - constructor(a: number, b: number, c: SecureRandom); - - constructor(a: unknown); - - constructor(a: string, b: number); - - public am(i: number, x: number, w: number, j: number, c: number, n: number): number; - - public DB: number; - - public DM: number; - - public DV: number; - - public FV: number; - - public F1: number; - - public F2: number; - - protected copyTo(r: Mutable): void; - - protected fromInt(x: number): void; - - protected fromString(s: string, b: number): void; - - protected clamp(): void; - - public toString(b: number): string; - - public negate(): BigInteger; - - public abs(): BigInteger; - - public compareTo(a: BigInteger): number; - - public bitLength(): number; - - protected dlShiftTo(n: number, r: Mutable): void; - - protected drShiftTo(n: number, r: Mutable): void; - - protected lShiftTo(n: number, r: Mutable): void; - - protected rShiftTo(n: number, r: Mutable): void; - - protected subTo(a: BigInteger, r: Mutable): void; - - protected multiplyTo(a: BigInteger, r: Mutable): void; - - protected squareTo(r: Mutable): void; - - protected divRemTo(m: BigInteger, q: Mutable, r: Mutable): void; - - public mod(a: BigInteger): BigInteger; - - protected invDigit(): number; - - protected isEven(): boolean; - - protected exp(e: number, z: Classic | Montgomery): BigInteger; - - public modPowInt(e: number, m: BigInteger): BigInteger; - - public static ZERO: BigInteger; - - public static ONE: BigInteger; - } - - class Classic { - constructor(m: BigInteger); - - public convert(x: BigInteger): BigInteger; - - public revert(x: BigInteger): BigInteger; - - public reduce(x: Mutable): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): void; - } - - class Montgomery { - constructor(m: BigInteger); - - public convert(x: BigInteger): BigInteger; - - public revert(x: BigInteger): BigInteger; - - public reduce(x: Mutable): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): void; - } - - //// KEYUTIL TYPES - - type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString; - - type AlgList = { - 'AES-256-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 32; ivlen: 16; }; - 'AES-192-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 24; ivlen: 16; }; - 'AES-128-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 16; ivlen: 16; }; - 'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8; }; - 'DES-CBC': { 'proc': DecryptDES; 'eproc': EncryptDES; keylen: 8; ivlen: 8; }; - }; - - type AlgName = keyof AlgList; - - type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA'; - - type GetKeyRSAParam = RSAKey | { - n: BigInteger; - e: number; - } | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | { - n: BigInteger; - e: number; - d: BigInteger; - } | { - kty: 'RSA'; - } & Record<'n' | 'e', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd', Base64URLString>; - - type GetKeyECDSAParam = KJUR.crypto.ECDSA | { - curve: KJUR.crypto.CurveName; - xy: HexString; - } | { - curve: KJUR.crypto.CurveName; - d: HexString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - d: Base64URLString; - }; - - type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>; - - type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string; - - class KEYUTIL { - public version: '1.0.0'; - - public parsePKCS5PEM(sPKCS5PEM: PEM): Partial> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>); - - public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>; - - public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String; - - public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString; - - public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM; - - public parseHexOfEncryptedPKCS8(sHEX: HexString): { - ciphertext: ASN1V; - encryptionSchemeAlg: 'TripleDES'; - encryptionSchemeIV: ASN1V; - pbkdf2Salt: ASN1V; - pbkdf2Iter: number; - }; - - public getPBKDF2KeyHexFromParam(info: ReturnType, passcode: string): HexString; - - private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString; - - public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType; - - public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): { - algparam: ASN1V | null; - algoid: ASN1V; - keyidx: Idx; - }; - - public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType; - - public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA; - - private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA; - - public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>; - - public parsePublicPKCS8Hex(pkcs8PubHex: HexString): { - algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null; - algoid: ASN1V; - key: ASN1V; - }; - - public static getKey(param: GetKeyRSAParam): RSAKey; - - public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA; - - public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA; - - public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>; - - public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>; - - public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do - - public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>; - - public static getJWKFromKey(keyObj: RSAKey): { - kty: 'RSA'; - } & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | { - kty: 'RSA'; - } & Record<'n' | 'e', Base64URLString>; - - public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - d: Base64URLString; - } | { - kty: 'EC'; - crv: KJUR.crypto.CurveName; - x: Base64URLString; - y: Base64URLString; - }; - } - - //// KJUR NAMESPACE (PARTIAL) - - namespace KJUR { - namespace crypto { - type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1'; - - class DSA { - public p: BigInteger | null; - - public q: BigInteger | null; - - public g: BigInteger | null; - - public y: BigInteger | null; - - public x: BigInteger | null; - - public type: 'DSA'; - - public isPrivate: boolean; - - public isPublic: boolean; - - public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void; - - public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void; - - public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void; - - public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void; - - public signWithMessageHash(sHashHex: HexString): HexString; - - public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean; - - public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger]; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: number): void; - } - - class ECDSA { - constructor(params?: { - curve?: CurveName; - prv?: HexString; - pub?: HexString; - }); - - public p: BigInteger | null; - - public q: BigInteger | null; - - public g: BigInteger | null; - - public y: BigInteger | null; - - public x: BigInteger | null; - - public type: 'EC'; - - public isPrivate: boolean; - - public isPublic: boolean; - - public getBigRandom(limit: BigInteger): BigInteger; - - public setNamedCurve(curveName: CurveName): void; - - public setPrivateKeyHex(prvKeyHex: HexString): void; - - public setPublicKeyHex(pubKeyHex: HexString): void; - - public getPublicKeyXYHex(): Record<'x' | 'y', HexString>; - - public getShortNISTPCurveName(): 'P-256' | 'P-384' | null; - - public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>; - - public signWithMessageHash(hashHex: HexString): HexString; - - public signHex(hashHex: HexString, privHex: HexString): HexString; - - public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean; - - public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger]; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: number): void; - - public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>; - - public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>; - - public static asn1SigToConcatSig(asn1Sig: HexString): HexString; - - public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV; - - public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV; - - public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV; - - public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null; - } - - class Signature { - constructor(params?: ({ - alg: string; - prov?: string; - } | {}) & ({ - psssaltlen: number; - } | {}) & ({ - prvkeypem: PEM; - prvkeypas?: never; - } | {})); - - private _setAlgNames(): void; - - private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString; - - public setAlgAndProvider(alg: string, prov: string): void; - - public init(key: GetKeyParam, pass?: string): void; - - public updateString(str: string): void; - - public updateHex(hex: HexString): void; - - public sign(): HexString; - - public signString(str: string): HexString; - - public signHex(hex: HexString): HexString; - - public verify(hSigVal: string): boolean | 0; - } - } - } - - //// RSAKEY TYPES - - class RSAKey { - public n: BigInteger | null; - - public e: number; - - public d: BigInteger | null; - - public p: BigInteger | null; - - public q: BigInteger | null; - - public dmp1: BigInteger | null; - - public dmq1: BigInteger | null; - - public coeff: BigInteger | null; - - public type: 'RSA'; - - public isPrivate?: boolean; - - public isPublic?: boolean; - - //// RSA PUBLIC - - protected doPublic(x: BigInteger): BigInteger; - - public setPublic(N: BigInteger, E: number): void; - - public setPublic(N: HexString, E: HexString): void; - - public encrypt(text: string): HexString | null; - - public encryptOAEP(text: string, hash?: string | ((s: string) => string), hashLen?: number): HexString | null; - - //// RSA PRIVATE - - protected doPrivate(x: BigInteger): BigInteger; - - public setPrivate(N: BigInteger, E: number, D: BigInteger): void; - - public setPrivate(N: HexString, E: HexString, D: HexString): void; - - public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void; - - public generate(B: number, E: HexString): void; - - public decrypt(ctext: HexString): string; - - public decryptOAEP(ctext: HexString, hash?: string | ((s: string) => string), hashLen?: number): string | null; - - //// RSA PEM - - public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx[]; - - public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx[]; - - public readPrivateKeyFromPEMString(keyPEM: PEM): void; - - public readPKCS5PrvKeyHex(h: HexString): void; - - public readPKCS8PrvKeyHex(h: HexString): void; - - public readPKCS5PubKeyHex(h: HexString): void; - - public readPKCS8PubKeyHex(h: HexString): void; - - public readCertPubKeyHex(h: HexString, nthPKI: Nth): void; - - //// RSA SIGN - - public sign(s: string, hashAlg: string): HexString; - - public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString; - - public signPSS(s: string, hashAlg: string, sLen: number): HexString; - - public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString; - - public verify(sMsg: string, hSig: HexString): boolean | 0; - - public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0; - - public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean; - - public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean; - - public static SALT_LEN_HLEN: -1; - - public static SALT_LEN_MAX: -2; - - public static SALT_LEN_RECOVER: -2; - } - - /// RNG TYPES - class SecureRandom { - public nextBytes(ba: Mutable): void; - } - - //// X509 TYPES - - type ExtInfo = { - critical: boolean; - oid: OID; - vidx: Idx; - }; - - type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>; - - type ExtCertificatePolicy = { - id: OIDName; - } & Partial<{ - cps: string; - } | { - unotice: string; - }>; - - class X509 { - public hex: HexString | null; - - public version: number; - - public foffset: number; - - public aExtInfo: null; - - public getVersion(): number; - - public getSerialNumberHex(): ASN1V; - - public getSignatureAlgorithmField(): OIDName; - - public getIssuerHex(): ASN1TLV; - - public getIssuerString(): HexString; - - public getSubjectHex(): ASN1TLV; - - public getSubjectString(): HexString; - - public getNotBefore(): TimeValue; - - public getNotAfter(): TimeValue; - - public getPublicKeyHex(): ASN1TLV; - - public getPublicKeyIdx(): Idx>; - - public getPublicKeyContentIdx(): Idx>; - - public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public getSignatureAlgorithmName(): OIDName; - - public getSignatureValueHex(): ASN1V; - - public verifySignature(pubKey: GetKeyParam): boolean | 0; - - public parseExt(): void; - - public getExtInfo(oidOrName: OID | string): ExtInfo | undefined; - - public getExtBasicConstraints(): ExtInfo | {} | { - cA: true; - pathLen?: number; - }; - - public getExtKeyUsageBin(): BinString; - - public getExtKeyUsageString(): string; - - public getExtSubjectKeyIdentifier(): ASN1V | undefined; - - public getExtAuthorityKeyIdentifier(): { - kid: ASN1V; - } | undefined; - - public getExtExtKeyUsageName(): OIDName[] | undefined; - - public getExtSubjectAltName(): Deprecated; - - public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined; - - public getExtCRLDistributionPointsURI(): string[] | undefined; - - public getExtAIAInfo(): ExtAIAInfo | undefined; - - public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined; - - public readCertPEM(sCertPEM: PEM): void; - - public readCertHex(sCertHex: HexString): void; - - public getInfo(): string; - - public static hex2dn(hex: HexString, idx?: Idx): string; - - public static hex2rdn(hex: HexString, idx?: Idx): string; - - public static hex2attrTypeValue(hex: HexString, idx?: Idx): string; - - public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA; - - public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): { - algparam: ASN1V | null; - leyhex: ASN1V; - algoid: ASN1V; - }; - } -} diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 5bb20a729..c3d059225 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -1,6 +1,6 @@ import cluster from 'node:cluster'; import chalk from 'chalk'; -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Logger from '@/services/logger.js'; import { envOption } from '../env.js'; @@ -12,7 +12,7 @@ import { workerMain } from './worker.js'; const logger = new Logger('core', 'cyan'); const clusterLogger = logger.createSubLogger('cluster', 'orange', false); -const ev = new Xev.default(); +const ev = new Xev(); /** * Init process diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts index 7f765463e..c2e6bea45 100644 --- a/packages/backend/src/config/load.ts +++ b/packages/backend/src/config/load.ts @@ -25,6 +25,7 @@ const path = process.env.NODE_ENV === 'test' export default function load() { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); + const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -45,6 +46,7 @@ export default function load() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; + mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, ''); if (!config.redis.prefix) config.redis.prefix = mixin.host; diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 58a27803c..948545db7 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -80,6 +80,7 @@ export type Mixin = { authUrl: string; driveUrl: string; userAgent: string; + clientEntry: string; }; export type Config = Source & Mixin; diff --git a/packages/backend/src/daemons/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts index bfef11054..1535abc6a 100644 --- a/packages/backend/src/daemons/queue-stats.ts +++ b/packages/backend/src/daemons/queue-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import { deliverQueue, inboxQueue } from '../queue/queues.js'; -const ev = new Xev.default(); +const ev = new Xev(); const interval = 10000; diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts index 327305ccc..faf4e6e4a 100644 --- a/packages/backend/src/daemons/server-stats.ts +++ b/packages/backend/src/daemons/server-stats.ts @@ -1,8 +1,8 @@ import si from 'systeminformation'; -import { default as Xev } from 'xev'; +import Xev from 'xev'; import * as osUtils from 'os-utils'; -const ev = new Xev.default(); +const ev = new Xev(); const interval = 2000; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index 491c1a174..eb5fc2e18 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -73,6 +73,7 @@ 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', 'gray', false); @@ -171,6 +172,7 @@ export const entities = [ Ad, PasswordResetRequest, UserPending, + Webhook, ...charts, ]; @@ -207,7 +209,11 @@ export const db = new DataSource({ }); export async function initDb() { - await db.connect(); + if (db.isInitialized) { + // nop + } else { + await db.connect(); + } } export async function resetDb() { diff --git a/packages/backend/src/misc/cafy-id.ts b/packages/backend/src/misc/cafy-id.ts deleted file mode 100644 index dd81c5c4c..000000000 --- a/packages/backend/src/misc/cafy-id.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Context } from 'cafy'; - -// eslint-disable-next-line @typescript-eslint/ban-types -export class ID extends Context { - public readonly name = 'ID'; - - constructor(optional = false, nullable = false) { - super(optional, nullable); - - this.push((v: any) => { - if (typeof v !== 'string') { - return new Error('must-be-an-id'); - } - return true; - }); - } - - public getType() { - return super.getType('String'); - } - - public makeOptional(): ID { - return new ID(true, false); - } - - public makeNullable(): ID { - return new ID(false, true); - } - - public makeOptionalNullable(): ID { - return new ID(true, true); - } -} diff --git a/packages/backend/src/misc/captcha.ts b/packages/backend/src/misc/captcha.ts index 293cbdcd3..9a87a4a3c 100644 --- a/packages/backend/src/misc/captcha.ts +++ b/packages/backend/src/misc/captcha.ts @@ -42,7 +42,8 @@ async function getCaptchaResponse(url: string, secret: string, response: string) headers: { 'User-Agent': config.userAgent, }, - timeout: 10 * 1000, + // TODO + //timeout: 10 * 1000, agent: getAgentByUrl, }).catch(e => { throw `${e.message || e}`; diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 47a5cd471..af6bf2fca 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -1,10 +1,10 @@ -import * as http from 'http'; -import * as https from 'https'; +import * as http from 'node:http'; +import * as https from 'node:https'; +import { URL } from 'node:url'; import CacheableLookup from 'cacheable-lookup'; import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import config from '@/config/index.js'; -import { URL } from 'node:url'; export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record) { const res = await getResponse({ @@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout = } export async function getResponse(args: { url: string, method: string, body?: string, headers: Record, timeout?: number, size?: number }) { - const timeout = args?.timeout || 10 * 1000; + const timeout = args.timeout || 10 * 1000; const controller = new AbortController(); setTimeout(() => { @@ -47,7 +47,7 @@ export async function getResponse(args: { url: string, method: string, body?: st headers: args.headers, body: args.body, timeout, - size: args?.size || 10 * 1024 * 1024, + size: args.size || 10 * 1024 * 1024, agent: getAgentByUrl, signal: controller.signal, }); @@ -120,9 +120,9 @@ export const httpsAgent = config.proxy */ export function getAgentByUrl(url: URL, bypassProxy = false) { if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) { - return url.protocol == 'http:' ? _http : _https; + return url.protocol === 'http:' ? _http : _https; } else { - return url.protocol == 'http:' ? httpAgent : httpsAgent; + return url.protocol === 'http:' ? httpAgent : httpsAgent; } } diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 93783873d..3f35ccee8 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -23,7 +23,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { } // ファイルが添付されているとき - if ((note.files || []).length != 0) { + if ((note.files || []).length !== 0) { summary += ` (📎${note.files!.length})`; } diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts index 86f1356c3..6a185d09f 100644 --- a/packages/backend/src/misc/populate-emojis.ts +++ b/packages/backend/src/misc/populate-emojis.ts @@ -63,7 +63,7 @@ export async function populateEmoji(emojiName: string, noteUserHost: string | nu const isLocal = emoji.host == null; const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため - const url = isLocal ? emojiUrl : `${config.url}/proxy/image.png?${query({ url: emojiUrl })}`; + const url = isLocal ? emojiUrl : `${config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; return { name: emojiName, diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 5b6981209..fdecc278d 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -89,7 +89,7 @@ export interface Schema extends OfSchema { readonly optional?: boolean; readonly items?: Schema; readonly properties?: Obj; - readonly required?: ReadonlyArray>; + readonly required?: ReadonlyArray, string>>; readonly description?: string; readonly example?: any; readonly format?: string; @@ -98,6 +98,9 @@ export interface Schema extends OfSchema { readonly default?: (this['type'] extends TypeStringef ? StringDefToType : any) | null; readonly maxLength?: number; readonly minLength?: number; + readonly maximum?: number; + readonly minimum?: number; + readonly pattern?: string; } type RequiredPropertyNames = { @@ -105,24 +108,26 @@ type RequiredPropertyNames = { // K is not optional s[K]['optional'] extends false ? K : // K has default value - s[K]['default'] extends null | string | number | boolean | Record ? K : never + s[K]['default'] extends null | string | number | boolean | Record ? K : + never }[keyof s]; -export interface Obj { [key: string]: Schema; } +export type Obj = Record; +// https://github.com/misskey-dev/misskey/issues/8535 +// To avoid excessive stack depth error, +// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). export type ObjType = - { -readonly [P in keyof s]?: SchemaType } & - { -readonly [P in RequiredProps]: SchemaType } & - { -readonly [P in RequiredPropertyNames]: SchemaType }; + UnionToIntersection< + { -readonly [R in RequiredPropertyNames]-?: SchemaType } & + { -readonly [R in RequiredProps]-?: SchemaType } & + { -readonly [P in keyof s]?: SchemaType } + >; type NullOrUndefined

= - p['nullable'] extends true - ? p['optional'] extends true - ? (T | null | undefined) - : (T | null) - : p['optional'] extends true - ? (T | undefined) - : T; + | (p['nullable'] extends true ? null : never) + | (p['optional'] extends true ? undefined : never) + | T; // https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection // Get intersection from union @@ -139,9 +144,9 @@ export type SchemaTypeDef

= p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( p['enum'] extends readonly string[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateにする?? - string + p['enum'][number] : + p['format'] extends 'date-time' ? string : // Dateにする?? + string ) : p['type'] extends 'boolean' ? boolean : p['type'] extends 'object' ? ( diff --git a/packages/backend/src/misc/webhook-cache.ts b/packages/backend/src/misc/webhook-cache.ts new file mode 100644 index 000000000..4bd233366 --- /dev/null +++ b/packages/backend/src/misc/webhook-cache.ts @@ -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; + } + } +}); diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts index 69cdc49ce..c6e2141a4 100644 --- a/packages/backend/src/models/entities/access-token.ts +++ b/packages/backend/src/models/entities/access-token.ts @@ -15,7 +15,6 @@ export class AccessToken { @Column('timestamp with time zone', { nullable: true, - default: null, }) public lastUsedAt: Date | null; @@ -29,7 +28,6 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public session: string | null; @@ -52,7 +50,6 @@ export class AccessToken { @Column({ ...id(), nullable: true, - default: null, }) public appId: App['id'] | null; @@ -65,21 +62,18 @@ export class AccessToken { @Column('varchar', { length: 128, nullable: true, - default: null, }) public name: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public description: string | null; @Column('varchar', { length: 512, nullable: true, - default: null, }) public iconUrl: string | null; diff --git a/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts index b82585620..295d1b486 100644 --- a/packages/backend/src/models/entities/auth-session.ts +++ b/packages/backend/src/models/entities/auth-session.ts @@ -23,7 +23,7 @@ export class AuthSession { ...id(), nullable: true, }) - public userId: User['id']; + public userId: User['id'] | null; @ManyToOne(type => User, { onDelete: 'CASCADE', diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts index da6b3c7a7..1386684c3 100644 --- a/packages/backend/src/models/entities/clip.ts +++ b/packages/backend/src/models/entities/clip.ts @@ -37,7 +37,7 @@ export class Clip { public isPublic: boolean; @Column('varchar', { - length: 2048, nullable: true, default: null, + length: 2048, nullable: true, comment: 'The description of the Clip.', }) public description: string | null; diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 3d375f0e3..a636d1d51 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -79,7 +79,6 @@ export class DriveFile { }) public properties: { width?: number; height?: number; orientation?: number; avgColor?: string }; - @Index() @Column('boolean') public storedInternal: boolean; diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts index b72ca7233..7332dd185 100644 --- a/packages/backend/src/models/entities/emoji.ts +++ b/packages/backend/src/models/entities/emoji.ts @@ -36,6 +36,7 @@ export class Emoji { @Column('varchar', { length: 512, + default: '', }) public publicUrl: string; diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts index bb24d6b30..7ea923438 100644 --- a/packages/backend/src/models/entities/instance.ts +++ b/packages/backend/src/models/entities/instance.ts @@ -107,53 +107,53 @@ export class Instance { public isSuspended: boolean; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, comment: 'The software of the Instance.', }) public softwareName: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public softwareVersion: string | null; @Column('boolean', { - nullable: true, default: null, + nullable: true, }) public openRegistrations: boolean | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public name: string | null; @Column('varchar', { - length: 4096, nullable: true, default: null, + length: 4096, nullable: true, }) public description: string | null; @Column('varchar', { - length: 128, nullable: true, default: null, + length: 128, nullable: true, }) public maintainerName: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public maintainerEmail: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public iconUrl: string | null; @Column('varchar', { - length: 256, nullable: true, default: null, + length: 256, nullable: true, }) public faviconUrl: string | null; @Column('varchar', { - length: 64, nullable: true, default: null, + length: 64, nullable: true, }) public themeColor: string | null; diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 4d58b5f04..80b5228bc 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -78,7 +78,7 @@ export class Meta { public blockedHosts: string[]; @Column('varchar', { - length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}', + length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}', }) public pinnedPages: string[]; @@ -346,14 +346,12 @@ export class Meta { @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultLightTheme: string | null; @Column('varchar', { length: 8192, - default: null, nullable: true, }) public defaultDarkTheme: string | null; diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts index b3a7e7a67..8f9e69063 100644 --- a/packages/backend/src/models/entities/muting.ts +++ b/packages/backend/src/models/entities/muting.ts @@ -17,7 +17,6 @@ export class Muting { @Index() @Column('timestamp with time zone', { nullable: true, - default: null, }) public expiresAt: Date | null; diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index da49d53b6..0ffeb85f6 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -53,8 +53,8 @@ export class Note { }) public threadId: string | null; - @Column('varchar', { - length: 8192, nullable: true, + @Column('text', { + nullable: true, }) public text: string | null; @@ -179,7 +179,7 @@ export class Note { @Index() @Column({ ...id(), - nullable: true, default: null, + nullable: true, comment: 'The ID of source channel.', }) public channelId: Channel['id'] | null; diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index f95cb144c..1778742ea 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -192,6 +192,7 @@ export class UserProfile { @Column('jsonb', { default: [], + comment: 'List of instances muted by the user.', }) public mutedInstances: string[]; diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index c76824c97..df92fb825 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -1,6 +1,6 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { DriveFile } from './drive-file.js'; import { id } from '../id.js'; +import { DriveFile } from './drive-file.js'; @Entity() @Index(['usernameLower', 'host'], { unique: true }) @@ -207,7 +207,7 @@ export class User { @Column('boolean', { default: false, - comment: 'Whether to show users replying to other users in the timeline' + comment: 'Whether to show users replying to other users in the timeline.', }) public showTimelineReplies: boolean; diff --git a/packages/backend/src/models/entities/webhook.ts b/packages/backend/src/models/entities/webhook.ts new file mode 100644 index 000000000..56b411f87 --- /dev/null +++ b/packages/backend/src/models/entities/webhook.ts @@ -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; +} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 54582347c..814b37d44 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -64,6 +64,7 @@ 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 = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); @@ -125,5 +126,6 @@ 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); diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 69dc1721c..b626359d9 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -1,6 +1,5 @@ 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'; import { toPuny } from '@/misc/convert-host.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js'; @@ -9,6 +8,7 @@ import config from '@/config/index.js'; import { query, appendQuery } from '@/prelude/url.js'; import { Meta } from '@/models/entities/meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; +import { Users, DriveFolders } from '../index.js'; type PackOptions = { detail?: boolean, @@ -29,7 +29,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { - const properties = JSON.parse(JSON.stringify(file.properties)); + const properties = structuredClone(file.properties); if (file.properties.orientation >= 5) { [properties.width, properties.height] = [properties.height, properties.width]; } @@ -111,7 +111,40 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ async pack( src: DriveFile['id'] | DriveFile, - options?: PackOptions + options?: PackOptions, + ): Promise> { + const opts = Object.assign({ + detail: false, + self: false, + }, options); + + const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); + + return await awaitAll>({ + id: file.id, + createdAt: file.createdAt.toISOString(), + name: file.name, + type: file.type, + md5: file.md5, + size: file.size, + isSensitive: file.isSensitive, + blurhash: file.blurhash, + properties: opts.self ? file.properties : this.getPublicProperties(file), + url: opts.self ? file.url : this.getPublicUrl(file, false), + thumbnailUrl: this.getPublicUrl(file, true), + comment: file.comment, + folderId: file.folderId, + folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { + detail: true, + }) : null, + userId: opts.withUser ? file.userId : null, + user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null, + }); + }, + + async packNullable( + src: DriveFile['id'] | DriveFile, + options?: PackOptions, ): Promise | null> { const opts = Object.assign({ detail: false, @@ -145,9 +178,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ 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); + options?: PackOptions, + ): Promise[]> { + const items = await Promise.all(files.map(f => this.packNullable(f, options))); + return items.filter((x): x is Packed<'DriveFile'> => x != null); }, }); diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts index 1bffb23fa..092b26b39 100644 --- a/packages/backend/src/models/repositories/page.ts +++ b/packages/backend/src/models/repositories/page.ts @@ -1,10 +1,10 @@ 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'; import { awaitAll } from '@/prelude/await-all.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { User } from '@/models/entities/user.js'; +import { Users, DriveFiles, PageLikes } from '../index.js'; export const PageRepository = db.getRepository(Page).extend({ async pack( @@ -14,7 +14,7 @@ export const PageRepository = db.getRepository(Page).extend({ const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - const attachedFiles: Promise[] = []; + const attachedFiles: Promise[] = []; const collectFile = (xs: any[]) => { for (const x of xs) { if (x.type === 'image') { @@ -73,7 +73,7 @@ export const PageRepository = db.getRepository(Page).extend({ script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, - attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), + attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)), likedCount: page.likedCount, isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined, }); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 2f4b7d678..541fbaf00 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -1,7 +1,6 @@ import { EntityRepository, Repository, In, Not } from 'typeorm'; import Ajv from 'ajv'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; import config from '@/config/index.js'; import { Packed } from '@/misc/schema.js'; import { awaitAll, Promiseable } from '@/prelude/await-all.js'; @@ -9,8 +8,9 @@ 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'; +import { Instance } from '../entities/instance.js'; +import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; const userInstanceCache = new Cache(1000 * 60 * 60 * 3); @@ -112,7 +112,7 @@ export const UserRepository = db.getRepository(User).extend({ const joinings = await UserGroupJoinings.findBy({ userId: userId }); const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message') - .where(`message.groupId = :groupId`, { groupId: j.userGroupId }) + .where('message.groupId = :groupId', { groupId: j.userGroupId }) .andWhere('message.userId != :userId', { userId: userId }) .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) .andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない @@ -204,8 +204,18 @@ export const UserRepository = db.getRepository(User).extend({ ); }, - getAvatarUrl(user: User): string { - // TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング + async getAvatarUrl(user: User): Promise { + if (user.avatar) { + return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); + } else if (user.avatarId) { + const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId }); + return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id); + } else { + return this.getIdenticonUrl(user.id); + } + }, + + getAvatarUrlSync(user: User): string { if (user.avatar) { return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id); } else { @@ -223,7 +233,7 @@ export const UserRepository = db.getRepository(User).extend({ options?: { detail?: D, includeSecrets?: boolean, - } + }, ): Promise> { const opts = Object.assign({ detail: false, @@ -274,7 +284,7 @@ export const UserRepository = db.getRepository(User).extend({ name: user.name, username: user.username, host: user.host, - avatarUrl: this.getAvatarUrl(user), + avatarUrl: this.getAvatarUrlSync(user), avatarBlurhash: user.avatar?.blurhash || null, avatarColor: null, // 後方互換性のため isAdmin: user.isAdmin || falsy, @@ -283,7 +293,7 @@ export const UserRepository = db.getRepository(User).extend({ isCat: user.isCat || falsy, instance: user.host ? userInstanceCache.fetch(user.host, () => Instances.findOneBy({ host: user.host! }), - v => v != null + v => v != null, ).then(instance => instance ? { name: instance.name, softwareName: instance.softwareName, @@ -403,7 +413,7 @@ export const UserRepository = db.getRepository(User).extend({ options?: { detail?: D, includeSecrets?: boolean, - } + }, ): Promise[]> { return Promise.all(users.map(u => this.pack(u, me, options))); }, diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index 5f9af88db..e97fdd5ef 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -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', diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 616bedc0d..253681695 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -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', diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index b679a552b..2d40290e4 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,4 +1,5 @@ import httpSignature from 'http-signature'; +import { v4 as uuid } from 'uuid'; import config from '@/config/index.js'; import { envOption } from '../env.js'; @@ -8,13 +9,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, webhookEventTypes } from '@/models/entities/webhook.js'; function renderError(e: Error): any { return { @@ -26,6 +29,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 +74,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,12 +263,36 @@ export function createCleanRemoteFilesJob() { }); } +export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) { + const data = { + type, + content, + webhookId: webhook.id, + userId: webhook.userId, + to: webhook.url, + secret: webhook.secret, + createdAt: Date.now(), + eventId: uuid(), + }; + + 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); diff --git a/packages/backend/src/queue/processors/webhook-deliver.ts b/packages/backend/src/queue/processors/webhook-deliver.ts new file mode 100644 index 000000000..d49206f68 --- /dev/null +++ b/packages/backend/src/queue/processors/webhook-deliver.ts @@ -0,0 +1,59 @@ +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'); + +export default async (job: Bull.Job) => { + try { + logger.debug(`delivering ${job.data.webhookId}`); + + 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({ + hookId: job.data.webhookId, + userId: job.data.userId, + eventId: job.data.eventId, + createdAt: job.data.createdAt, + type: job.data.type, + body: 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; + } + } +}; diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts index d612dee45..f3a267790 100644 --- a/packages/backend/src/queue/queues.ts +++ b/packages/backend/src/queue/queues.ts @@ -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>('system'); export const endedPollNotificationQueue = initializeQueue('endedPollNotification'); @@ -8,6 +8,7 @@ export const deliverQueue = initializeQueue('deliver', config.de export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); export const dbQueue = initializeQueue('db'); export const objectStorageQueue = initializeQueue('objectStorage'); +export const webhookDeliverQueue = initializeQueue('webhookDeliver', 64); export const queues = [ systemQueue, @@ -16,4 +17,5 @@ export const queues = [ inboxQueue, dbQueue, objectStorageQueue, + webhookDeliverQueue, ]; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 5191caea4..6c0b9d9bf 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -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,17 @@ export type EndedPollNotificationJobData = { noteId: Note['id']; }; +export type WebhookDeliverJobData = { + type: string; + content: unknown; + webhookId: Webhook['id']; + userId: User['id']; + to: string; + secret: string; + createdAt: number; + eventId: string; +}; + export type ThinUser = { id: User['id']; }; diff --git a/packages/backend/src/remote/activitypub/ap-request.ts b/packages/backend/src/remote/activitypub/ap-request.ts index 96bfec3b1..8b55f2247 100644 --- a/packages/backend/src/remote/activitypub/ap-request.ts +++ b/packages/backend/src/remote/activitypub/ap-request.ts @@ -95,7 +95,7 @@ function genSigningString(request: Request, includeHeaders: string[]) { function lcObjectKey(src: Record) { const dst: Record = {}; - for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; + for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; return dst; } diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 9f21dc4cc..4c1999e4c 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -79,37 +79,46 @@ export default class DeliverManager { const inboxes = new Set(); - // build inbox list - for (const recipe of this.recipes) { - if (isFollowers(recipe)) { - // 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; - }[]; + /* + build inbox list - for (const following of followers) { - 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 != null, + ) + .forEach(recipe => inboxes.add(recipe.to.inbox!)); + // deliver for (const inbox of inboxes) { deliver(this.actor, this.activity, inbox); diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts index 7f1519ac2..f7b0bcecd 100644 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ b/packages/backend/src/remote/activitypub/kernel/read.ts @@ -18,7 +18,7 @@ export const performReadActivity = async (actor: CacheableRemoteUser, activity: return `skip: message not found`; } - if (actor.id != message.recipientId) { + if (actor.id !== message.recipientId) { return `skip: actor is not a message recipient`; } diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts index 8f6eab685..a6e3929b0 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -1,6 +1,6 @@ import unfollow from '@/services/following/delete.js'; import cancelRequest from '@/services/following/requests/cancel.js'; -import {IAccept} from '../../type.js'; +import { IAccept } from '../../type.js'; import { CacheableRemoteUser } from '@/models/entities/user.js'; import { Followings } from '@/models/index.js'; import DbResolver from '../../db-resolver.js'; diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts index 5132c6ef9..362a543ec 100644 --- a/packages/backend/src/remote/activitypub/misc/ld-signature.ts +++ b/packages/backend/src/remote/activitypub/misc/ld-signature.ts @@ -113,7 +113,8 @@ export class LdSignature { headers: { Accept: 'application/ld+json, application/json', }, - timeout: this.loderTimeout, + // TODO + //timeout: this.loderTimeout, agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent, }).then(res => { if (!res.ok) { diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts index a16009296..13f77424e 100644 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ b/packages/backend/src/remote/activitypub/models/mention.ts @@ -1,9 +1,9 @@ -import { toArray, unique } from '@/prelude/array.js'; -import { IObject, isMention, IApMention } from '../type.js'; -import { resolvePerson } from './person.js'; import promiseLimit from 'promise-limit'; -import Resolver from '../resolver.js'; +import { toArray, unique } from '@/prelude/array.js'; import { CacheableUser, User } from '@/models/entities/user.js'; +import { IObject, isMention, IApMention } from '../type.js'; +import Resolver from '../resolver.js'; +import { resolvePerson } from './person.js'; export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); @@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef const limit = promiseLimit(2); const mentionedUsers = (await Promise.all( - hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) + hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))), )).filter((x): x is CacheableUser => x != null); return mentionedUsers; diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 88661865d..6097e3b6e 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -1,17 +1,8 @@ import { URL } from 'node:url'; import promiseLimit from 'promise-limit'; -import $, { Context } from 'cafy'; import config from '@/config/index.js'; -import Resolver from '../resolver.js'; -import { resolveImage } from './image.js'; -import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; -import { fromHtml } from '../../../mfm/from-html.js'; -import { htmlToMfm } from '../misc/html-to-mfm.js'; -import { resolveNote, extractEmojis } from './note.js'; import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { extractApHashtags } from './tag.js'; -import { apLogger } from '../logger.js'; import { Note } from '@/models/entities/note.js'; import { updateUsertags } from '@/services/update-hashtag.js'; import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; @@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js'; import { uriPersonCache } from '@/services/user-cache.js'; import { publishInternalEvent } from '@/services/stream.js'; import { db } from '@/db/postgre.js'; +import { apLogger } from '../logger.js'; +import { htmlToMfm } from '../misc/html-to-mfm.js'; +import { fromHtml } from '../../../mfm/from-html.js'; +import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js'; +import Resolver from '../resolver.js'; +import { extractApHashtags } from './tag.js'; +import { resolveNote, extractEmojis } from './note.js'; +import { resolveImage } from './image.js'; const logger = apLogger; @@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor { throw new Error(`invalid Actor type '${x.type}'`); } - const validate = (name: string, value: any, validater: Context) => { - const e = validater.test(value); - if (e) throw new Error(`invalid Actor: ${name} ${e.message}`); - }; + if (!(typeof x.id === 'string' && x.id.length > 0)) { + throw new Error('invalid Actor: wrong id'); + } - validate('id', x.id, $.default.str.min(1)); - validate('inbox', x.inbox, $.default.str.min(1)); - validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/)); + if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) { + throw new Error('invalid Actor: wrong inbox'); + } + + if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) { + throw new Error('invalid Actor: wrong username'); + } // These fields are only informational, and some AP software allows these // fields to be very long. If they are too long, we cut them off. This way // we can at least see these users and their activities. - validate('name', truncate(x.name, nameLength), $.default.optional.nullable.str); - validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str); + if (x.name) { + if (!(typeof x.name === 'string' && x.name.length > 0)) { + throw new Error('invalid Actor: wrong name'); + } + x.name = truncate(x.name, nameLength); + } + if (x.summary) { + if (!(typeof x.summary === 'string' && x.summary.length > 0)) { + throw new Error('invalid Actor: wrong summary'); + } + x.summary = truncate(x.summary, summaryLength); + } const idHost = toPuny(new URL(x.id!).hostname); if (idHost !== expectHost) { @@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise): Promise { +export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ @@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint if (resolver == null) resolver = new Resolver(); - const object = hint || await resolver.resolve(uri) as any; + const object = hint || await resolver.resolve(uri); const person = validateActor(object, uri); @@ -400,10 +412,10 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise any } = { - 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), - 'misskey:authentication:github': (id, login) => ({ id, login }), - 'misskey:authentication:discord': (id, name) => $discord(id, name), -}; + 'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }), + 'misskey:authentication:github': (id, login) => ({ id, login }), + 'misskey:authentication:discord': (id, name) => $discord(id, name), + }; const $discord = (id: string, name: string) => { if (typeof name !== 'string') { @@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) { // Resolve to (Ordered)Collection Object const collection = await resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`); + if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); // Resolve to Object(may be Note) arrays const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts index 9e75864c6..034501572 100644 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ b/packages/backend/src/remote/activitypub/models/question.ts @@ -69,7 +69,7 @@ export async function updateQuestion(value: any) { const oldCount = poll.votes[poll.choices.indexOf(choice)]; const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; - if (oldCount != newCount) { + if (oldCount !== newCount) { changed = true; poll.votes[poll.choices.indexOf(choice)] = newCount; } diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts index 6fbc11580..58eadddba 100644 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ b/packages/backend/src/remote/activitypub/renderer/flag.ts @@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js'; // to anonymise reporters, the reporting actor must be a system user // object has to be a uri or array of uris -export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => { +export const renderFlag = (user: ILocalUser, object: [string], content: string) => { return { type: 'Flag', actor: `${config.url}/users/${user.id}`, diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 679c8bbfe..e8d429e5d 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,15 +1,15 @@ -import renderDocument from './document.js'; -import renderHashtag from './hashtag.js'; -import renderMention from './mention.js'; -import renderEmoji from './emoji.js'; +import { In, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import toHtml from '../misc/get-note-html.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; -import { In, IsNull } from 'typeorm'; import { Emoji } from '@/models/entities/emoji.js'; import { Poll } from '@/models/entities/poll.js'; +import toHtml from '../misc/get-note-html.js'; +import renderEmoji from './emoji.js'; +import renderMention from './mention.js'; +import renderHashtag from './hashtag.js'; +import renderDocument from './document.js'; export default async function renderNote(note: Note, dive = true, isTalk = false): Promise> { const getPromisedFiles = async (ids: string[]) => { @@ -83,7 +83,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const files = await getPromisedFiles(note.fileIds); const text = note.text; - let poll: Poll | null; + let poll: Poll | null = null; if (note.hasPoll) { poll = await Polls.findOneBy({ noteId: note.id }); @@ -159,7 +159,7 @@ export async function getEmojis(names: string[]): Promise { names.map(name => Emojis.findOneBy({ name, host: IsNull(), - })) + })), ); return emojis.filter(emoji => emoji != null) as Emoji[]; diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index c1269c75c..334eae984 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -2,10 +2,10 @@ import config from '@/config/index.js'; import { getJson } from '@/misc/fetch.js'; import { ILocalUser } from '@/models/entities/user.js'; import { getInstanceActor } from '@/services/instance-actor.js'; -import { signedGet } from './request.js'; -import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { extractDbHost } from '@/misc/convert-host.js'; +import { signedGet } from './request.js'; +import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; export default class Resolver { private history: Set; @@ -56,13 +56,13 @@ export default class Resolver { this.user = await getInstanceActor(); } - const object = this.user + const object = (this.user ? await signedGet(value, this.user) - : await getJson(value, 'application/activity+json, application/ld+json'); + : await getJson(value, 'application/activity+json, application/ld+json')) as IObject; if (object == null || ( Array.isArray(object['@context']) ? - !object['@context'].includes('https://www.w3.org/ns/activitystreams') : + !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { throw new Error('invalid response'); diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts index 2051d2624..ef5b98b59 100644 --- a/packages/backend/src/remote/activitypub/type.ts +++ b/packages/backend/src/remote/activitypub/type.ts @@ -2,7 +2,7 @@ export type obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; export interface IObject { - '@context': string | obj | obj[]; + '@context': string | string[] | obj | obj[]; type: string | string[]; id?: string; summary?: string; @@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string { export function getApId(value: string | IObject): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error(`cannot detemine id`); + throw new Error('cannot detemine id'); } /** @@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string { export function getApType(value: IObject): string { if (typeof value.type === 'string') return value.type; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - throw new Error(`cannot detect type`); + throw new Error('cannot detect type'); } export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { diff --git a/packages/backend/src/remote/webfinger.ts b/packages/backend/src/remote/webfinger.ts index 9d3bfab24..337df34c2 100644 --- a/packages/backend/src/remote/webfinger.ts +++ b/packages/backend/src/remote/webfinger.ts @@ -15,7 +15,7 @@ type IWebFinger = { export default async function(query: string): Promise { const url = genUrl(query); - return await getJson(url, 'application/jrd+json, application/json'); + return await getJson(url, 'application/jrd+json, application/json') as IWebFinger; } function genUrl(query: string) { diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts index 4d4f73316..beb48713a 100644 --- a/packages/backend/src/server/activitypub/followers.ts +++ b/packages/backend/src/server/activitypub/followers.ts @@ -1,32 +1,26 @@ import Router from '@koa/router'; +import { FindOptionsWhere, IsNull, LessThan } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import * as url from '@/prelude/url.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { IsNull, LessThan } from 'typeorm'; +import { Following } from '@/models/entities/following.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'cursor' parameter - const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); - - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; - - // Validate parameters - if (cursorErr || pageErr) { + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { ctx.status = 400; return; } + const page = ctx.request.query.page === 'true'; + const user = await Users.findOneBy({ id: userId, host: IsNull(), @@ -57,7 +51,7 @@ export default async (ctx: Router.RouterContext) => { if (page) { const query = { followeeId: user.id, - } as any; + } as FindOptionsWhere; // カーソルが指定されている場合 if (cursor) { @@ -86,7 +80,7 @@ export default async (ctx: Router.RouterContext) => { inStock ? `${partOf}?${url.query({ page: 'true', cursor: followings[followings.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts index 0af1f424f..3a25a6316 100644 --- a/packages/backend/src/server/activitypub/following.ts +++ b/packages/backend/src/server/activitypub/following.ts @@ -1,33 +1,26 @@ import Router from '@koa/router'; +import { LessThan, IsNull, FindOptionsWhere } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import * as url from '@/prelude/url.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js'; -import { setResponseType } from '../activitypub.js'; import { Users, Followings, UserProfiles } from '@/models/index.js'; -import { LessThan, IsNull, FindOptionsWhere } from 'typeorm'; import { Following } from '@/models/entities/following.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'cursor' parameter - const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor); - - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; - - // Validate parameters - if (cursorErr || pageErr) { + const cursor = ctx.request.query.cursor; + if (cursor != null && typeof cursor !== 'string') { ctx.status = 400; return; } + const page = ctx.request.query.page === 'true'; + const user = await Users.findOneBy({ id: userId, host: IsNull(), @@ -87,7 +80,7 @@ export default async (ctx: Router.RouterContext) => { inStock ? `${partOf}?${url.query({ page: 'true', cursor: followings[followings.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts index 6b9592bcf..7a2586998 100644 --- a/packages/backend/src/server/activitypub/outbox.ts +++ b/packages/backend/src/server/activitypub/outbox.ts @@ -1,36 +1,37 @@ import Router from '@koa/router'; +import { Brackets, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import $ from 'cafy'; -import { ID } from '@/misc/cafy-id.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js'; import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js'; -import { setResponseType } from '../activitypub.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; import renderCreate from '@/remote/activitypub/renderer/create.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; import { countIf } from '@/prelude/array.js'; import * as url from '@/prelude/url.js'; import { Users, Notes } from '@/models/index.js'; -import { makePaginationQuery } from '../api/common/make-pagination-query.js'; -import { Brackets, IsNull } from 'typeorm'; import { Note } from '@/models/entities/note.js'; +import { makePaginationQuery } from '../api/common/make-pagination-query.js'; +import { setResponseType } from '../activitypub.js'; export default async (ctx: Router.RouterContext) => { const userId = ctx.params.user; - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.default.optional.type(ID).get(ctx.request.query.since_id); + const sinceId = ctx.request.query.since_id; + if (sinceId != null && typeof sinceId !== 'string') { + ctx.status = 400; + return; + } - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.default.optional.type(ID).get(ctx.request.query.until_id); + const untilId = ctx.request.query.until_id; + if (untilId != null && typeof untilId !== 'string') { + ctx.status = 400; + return; + } - // Get 'page' parameter - const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page); - const page: boolean = ctx.request.query.page === 'true'; + const page = ctx.request.query.page === 'true'; - // Validate parameters - if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) { + if (countIf(x => x != null, [sinceId, untilId]) > 1) { ctx.status = 400; return; } @@ -52,8 +53,8 @@ export default async (ctx: Router.RouterContext) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) .andWhere('note.userId = :userId', { userId: user.id }) .andWhere(new Brackets(qb => { qb - .where(`note.visibility = 'public'`) - .orWhere(`note.visibility = 'home'`); + .where('note.visibility = \'public\'') + .orWhere('note.visibility = \'home\''); })) .andWhere('note.localOnly = FALSE'); @@ -76,7 +77,7 @@ export default async (ctx: Router.RouterContext) => { notes.length ? `${partOf}?${url.query({ page: 'true', until_id: notes[notes.length - 1].id, - })}` : undefined + })}` : undefined, ); ctx.body = renderActivity(rendered); @@ -85,7 +86,7 @@ export default async (ctx: Router.RouterContext) => { // index page const rendered = renderOrderedCollection(partOf, user.notesCount, `${partOf}?page=true`, - `${partOf}?page=true&since_id=000000000000000000000000` + `${partOf}?page=true&since_id=000000000000000000000000`, ); ctx.body = renderActivity(rendered); ctx.set('Cache-Control', 'public, max-age=180'); diff --git a/packages/backend/src/server/api/2fa.ts b/packages/backend/src/server/api/2fa.ts index e1c226979..96b9316e4 100644 --- a/packages/backend/src/server/api/2fa.ts +++ b/packages/backend/src/server/api/2fa.ts @@ -1,6 +1,6 @@ import * as crypto from 'node:crypto'; -import config from '@/config/index.js'; import * as jsrsasign from 'jsrsasign'; +import config from '@/config/index.js'; const ECC_PRELUDE = Buffer.from([0x04]); const NULL_BYTE = Buffer.from([0]); @@ -121,14 +121,14 @@ export function verifyLogin({ signature: Buffer, challenge: string }) { - if (clientData.type != 'webauthn.get') { + if (clientData.type !== 'webauthn.get') { throw new Error('type is not webauthn.get'); } - if (hash(clientData.challenge).toString('hex') != challenge) { + if (hash(clientData.challenge).toString('hex') !== challenge) { throw new Error('challenge mismatch'); } - if (clientData.origin != config.scheme + '://' + config.host) { + if (clientData.origin !== config.scheme + '://' + config.host) { throw new Error('origin mismatch'); } @@ -145,14 +145,14 @@ export function verifyLogin({ export const procedures = { none: { - verify({ publicKey }: {publicKey: Map}) { + verify({ publicKey }: { publicKey: Map }) { const negTwo = publicKey.get(-2); - if (!negTwo || negTwo.length != 32) { + if (!negTwo || negTwo.length !== 32) { throw new Error('invalid or no -2 key given'); } const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { + if (!negThree || negThree.length !== 32) { throw new Error('invalid or no -3 key given'); } @@ -183,7 +183,7 @@ export const procedures = { rpIdHash: Buffer, credentialId: Buffer, }) { - if (attStmt.alg != -7) { + if (attStmt.alg !== -7) { throw new Error('alg mismatch'); } @@ -196,11 +196,11 @@ export const procedures = { const negTwo = publicKey.get(-2); - if (!negTwo || negTwo.length != 32) { + if (!negTwo || negTwo.length !== 32) { throw new Error('invalid or no -2 key given'); } const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { + if (!negThree || negThree.length !== 32) { throw new Error('invalid or no -3 key given'); } @@ -263,7 +263,7 @@ export const procedures = { .map((key: any) => PEMString(key)) .concat([GSR2]); - if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') { + if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { throw new Error('invalid common name'); } @@ -283,11 +283,11 @@ export const procedures = { const negTwo = publicKey.get(-2); - if (!negTwo || negTwo.length != 32) { + if (!negTwo || negTwo.length !== 32) { throw new Error('invalid or no -2 key given'); } const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { + if (!negThree || negThree.length !== 32) { throw new Error('invalid or no -3 key given'); } @@ -332,11 +332,11 @@ export const procedures = { const negTwo = publicKey.get(-2); - if (!negTwo || negTwo.length != 32) { + if (!negTwo || negTwo.length !== 32) { throw new Error('invalid or no -2 key given'); } const negThree = publicKey.get(-3); - if (!negThree || negThree.length != 32) { + if (!negThree || negThree.length !== 32) { throw new Error('invalid or no -3 key given'); } @@ -353,7 +353,7 @@ export const procedures = { // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation throw new Error('ECDAA-Verify is not supported'); } else { - if (attStmt.alg != -7) throw new Error('alg mismatch'); + if (attStmt.alg !== -7) throw new Error('alg mismatch'); throw new Error('self attestation is not supported'); } @@ -377,7 +377,7 @@ export const procedures = { credentialId: Buffer }) { const x5c: Buffer[] = attStmt.x5c; - if (x5c.length != 1) { + if (x5c.length !== 1) { throw new Error('x5c length does not match expectation'); } @@ -387,11 +387,11 @@ export const procedures = { const negTwo: Buffer = publicKey.get(-2); - if (!negTwo || negTwo.length != 32) { + if (!negTwo || negTwo.length !== 32) { throw new Error('invalid or no -2 key given'); } const negThree: Buffer = publicKey.get(-3); - if (!negThree || negThree.length != 32) { + if (!negThree || negThree.length !== 32) { throw new Error('invalid or no -3 key given'); } diff --git a/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts index 3638518e6..c4c18ffa0 100644 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ b/packages/backend/src/server/api/common/read-messaging-message.ts @@ -1,6 +1,7 @@ import { publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; import { publishMessagingStream } from '@/services/stream.js'; import { publishMessagingIndexStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import { User, IRemoteUser } from '@/models/entities/user.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; @@ -50,6 +51,21 @@ export async function readUserMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのユーザーとのメッセージで未読がなければイベント発行 + const count = await MessagingMessages.count({ + where: { + userId: otherpartyId, + recipientId: userId, + isRead: false, + }, + take: 1 + }); + + if (!count) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId }); + } } } @@ -104,6 +120,19 @@ export async function readGroupMessagingMessage( if (!await Users.getHasUnreadMessagingMessage(userId)) { // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); + pushNotification(userId, 'readAllMessagingMessages', undefined); + } else { + // そのグループにおいて未読がなければイベント発行 + const unreadExist = await MessagingMessages.createQueryBuilder('message') + .where(`message.groupId = :groupId`, { groupId: groupId }) + .andWhere('message.userId != :userId', { userId: userId }) + .andWhere('NOT (:userId = ANY(message.reads))', { userId: userId }) + .andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない + .getOne().then(x => x != null); + + if (!unreadExist) { + pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId }); + } } } diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts index 1f575042a..0dad35bcc 100644 --- a/packages/backend/src/server/api/common/read-notification.ts +++ b/packages/backend/src/server/api/common/read-notification.ts @@ -1,4 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import { User } from '@/models/entities/user.js'; import { Notification } from '@/models/entities/notification.js'; import { Notifications, Users } from '@/models/index.js'; @@ -16,28 +17,29 @@ export async function readNotification( isRead: true, }); - post(userId); + if (!await Users.getHasUnreadNotification(userId)) return postReadAllNotifications(userId); + else return postReadNotifications(userId, notificationIds); } export async function readNotificationByQuery( userId: User['id'], query: Record ) { - // Update documents - await Notifications.update({ + const notificationIds = await Notifications.find({ ...query, notifieeId: userId, isRead: false, - }, { - isRead: true, - }); + }).then(notifications => notifications.map(notification => notification.id)); - post(userId); + return readNotification(userId, notificationIds); } -async function post(userId: User['id']) { - if (!await Users.getHasUnreadNotification(userId)) { - // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 - publishMainStream(userId, 'readAllNotifications'); - } +function postReadAllNotifications(userId: User['id']) { + publishMainStream(userId, 'readAllNotifications'); + return pushNotification(userId, 'readAllNotifications', undefined); +} + +function postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) { + publishMainStream(userId, 'readNotifications', notificationIds); + return pushNotification(userId, 'readNotifications', { notificationIds }); } diff --git a/packages/backend/src/server/api/common/signin.ts b/packages/backend/src/server/api/common/signin.ts index f1dccee2c..038fd8d96 100644 --- a/packages/backend/src/server/api/common/signin.ts +++ b/packages/backend/src/server/api/common/signin.ts @@ -9,7 +9,7 @@ import { publishMainStream } from '@/services/stream.js'; export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { if (redirect) { //#region Cookie - ctx.cookies.set('igi', user.token, { + ctx.cookies.set('igi', user.token!, { path: '/', // SEE: https://github.com/koajs/koa/issues/974 // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 6b4eff078..e2db03f13 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -1,5 +1,6 @@ import { Schema } from '@/misc/schema.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; @@ -201,6 +202,11 @@ import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_update from './endpoints/i/update.js'; import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js'; +import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; +import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; +import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; +import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; +import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; import * as ep___messaging_history from './endpoints/messaging/history.js'; import * as ep___messaging_messages from './endpoints/messaging/messages.js'; import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js'; @@ -304,6 +310,7 @@ import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; const eps = [ + ['admin/meta', ep___admin_meta], ['admin/abuse-user-reports', ep___admin_abuseUserReports], ['admin/accounts/create', ep___admin_accounts_create], ['admin/accounts/delete', ep___admin_accounts_delete], @@ -505,6 +512,11 @@ const eps = [ ['i/update-email', ep___i_updateEmail], ['i/update', ep___i_update], ['i/user-group-invites', ep___i_userGroupInvites], + ['i/webhooks/create', ep___i_webhooks_create], + ['i/webhooks/list', ep___i_webhooks_list], + ['i/webhooks/show', ep___i_webhooks_show], + ['i/webhooks/update', ep___i_webhooks_update], + ['i/webhooks/delete', ep___i_webhooks_delete], ['messaging/history', ep___messaging_history], ['messaging/messages', ep___messaging_messages], ['messaging/messages/create', ep___messaging_messages_create], diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 1d8eb1d61..7a5758d75 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -1,5 +1,6 @@ -import define from '../../../define.js'; import { Announcements, AnnouncementReads } from '@/models/index.js'; +import { Announcement } from '@/models/entities/announcement.js'; +import define from '../../../define.js'; import { makePaginationQuery } from '../../../common/make-pagination-query.js'; export const meta = { @@ -68,11 +69,21 @@ export default define(meta, paramDef, async (ps) => { const announcements = await query.take(ps.limit).getMany(); + const reads = new Map(); + for (const announcement of announcements) { - (announcement as any).reads = await AnnouncementReads.countBy({ + reads.set(announcement, await AnnouncementReads.countBy({ announcementId: announcement.id, - }); + })); } - return announcements; + return announcements.map(announcement => ({ + id: announcement.id, + createdAt: announcement.createdAt.toISOString(), + updatedAt: announcement.updatedAt?.toISOString() ?? null, + title: announcement.title, + text: announcement.text, + imageUrl: announcement.imageUrl, + reads: reads.get(announcement)!, + })); }); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts index 646d85a1e..119c4db19 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/files.ts @@ -27,7 +27,12 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, - hostname: { type: 'string', nullable: true, default: null }, + hostname: { + type: 'string', + nullable: true, + default: null, + description: 'The local host is represented with `null`.', + }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 4b27fc018..039df74f1 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -40,6 +40,7 @@ export const meta = { userHost: { type: 'string', optional: false, nullable: true, + description: 'The local host is represented with `null`.', }, md5: { type: 'string', @@ -151,11 +152,20 @@ export const meta = { export const paramDef = { type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - url: { type: 'string' }, - }, - required: [], + anyOf: [ + { + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], + }, + { + properties: { + url: { type: 'string' }, + }, + required: ['url'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index f19c3ddbd..d16689a28 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -40,6 +40,7 @@ export const meta = { host: { type: 'string', optional: false, nullable: true, + description: 'The local host is represented with `null`.', }, url: { type: 'string', @@ -54,7 +55,12 @@ export const paramDef = { type: 'object', properties: { query: { type: 'string', nullable: true, default: null }, - host: { type: 'string', nullable: true, default: null }, + host: { + type: 'string', + nullable: true, + default: null, + description: 'Use `null` to represent the local host.', + }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index f488a71a0..6192978fa 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -38,8 +38,9 @@ export const meta = { optional: false, nullable: true, }, host: { - type: 'string', - optional: false, nullable: true, + type: 'null', + optional: false, + description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.', }, url: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 6063f3e3b..cff58d617 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -17,7 +17,11 @@ export const paramDef = { ids: { type: 'array', items: { type: 'string', format: 'misskey:id', } }, - category: { type: 'string', nullable: true }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, }, required: ['ids'], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index e26514e0c..5b547b3b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -23,7 +23,11 @@ export const paramDef = { properties: { id: { type: 'string', format: 'misskey:id' }, name: { type: 'string' }, - category: { type: 'string', nullable: true }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, aliases: { type: 'array', items: { type: 'string', } }, diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts index b85a677e8..a01e9f3c6 100644 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts @@ -1,5 +1,6 @@ import define from '../../../define.js'; import { Users } from '@/models/index.js'; +import { publishInternalEvent } from '@/services/stream.js'; export const meta = { tags: ['admin'], diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 1ec86fef2..1575d81d5 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -1,5 +1,5 @@ -import define from '../../define.js'; import { Users } from '@/models/index.js'; +import define from '../../define.js'; export const meta = { tags: ['admin'], @@ -24,10 +24,15 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, - state: { type: 'string', enum: ['all', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: "all" }, - origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" }, - username: { type: 'string', default: null }, - hostname: { type: 'string', default: null }, + state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' }, + origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, + username: { type: 'string', nullable: true, default: null }, + hostname: { + type: 'string', + nullable: true, + default: null, + description: 'The local host is represented with `null`.', + }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 3c39bf0f3..b23ee9e3d 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -397,12 +397,14 @@ export default define(meta, paramDef, async (ps, me) => { } await db.transaction(async transactionalEntityManager => { - const meta = await transactionalEntityManager.findOne(Meta, { + const metas = await transactionalEntityManager.find(Meta, { order: { id: 'DESC', }, }); + const meta = metas[0]; + if (meta) { await transactionalEntityManager.update(Meta, meta.id, set); } else { diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index 004e4c131..8aac55b4a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -57,13 +57,9 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchAntenna); } - const antennaQuery = AntennaNotes.createQueryBuilder('joining') - .select('joining.noteId') - .where('joining.antennaId = :antennaId', { antennaId: antenna.id }); - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .andWhere(`note.id IN (${ antennaQuery.getQuery() })`) + .innerJoin(AntennaNotes.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -75,7 +71,7 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(antennaQuery.getParameters()); + .andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id }); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/channels/pin-note.ts b/packages/backend/src/server/api/endpoints/channels/pin-note.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index a2dbef12e..4afe4222a 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -20,7 +20,7 @@ export const paramDef = { type: 'object', properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, - isPublic: { type: 'boolean' }, + isPublic: { type: 'boolean', default: false }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, required: ['name'], diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts index 4b6782fca..4ace747ef 100644 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ b/packages/backend/src/server/api/endpoints/clips/notes.ts @@ -57,12 +57,8 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchClip); } - const clipQuery = ClipNotes.createQueryBuilder('joining') - .select('joining.noteId') - .where('joining.clipId = :clipId', { clipId: clip.id }); - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.id IN (${ clipQuery.getQuery() })`) + .innerJoin(ClipNotes.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -74,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(clipQuery.getParameters()); + .andWhere('clipNote.clipId = :clipId', { clipId: clip.id }); if (user) { generateVisibilityQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index b6a2cf720..0939ae336 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -48,7 +48,6 @@ export const paramDef = { } as const; // eslint-disable-next-line import/no-default-export -// @ts-ignore export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { // Get 'name' parameter let name = ps.name || file.originalname; diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts index f9b4ea89e..0b74cb9f0 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts @@ -1,5 +1,5 @@ -import define from '../../../define.js'; import { DriveFiles } from '@/models/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['drive'], diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts index c8e9d3dd9..fb19345fe 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts @@ -1,7 +1,7 @@ -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Users } from '@/models/index.js'; +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['drive'], @@ -28,27 +28,30 @@ export const meta = { code: 'ACCESS_DENIED', id: '25b73c73-68b1-41d0-bad1-381cfdf6579f', }, - - fileIdOrUrlRequired: { - message: 'fileId or url required.', - code: 'INVALID_PARAM', - id: '89674805-722c-440c-8d88-5641830dc3e4', - }, }, } as const; export const paramDef = { type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - url: { type: 'string' }, - }, - required: [], + anyOf: [ + { + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], + }, + { + properties: { + url: { type: 'string' }, + }, + required: ['url'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - let file: DriveFile | undefined; + let file: DriveFile | null = null; if (ps.fileId) { file = await DriveFiles.findOneBy({ id: ps.fileId }); @@ -62,8 +65,6 @@ export default define(meta, paramDef, async (ps, user) => { thumbnailUrl: ps.url, }], }); - } else { - throw new ApiError(meta.errors.fileIdOrUrlRequired); } if (file == null) { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index e27297176..07e5c07c6 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -22,7 +22,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { - host: { type: 'string', nullable: true }, + host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' }, blocked: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 0116a55fb..1afb34bfd 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -50,10 +50,10 @@ export default define(meta, paramDef, async (ps, user) => { const clientData = JSON.parse(ps.clientDataJSON); - if (clientData.type != 'webauthn.create') { + if (clientData.type !== 'webauthn.create') { throw new Error('not a creation attestation'); } - if (clientData.origin != config.scheme + '://' + config.host) { + if (clientData.origin !== config.scheme + '://' + config.host) { throw new Error('origin mismatch'); } @@ -78,7 +78,7 @@ export default define(meta, paramDef, async (ps, user) => { const credentialId = authData.slice(55, 55 + credentialIdLength); const publicKeyData = authData.slice(55 + credentialIdLength); const publicKey: Map = await cborDecodeFirst(publicKeyData); - if (publicKey.get(3) != -7) { + if (publicKey.get(3) !== -7) { throw new Error('alg mismatch'); } diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts index 3301808e7..68bd103a6 100644 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts @@ -27,7 +27,7 @@ export default define(meta, paramDef, async (ps, user) => { take: ps.limit, skip: ps.offset, order: { - id: ps.sort == 'asc' ? 1 : -1, + id: ps.sort === 'asc' ? 1 : -1, }, }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts new file mode 100644 index 000000000..2e2fd00b8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -0,0 +1,43 @@ +import define from '../../../define.js'; +import { genId } from '@/misc/gen-id.js'; +import { Webhooks } from '@/models/index.js'; +import { publishInternalEvent } from '@/services/stream.js'; +import { webhookEventTypes } from '@/models/entities/webhook.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + + kind: 'write:account', +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 100 }, + url: { type: 'string', minLength: 1, maxLength: 1024 }, + secret: { type: 'string', minLength: 1, maxLength: 1024 }, + on: { type: 'array', items: { + type: 'string', enum: webhookEventTypes, + } }, + }, + required: ['name', 'url', 'secret', 'on'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const webhook = await Webhooks.insert({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + }).then(x => Webhooks.findOneByOrFail(x.identifiers[0])); + + publishInternalEvent('webhookCreated', webhook); + + return webhook; +}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts new file mode 100644 index 000000000..2821eaa5f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts @@ -0,0 +1,44 @@ +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; +import { Webhooks } from '@/models/index.js'; +import { publishInternalEvent } from '@/services/stream.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchWebhook: { + message: 'No such webhook.', + code: 'NO_SUCH_WEBHOOK', + id: 'bae73e5a-5522-4965-ae19-3a8688e71d82', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + webhookId: { type: 'string', format: 'misskey:id' }, + }, + required: ['webhookId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const webhook = await Webhooks.findOneBy({ + id: ps.webhookId, + userId: user.id, + }); + + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } + + await Webhooks.delete(webhook.id); + + publishInternalEvent('webhookDeleted', webhook); +}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts new file mode 100644 index 000000000..54e456373 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -0,0 +1,25 @@ +import define from '../../../define.js'; +import { Webhooks } from '@/models/index.js'; + +export const meta = { + tags: ['webhooks', 'account'], + + requireCredential: true, + + kind: 'read:account', +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const webhooks = await Webhooks.findBy({ + userId: me.id, + }); + + return webhooks; +}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts new file mode 100644 index 000000000..02fa1edb5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -0,0 +1,41 @@ +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; +import { Webhooks } from '@/models/index.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + + kind: 'read:account', + + errors: { + noSuchWebhook: { + message: 'No such webhook.', + code: 'NO_SUCH_WEBHOOK', + id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + webhookId: { type: 'string', format: 'misskey:id' }, + }, + required: ['webhookId'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const webhook = await Webhooks.findOneBy({ + id: ps.webhookId, + userId: user.id, + }); + + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } + + return webhook; +}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts new file mode 100644 index 000000000..f87b9753f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -0,0 +1,59 @@ +import define from '../../../define.js'; +import { ApiError } from '../../../error.js'; +import { Webhooks } from '@/models/index.js'; +import { publishInternalEvent } from '@/services/stream.js'; +import { webhookEventTypes } from '@/models/entities/webhook.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchWebhook: { + message: 'No such webhook.', + code: 'NO_SUCH_WEBHOOK', + id: 'fb0fea69-da18-45b1-828d-bd4fd1612518', + }, + }, + +} as const; + +export const paramDef = { + type: 'object', + properties: { + webhookId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', minLength: 1, maxLength: 100 }, + url: { type: 'string', minLength: 1, maxLength: 1024 }, + secret: { type: 'string', minLength: 1, maxLength: 1024 }, + on: { type: 'array', items: { + type: 'string', enum: webhookEventTypes, + } }, + active: { type: 'boolean' }, + }, + required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const webhook = await Webhooks.findOneBy({ + id: ps.webhookId, + userId: user.id, + }); + + if (webhook == null) { + throw new ApiError(meta.errors.noSuchWebhook); + } + + await Webhooks.update(webhook.id, { + name: ps.name, + url: ps.url, + secret: ps.secret, + on: ps.on, + active: ps.active, + }); + + publishInternalEvent('webhookUpdated', webhook); +}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts index 9760709c2..dbf1f6c86 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages.ts @@ -47,14 +47,25 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, - groupId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, markAsRead: { type: 'boolean', default: true }, }, - required: [], + anyOf: [ + { + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + properties: { + groupId: { type: 'string', format: 'misskey:id' }, + }, + required: ['groupId'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export @@ -126,7 +137,5 @@ export default define(meta, paramDef, async (ps, user) => { return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { populateGroup: false, }))); - } else { - throw new Error(); } }); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index 8c1226b0f..405af5ec1 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -67,12 +67,23 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, - groupId: { type: 'string', format: 'misskey:id' }, text: { type: 'string', nullable: true, maxLength: 3000 }, fileId: { type: 'string', format: 'misskey:id' }, }, - required: [], + anyOf: [ + { + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + properties: { + groupId: { type: 'string', format: 'misskey:id' }, + }, + required: ['groupId'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 057d22f33..e1ae282a9 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -169,6 +169,7 @@ export const meta = { host: { type: 'string', optional: false, nullable: true, + description: 'The local host is represented with `null`.', }, url: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts index 0c3a3453f..7e857e673 100644 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ b/packages/backend/src/server/api/endpoints/mute/create.ts @@ -38,7 +38,11 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, - expiresAt: { type: 'integer', nullable: true }, + expiresAt: { + type: 'integer', + nullable: true, + description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.', + }, }, required: ['userId'], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts index 96657f8d3..2733c826e 100644 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ b/packages/backend/src/server/api/endpoints/notes.ts @@ -19,7 +19,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { - local: { type: 'boolean' }, + local: { type: 'boolean', default: false }, reply: { type: 'boolean' }, renote: { type: 'boolean' }, withFiles: { type: 'boolean' }, @@ -52,19 +52,19 @@ export default define(meta, paramDef, async (ps) => { query.andWhere('note.userHost IS NULL'); } - if (ps.reply != undefined) { + if (ps.reply !== undefined) { query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); } - if (ps.renote != undefined) { + if (ps.renote !== undefined) { query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); } - if (ps.withFiles != undefined) { + if (ps.withFiles !== undefined) { query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); } - if (ps.poll != undefined) { + if (ps.poll !== undefined) { query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); } diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 8f5d21db6..b991a495f 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -57,7 +57,7 @@ export default define(meta, paramDef, async (ps, user) => { conversation.push(p); } - if (conversation.length == ps.limit) { + if (conversation.length === ps.limit) { return; } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 961983f5f..40a3ba73c 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -1,14 +1,15 @@ import ms from 'ms'; +import { In } from 'typeorm'; import create from '@/services/note/create.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; import { User } from '@/models/entities/user.js'; import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { Note } from '@/models/entities/note.js'; -import { noteVisibilities } from '../../../../types.js'; import { Channel } from '@/models/entities/channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { noteVisibilities } from '../../../../types.js'; +import { ApiError } from '../../error.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], @@ -59,12 +60,6 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, - contentRequired: { - message: 'Content required. You need to set text, fileIds, renoteId or poll.', - code: 'CONTENT_REQUIRED', - id: '6f57e42b-c348-439b-bc45-993995cc515a', - }, - cannotCreateAlreadyExpiredPoll: { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', @@ -88,33 +83,45 @@ export const meta = { export const paramDef = { type: 'object', properties: { - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: "public" }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - text: { type: 'string', nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH, default: null }, + text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, cw: { type: 'string', nullable: true, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, - fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: { - type: 'string', format: 'misskey:id', - } }, - mediaIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: { - type: 'string', format: 'misskey:id', - } }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + deprecated: true, + description: 'Use `fileIds` instead. If both are specified, this property is discarded.', + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, poll: { - type: 'object', nullable: true, + type: 'object', + nullable: true, properties: { choices: { - type: 'array', uniqueItems: true, minItems: 2, maxItems: 10, - items: { - type: 'string', minLength: 1, maxLength: 50, - }, + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, }, multiple: { type: 'boolean', default: false }, expiresAt: { type: 'integer', nullable: true }, @@ -123,36 +130,62 @@ export const paramDef = { required: ['choices'], }, }, - required: [], + anyOf: [ + { + // (re)note with text, files and poll are optional + properties: { + text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false }, + }, + required: ['text'], + }, + { + // (re)note with files, text and poll are optional + required: ['fileIds'], + }, + { + // (re)note with files, text and poll are optional + required: ['mediaIds'], + }, + { + // (re)note with poll, text and files are optional + properties: { + poll: { type: 'object', nullable: false }, + }, + required: ['poll'], + }, + { + // pure renote + required: ['renoteId'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { let visibleUsers: User[] = []; if (ps.visibleUserIds) { - visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOneBy({ id })))) - .filter(x => x != null) as User[]; + visibleUsers = await Users.findBy({ + id: In(ps.visibleUserIds), + }); } let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - files = (await Promise.all(fileIds.map(fileId => - DriveFiles.findOneBy({ - id: fileId, - userId: user.id, - }) - ))).filter(file => file != null) as DriveFile[]; + files = await DriveFiles.findBy({ + userId: user.id, + id: In(fileIds), + }); } - let renote: Note | null; + let renote: Note | null = null; if (ps.renoteId != null) { // Fetch renote to note renote = await Notes.findOneBy({ id: ps.renoteId }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (renote.renoteId && !renote.text && !renote.fileIds) { + } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { throw new ApiError(meta.errors.cannotReRenote); } @@ -168,17 +201,14 @@ export default define(meta, paramDef, async (ps, user) => { } } - let reply: Note | null; + let reply: Note | null = null; if (ps.replyId != null) { // Fetch reply reply = await Notes.findOneBy({ id: ps.replyId }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } - - // 返信対象が引用でないRenoteだったらエラー - if (reply.renoteId && !reply.text && !reply.fileIds) { + } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } @@ -204,12 +234,7 @@ export default define(meta, paramDef, async (ps, user) => { } } - // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー - if (!(ps.text || files.length || renote || ps.poll)) { - throw new ApiError(meta.errors.contentRequired); - } - - let channel: Channel | undefined; + let channel: Channel | null = null; if (ps.channelId != null) { channel = await Channels.findOneBy({ id: ps.channelId }); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 09a819466..cb402ecaa 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -35,7 +35,11 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 7c9c12296..f9893527e 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -48,7 +48,11 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index bb0bbe2a2..03edf30b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -37,7 +37,11 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, fileType: { type: 'array', items: { type: 'string', } }, diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 6380b331f..6244b55cf 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -110,7 +110,7 @@ export default define(meta, paramDef, async (ps, user) => { if (exist.length) { if (poll.multiple) { - if (exist.some(x => x.choice == ps.choice)) { + if (exist.some(x => x.choice === ps.choice)) { throw new ApiError(meta.errors.alreadyVoted); } } else { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 3555424fa..fbb065329 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -1,8 +1,8 @@ +import { DeepPartial, FindOptionsWhere } from 'typeorm'; +import { NoteReactions } from '@/models/index.js'; +import { NoteReaction } from '@/models/entities/note-reaction.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; -import { NoteReactions } from '@/models/index.js'; -import { DeepPartial } from 'typeorm'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; export const meta = { tags: ['notes', 'reactions'], @@ -45,7 +45,7 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = { noteId: ps.noteId, - } as DeepPartial; + } as FindOptionsWhere; if (ps.type) { // ローカルリアクションはホスト名が . とされているが diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index c6503eb05..bb85c9200 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -25,21 +25,44 @@ export const meta = { export const paramDef = { type: 'object', properties: { - tag: { type: 'string' }, - query: { type: 'array', items: { - type: 'array', items: { - type: 'string', - }, - } }, reply: { type: 'boolean', nullable: true, default: null }, renote: { type: 'boolean', nullable: true, default: null }, - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, poll: { type: 'boolean', nullable: true, default: null }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, - required: [], + anyOf: [ + { + properties: { + tag: { type: 'string', minLength: 1 }, + }, + required: ['tag'], + }, + { + properties: { + query: { + type: 'array', + description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.', + items: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + }, + minItems: 1, + }, + }, + required: ['query'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index e77892b15..af9b5f0a1 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -35,7 +35,11 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - host: { type: 'string', nullable: true }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, }, diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index fde66b241..0f976d18b 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -38,7 +38,11 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 068df6940..5e40e7106 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -1,12 +1,12 @@ -import define from '../../define.js'; -import { getNote } from '../../common/getters.js'; -import { ApiError } from '../../error.js'; +import { URLSearchParams } from 'node:url'; import fetch from 'node-fetch'; import config from '@/config/index.js'; import { getAgentByUrl } from '@/misc/fetch.js'; -import { URLSearchParams } from 'node:url'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Notes } from '@/models/index.js'; +import { ApiError } from '../../error.js'; +import { getNote } from '../../common/getters.js'; +import define from '../../define.js'; export const meta = { tags: ['notes'], @@ -75,11 +75,17 @@ export default define(meta, paramDef, async (ps, user) => { Accept: 'application/json, */*', }, body: params, - timeout: 10000, + // TODO + //timeout: 10000, agent: getAgentByUrl, }); - const json = await res.json(); + const json = (await res.json()) as { + translations: { + detected_source_language: string; + text: string; + }[]; + }; return { sourceLang: json.translations[0].detected_source_language, diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 866e306d8..fd4a87903 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -42,7 +42,11 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { type: 'boolean' }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, }, required: ['listId'], } as const; @@ -59,12 +63,8 @@ export default define(meta, paramDef, async (ps, user) => { } //#region Construct query - const listQuery = UserListJoinings.createQueryBuilder('joining') - .select('joining.userId') - .where('joining.userListId = :userListId', { userListId: list.id }); - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere(`note.userId IN (${ listQuery.getQuery() })`) + .innerJoin(UserListJoinings.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') @@ -76,7 +76,7 @@ export default define(meta, paramDef, async (ps, user) => { .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') - .setParameters(listQuery.getParameters()); + .andWhere('userListJoining.userListId = :userListId', { userListId: list.id }); generateVisibilityQuery(query, user); diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts index abefe07be..4575cba43 100644 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,4 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; +import { pushNotification } from '@/services/push-notification.js'; import define from '../../define.js'; import { Notifications } from '@/models/index.js'; @@ -28,4 +29,5 @@ export default define(meta, paramDef, async (ps, user) => { // 全ての通知を読みましたよというイベントを発行 publishMainStream(user.id, 'readAllNotifications'); + pushNotification(user.id, 'readAllNotifications', undefined); }); diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts index c7bc5dc0a..65e96d486 100644 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ b/packages/backend/src/server/api/endpoints/notifications/read.ts @@ -1,10 +1,12 @@ -import { publishMainStream } from '@/services/stream.js'; import define from '../../define.js'; -import { Notifications } from '@/models/index.js'; import { readNotification } from '../../common/read-notification.js'; -import { ApiError } from '../../error.js'; export const meta = { + desc: { + 'ja-JP': '通知を既読にします。', + 'en-US': 'Mark a notification as read.' + }, + tags: ['notifications', 'account'], requireCredential: true, @@ -21,23 +23,26 @@ export const meta = { } as const; export const paramDef = { - type: 'object', - properties: { - notificationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['notificationId'], + oneOf: [ + { + type: 'object', + properties: { + notificationId: { type: 'string', format: 'misskey:id' }, + }, + required: ['notificationId'], + }, + { + type: 'object', + properties: { + notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } }, + }, + required: ['notificationIds'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const notification = await Notifications.findOneBy({ - notifieeId: user.id, - id: ps.notificationId, - }); - - if (notification == null) { - throw new ApiError(meta.errors.noSuchNotification); - } - - readNotification(user.id, [notification.id]); + if ('notificationId' in ps) return readNotification(user.id, [ps.notificationId]); + return readNotification(user.id, ps.notificationIds); }); diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 1c218acfd..5d37e86b9 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -1,8 +1,8 @@ -import define from '../../define.js'; -import { ApiError } from '../../error.js'; +import { IsNull } from 'typeorm'; import { Pages, Users } from '@/models/index.js'; import { Page } from '@/models/entities/page.js'; -import { IsNull } from 'typeorm'; +import define from '../../define.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['pages'], @@ -26,17 +26,26 @@ export const meta = { export const paramDef = { type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - name: { type: 'string' }, - username: { type: 'string' }, - }, - required: [], + anyOf: [ + { + properties: { + pageId: { type: 'string', format: 'misskey:id' }, + }, + required: ['pageId'], + }, + { + properties: { + name: { type: 'string' }, + username: { type: 'string' }, + }, + required: ['name', 'username'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - let page: Page | undefined; + let page: Page | null = null; if (ps.pageId) { page = await Pages.findOneBy({ id: ps.pageId }); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 5de624312..26b1f20df 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -38,14 +38,29 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, - username: { type: 'string' }, - host: { type: 'string', nullable: true }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, - required: [], + anyOf: [ + { + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + properties: { + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, + required: ['username', 'host'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 55460f7c6..42cf5216e 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -38,14 +38,29 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, - username: { type: 'string' }, - host: { type: 'string', nullable: true }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, }, - required: [], + anyOf: [ + { + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + properties: { + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, + required: ['username', 'host'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 897b5de3f..f74d80e2a 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -28,7 +28,10 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, detail: { type: 'boolean', default: true }, }, - required: [], + anyOf: [ + { required: ['username'] }, + { required: ['host'] }, + ], } as const; // TODO: avatar,bannerをJOINしたいけどエラーになる diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 775a4b29f..183ff1b8b 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -23,9 +23,9 @@ export const meta = { items: { type: 'object', ref: 'UserDetailed', - } + }, }, - ] + ], }, errors: { @@ -46,15 +46,33 @@ export const meta = { export const paramDef = { type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - userIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - username: { type: 'string' }, - host: { type: 'string', nullable: true }, - }, - required: [], + anyOf: [ + { + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], + }, + { + properties: { + userIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + }, + required: ['userIds'], + }, + { + properties: { + username: { type: 'string' }, + host: { + type: 'string', + nullable: true, + description: 'The local host is represented with `null`.', + }, + }, + required: ['username'], + }, + ], } as const; // eslint-disable-next-line import/no-default-export diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 3f7118ad2..7b66657ad 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -24,17 +24,17 @@ export default async (ctx: Koa.Context) => { ctx.body = { error }; } - if (typeof username != 'string') { + if (typeof username !== 'string') { ctx.status = 400; return; } - if (typeof password != 'string') { + if (typeof password !== 'string') { ctx.status = 400; return; } - if (token != null && typeof token != 'string') { + if (token != null && typeof token !== 'string') { ctx.status = 400; return; } diff --git a/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts index 04197574c..97cbcbecd 100644 --- a/packages/backend/src/server/api/service/discord.ts +++ b/packages/backend/src/server/api/service/discord.ts @@ -1,16 +1,16 @@ import Koa from 'koa'; import Router from '@koa/router'; -import { getJson } from '@/misc/fetch.js'; import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { getJson } from '@/misc/fetch.js'; import config from '@/config/index.js'; import { publishMainStream } from '@/services/stream.js'; -import { redisClient } from '../../../db/redis.js'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import { redisClient } from '../../../db/redis.js'; +import signin from '../common/signin.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -54,7 +54,7 @@ router.get('/disconnect/discord', async ctx => { integrations: profile.integrations, }); - ctx.body = `Discordの連携を解除しました :v:`; + ctx.body = 'Discordの連携を解除しました :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -140,7 +140,7 @@ router.get('/dc/cb', async ctx => { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -174,17 +174,17 @@ router.get('/dc/cb', async ctx => { } })); - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { 'Authorization': `Bearer ${accessToken}`, - }); + })) as Record; - if (!id || !username || !discriminator) { + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { ctx.throw(400, 'invalid session'); return; } const profile = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'discord'->>'id' = :id`, { id: id }) + .where('"integrations"->\'discord\'->>\'id\' = :id', { id: id }) .andWhere('"userHost" IS NULL') .getOne(); @@ -211,7 +211,7 @@ router.get('/dc/cb', async ctx => { } else { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -245,10 +245,10 @@ router.get('/dc/cb', async ctx => { } })); - const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { + const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { 'Authorization': `Bearer ${accessToken}`, - }); - if (!id || !username || !discriminator) { + })) as Record; + if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts index 61bb768a6..04dbd1f7a 100644 --- a/packages/backend/src/server/api/service/github.ts +++ b/packages/backend/src/server/api/service/github.ts @@ -1,16 +1,16 @@ import Koa from 'koa'; import Router from '@koa/router'; -import { getJson } from '@/misc/fetch.js'; import { OAuth2 } from 'oauth'; +import { v4 as uuid } from 'uuid'; +import { IsNull } from 'typeorm'; +import { getJson } from '@/misc/fetch.js'; import config from '@/config/index.js'; import { publishMainStream } from '@/services/stream.js'; -import { redisClient } from '../../../db/redis.js'; -import { v4 as uuid } from 'uuid'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import { redisClient } from '../../../db/redis.js'; +import signin from '../common/signin.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -54,7 +54,7 @@ router.get('/disconnect/github', async ctx => { integrations: profile.integrations, }); - ctx.body = `GitHubの連携を解除しました :v:`; + ctx.body = 'GitHubの連携を解除しました :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -138,7 +138,7 @@ router.get('/gh/cb', async ctx => { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -167,16 +167,16 @@ router.get('/gh/cb', async ctx => { } })); - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { 'Authorization': `bearer ${accessToken}`, - }); - if (!login || !id) { + })) as Record; + if (typeof login !== 'string' || typeof id !== 'string') { ctx.throw(400, 'invalid session'); return; } const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'github'->>'id' = :id`, { id: id }) + .where('"integrations"->\'github\'->>\'id\' = :id', { id: id }) .andWhere('"userHost" IS NULL') .getOne(); @@ -189,7 +189,7 @@ router.get('/gh/cb', async ctx => { } else { const code = ctx.query.code; - if (!code) { + if (!code || typeof code !== 'string') { ctx.throw(400, 'invalid session'); return; } @@ -219,11 +219,11 @@ router.get('/gh/cb', async ctx => { } })); - const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { + const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { 'Authorization': `bearer ${accessToken}`, - }); + })) as Record; - if (!login || !id) { + if (typeof login !== 'string' || typeof id !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/service/twitter.ts b/packages/backend/src/server/api/service/twitter.ts index e72b71e2f..2b4f9f6da 100644 --- a/packages/backend/src/server/api/service/twitter.ts +++ b/packages/backend/src/server/api/service/twitter.ts @@ -2,14 +2,14 @@ import Koa from 'koa'; import Router from '@koa/router'; import { v4 as uuid } from 'uuid'; import autwh from 'autwh'; -import { redisClient } from '../../../db/redis.js'; +import { IsNull } from 'typeorm'; import { publishMainStream } from '@/services/stream.js'; import config from '@/config/index.js'; -import signin from '../common/signin.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, UserProfiles } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; -import { IsNull } from 'typeorm'; +import signin from '../common/signin.js'; +import { redisClient } from '../../../db/redis.js'; function getUserToken(ctx: Koa.BaseContext): string | null { return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1]; @@ -53,7 +53,7 @@ router.get('/disconnect/twitter', async ctx => { integrations: profile.integrations, }); - ctx.body = `Twitterの連携を解除しました :v:`; + ctx.body = 'Twitterの連携を解除しました :v:'; // Publish i updated event publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { @@ -132,10 +132,16 @@ router.get('/tw/cb', async ctx => { const twCtx = await get; - const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier); + const verifier = ctx.query.oauth_verifier; + if (!verifier || typeof verifier !== 'string') { + ctx.throw(400, 'invalid session'); + return; + } + + const result = await twAuth!.done(JSON.parse(twCtx), verifier); const link = await UserProfiles.createQueryBuilder() - .where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId }) + .where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId }) .andWhere('"userHost" IS NULL') .getOne(); @@ -148,7 +154,7 @@ router.get('/tw/cb', async ctx => { } else { const verifier = ctx.query.oauth_verifier; - if (verifier == null) { + if (!verifier || typeof verifier !== 'string') { ctx.throw(400, 'invalid session'); return; } diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index 043d03ab8..b67600474 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Channel from '../channel.js'; -const ev = new Xev.default(); +const ev = new Xev(); export default class extends Channel { public readonly chName = 'queueStats'; diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index 0da189576..db75a6fa3 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -1,7 +1,7 @@ -import { default as Xev } from 'xev'; +import Xev from 'xev'; import Channel from '../channel.js'; -const ev = new Xev.default(); +const ev = new Xev(); export default class extends Channel { public readonly chName = 'serverStats'; diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts index b80347828..2d23145f1 100644 --- a/packages/backend/src/server/api/stream/index.ts +++ b/packages/backend/src/server/api/stream/index.ts @@ -1,27 +1,25 @@ -import * as websocket from 'websocket'; -import { readNotification } from '../common/read-notification.js'; -import call from '../call.js'; -import readNote from '@/services/note/read.js'; -import Channel from './channel.js'; -import channels from './channels/index.js'; import { EventEmitter } from 'events'; +import * as websocket from 'websocket'; +import readNote from '@/services/note/read.js'; import { User } from '@/models/entities/user.js'; import { Channel as ChannelModel } from '@/models/entities/channel.js'; import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; -import { ApiError } from '../error.js'; import { AccessToken } from '@/models/entities/access-token.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; import { UserGroup } from '@/models/entities/user-group.js'; -import { StreamEventEmitter, StreamMessages } from './types.js'; import { Packed } from '@/misc/schema.js'; +import { readNotification } from '../common/read-notification.js'; +import channels from './channels/index.js'; +import Channel from './channel.js'; +import { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection */ export default class Connection { public user?: User; - public userProfile?: UserProfile; + public userProfile?: UserProfile | null; public following: Set = new Set(); public muting: Set = new Set(); public blocking: Set = new Set(); // "被"blocking @@ -84,7 +82,7 @@ export default class Connection { this.muting.delete(data.body.id); break; - // TODO: block events + // TODO: block events case 'followChannel': this.followingChannels.add(data.body.id); @@ -126,7 +124,6 @@ export default class Connection { const { type, body } = obj; switch (type) { - case 'api': this.onApiRequest(body); break; case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; case 's': this.onSubscribeNote(body); break; // alias @@ -183,31 +180,6 @@ export default class Connection { } } - /** - * APIリクエスト要求時 - */ - private async onApiRequest(payload: any) { - // 新鮮なデータを利用するためにユーザーをフェッチ - const user = this.user ? await Users.findOneBy({ id: this.user.id }) : null; - - const endpoint = payload.endpoint || payload.ep; // alias - - // 呼び出し - call(endpoint, user, this.token, payload.data).then(res => { - this.sendMessageToWs(`api:${payload.id}`, { res }); - }).catch((e: ApiError) => { - this.sendMessageToWs(`api:${payload.id}`, { - error: { - message: e.message, - code: e.code, - id: e.id, - kind: e.kind, - ...(e.info ? { info: e.info } : {}), - }, - }); - }); - } - private onReadNotification(payload: any) { if (!payload.id) return; readNotification(this.user!.id, [payload.id]); diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index bea863eb7..3b0a75d79 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -15,6 +15,7 @@ import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { Signin } from '@/models/entities/signin.js'; import { Page } from '@/models/entities/page.js'; import { Packed } from '@/misc/schema.js'; +import { Webhook } from '@/models/entities/webhook'; //#region Stream type-body definitions export interface InternalStreamTypes { @@ -23,6 +24,9 @@ export interface InternalStreamTypes { userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; }; userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; }; remoteUserUpdated: { id: User['id']; }; + webhookCreated: Webhook; + webhookDeleted: Webhook; + webhookUpdated: Webhook; antennaCreated: Antenna; antennaDeleted: Antenna; antennaUpdated: Antenna; diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index 2a34edac6..f8e42d27f 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -1,4 +1,4 @@ -import * as http from 'http'; +import * as http from 'node:http'; import * as websocket from 'websocket'; import MainStreamConnection from './stream/index.js'; diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts index 6bc220b36..027d078ce 100644 --- a/packages/backend/src/server/file/send-drive-file.ts +++ b/packages/backend/src/server/file/send-drive-file.ts @@ -11,7 +11,7 @@ import { DriveFiles } from '@/models/index.js'; import { InternalStorage } from '@/services/drive/internal-storage.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; -import { convertToJpeg, convertToPng, convertToPngOrJpeg } from '@/services/drive/image-processor.js'; +import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js'; import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail.js'; import { StatusError } from '@/misc/fetch.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; @@ -64,10 +64,8 @@ export default async function(ctx: Koa.Context) { const convertFile = async () => { if (isThumbnail) { - if (['image/jpeg', 'image/webp'].includes(mime)) { - return await convertToJpeg(path, 498, 280); - } else if (['image/png', 'image/svg+xml'].includes(mime)) { - return await convertToPngOrJpeg(path, 498, 280); + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(mime)) { + return await convertToWebp(path, 498, 280); } else if (mime.startsWith('video/')) { return await GenerateVideoThumbnail(path); } diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts index a68cebfeb..c1a2a6dff 100644 --- a/packages/backend/src/server/index.ts +++ b/packages/backend/src/server/index.ts @@ -3,30 +3,30 @@ */ import * as fs from 'node:fs'; -import * as http from 'http'; +import * as http from 'node:http'; import Koa from 'koa'; import Router from '@koa/router'; import mount from 'koa-mount'; import koaLogger from 'koa-logger'; import * as slow from 'koa-slow'; -import activityPub from './activitypub.js'; -import nodeinfo from './nodeinfo.js'; -import wellKnown from './well-known.js'; +import { IsNull } from 'typeorm'; import config from '@/config/index.js'; -import apiServer from './api/index.js'; -import fileServer from './file/index.js'; -import proxyServer from './proxy/index.js'; -import webServer from './web/index.js'; import Logger from '@/services/logger.js'; -import { envOption } from '../env.js'; import { UserProfiles, Users } from '@/models/index.js'; import { genIdenticon } from '@/misc/gen-identicon.js'; import { createTemp } from '@/misc/create-temp.js'; import { publishMainStream } from '@/services/stream.js'; import * as Acct from '@/misc/acct.js'; +import { envOption } from '../env.js'; +import activityPub from './activitypub.js'; +import nodeinfo from './nodeinfo.js'; +import wellKnown from './well-known.js'; +import apiServer from './api/index.js'; +import fileServer from './file/index.js'; +import proxyServer from './proxy/index.js'; +import webServer from './web/index.js'; import { initializeStreamingServer } from './api/streaming.js'; -import { IsNull } from 'typeorm'; export const serverLogger = new Logger('server', 'gray', false); @@ -81,7 +81,7 @@ router.get('/avatar/@:acct', async ctx => { }); if (user) { - ctx.redirect(Users.getAvatarUrl(user)); + ctx.redirect(Users.getAvatarUrlSync(user)); } else { ctx.redirect('/static-assets/user-unknown.png'); } diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts index 3cc5b827a..48887bf12 100644 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ b/packages/backend/src/server/proxy/proxy-media.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import Koa from 'koa'; import { serverLogger } from '../index.js'; -import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-processor.js'; +import { IImage, convertToWebp } from '@/services/drive/image-processor.js'; import { createTemp } from '@/misc/create-temp.js'; import { downloadUrl } from '@/misc/download-url.js'; import { detectType } from '@/misc/get-file-info.js'; @@ -27,11 +27,11 @@ export async function proxyMedia(ctx: Koa.Context) { let image: IImage; if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) { - image = await convertToPng(path, 498, 280); + image = await convertToWebp(path, 498, 280); } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) { - image = await convertToJpeg(path, 200, 200); + image = await convertToWebp(path, 200, 200); } else if (['image/svg+xml'].includes(mime)) { - image = await convertToPng(path, 2048, 2048); + image = await convertToWebp(path, 2048, 2048, 1); } else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { throw new StatusError('Rejected type', 403, 'Rejected type'); } else { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 751e8619b..a9ee0df4f 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -58,15 +58,11 @@ ? `?salt=${localStorage.getItem('salt')}` : ''; - const script = document.createElement('script'); - script.setAttribute('src', `/assets/app.${v}.js${salt}`); - script.setAttribute('async', 'true'); - script.setAttribute('defer', 'true'); - script.addEventListener('error', async () => { - await checkUpdate(); - renderError('APP_FETCH_FAILED'); - }); - document.head.appendChild(script); + import(`/assets/${CLIENT_ENTRY}${salt}`) + .catch(async () => { + await checkUpdate(); + renderError('APP_FETCH_FAILED'); + }) //#endregion //#region Theme diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts index eba8dc58d..4abe2885c 100644 --- a/packages/backend/src/server/web/feed.ts +++ b/packages/backend/src/server/web/feed.ts @@ -1,8 +1,8 @@ import { Feed } from 'feed'; +import { In, IsNull } from 'typeorm'; import config from '@/config/index.js'; import { User } from '@/models/entities/user.js'; -import { Notes, DriveFiles, UserProfiles } from '@/models/index.js'; -import { In, IsNull } from 'typeorm'; +import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js'; export default async function(user: User) { const author = { @@ -29,7 +29,7 @@ export default async function(user: User) { generator: 'Misskey', description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, - image: user.avatarUrl ? user.avatarUrl : undefined, + image: await Users.getAvatarUrl(user), feedLinks: { json: `${author.link}.json`, atom: `${author.link}.atom`, diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 48bf6f733..9e31f2389 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -4,6 +4,7 @@ import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { PathOrFileDescriptor, readFileSync } from 'node:fs'; import ms from 'ms'; import Koa from 'koa'; import Router from '@koa/router'; @@ -11,20 +12,20 @@ import send from 'koa-send'; import favicon from 'koa-favicon'; import views from 'koa-views'; import { createBullBoard } from '@bull-board/api'; -import { BullAdapter } from '@bull-board/api/bullAdapter.js'; +import { BullAdapter } from '@bull-board/api/bullAdapter.js'; import { KoaAdapter } from '@bull-board/koa'; -import packFeed from './feed.js'; +import { In, IsNull } from 'typeorm'; import { fetchMeta } from '@/misc/fetch-meta.js'; -import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; import config from '@/config/index.js'; import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js'; import * as Acct from '@/misc/acct.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; +import { queues } from '@/queue/queues.js'; +import { genOpenapiSpec } from '../api/openapi/gen-spec.js'; import { urlPreviewHandler } from './url-preview.js'; import { manifestHandler } from './manifest.js'; -import { queues } from '@/queue/queues.js'; -import { IsNull } from 'typeorm'; +import packFeed from './feed.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -32,6 +33,7 @@ const _dirname = dirname(_filename); const staticAssets = `${_dirname}/../../../assets/`; const clientAssets = `${_dirname}/../../../../client/assets/`; const assets = `${_dirname}/../../../../../built/_client_dist_/`; +const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; // Init app const app = new Koa(); @@ -72,6 +74,9 @@ app.use(views(_dirname + '/views', { extension: 'pug', options: { version: config.version, + clientEntry: () => process.env.NODE_ENV === 'production' ? + config.clientEntry : + JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''), config, }, })); @@ -127,7 +132,7 @@ router.get('/twemoji/(.*)', async ctx => { return; } - ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); + ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); await send(ctx as any, path, { root: `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, @@ -136,9 +141,10 @@ router.get('/twemoji/(.*)', async ctx => { }); // ServiceWorker -router.get('/sw.js', async ctx => { - await send(ctx as any, `/sw.${config.version}.js`, { - root: assets, +router.get(`/sw.js`, async ctx => { + await send(ctx as any, `/sw.js`, { + root: swAssets, + maxage: ms('10 minutes'), }); }); @@ -235,6 +241,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { await ctx.render('user', { user, profile, me, + avatarUrl: await Users.getAvatarUrl(user), sub: ctx.params.sub, instanceName: meta.name || 'Misskey', icon: meta.iconUrl, @@ -265,7 +272,10 @@ router.get('/users/:user', async ctx => { // Note router.get('/notes/:note', async (ctx, next) => { - const note = await Notes.findOneBy({ id: ctx.params.note }); + const note = await Notes.findOneBy({ + id: ctx.params.note, + visibility: In(['public', 'home']), + }); if (note) { const _note = await Notes.pack(note); @@ -274,6 +284,7 @@ router.get('/notes/:note', async (ctx, next) => { await ctx.render('note', { note: _note, profile, + avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })), // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), instanceName: meta.name || 'Misskey', @@ -281,11 +292,7 @@ router.get('/notes/:note', async (ctx, next) => { themeColor: meta.themeColor, }); - if (['public', 'home'].includes(note.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); - } else { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - } + ctx.set('Cache-Control', 'public, max-age=180'); return; } @@ -315,6 +322,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { await ctx.render('page', { page: _page, profile, + avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: page.userId })), instanceName: meta.name || 'Misskey', icon: meta.iconUrl, themeColor: meta.themeColor, @@ -346,6 +354,7 @@ router.get('/clips/:clip', async (ctx, next) => { await ctx.render('clip', { clip: _clip, profile, + avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: clip.userId })), instanceName: meta.name || 'Misskey', icon: meta.iconUrl, themeColor: meta.themeColor, @@ -370,6 +379,7 @@ router.get('/gallery/:post', async (ctx, next) => { await ctx.render('gallery-post', { post: _post, profile, + avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: post.userId })), instanceName: meta.name || 'Misskey', icon: meta.iconUrl, themeColor: meta.themeColor, @@ -434,7 +444,7 @@ router.get('/cli', async ctx => { }); }); -const override = (source: string, target: string, depth: number = 0) => +const override = (source: string, target: string, depth = 0) => [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); router.get('/flush', async ctx => { diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts index bcbf9b76a..61d766006 100644 --- a/packages/backend/src/server/web/manifest.ts +++ b/packages/backend/src/server/web/manifest.ts @@ -1,16 +1,16 @@ import Koa from 'koa'; -import manifest from './manifest.json' assert { type: 'json' }; import { fetchMeta } from '@/misc/fetch-meta.js'; +import manifest from './manifest.json' assert { type: 'json' }; export const manifestHandler = async (ctx: Koa.Context) => { - const json = JSON.parse(JSON.stringify(manifest)); + const res = structuredClone(manifest); const instance = await fetchMeta(true); - json.short_name = instance.name || 'Misskey'; - json.name = instance.name || 'Misskey'; - if (instance.themeColor) json.theme_color = instance.themeColor; + res.short_name = instance.name || 'Misskey'; + res.name = instance.name || 'Misskey'; + if (instance.themeColor) res.theme_color = instance.themeColor; ctx.set('Cache-Control', 'max-age=300'); - ctx.body = json; + ctx.body = res; }; diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index 43fbe1ab0..9c4cd4b9b 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -16,7 +16,7 @@ html { transition: opacity 0.5s ease; } -#splash > img { +#splashIcon { position: absolute; top: 0; right: 0; @@ -27,3 +27,48 @@ html { height: 64px; pointer-events: none; } + +#splashSpinner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + display: inline-block; + width: 28px; + height: 28px; + transform: translateY(70px); +} + +#splashSpinner:before, +#splashSpinner:after { + content: " "; + display: block; + box-sizing: border-box; + width: 28px; + height: 28px; + border-radius: 50%; + border: solid 4px; +} + +#splashSpinner:before { + border-color: currentColor; + opacity: 0.3; +} + +#splashSpinner:after { + position: absolute; + top: 0; + border-color: currentColor transparent transparent transparent; + animation: splashSpinner 0.5s linear infinite; +} + +@keyframes splashSpinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts index 6bd8ead5b..1e259649f 100644 --- a/packages/backend/src/server/web/url-preview.ts +++ b/packages/backend/src/server/web/url-preview.ts @@ -56,7 +56,7 @@ export const urlPreviewHandler = async (ctx: Koa.Context) => { function wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) - ? `${config.url}/proxy/preview.jpg?${query({ + ? `${config.url}/proxy/preview.webp?${query({ url, preview: '1', })}` diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index abacb1ccf..d79354d11 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -50,6 +50,10 @@ html style include ../style.css + script. + var VERSION = "#{version}"; + var CLIENT_ENTRY = "#{clientEntry()}"; + script include ../boot.js @@ -59,5 +63,6 @@ html br | Please turn on your JavaScript div#splash - img(src= icon || '/static-assets/splash.png') + img#splashIcon(src= icon || '/static-assets/splash.png') + div#splashSpinner block content diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug index 7a84d50f6..4c692bf59 100644 --- a/packages/backend/src/server/web/views/clip.pug +++ b/packages/backend/src/server/web/views/clip.pug @@ -16,7 +16,7 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= clip.description) meta(property='og:url' content= url) - meta(property='og:image' content= user.avatarUrl) + meta(property='og:image' content= avatarUrl) block meta if profile.noCrawle diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 34b03f983..65696ea13 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -17,7 +17,7 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= summary) meta(property='og:url' content= url) - meta(property='og:image' content= user.avatarUrl) + meta(property='og:image' content= avatarUrl) block meta if user.host || isRenote || profile.noCrawle diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index b6c954802..4219e76a5 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -16,7 +16,7 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= page.summary) meta(property='og:url' content= url) - meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : user.avatarUrl) + meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl) block meta if profile.noCrawle diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index 2adec0f88..119993fdb 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -3,7 +3,6 @@ extends ./base block vars - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; - - const img = user.avatarUrl || null; block title = `${title} | ${instanceName}` @@ -16,7 +15,7 @@ block og meta(property='og:title' content= title) meta(property='og:description' content= profile.description) meta(property='og:url' content= url) - meta(property='og:image' content= img) + meta(property='og:image' content= avatarUrl) block meta if user.host || profile.noCrawle diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts index 7530b4e0b..1d094f2ed 100644 --- a/packages/backend/src/server/well-known.ts +++ b/packages/backend/src/server/well-known.ts @@ -41,6 +41,7 @@ router.options(allPath, async ctx => { router.get('/.well-known/host-meta', async ctx => { ctx.set('Content-Type', xrd); ctx.body = XRD({ element: 'Link', attributes: { + rel: 'lrdd', type: xrd, template: `${config.url}${webFingerPath}?resource={uri}`, } }); diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts index 86c7d7967..b2be78b22 100644 --- a/packages/backend/src/services/blocking/create.ts +++ b/packages/backend/src/services/blocking/create.ts @@ -10,6 +10,8 @@ import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLis import { perUserFollowingChart } from '@/services/chart/index.js'; import { genId } from '@/misc/gen-id.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; +import { webhookDeliver } from '@/queue/index.js'; export default async function(blocker: User, blockee: User) { await Promise.all([ @@ -57,9 +59,16 @@ async function cancelRequest(follower: User, followee: User) { if (Users.isLocalUser(follower)) { Users.pack(followee, follower, { detail: true, - }).then(packed => { + }).then(async packed => { publishUserEvent(follower.id, 'unfollow', packed); publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } }); } @@ -86,25 +95,27 @@ async function unFollow(follower: User, followee: User) { return; } - Followings.delete(following.id); - - //#region Decrement following count - Users.decrement({ id: follower.id }, 'followingCount', 1); - //#endregion - - //#region Decrement followers count - Users.decrement({ id: followee.id }, 'followersCount', 1); - //#endregion - - perUserFollowingChart.update(follower, followee, false); + await Promise.all([ + Followings.delete(following.id), + Users.decrement({ id: follower.id }, 'followingCount', 1), + Users.decrement({ id: followee.id }, 'followersCount', 1), + perUserFollowingChart.update(follower, followee, false), + ]); // Publish unfollow event if (Users.isLocalUser(follower)) { Users.pack(followee, follower, { detail: true, - }).then(packed => { + }).then(async packed => { publishUserEvent(follower.id, 'unfollow', packed); publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } }); } diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts index 9a53db1f3..d53a4235b 100644 --- a/packages/backend/src/services/create-notification.ts +++ b/packages/backend/src/services/create-notification.ts @@ -1,5 +1,5 @@ import { publishMainStream } from '@/services/stream.js'; -import pushSw from './push-notification.js'; +import { pushNotification } from '@/services/push-notification.js'; import { Notifications, Mutings, UserProfiles, Users } from '@/models/index.js'; import { genId } from '@/misc/gen-id.js'; import { User } from '@/models/entities/user.js'; @@ -52,8 +52,8 @@ export async function createNotification( //#endregion publishMainStream(notifieeId, 'unreadNotification', packed); + pushNotification(notifieeId, 'notification', packed); - pushSw(notifieeId, 'notification', packed); if (type === 'follow') sendEmailNotification.follow(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); if (type === 'receiveFollowRequest') sendEmailNotification.receiveFollowRequest(notifieeId, await Users.findOneByOrFail({ id: data.notifierId! })); }, 2000); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 549b11c9f..cfbcb60dd 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -7,7 +7,7 @@ import { deleteFile } from './delete-file.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; import { driveLogger } from './logger.js'; -import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng, convertSharpToPngOrJpeg } from './image-processor.js'; +import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; import { contentDisposition } from '@/misc/content-disposition.js'; import { getFileInfo } from '@/misc/get-file-info.js'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; @@ -179,6 +179,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool } let img: sharp.Sharp | null = null; + let satisfyWebpublic: boolean; try { img = sharp(path); @@ -192,6 +193,13 @@ export async function generateAlts(path: string, type: string, generateWeb: bool thumbnail: null, }; } + + satisfyWebpublic = !!( + type !== 'image/svg+xml' && type !== 'image/webp' && + !(metadata.exif || metadata.iptc || metadata.xmp || metadata.tifftagPhotoshop) && + metadata.width && metadata.width <= 2048 && + metadata.height && metadata.height <= 2048 + ); } catch (err) { logger.warn(`sharp failed: ${err}`); return { @@ -203,15 +211,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool // #region webpublic let webpublic: IImage | null = null; - if (generateWeb) { + if (generateWeb && !satisfyWebpublic) { logger.info(`creating web image`); try { - if (['image/jpeg'].includes(type)) { + if (['image/jpeg', 'image/webp'].includes(type)) { webpublic = await convertSharpToJpeg(img, 2048, 2048); - } else if (['image/webp'].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048); - } else if (['image/png', 'image/svg+xml'].includes(type)) { + } else if (['image/png'].includes(type)) { + webpublic = await convertSharpToPng(img, 2048, 2048); + } else if (['image/svg+xml'].includes(type)) { webpublic = await convertSharpToPng(img, 2048, 2048); } else { logger.debug(`web image not created (not an required image)`); @@ -220,7 +228,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool logger.warn(`web image not created (an error occured)`, err as Error); } } else { - logger.info(`web image not created (from remote)`); + if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`); + else logger.info(`web image not created (from remote)`); } // #endregion webpublic @@ -228,10 +237,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool let thumbnail: IImage | null = null; try { - if (['image/jpeg', 'image/webp'].includes(type)) { - thumbnail = await convertSharpToJpeg(img, 498, 280); - } else if (['image/png', 'image/svg+xml'].includes(type)) { - thumbnail = await convertSharpToPngOrJpeg(img, 498, 280); + if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { + thumbnail = await convertSharpToWebp(img, 498, 280); } else { logger.debug(`thumbnail not created (not an required file)`); } diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts index 04a7a8334..da93bc97c 100644 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ b/packages/backend/src/services/drive/generate-video-thumbnail.ts @@ -27,6 +27,7 @@ export async function GenerateVideoThumbnail(path: string): Promise { const outPath = `${outDir}/output.png`; + // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) const thumbnail = await convertToJpeg(outPath, 498, 280); // cleanup diff --git a/packages/backend/src/services/drive/image-processor.ts b/packages/backend/src/services/drive/image-processor.ts index 146dcfb6c..2c564ea59 100644 --- a/packages/backend/src/services/drive/image-processor.ts +++ b/packages/backend/src/services/drive/image-processor.ts @@ -38,11 +38,11 @@ export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, heig * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ -export async function convertToWebp(path: string, width: number, height: number): Promise { - return convertSharpToWebp(await sharp(path), width, height); +export async function convertToWebp(path: string, width: number, height: number, quality: number = 85): Promise { + return convertSharpToWebp(await sharp(path), width, height, quality); } -export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number): Promise { +export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality: number = 85): Promise { const data = await sharp .resize(width, height, { fit: 'inside', @@ -50,7 +50,7 @@ export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, heig }) .rotate() .webp({ - quality: 85, + quality, }) .toBuffer(); @@ -85,23 +85,3 @@ export async function convertSharpToPng(sharp: sharp.Sharp, width: number, heigh type: 'image/png', }; } - -/** - * Convert to PNG or JPEG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToPngOrJpeg(path: string, width: number, height: number): Promise { - return convertSharpToPngOrJpeg(await sharp(path), width, height); -} - -export async function convertSharpToPngOrJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - const stats = await sharp.stats(); - const metadata = await sharp.metadata(); - - // 不透明で300x300pxの範囲を超えていればJPEG - if (stats.isOpaque && ((metadata.width && metadata.width >= 300) || (metadata.height && metadata!.height >= 300))) { - return await convertSharpToJpeg(sharp, width, height); - } else { - return await convertSharpToPng(sharp, width, height); - } -} diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts index 5007fff6e..79b1b8c2e 100644 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ b/packages/backend/src/services/drive/upload-from-url.ts @@ -29,7 +29,7 @@ export async function uploadFromUrl({ sensitive = false, force = false, isLink = false, - comment = null + comment = null, }: Args): Promise { let name = new URL(url).pathname.split('/').pop() || null; if (name == null || !DriveFiles.validateFileName(name)) { @@ -38,7 +38,7 @@ export async function uploadFromUrl({ // If the comment is same as the name, skip comment // (image.name is passed in when receiving attachment) - if (comment !== null && name == comment) { + if (comment !== null && name === comment) { comment = null; } diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts index 2b6f82a91..d5294c5fe 100644 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ b/packages/backend/src/services/fetch-instance-metadata.ts @@ -97,7 +97,7 @@ async function fetchNodeinfo(instance: Instance): Promise { } else { throw e.statusCode || e.message; } - }); + }) as Record; if (wellknown.links == null || !Array.isArray(wellknown.links)) { throw 'No wellknown links'; @@ -121,7 +121,7 @@ async function fetchNodeinfo(instance: Instance): Promise { logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); - return info; + return info as NodeInfo; } catch (e) { logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`); @@ -142,12 +142,12 @@ async function fetchDom(instance: Instance): Promise { return doc; } -async function fetchManifest(instance: Instance): Promise | null> { +async function fetchManifest(instance: Instance): Promise | null> { const url = 'https://' + instance.host; const manifestUrl = url + '/manifest.json'; - const manifest = await getJson(manifestUrl); + const manifest = await getJson(manifestUrl) as Record; return manifest; } @@ -167,7 +167,8 @@ async function fetchFaviconUrl(instance: Instance, doc: DOMWindow['document'] | const faviconUrl = url + '/favicon.ico'; const favicon = await fetch(faviconUrl, { - timeout: 10000, + // TODO + //timeout: 10000, agent: getAgentByUrl, }); diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts index 0daf30dda..72c24676b 100644 --- a/packages/backend/src/services/following/create.ts +++ b/packages/backend/src/services/following/create.ts @@ -15,6 +15,8 @@ import { genId } from '@/misc/gen-id.js'; import { createNotification } from '../create-notification.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { Packed } from '@/misc/schema.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; +import { webhookDeliver } from '@/queue/index.js'; const logger = new Logger('following/create'); @@ -65,8 +67,10 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[ if (alreadyFollowed) return; //#region Increment counts - Users.increment({ id: follower.id }, 'followingCount', 1); - Users.increment({ id: followee.id }, 'followersCount', 1); + await Promise.all([ + Users.increment({ id: follower.id }, 'followingCount', 1), + Users.increment({ id: followee.id }, 'followersCount', 1), + ]); //#endregion //#region Update instance stats @@ -89,15 +93,31 @@ export async function insertFollowingDoc(followee: { id: User['id']; host: User[ if (Users.isLocalUser(follower)) { Users.pack(followee.id, follower, { detail: true, - }).then(packed => { + }).then(async packed => { publishUserEvent(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">); publishMainStream(follower.id, 'follow', packed as Packed<"UserDetailedNotMe">); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'follow', { + user: packed, + }); + } }); } // Publish followed event if (Users.isLocalUser(followee)) { - Users.pack(follower.id, followee).then(packed => publishMainStream(followee.id, 'followed', packed)); + Users.pack(follower.id, followee).then(async packed => { + publishMainStream(followee.id, 'followed', packed); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'followed', { + user: packed, + }); + } + }); // 通知を作成 createNotification(followee.id, 'follow', { diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts index 35fd664b5..91b5a3d61 100644 --- a/packages/backend/src/services/following/delete.ts +++ b/packages/backend/src/services/following/delete.ts @@ -3,12 +3,13 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderUndo from '@/remote/activitypub/renderer/undo.js'; import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { deliver } from '@/queue/index.js'; +import { deliver, webhookDeliver } from '@/queue/index.js'; import Logger from '../logger.js'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { User } from '@/models/entities/user.js'; import { Followings, Users, Instances } from '@/models/index.js'; import { instanceChart, perUserFollowingChart } from '@/services/chart/index.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; const logger = new Logger('following/delete'); @@ -31,9 +32,16 @@ export default async function(follower: { id: User['id']; host: User['host']; ur if (!silent && Users.isLocalUser(follower)) { Users.pack(followee.id, follower, { detail: true, - }).then(packed => { + }).then(async packed => { publishUserEvent(follower.id, 'unfollow', packed); publishMainStream(follower.id, 'unfollow', packed); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'unfollow', { + user: packed, + }); + } }); } @@ -50,12 +58,11 @@ export default async function(follower: { id: User['id']; host: User['host']; ur } export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) { - //#region Decrement following count - Users.decrement({ id: follower.id }, 'followingCount', 1); - //#endregion - - //#region Decrement followers count - Users.decrement({ id: followee.id }, 'followersCount', 1); + //#region Decrement following / followers counts + await Promise.all([ + Users.decrement({ id: follower.id }, 'followingCount', 1), + Users.decrement({ id: followee.id }, 'followersCount', 1), + ]); //#endregion //#region Update instance stats diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts index 2d1db3c34..691fca245 100644 --- a/packages/backend/src/services/following/reject.ts +++ b/packages/backend/src/services/following/reject.ts @@ -1,11 +1,12 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { deliver } from '@/queue/index.js'; +import { deliver, webhookDeliver } from '@/queue/index.js'; import { publishMainStream, publishUserEvent } from '@/services/stream.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { Users, FollowRequests, Followings } from '@/models/index.js'; import { decrementFollowing } from './delete.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; type Local = ILocalUser | { id: ILocalUser['id']; @@ -111,4 +112,11 @@ async function publishUnfollow(followee: Both, follower: Local) { publishUserEvent(follower.id, 'unfollow', packedFollowee); publishMainStream(follower.id, 'unfollow', packedFollowee); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'unfollow', { + user: packedFollowee, + }); + } } diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index e5cd5a30d..e6b320492 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -5,7 +5,7 @@ import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/i import { genId } from '@/misc/gen-id.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; -import pushNotification from '../push-notification.js'; +import { pushNotification } from '@/services/push-notification.js'; import { Not } from 'typeorm'; import { Note } from '@/models/entities/note.js'; import renderNote from '@/remote/activitypub/renderer/note.js'; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 2ed194b7e..ceb5e8cc7 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -35,9 +35,11 @@ import { Channel } from '@/models/entities/channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { getAntennas } from '@/misc/antenna-cache.js'; import { endedPollNotificationQueue } from '@/queue/queues.js'; +import { webhookDeliver } from '@/queue/index.js'; import { Cache } from '@/misc/cache.js'; import { UserProfile } from '@/models/entities/user-profile.js'; import { db } from '@/db/postgre.js'; +import { getActiveWebhooks } from '@/misc/webhook-cache.js'; const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -185,6 +187,8 @@ export default async (user: { id: User['id']; username: User['username']; host: if (data.text) { data.text = data.text.trim(); + } else { + data.text = null; } let tags = data.apHashtags; @@ -345,6 +349,15 @@ export default async (user: { id: User['id']; username: User['username']; host: publishNotesStream(noteObj); + getActiveWebhooks().then(webhooks => { + webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'note', { + note: noteObj, + }); + } + }); + const nm = new NotificationManager(user, note); const nmRelatedPromises = []; @@ -365,6 +378,13 @@ export default async (user: { id: User['id']; username: User['username']; host: if (!threadMuted) { nm.push(data.reply.userId, 'reply'); publishMainStream(data.reply.userId, 'reply', noteObj); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'reply', { + note: noteObj, + }); + } } } } @@ -384,6 +404,13 @@ export default async (user: { id: User['id']; username: User['username']; host: // Publish event if ((user.id !== data.renote.userId) && data.renote.userHost === null) { publishMainStream(data.renote.userId, 'renote', noteObj); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'renote', { + note: noteObj, + }); + } } } @@ -620,6 +647,13 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, publishMainStream(u.id, 'mention', detailPackedNote); + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'mention', { + note: detailPackedNote, + }); + } + // Create notification nm.push(u.id, 'mention'); } diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index ffd609dd8..496320016 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,3 +1,4 @@ +import { Brackets, In } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; @@ -5,15 +6,14 @@ import renderUndo from '@/remote/activitypub/renderer/undo.js'; import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; import config from '@/config/index.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Notes, Users, Instances } from '@/models/index.js'; import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js'; import { countSameRenotes } from '@/misc/count-same-renotes.js'; +import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; import { deliverToRelays } from '../relay.js'; -import { Brackets, In } from 'typeorm'; /** * 投稿を削除します。 @@ -40,7 +40,7 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us //#region ローカルの投稿なら削除アクティビティを配送 if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null; + let renote: Note | null = null; // if deletd note is renote if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { @@ -113,7 +113,7 @@ async function getMentionedRemoteUsers(note: Note) { const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); if (uris.length > 0) { where.push( - { uri: In(uris) } + { uri: In(uris) }, ); } diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts index 41122c92e..5c3bafbb3 100644 --- a/packages/backend/src/services/push-notification.ts +++ b/packages/backend/src/services/push-notification.ts @@ -5,8 +5,15 @@ import { fetchMeta } from '@/misc/fetch-meta.js'; import { Packed } from '@/misc/schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -type notificationType = 'notification' | 'unreadMessagingMessage'; -type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>; +// Defined also packages/sw/types.ts#L14-L21 +type pushNotificationsTypes = { + 'notification': Packed<'Notification'>; + 'unreadMessagingMessage': Packed<'MessagingMessage'>; + 'readNotifications': { notificationIds: string[] }; + 'readAllNotifications': undefined; + 'readAllMessagingMessages': undefined; + 'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string }; +}; // プッシュメッセージサーバーには文字数制限があるため、内容を削減します function truncateNotification(notification: Packed<'Notification'>): any { @@ -17,12 +24,11 @@ function truncateNotification(notification: Packed<'Notification'>): any { ...notification.note, // textをgetNoteSummaryしたものに置き換える text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), - ...{ - cw: undefined, - reply: undefined, - renote: undefined, - user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる - } + + cw: undefined, + reply: undefined, + renote: undefined, + user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる } }; } @@ -30,7 +36,7 @@ function truncateNotification(notification: Packed<'Notification'>): any { return notification; } -export default async function(userId: string, type: notificationType, body: notificationBody) { +export async function pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]) { const meta = await fetchMeta(); if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index 1ab45588d..08bf72cc2 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -88,7 +88,7 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act })); if (relays.length === 0) return; - const copy = JSON.parse(JSON.stringify(activity)); + const copy = structuredClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; const signed = await attachLdSignature(copy, user); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 32a030f93..09e812f43 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -4,7 +4,7 @@ import * as misskey from 'misskey-js'; import fetch from 'node-fetch'; import FormData from 'form-data'; import * as childProcess from 'child_process'; -import * as http from 'http'; +import * as http from 'node:http'; import loadConfig from '../src/config/load.js'; import { SIGKILL } from 'constants'; import { entities } from '../src/db/postgre.js'; diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 3120851aa..22338a497 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -25,9 +25,14 @@ "rootDir": "./src", "baseUrl": "./", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, "outDir": "./built", + "types": [ + "node" + ], "typeRoots": [ "./node_modules/@types", "./src/@types" diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 3d5d38d69..fd91be84a 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -35,20 +35,20 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@bull-board/api@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.10.1.tgz#c9608d501c887abcfa8f1907bc3dedee179bdea3" - integrity sha512-ZYjNBdoBQu+UVbLAHQuEhJL96C+i7vYioc2n7FL/XoVea44XIw2WiKFcFxq0LnActPErja26QyZBQht23ph1lg== +"@bull-board/api@3.10.4": + version "3.10.4" + resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.10.4.tgz#f29d95a9624224ceec0f3ff26ef2c2bba8106921" + integrity sha512-JJjMg8O/ELeaqkuL1Wsdn6rdQfH+/2+BfnFD0B7j4ZCtLVAPfsOUZYpLqSKUgaNizwp1nTw0e3L/EI0yvX5aiw== dependencies: redis-info "^3.0.8" -"@bull-board/koa@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.10.1.tgz#205641ae9721ec71303c4f16dc27eca1f71ca131" - integrity sha512-+mxdnu7idjd75WqUklJbPzrQU6NJzgQCT+BLKCyqOBsWzpfEwaac6QaIXOiuPwgwG2VjH90HWIcWr+2BQB9c1w== +"@bull-board/koa@3.10.4": + version "3.10.4" + resolved "https://registry.yarnpkg.com/@bull-board/koa/-/koa-3.10.4.tgz#8e54600bfd8e003a8d5838ae6e65f9ec8c9979f7" + integrity sha512-NO0kzgVrl5lGNnX6maBAuP6aecGvROGka3RJSALubDfsrQ3aWNuY2BjUMUvm4ZDVfAeYT3wPaak8rdRCwxYE2g== dependencies: - "@bull-board/api" "3.10.1" - "@bull-board/ui" "3.10.1" + "@bull-board/api" "3.10.4" + "@bull-board/ui" "3.10.4" ejs "^3.1.6" koa "^2.13.1" koa-mount "^4.0.0" @@ -56,12 +56,12 @@ koa-static "^5.0.0" koa-views "^7.0.1" -"@bull-board/ui@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.10.1.tgz#edf7c7752a78d9829f7a944bb87a0e70812b749f" - integrity sha512-K2qEAvTuyHZxUdK31HaBb9sdTFSOSKAZkxsl/LeiT4FGNF/h54iYGmWF9+HSFytggcnGdM0XnK3wLihCaIQAOQ== +"@bull-board/ui@3.10.4": + version "3.10.4" + resolved "https://registry.yarnpkg.com/@bull-board/ui/-/ui-3.10.4.tgz#6455b4e75fdbec1bc2ee84fde2a6a283b3c77bc9" + integrity sha512-nqnE3wqqpso7ORPcmcGVesYeFkHwv3AsBdRV2W0VLtfBPGzMdqZ1sJeSTAmlanFZnvTprU4Eg/G0DcEeMUTGhA== dependencies: - "@bull-board/api" "3.10.1" + "@bull-board/api" "3.10.4" "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" @@ -110,10 +110,10 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== +"@eslint/eslintrc@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" + integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -244,10 +244,10 @@ require-from-string "^2.0.2" uri-js "^4.2.2" -"@redocly/openapi-core@1.0.0-beta.90": - version "1.0.0-beta.90" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.90.tgz#edf53b23314368e190b005e1958c1f4a7dfaa2c3" - integrity sha512-MvkME+AWCBexyJyNp/sVFRUBjxCSk5CQ+CAozkwm0t/HusXp9G+kH26+e9giD6Fms129smr1qp3pCAUbwJZzZA== +"@redocly/openapi-core@1.0.0-beta.93": + version "1.0.0-beta.93" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.93.tgz#882db8684598217f621adc7349288229253a0038" + integrity sha512-xQj7UnjPj3mKtkyRrm+bjzEoyo0CVNjGP4pV6BzQ0vgKf0Jqq7apFC703psyBH+JscYr7NKK1hPQU76ylhFDdg== dependencies: "@redocly/ajv" "^8.6.4" "@types/node" "^14.11.8" @@ -527,6 +527,11 @@ resolved "https://registry.yarnpkg.com/@types/jsonld/-/jsonld-1.5.6.tgz#4396c0b17128abf5773bb68b5453b88fc565b0d4" integrity sha512-OUcfMjRie5IOrJulUQwVNvV57SOdKcTfBj3pjXNxzXqeOIrY2aGDNGW/Tlp83EQPkz4tCE6YWVrGuc/ZeaAQGg== +"@types/jsrsasign@10.2.1": + version "10.2.1" + resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.2.1.tgz#b82882523dfb5c476673dbef344ad838f96fb43d" + integrity sha512-piCIOMY0+d2wwRNcRw56VBqFYCYYeZ1c/NlUKVHTV3Y9j1RE2qpgCQvClI6yhH2sk8OoXiah43i9FmnC5tL2RQ== + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -649,10 +654,10 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/mocha@9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" - integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== +"@types/mocha@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== "@types/node-fetch@3.0.3": version "3.0.3" @@ -666,10 +671,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.2.tgz#331b7b9f8621c638284787c5559423822fdffc50" integrity sha512-LSw8TZt12ZudbpHc6EkIyDM3nHVWKYrAvGy6EAJfNfjusbwnThqjqxUKKRwuV3iWYeW/LYMzNgaq3MaLffQ2xA== -"@types/node@17.0.23": - version "17.0.23" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" - integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== +"@types/node@17.0.25": + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== "@types/node@^14.11.8": version "14.17.9" @@ -777,6 +782,11 @@ dependencies: htmlparser2 "^6.0.0" +"@types/semver@7.3.9": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== + "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -785,10 +795,10 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/sharp@0.30.0": - version "0.30.0" - resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.0.tgz#58cb016c8fdc558b4c5771ad1f3668336685c843" - integrity sha512-bZ0Y/JVlrOyqwlBMJ2taEgnwFavjLnyZmLOLecmOesuG5kR2Lx9b2fM4osgfVjLJi8UlE+t3R1JzRVMxF6MbfA== +"@types/sharp@0.30.2": + version "0.30.2" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.30.2.tgz#df5ff34140b3bad165482e6f3d26b08e42a0503a" + integrity sha512-uLCBwjDg/BTcQit0dpNGvkIjvH3wsb8zpaJePCjvONBBSfaKHoxXBIuq1MT8DMQEfk2fKYnpC9QExCgFhkGkMQ== dependencies: "@types/node" "*" @@ -804,11 +814,6 @@ dependencies: "@types/node" "*" -"@types/throttle-debounce@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776" - integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ== - "@types/tinycolor2@1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706" @@ -850,14 +855,14 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.16.0.tgz#78f246dd8d1b528fc5bfca99a8a64d4023a3d86d" - integrity sha512-SJoba1edXvQRMmNI505Uo4XmGbxCK9ARQpkvOd00anxzri9RNQk0DDCxD+LIl+jYhkzOJiOMMKYEHnHEODjdCw== +"@typescript-eslint/eslint-plugin@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1" + integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q== dependencies: - "@typescript-eslint/scope-manager" "5.16.0" - "@typescript-eslint/type-utils" "5.16.0" - "@typescript-eslint/utils" "5.16.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/type-utils" "5.20.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -865,69 +870,69 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/parser@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.16.0.tgz#e4de1bde4b4dad5b6124d3da227347616ed55508" - integrity sha512-fkDq86F0zl8FicnJtdXakFs4lnuebH6ZADDw6CYQv0UZeIjHvmEw87m9/29nk2Dv5Lmdp0zQ3zDQhiMWQf/GbA== +"@typescript-eslint/parser@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b" + integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w== dependencies: - "@typescript-eslint/scope-manager" "5.16.0" - "@typescript-eslint/types" "5.16.0" - "@typescript-eslint/typescript-estree" "5.16.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.16.0.tgz#7e7909d64bd0c4d8aef629cdc764b9d3e1d3a69a" - integrity sha512-P+Yab2Hovg8NekLIR/mOElCDPyGgFZKhGoZA901Yax6WR6HVeGLbsqJkZ+Cvk5nts/dAlFKm8PfL43UZnWdpIQ== +"@typescript-eslint/scope-manager@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980" + integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg== dependencies: - "@typescript-eslint/types" "5.16.0" - "@typescript-eslint/visitor-keys" "5.16.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" -"@typescript-eslint/type-utils@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.16.0.tgz#b482bdde1d7d7c0c7080f7f2f67ea9580b9e0692" - integrity sha512-SKygICv54CCRl1Vq5ewwQUJV/8padIWvPgCxlWPGO/OgQLCijY9G7lDu6H+mqfQtbzDNlVjzVWQmeqbLMBLEwQ== +"@typescript-eslint/type-utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3" + integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw== dependencies: - "@typescript-eslint/utils" "5.16.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.16.0.tgz#5827b011982950ed350f075eaecb7f47d3c643ee" - integrity sha512-oUorOwLj/3/3p/HFwrp6m/J2VfbLC8gjW5X3awpQJ/bSG+YRGFS4dpsvtQ8T2VNveV+LflQHjlLvB6v0R87z4g== +"@typescript-eslint/types@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" + integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== -"@typescript-eslint/typescript-estree@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.16.0.tgz#32259459ec62f5feddca66adc695342f30101f61" - integrity sha512-SE4VfbLWUZl9MR+ngLSARptUv2E8brY0luCdgmUevU6arZRY/KxYoLI/3V/yxaURR8tLRN7bmZtJdgmzLHI6pQ== +"@typescript-eslint/typescript-estree@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0" + integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w== dependencies: - "@typescript-eslint/types" "5.16.0" - "@typescript-eslint/visitor-keys" "5.16.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.16.0.tgz#42218b459d6d66418a4eb199a382bdc261650679" - integrity sha512-iYej2ER6AwmejLWMWzJIHy3nPJeGDuCqf8Jnb+jAQVoPpmWzwQOfa9hWVB8GIQE5gsCv/rfN4T+AYb/V06WseQ== +"@typescript-eslint/utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5" + integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.16.0" - "@typescript-eslint/types" "5.16.0" - "@typescript-eslint/typescript-estree" "5.16.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.16.0": - version "5.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.16.0.tgz#f27dc3b943e6317264c7492e390c6844cd4efbbb" - integrity sha512-jqxO8msp5vZDhikTwq9ubyMHqZ67UIvawohr4qF3KhlpL7gzSjOd+8471H3nh5LyABkaI85laEKKU8SnGUK5/g== +"@typescript-eslint/visitor-keys@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a" + integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg== dependencies: - "@typescript-eslint/types" "5.16.0" + "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" "@ungap/promise-all-settled@1.1.2": @@ -1079,7 +1084,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1087,6 +1092,13 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -1136,13 +1148,13 @@ archiver-utils@^2.1.0: normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== +archiver@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" + integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== dependencies: archiver-utils "^2.1.0" - async "^3.2.0" + async "^3.2.3" buffer-crc32 "^0.2.1" readable-stream "^3.6.0" readdir-glob "^1.0.0" @@ -1232,11 +1244,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - async@>=0.2.9: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" @@ -1249,10 +1256,10 @@ async@^2.6.0: dependencies: lodash "^4.17.14" -async@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" - integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== +async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -1271,10 +1278,10 @@ autwh@0.1.0: dependencies: oauth "0.9.15" -aws-sdk@2.1100.0: - version "2.1100.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1100.0.tgz#20bbabc12fbc316067ba02af66bf371a455af9e3" - integrity sha512-StLSQCYFmFPxjoMntIb+8jUZ0vzmq3xkrwG5e/4qU1bSGWCmhhjvz6c+4j38AnIy8MFV1+tV8RArbhLUEV2dGw== +aws-sdk@2.1120.0: + version "2.1120.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1120.0.tgz#a299f595448019c4b4b69fa9aa57fd58658497a6" + integrity sha512-3cKXUFxC3CDBbJ/JlXEKmJZKFZhqGii7idGaLxvV5/OzqEDUstYkHGX3TCJdQRHrRwpFvRVOekXSwLxBltqXuQ== dependencies: buffer "4.9.2" events "1.1.1" @@ -1301,9 +1308,9 @@ babel-walk@3.0.0-canary-5: "@babel/types" "^7.9.6" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base32.js@0.0.1: version "0.0.1" @@ -1332,11 +1339,6 @@ bcryptjs@2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= -big-integer@^1.6.16: - version "1.6.48" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" - integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== - big-integer@^1.6.17: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" @@ -1402,6 +1404,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1409,15 +1418,14 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -broadcast-channel@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.10.0.tgz#d19fb902df227df40b1b580351713d30c302d198" - integrity sha512-hOUh312XyHk6JTVyX9cyXaH1UYs+2gHVtnW16oQAu9FL7ALcXGXc/YoJWqlkV8vUn14URQPMmRi4A9q4UrwVEQ== +broadcast-channel@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.11.0.tgz#b9ebc7ce1326120088e61d2197477496908a1a9e" + integrity sha512-4FS1Zk+ttekfXHq5I2R7KhN9AsnZUFVV5SczrTtnZPuf5w+jw+fqM1PJHuHzwEXJezJeCbJxoZMDcFqsIN2c1Q== dependencies: "@babel/runtime" "^7.16.0" detect-node "^2.1.0" - microseconds "0.2.0" - nano-time "1.0.0" + microtime "3.0.0" oblivious-set "1.0.0" p-queue "6.6.2" rimraf "3.0.2" @@ -1495,10 +1503,10 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "~3.7.0" -bull@4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.1.tgz#83daaefc3118876450b21d7a02bc11ea28a2440e" - integrity sha512-ojH5AfOchKQsQwwE+thViS1pMpvREGC+Ov1+3HXsQqn5Q27ZSGkgMriMqc6c9J9rvQ/+D732pZE+TN1+2LRWVg== +bull@4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.8.2.tgz#0d02fe389777abe29d50fd46d123bc62e074cfcd" + integrity sha512-S7CNIL9+vsbLKwOGkUI6mawY5iABKQJLZn5a7KPnxAZrDhFXkrxsHHXLCKUR/+Oqys3Vk5ElWdj0SLtK84b1Nw== dependencies: cron-parser "^4.2.1" debuglog "^1.0.0" @@ -1591,11 +1599,6 @@ cacheable-request@^7.0.2: normalize-url "^6.0.1" responselike "^2.0.0" -cafy@15.2.1: - version "15.2.1" - resolved "https://registry.yarnpkg.com/cafy/-/cafy-15.2.1.tgz#5a55eaeb721c604c7dca652f3d555c392e5f995a" - integrity sha512-g2zOmFb63p6XcZ/zeMWKYP8YKQYNWnhJmi6K71Ql4EAFTAay31xF0PBPtdBCCfQ0fiETgWTMxKtySAVI/Od6aQ== - call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1683,7 +1686,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: +chalk@^4.0.2, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1864,10 +1867,10 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-4.2.1.tgz#498aee5fce7fc982606c8875cab080ac0547c884" - integrity sha512-MFJr0uY4RvTQUKvPq7dh9grVOTYSFeXja2mBXioCGjnjJoXrAp9jJ1NQTDR73c9nwBSAQiNKloKl5zq9WB9UPw== +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" color-string "^1.9.0" @@ -2476,11 +2479,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= ejs@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + version "3.1.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" + integrity sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw== dependencies: - jake "^10.6.1" + jake "^10.8.5" emoji-regex@^8.0.0: version "8.0.0" @@ -2650,32 +2653,32 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" -eslint-module-utils@^2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz#1d0aa455dcf41052339b63cada8ab5fd57577129" - integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg== +eslint-module-utils@^2.7.3: + version "2.7.3" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== dependencies: debug "^3.2.7" find-up "^2.1.0" -eslint-plugin-import@2.25.4: - version "2.25.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" - integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== +eslint-plugin-import@2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== dependencies: array-includes "^3.1.4" array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.2" + eslint-module-utils "^2.7.3" has "^1.0.3" - is-core-module "^2.8.0" + is-core-module "^2.8.1" is-glob "^4.0.3" - minimatch "^3.0.4" + minimatch "^3.1.2" object.values "^1.1.5" - resolve "^1.20.0" - tsconfig-paths "^3.12.0" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" eslint-scope@^5.1.1: version "5.1.1" @@ -2715,12 +2718,12 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.12.0.tgz#c7a5bd1cfa09079aae64c9076c07eada66a46e8e" - integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q== +eslint@8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.14.0.tgz#62741f159d9eb4a79695b28ec4989fcdec623239" + integrity sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw== dependencies: - "@eslint/eslintrc" "^1.2.1" + "@eslint/eslintrc" "^1.2.2" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -2961,11 +2964,11 @@ file-type@17.1.1: token-types "^5.0.0-alpha.2" filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + version "1.0.3" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83" + integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q== dependencies: - minimatch "^3.0.4" + minimatch "^5.0.1" fill-range@^7.0.1: version "7.0.1" @@ -3705,10 +3708,10 @@ ip-address@^7.1.0: jsbn "1.1.0" sprintf-js "1.1.2" -ip-cidr@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.4.tgz#a915c47e00f47ea8d5f8ed662ea6161471c44375" - integrity sha512-pKNiqmBlTvEkhaLAa3+FOmYSY0/jjADVxxjA3NbujZZTT8mjLI90Q+6mwg6kd0fNm0RuAOkWJ1u1a/ETmlrPNQ== +ip-cidr@3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/ip-cidr/-/ip-cidr-3.0.7.tgz#22708dd4f2d3f6397c0fb7d647b44e3c565937e9" + integrity sha512-0cBBICDnmmpAdULMbMVdi4f0mSG+VWY/QBPL/OIIjuom14x7Y63VhpS/uSAOycasXOeGXah5y0eu//PDU51aNw== dependencies: ip-address "^7.1.0" jsbn "^1.1.0" @@ -3777,10 +3780,10 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" -is-core-module@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== dependencies: has "^1.0.3" @@ -3987,13 +3990,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== dependencies: - async "0.9.x" - chalk "^2.4.2" + async "^3.2.3" + chalk "^4.0.2" filelist "^1.0.1" minimatch "^3.0.4" @@ -4177,10 +4180,10 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" -jsrsasign@8.0.20: - version "8.0.20" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.20.tgz#37d8029c9d8f794d8ac8d8998bce319921491f11" - integrity sha512-JTXt9+nqdynIB8wFsS6e8ffHhIjilhywXwdaEVHSj9OVmwldG2H0EoCqkQ+KXkm2tVqREfH/HEmklY4k1/6Rcg== +jsrsasign@10.5.19: + version "10.5.19" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.19.tgz#61cd378190c3e65bd1a26a088696736e4437a806" + integrity sha512-GgOdly2Ee9nS+qxOjLkQKaoSTKqlk6lFKcKLPlNJOApoOUcqL2z+l4dAcBzYnZkA3tg+LwFOyQnqbuFn5IPdvw== jstransformer@1.0.0: version "1.0.0" @@ -4655,10 +4658,13 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" -microseconds@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== +microtime@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.0.0.tgz#d140914bde88aa89b4f9fd2a18620b435af0f39b" + integrity sha512-SirJr7ZL4ow2iWcb54bekS4aWyBQNVcEDBiwAz9D/sTgY59A+uE8UJU15cp5wyZmPBwg/3zf8lyCJ5NUe1nVlQ== + dependencies: + node-addon-api "^1.2.0" + node-gyp-build "^3.8.0" mime-db@1.44.0: version "1.44.0" @@ -4711,13 +4717,20 @@ minimatch@4.2.1: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -4839,9 +4852,9 @@ mocha@9.2.2: yargs-unparser "2.0.0" moment@^2.22.2: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== ms@2.0.0: version "2.0.0" @@ -4911,13 +4924,6 @@ nan@^2.14.2, nan@^2.15.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nano-time@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8= - dependencies: - big-integer "^1.6.16" - nanoid@3.3.1, nanoid@^3.1.30: version "3.3.1" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" @@ -4969,6 +4975,11 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-addon-api@^1.2.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" + integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== + node-addon-api@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" @@ -5012,6 +5023,11 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-gyp-build@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" + integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== + node-gyp-build@^4.2.3: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -5419,7 +5435,7 @@ path-key@^4.0.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -6075,6 +6091,15 @@ resolve@^1.15.1, resolve@^1.20.0: is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" @@ -6197,10 +6222,10 @@ seedrandom@3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -semver@7.3.5, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@7.3.7, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -6216,6 +6241,13 @@ semver@^7.3.2, semver@^7.3.4: dependencies: lru-cache "^6.0.0" +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -6251,16 +6283,16 @@ sha.js@^2.4.11: inherits "^2.0.1" safe-buffer "^5.0.1" -sharp@0.30.3: - version "0.30.3" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.3.tgz#315a1817423a4d1cde5119a21c99c234a7a6fb37" - integrity sha512-rjpfJFK58ZOFSG8sxYSo3/JQb4ej095HjXp9X7gVu7gEn1aqSG8TCW29h/Rr31+PXrFADo1H/vKfw0uhMQWFtg== +sharp@0.30.4: + version "0.30.4" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73" + integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q== dependencies: - color "^4.2.1" + color "^4.2.3" detect-libc "^2.0.1" node-addon-api "^4.3.0" prebuild-install "^7.0.1" - semver "^7.3.5" + semver "^7.3.7" simple-get "^4.0.1" tar-fs "^2.1.1" tunnel-agent "^0.6.0" @@ -6602,6 +6634,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -6614,10 +6651,10 @@ syslog-pro@1.0.0: dependencies: moment "^2.22.2" -systeminformation@5.11.9: - version "5.11.9" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.9.tgz#95f2334e739dd224178948a2afaced7d9abfdf9d" - integrity sha512-eeMtL9UJFR/LYG+2rpeAgZ0Va4ojlNQTkYiQH/xbbPwDjDMsaetj3Pkc+C1aH5G8mav6HvDY8kI4Vl4noksSkA== +systeminformation@5.11.14: + version "5.11.14" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.11.14.tgz#21fcb6f05d33e17d69c236b9c1b3d9c53d1d2b3a" + integrity sha512-m8CJx3fIhKohanB0ExTk5q53uI1J0g5B09p77kU+KxnxRVpADVqTAwCg1PFelqKsj4LHd+qmVnumb511Hg4xow== tapable@^2.2.0: version "2.2.0" @@ -6697,11 +6734,6 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -throttle-debounce@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb" - integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg== - through@2: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -6821,7 +6853,7 @@ tsc-alias@1.4.1: mylas "^2.1.4" normalize-path "^3.0.0" -tsconfig-paths@3.14.1: +tsconfig-paths@3.14.1, tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== @@ -6831,16 +6863,6 @@ tsconfig-paths@3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig-paths@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" - integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" - strip-bom "^3.0.0" - tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -6939,10 +6961,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeorm@0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.4.tgz#6608f7efb15c40f3fa2863cefb45ff78a208c40c" - integrity sha512-6v3HH12viDhIQwQDod/B0Plt1o7IYIVDxP7zwatD6fzN+IDdqTTinW/sWNw84Edpbhh2t7XILTaQEqj0NXFP/Q== +typeorm@0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.6.tgz#65203443a1b684bb746785913fe2b0877aa991c0" + integrity sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw== dependencies: "@sqltools/formatter" "^1.2.2" app-root-path "^3.0.0" @@ -7284,10 +7306,10 @@ ws@^8.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== -xev@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/xev/-/xev-2.0.1.tgz#24484173a22115bc8a990ef5d4d5129695b827a7" - integrity sha512-icDf9M67bDge0F2qf02WKZq+s7mMO/SbPv67ZQPym6JThLEOdlWWLdB7VTVgRJp3ekgaiVItCAyH6aoKCPvfIA== +xev@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/xev/-/xev-3.0.2.tgz#3f4080bd8bed0d3479c674050e3696da98d22a4d" + integrity sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw== xml-js@^1.6.11: version "1.6.11" diff --git a/packages/client/@types/theme.d.ts b/packages/client/@types/theme.d.ts new file mode 100644 index 000000000..b8b906b82 --- /dev/null +++ b/packages/client/@types/theme.d.ts @@ -0,0 +1,5 @@ +import { Theme } from '../src/scripts/theme'; + +declare module '@/themes/*.json5' { + export = Theme; +} diff --git a/packages/client/package.json b/packages/client/package.json index 6551b6aca..1d62d78d8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,9 +1,9 @@ { "private": true, "scripts": { - "watch": "webpack --watch", - "build": "webpack", - "lint": "eslint --quiet src/**/*.{ts,vue}" + "watch": "vite build --watch --mode development", + "build": "vite build", + "lint": "eslint --quiet \"src/**/*.{ts,vue}\"" }, "resolutions": { "chokidar": "^3.3.1", @@ -12,56 +12,35 @@ "dependencies": { "@discordapp/twemoji": "13.1.1", "@fortawesome/fontawesome-free": "6.1.1", + "@rollup/plugin-alias": "3.1.9", + "@rollup/plugin-json": "4.1.0", "@syuilo/aiscript": "0.11.1", - "@types/escape-regexp": "0.0.1", - "@types/glob": "7.2.0", - "@types/gulp": "4.0.9", - "@types/gulp-rename": "2.0.1", - "@types/is-url": "1.2.30", - "@types/katex": "0.11.1", - "@types/matter-js": "0.17.7", - "@types/mocha": "9.1.0", - "@types/oauth": "0.9.1", - "@types/parse5": "6.0.3", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.4.2", - "@types/random-seed": "0.3.3", - "@types/seedrandom": "3.0.2", - "@types/throttle-debounce": "2.1.0", - "@types/tinycolor2": "1.4.3", - "@types/uuid": "8.3.4", - "@types/webpack": "5.28.0", - "@types/webpack-stream": "3.2.12", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "@typescript-eslint/parser": "5.16.0", - "@vue/compiler-sfc": "3.2.31", + "@typescript-eslint/parser": "5.20.0", + "@vitejs/plugin-vue": "2.3.1", + "@vue/compiler-sfc": "3.2.33", "abort-controller": "3.0.0", "autobind-decorator": "2.4.0", "autosize": "5.0.1", "autwh": "0.1.0", "blurhash": "1.1.5", - "broadcast-channel": "4.10.0", + "broadcast-channel": "4.11.0", + "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2", "chart.js": "3.7.1", "chartjs-adapter-date-fns": "2.0.0", "chartjs-plugin-gradient": "0.2.2", "chartjs-plugin-zoom": "1.2.1", "compare-versions": "4.1.3", "content-disposition": "0.5.4", - "css-loader": "6.7.1", - "cssnano": "5.1.5", "date-fns": "2.28.0", "escape-regexp": "0.0.1", - "eslint": "8.11.0", - "eslint-plugin-vue": "8.5.0", + "eslint": "8.14.0", + "eslint-plugin-vue": "8.7.1", "eventemitter3": "4.0.7", "feed": "4.2.2", "glob": "7.2.0", "idb-keyval": "6.1.0", "insert-text-at-cursor": "0.3.0", - "ip-cidr": "3.0.4", "json5": "2.2.1", - "json5-loader": "4.0.1", "katex": "0.15.3", "matter-js": "0.18.0", "mfm-js": "0.21.0", @@ -70,11 +49,9 @@ "ms": "2.1.3", "nested-property": "4.0.0", "parse5": "6.0.1", - "photoswipe": "git+https://github.com/dimsemenov/photoswipe#v5-beta", + "photoswipe": "5.2.4", "portscanner": "2.2.0", - "postcss": "8.4.12", - "postcss-loader": "6.2.1", - "prismjs": "1.27.0", + "prismjs": "1.28.0", "private-ip": "2.3.3", "promise-limit": "2.7.0", "pug": "3.0.2", @@ -84,19 +61,17 @@ "random-seed": "0.3.0", "reflect-metadata": "0.1.13", "rndstr": "1.0.0", + "rollup": "2.70.2", "s-age": "1.1.2", - "sass": "1.49.9", - "sass-loader": "12.6.0", + "sass": "1.50.1", "seedrandom": "3.0.5", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "style-loader": "3.3.1", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.139.0", - "throttle-debounce": "3.0.1", + "three": "0.139.2", + "throttle-debounce": "4.0.1", "tinycolor2": "1.4.2", - "ts-loader": "9.2.8", "tsc-alias": "1.5.0", "tsconfig-paths": "3.14.1", "twemoji-parser": "14.0.0", @@ -104,23 +79,38 @@ "uuid": "8.3.2", "v-debounce": "0.1.2", "vanilla-tilt": "1.7.2", - "vue": "3.2.31", - "vue-loader": "17.0.0", + "vite": "2.9.6", + "vue": "3.2.33", "vue-prism-editor": "2.0.0-alpha.2", "vue-router": "4.0.14", - "vue-style-loader": "4.1.3", - "vue-svg-loader": "0.17.0-beta.2", "vuedraggable": "4.0.1", - "webpack": "5.70.0", - "webpack-cli": "4.9.2", "websocket": "1.0.34", "ws": "8.5.0" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "5.16.0", + "@types/escape-regexp": "0.0.1", + "@types/glob": "7.2.0", + "@types/gulp": "4.0.9", + "@types/gulp-rename": "2.0.1", + "@types/is-url": "1.2.30", + "@types/katex": "0.14.0", + "@types/matter-js": "0.17.7", + "@types/mocha": "9.1.1", + "@types/oauth": "0.9.1", + "@types/parse5": "6.0.3", + "@types/punycode": "2.1.0", + "@types/qrcode": "1.4.2", + "@types/random-seed": "0.3.3", + "@types/seedrandom": "3.0.2", + "@types/throttle-debounce": "4.0.0", + "@types/tinycolor2": "1.4.3", + "@types/uuid": "8.3.4", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.20.0", "cross-env": "7.0.3", - "cypress": "9.5.2", - "eslint-plugin-import": "2.25.4", + "cypress": "9.5.4", + "eslint-plugin-import": "2.26.0", "start-server-and-test": "1.14.0" } } diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 4772c0baa..6f806ccc5 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -1,5 +1,5 @@ import { del, get, set } from '@/scripts/idb-proxy'; -import { reactive } from 'vue'; +import { defineAsyncComponent, reactive } from 'vue'; import * as misskey from 'misskey-js'; import { apiUrl } from '@/config'; import { waiting, api, popup, popupMenu, success, alert } from '@/os'; @@ -72,7 +72,7 @@ export async function addAccount(id: Account['id'], token: Account['token']) { } } -function fetchAccount(token): Promise { +function fetchAccount(token: string): Promise { return new Promise((done, fail) => { // Fetch user fetch(`${apiUrl}/i`, { @@ -141,7 +141,7 @@ export async function openAccountMenu(opts: { onChoose?: (account: misskey.entities.UserDetailed) => void; }, ev: MouseEvent) { function showSigninDialog() { - popup(import('@/components/signin-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); success(); @@ -150,7 +150,7 @@ export async function openAccountMenu(opts: { } function createAccount() { - popup(import('@/components/signup-dialog.vue'), {}, { + popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, { done: res => { addAccount(res.id, res.i); switchAccountWithToken(res.i); diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index b67cef209..46d45b690 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -43,20 +43,20 @@ export default defineComponent({ MkSwitch, }, - emits: ['resolved'], - props: { report: { type: Object, required: true, } - } + }, + + emits: ['resolved'], data() { return { forward: this.report.forwarded, }; - } + }, methods: { acct, diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue index 59b8e9730..18dd1e3f4 100644 --- a/packages/client/src/components/analog-clock.vue +++ b/packages/client/src/components/analog-clock.vue @@ -42,7 +42,7 @@ diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index 262eae0de..aaf7ca3ca 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -31,7 +31,7 @@ diff --git a/packages/client/src/components/global/header.vue b/packages/client/src/components/global/header.vue index e558614c1..63db19a52 100644 --- a/packages/client/src/components/global/header.vue +++ b/packages/client/src/components/global/header.vue @@ -38,7 +38,7 @@ - diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index 37076652f..6ac410762 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -91,7 +91,8 @@ export default defineComponent({ let style; switch (token.props.name) { case 'tada': { - style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : ''); + const speed = validTime(token.props.args.speed) || '1s'; + style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : ''); break; } case 'jelly': { @@ -123,11 +124,13 @@ export default defineComponent({ break; } case 'jump': { - style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; break; } case 'bounce': { - style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : ''; + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; break; } case 'flip': { @@ -168,7 +171,8 @@ export default defineComponent({ }, genEl(token.children)); } case 'rainbow': { - style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : ''; + const speed = validTime(token.props.args.speed) || '1s'; + style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; break; } case 'sparkle': { diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 1a360f990..3791c576e 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -72,7 +72,7 @@ diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue index 12171530b..6fe2ac55a 100644 --- a/packages/client/src/pages/settings/notifications.vue +++ b/packages/client/src/pages/settings/notifications.vue @@ -1,71 +1,59 @@ diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue index a9903acc7..82e174a5b 100644 --- a/packages/client/src/pages/settings/other.vue +++ b/packages/client/src/pages/settings/other.vue @@ -1,66 +1,44 @@ - diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue index d35d20d17..6ece53146 100644 --- a/packages/client/src/pages/settings/plugin.install.vue +++ b/packages/client/src/pages/settings/plugin.install.vue @@ -1,19 +1,19 @@ - diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue index 7a3ab9d15..873a022cb 100644 --- a/packages/client/src/pages/settings/plugin.vue +++ b/packages/client/src/pages/settings/plugin.vue @@ -1,38 +1,38 @@ - diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue index a188ba353..963ac81df 100644 --- a/packages/client/src/pages/settings/reaction.vue +++ b/packages/client/src/pages/settings/reaction.vue @@ -54,7 +54,7 @@ diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue index 490a1b551..d01e87c1f 100644 --- a/packages/client/src/pages/settings/sounds.vue +++ b/packages/client/src/pages/settings/sounds.vue @@ -1,24 +1,24 @@ - diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue index 2d3514342..0ef098f31 100644 --- a/packages/client/src/pages/settings/theme.install.vue +++ b/packages/client/src/pages/settings/theme.install.vue @@ -13,7 +13,7 @@ diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index d134a092b..64b384bdc 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -85,12 +85,11 @@ - diff --git a/packages/client/src/pages/settings/webhook.edit.vue b/packages/client/src/pages/settings/webhook.edit.vue new file mode 100644 index 000000000..bb3a25407 --- /dev/null +++ b/packages/client/src/pages/settings/webhook.edit.vue @@ -0,0 +1,89 @@ + + + diff --git a/packages/client/src/pages/settings/webhook.new.vue b/packages/client/src/pages/settings/webhook.new.vue new file mode 100644 index 000000000..9bb492c49 --- /dev/null +++ b/packages/client/src/pages/settings/webhook.new.vue @@ -0,0 +1,81 @@ + + + diff --git a/packages/client/src/pages/settings/webhook.vue b/packages/client/src/pages/settings/webhook.vue new file mode 100644 index 000000000..c9af8b676 --- /dev/null +++ b/packages/client/src/pages/settings/webhook.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue index c11707b6c..97a15da5b 100644 --- a/packages/client/src/pages/settings/word-mute.vue +++ b/packages/client/src/pages/settings/word-mute.vue @@ -1,35 +1,35 @@ - diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index a53e23c1c..5f9f1b978 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -67,9 +67,9 @@ diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue index c7fddbc49..57008aeae 100644 --- a/packages/client/src/ui/classic.header.vue +++ b/packages/client/src/ui/classic.header.vue @@ -39,7 +39,7 @@