diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9da27f467..4e42fa931 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: submodules: true - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'yarn' cache-dependency-path: | packages/backend/yarn.lock @@ -31,7 +31,7 @@ jobs: submodules: true - uses: actions/setup-node@v3 with: - node-version: 16.x + node-version: 18.x cache: 'yarn' cache-dependency-path: | packages/client/yarn.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d57d85c87..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: @@ -57,7 +57,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [16.x] + node-version: [18.x] browser: [chrome] services: diff --git a/CHANGELOG.md b/CHANGELOG.md index ac5dd0631..8842014a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## 12.x.x (unreleased) ### Improvements -- +- API: notifications/readは配列でも受け付けるように +- /share のクエリでリプライやファイル等の情報を渡せるように +- ページロードエラーページにリロードボタンを追加 ### Bugfixes - @@ -20,15 +22,19 @@ You should also include the user name that made the change. ### Improvements - Client: Preferences Registry +- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @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 ## 12.110.1 (2022/04/23) ### Bugfixes - Fix GOP rendering @syuilo -- Improve performance of antenna, clip, and list @xianon +- Improve performance of antenna, clip, and list @xianonn ## 12.110.0 (2022/04/11) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a52c67d34..10e64ba55 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: "返信を含む" @@ -1693,8 +1693,9 @@ _notification: youWereFollowed: "フォローされました" youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" - youWereInvitedToGroup: "グループに招待されました" + youWereInvitedToGroup: "{userName}があなたをグループに招待しました" pollEnded: "アンケートの結果が出ました" + emptyPushNotificationMessage: "プッシュ通知の更新をしました" _types: all: "すべて" @@ -1711,6 +1712,11 @@ _notification: groupInvited: "グループに招待された" app: "連携アプリからの通知" + _actions: + followBack: "フォローバック" + reply: "返信" + renote: "Renote" + _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" 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/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/web/index.ts b/packages/backend/src/server/web/index.ts index e80bf45d1..061ea5060 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -32,6 +32,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(); @@ -136,9 +137,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'), }); }); 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/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/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/yarn.lock b/packages/backend/yarn.lock index 8fbfa6459..fd91be84a 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -1084,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== @@ -1092,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" @@ -1237,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" @@ -1306,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" @@ -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" @@ -1677,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== @@ -2470,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" @@ -2955,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" @@ -3981,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" @@ -4708,20 +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== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.1.2: +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" 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/scratchpad.vue b/packages/client/src/pages/scratchpad.vue index f871dc48e..eb91938db 100644 --- a/packages/client/src/pages/scratchpad.vue +++ b/packages/client/src/pages/scratchpad.vue @@ -6,20 +6,20 @@ - +
{{ log.text }}
- {{ $ts.scratchpadDescription }} + {{ i18n.ts.scratchpadDescription }}
- diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index c153b4d28..127cbcd4c 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -37,8 +37,8 @@ - diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue index f84a209b6..bcc2ee85a 100644 --- a/packages/client/src/pages/settings/instance-mute.vue +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -1,67 +1,51 @@ - diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue index ca36c9166..75c620094 100644 --- a/packages/client/src/pages/settings/integration.vue +++ b/packages/client/src/pages/settings/integration.vue @@ -1,133 +1,98 @@ - diff --git a/packages/client/src/scripts/get-user-name.ts b/packages/client/src/scripts/get-user-name.ts new file mode 100644 index 000000000..d499ea020 --- /dev/null +++ b/packages/client/src/scripts/get-user-name.ts @@ -0,0 +1,3 @@ +export default function(user: { name?: string | null, username: string }): string { + return user.name || user.username; +} diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts index d6dbd5dbd..7bacfbdf0 100644 --- a/packages/client/src/scripts/initialize-sw.ts +++ b/packages/client/src/scripts/initialize-sw.ts @@ -4,26 +4,26 @@ import { api } from '@/os'; import { lang } from '@/config'; export async function initializeSw() { - if (instance.swPublickey && - ('serviceWorker' in navigator) && - ('PushManager' in window) && - $i && $i.token) { - navigator.serviceWorker.register(`/sw.js`); + if (!('serviceWorker' in navigator)) return; - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); + navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); + navigator.serviceWorker.ready.then(registration => { + registration.active?.postMessage({ + msg: 'initialize', + lang, + }); + + if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) { // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(instance.swPublickey) - }).then(subscription => { + }) + .then(subscription => { function encode(buffer: ArrayBuffer | null) { return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); } - + // Register api('sw/register', { endpoint: subscription.endpoint, @@ -37,15 +37,15 @@ export async function initializeSw() { if (err.name === 'NotAllowedError') { return; } - + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが // 既に存在していることが原因でエラーになった可能性があるので、 // そのサブスクリプションを解除しておく const subscription = await registration.pushManager.getSubscription(); if (subscription) subscription.unsubscribe(); }); - }); - } + } + }); } /** diff --git a/packages/client/src/sw/compose-notification.ts b/packages/client/src/sw/compose-notification.ts deleted file mode 100644 index e271d3094..000000000 --- a/packages/client/src/sw/compose-notification.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Notification composer of Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import * as misskey from 'misskey-js'; - -function getUserName(user: misskey.entities.User): string { - return user.name || user.username; -} - -export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> { - if (!i18n) { - console.log('no i18n'); - return; - } - - switch (type) { - case 'driveFileCreated': // TODO (Server Side) - return [i18n.t('_notification.fileUploaded'), { - body: data.name, - icon: data.url - }]; - case 'notification': - switch (data.type) { - case 'mention': - return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reply': - return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'renote': - return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'quote': - return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reaction': - return [`${data.reaction} ${getUserName(data.user)}`, { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollVote': - return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollEnded': - return [i18n.t('_notification.pollEnded'), { - body: data.note.text, - }]; - - case 'follow': - return [i18n.t('_notification.youWereFollowed'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'receiveFollowRequest': - return [i18n.t('_notification.youReceivedFollowRequest'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'followRequestAccepted': - return [i18n.t('_notification.yourFollowRequestAccepted'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'groupInvited': - return [i18n.t('_notification.youWereInvitedToGroup'), { - body: data.group.name - }]; - - default: - return null; - } - case 'unreadMessagingMessage': - if (data.groupId === null) { - return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), { - icon: data.user.avatarUrl, - tag: `messaging:user:${data.user.id}` - }]; - } - return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), { - icon: data.user.avatarUrl, - tag: `messaging:group:${data.group.id}` - }]; - default: - return null; - } -} diff --git a/packages/client/src/sw/sw.ts b/packages/client/src/sw/sw.ts deleted file mode 100644 index 68c650c77..000000000 --- a/packages/client/src/sw/sw.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import { get, set } from 'idb-keyval'; -import composeNotification from '@/sw/compose-notification'; -import { I18n } from '@/scripts/i18n'; - -//#region Variables -const version = _VERSION_; -const cacheName = `mk-cache-${version}`; - -let lang: string; -let i18n: I18n; -let pushesPool: any[] = []; -//#endregion - -//#region Startup -get('lang').then(async prelang => { - if (!prelang) return; - lang = prelang; - return fetchLocale(); -}); -//#endregion - -//#region Lifecycle: Install -self.addEventListener('install', ev => { - self.skipWaiting(); -}); -//#endregion - -//#region Lifecycle: Activate -self.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== cacheName) - .map(name => caches.delete(name)) - )) - .then(() => self.clients.claim()) - ); -}); -//#endregion - -//#region When: Fetching -self.addEventListener('fetch', ev => { - // Nothing to do -}); -//#endregion - -//#region When: Caught Notification -self.addEventListener('push', ev => { - // クライアント取得 - ev.waitUntil(self.clients.matchAll({ - includeUncontrolled: true - }).then(async clients => { - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length != 0) return; - - const { type, body } = ev.data?.json(); - - // localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく - if (!i18n) return pushesPool.push({ type, body }); - - const n = await composeNotification(type, body, i18n); - if (n) return self.registration.showNotification(...n); - })); -}); -//#endregion - -//#region When: Caught a message from the client -self.addEventListener('message', ev => { - switch(ev.data) { - case 'clear': - return; // TODO - default: - break; - } - - if (typeof ev.data === 'object') { - // E.g. '[object Array]' → 'array' - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - lang = ev.data.lang; - set('lang', lang); - fetchLocale(); - } - } - } -}); -//#endregion - -//#region Function: (Re)Load i18n instance -async function fetchLocale() { - //#region localeファイルの読み込み - // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う - const localeUrl = `/assets/locales/${lang}.${version}.json`; - let localeRes = await caches.match(localeUrl); - - if (!localeRes) { - localeRes = await fetch(localeUrl); - const clone = localeRes?.clone(); - if (!clone?.clone().ok) return; - - caches.open(cacheName).then(cache => cache.put(localeUrl, clone)); - } - - i18n = new I18n(await localeRes.json()); - //#endregion - - //#region i18nをきちんと読み込んだ後にやりたい処理 - for (const { type, body } of pushesPool) { - const n = await composeNotification(type, body, i18n); - if (n) self.registration.showNotification(...n); - } - pushesPool = []; - //#endregion -} -//#endregion diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 50d95539d..62e97a11e 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -21,6 +21,7 @@ import { popup, popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; +import { swInject } from './sw-inject'; import { stream } from '@/stream'; export default defineComponent({ @@ -49,6 +50,11 @@ export default defineComponent({ if ($i) { const connection = stream.useChannel('main', null, 'UI'); connection.on('notification', onNotification); + + //#region Listen message from SW + if ('serviceWorker' in navigator) { + swInject(); + } } return { diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts new file mode 100644 index 000000000..e3e2ddd7e --- /dev/null +++ b/packages/client/src/ui/_common_/sw-inject.ts @@ -0,0 +1,45 @@ +import { inject } from 'vue'; +import { post } from '@/os'; +import { $i, login } from '@/account'; +import { defaultStore } from '@/store'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { router } from '@/router'; + +export function swInject() { + const navHook = inject('navHook', null); + const sideViewHook = inject('sideViewHook', null); + + navigator.serviceWorker.addEventListener('message', ev => { + if (_DEV_) { + console.log('sw msg', ev.data); + } + + const data = ev.data; // as SwMessage + if (data.type !== 'order') return; + + if (data.loginId !== $i?.id) { + return getAccountFromId(data.loginId).then(account => { + if (!account) return; + return login(account.token, data.url); + }); + } + + switch (data.order) { + case 'post': + return post(data.options); + case 'push': + if (router.currentRoute.value.path === data.url) { + return window.scroll({ top: 0, behavior: 'smooth' }); + } + if (navHook) { + return navHook(data.url); + } + if (sideViewHook && defaultStore.state.defaultSideView && data.url !== '/') { + return sideViewHook(data.url); + } + return router.push(data.url); + default: + return; + } + }); +} diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index f344ae09c..a06b0356c 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -29,8 +29,7 @@ ], "lib": [ "esnext", - "dom", - "webworker" + "dom" ] }, "compileOnSave": false, diff --git a/packages/client/webpack.config.js b/packages/client/webpack.config.js index a50851e17..adb5fb81b 100644 --- a/packages/client/webpack.config.js +++ b/packages/client/webpack.config.js @@ -37,7 +37,6 @@ const postcss = { module.exports = { entry: { app: './src/init.ts', - sw: './src/sw/sw.ts' }, module: { rules: [{ diff --git a/packages/sw/.eslintrc.js b/packages/sw/.eslintrc.js new file mode 100644 index 000000000..9d56daca8 --- /dev/null +++ b/packages/sw/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + root: true, + env: { + "node": false + }, + parserOptions: { + "parser": "@typescript-eslint/parser", + tsconfigRootDir: __dirname, + //project: ['./tsconfig.json'], + }, + extends: [ + //"../shared/.eslintrc.js", + ], + globals: { + "require": false, + "_DEV_": false, + "_LANGS_": false, + "_VERSION_": false, + "_ENV_": false, + "_PERF_PREFIX_": false, + } +} diff --git a/packages/sw/.npmrc b/packages/sw/.npmrc new file mode 100644 index 000000000..6b5f38e89 --- /dev/null +++ b/packages/sw/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/packages/sw/.yarnrc b/packages/sw/.yarnrc new file mode 100644 index 000000000..788570fcd --- /dev/null +++ b/packages/sw/.yarnrc @@ -0,0 +1 @@ +network-timeout 600000 diff --git a/packages/sw/build.js b/packages/sw/build.js new file mode 100644 index 000000000..72d9db9c0 --- /dev/null +++ b/packages/sw/build.js @@ -0,0 +1,37 @@ +const esbuild = require('esbuild'); +const locales = require('../../locales'); +const meta = require('../../package.json'); +const watch = process.argv[2]?.includes('watch'); + +console.log('Starting SW building...'); + +esbuild.build({ + entryPoints: [ `${__dirname}/src/sw.ts` ], + bundle: true, + format: 'esm', + treeShaking: true, + minify: process.env.NODE_ENV === 'production', + absWorkingDir: __dirname, + outbase: `${__dirname}/src`, + outdir: `${__dirname}/../../built/_sw_dist_`, + loader: { + '.ts': 'ts' + }, + tsconfig: `${__dirname}/tsconfig.json`, + define: { + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + }, + watch: watch ? { + onRebuild(error, result) { + if (error) console.error('SW: watch build failed:', error); + else console.log('SW: watch build succeeded:', result); + }, + } : false, +}).then(result => { + if (watch) console.log('watching...'); + else console.log('done,', JSON.stringify(result)); +}); diff --git a/packages/sw/package.json b/packages/sw/package.json new file mode 100644 index 000000000..41dfe19b8 --- /dev/null +++ b/packages/sw/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "watch": "node build.js watch", + "build": "node build.js", + "lint": "eslint --quiet src/**/*.{ts}" + }, + "resolutions": {}, + "dependencies": { + "esbuild": "^0.14.13", + "idb-keyval": "^6.0.3", + "misskey-js": "0.0.14" + }, + "devDependencies": { + "eslint": "^8.2.0" + } +} diff --git a/packages/sw/src/filters/user.ts b/packages/sw/src/filters/user.ts new file mode 100644 index 000000000..09437eb19 --- /dev/null +++ b/packages/sw/src/filters/user.ts @@ -0,0 +1,14 @@ +import * as misskey from 'misskey-js'; +import * as Acct from 'misskey-js/built/acct'; + +export const acct = (user: misskey.Acct) => { + return Acct.toString(user); +}; + +export const userName = (user: misskey.entities.User) => { + return user.name || user.username; +}; + +export const userPage = (user: misskey.Acct, path?, absolute = false) => { + return `${absolute ? origin : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; +}; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts new file mode 100644 index 000000000..6d7ba7d52 --- /dev/null +++ b/packages/sw/src/scripts/create-notification.ts @@ -0,0 +1,237 @@ +/* + * Notification manager for SW + */ +declare var self: ServiceWorkerGlobalScope; + +import { swLang } from '@/scripts/lang'; +import { cli } from '@/scripts/operations'; +import { pushNotificationDataMap } from '@/types'; +import getUserName from '@/scripts/get-user-name'; +import { I18n } from '@/scripts/i18n'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; + +export async function createNotification(data: pushNotificationDataMap[K]) { + const n = await composeNotification(data); + + if (n) { + return self.registration.showNotification(...n); + } else { + console.error('Could not compose notification', data); + return createEmptyNotification(); + } +} + +async function composeNotification(data: pushNotificationDataMap[K]): Promise<[string, NotificationOptions] | null> { + if (!swLang.i18n) swLang.fetchLocale(); + const i18n = await swLang.i18n as I18n; + const { t } = i18n; + switch (data.type) { + /* + case 'driveFileCreated': // TODO (Server Side) + return [t('_notification.fileUploaded'), { + body: body.name, + icon: body.url, + data + }]; + */ + case 'notification': + switch (data.body.type) { + case 'follow': + // users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用 + const account = await getAccountFromId(data.userId); + if (!account) return null; + const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token); + return [t('_notification.youWereFollowed'), { + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, + data, + actions: userDetail.isFollowing ? [] : [ + { + action: 'follow', + title: t('_notification._actions.followBack') + } + ], + }]; + + case 'mention': + return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + } + ], + }]; + + case 'reply': + return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + } + ], + }]; + + case 'renote': + return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'showUser', + title: getUserName(data.body.user) + } + ], + }]; + + case 'quote': + return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'reply', + title: t('_notification._actions.reply') + }, + ...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ + { + action: 'renote', + title: t('_notification._actions.renote') + } + ] : []) + ], + }]; + + case 'reaction': + return [`${data.body.reaction} ${getUserName(data.body.user)}`, { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'showUser', + title: getUserName(data.body.user) + } + ], + }]; + + case 'pollVote': + return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { + body: data.body.note.text || '', + icon: data.body.user.avatarUrl, + data, + }]; + + case 'pollEnded': + return [t('_notification.pollEnded'), { + body: data.body.note.text || '', + data, + }]; + + case 'receiveFollowRequest': + return [t('_notification.youReceivedFollowRequest'), { + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, + data, + actions: [ + { + action: 'accept', + title: t('accept') + }, + { + action: 'reject', + title: t('reject') + } + ], + }]; + + case 'followRequestAccepted': + return [t('_notification.yourFollowRequestAccepted'), { + body: getUserName(data.body.user), + icon: data.body.user.avatarUrl, + data, + }]; + + case 'groupInvited': + return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), { + body: data.body.invitation.group.name, + data, + actions: [ + { + action: 'accept', + title: t('accept') + }, + { + action: 'reject', + title: t('reject') + } + ], + }]; + + case 'app': + return [data.body.header || data.body.body, { + body: data.body.header && data.body.body, + icon: data.body.icon, + data + }]; + + default: + return null; + } + case 'unreadMessagingMessage': + if (data.body.groupId === null) { + return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { + icon: data.body.user.avatarUrl, + tag: `messaging:user:${data.body.userId}`, + data, + renotify: true, + }]; + } + return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { + icon: data.body.user.avatarUrl, + tag: `messaging:group:${data.body.groupId}`, + data, + renotify: true, + }]; + default: + return null; + } +} + +export async function createEmptyNotification() { + return new Promise(async res => { + if (!swLang.i18n) swLang.fetchLocale(); + const i18n = await swLang.i18n as I18n; + const { t } = i18n; + + await self.registration.showNotification( + t('_notification.emptyPushNotificationMessage'), + { + silent: true, + tag: 'read_notification', + } + ); + + res(); + + setTimeout(async () => { + for (const n of + [ + ...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })), + ...(await self.registration.getNotifications({ tag: 'read_notification' })) + ] + ) { + n.close(); + } + }, 1000); + }); +} diff --git a/packages/sw/src/scripts/get-account-from-id.ts b/packages/sw/src/scripts/get-account-from-id.ts new file mode 100644 index 000000000..be4cfaeba --- /dev/null +++ b/packages/sw/src/scripts/get-account-from-id.ts @@ -0,0 +1,7 @@ +import { get } from 'idb-keyval'; + +export async function getAccountFromId(id: string) { + const accounts = await get('accounts') as { token: string; id: string; }[]; + if (!accounts) console.log('Accounts are not recorded'); + return accounts.find(e => e.id === id); +} diff --git a/packages/sw/src/scripts/get-user-name.ts b/packages/sw/src/scripts/get-user-name.ts new file mode 100644 index 000000000..d499ea020 --- /dev/null +++ b/packages/sw/src/scripts/get-user-name.ts @@ -0,0 +1,3 @@ +export default function(user: { name?: string | null, username: string }): string { + return user.name || user.username; +} diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts new file mode 100644 index 000000000..3fe88e551 --- /dev/null +++ b/packages/sw/src/scripts/i18n.ts @@ -0,0 +1,29 @@ +export class I18n> { + public ts: T; + + constructor(locale: T) { + this.ts = locale; + + //#region BIND + this.t = this.t.bind(this); + //#endregion + } + + // string にしているのは、ドット区切りでのパス指定を許可するため + // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも + public t(key: string, args?: Record): string { + try { + let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; + + if (args) { + for (const [k, v] of Object.entries(args)) { + str = str.replace(`{${k}}`, v); + } + } + return str; + } catch (err) { + console.warn(`missing localization '${key}'`); + return key; + } + } +} diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts new file mode 100644 index 000000000..2d05404ef --- /dev/null +++ b/packages/sw/src/scripts/lang.ts @@ -0,0 +1,47 @@ +/* + * Language manager for SW + */ +declare var self: ServiceWorkerGlobalScope; + +import { get, set } from 'idb-keyval'; +import { I18n } from '@/scripts/i18n'; + +class SwLang { + public cacheName = `mk-cache-${_VERSION_}`; + + public lang: Promise = get('lang').then(async prelang => { + if (!prelang) return 'en-US'; + return prelang; + }); + + public setLang(newLang: string) { + this.lang = Promise.resolve(newLang); + set('lang', newLang); + return this.fetchLocale(); + } + + public i18n: Promise> | null = null; + + public fetchLocale() { + return this.i18n = this._fetch(); + } + + private async _fetch() { + // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う + const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`; + let localeRes = await caches.match(localeUrl); + + // _DEV_がtrueの場合は常に最新化 + if (!localeRes || _DEV_) { + localeRes = await fetch(localeUrl); + const clone = localeRes?.clone(); + if (!clone?.clone().ok) Error('locale fetching error'); + + caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone)); + } + + return new I18n(await localeRes.json()); + } +} + +export const swLang = new SwLang(); diff --git a/packages/sw/src/scripts/login-id.ts b/packages/sw/src/scripts/login-id.ts new file mode 100644 index 000000000..0f9c6be4a --- /dev/null +++ b/packages/sw/src/scripts/login-id.ts @@ -0,0 +1,11 @@ +export function getUrlWithLoginId(url: string, loginId: string) { + const u = new URL(url, origin); + u.searchParams.append('loginId', loginId); + return u.toString(); +} + +export function getUrlWithoutLoginId(url: string) { + const u = new URL(url); + u.searchParams.delete('loginId'); + return u.toString(); +} diff --git a/packages/sw/src/scripts/notification-read.ts b/packages/sw/src/scripts/notification-read.ts new file mode 100644 index 000000000..8433f902b --- /dev/null +++ b/packages/sw/src/scripts/notification-read.ts @@ -0,0 +1,50 @@ +declare var self: ServiceWorkerGlobalScope; + +import { get } from 'idb-keyval'; +import { pushNotificationDataMap } from '@/types'; +import { api } from '@/scripts/operations'; + +type Accounts = { + [x: string]: { + queue: string[], + timeout: number | null + } +}; + +class SwNotificationReadManager { + private accounts: Accounts = {}; + + public async construct() { + const accounts = await get('accounts'); + if (!accounts) Error('Accounts are not recorded'); + + this.accounts = accounts.reduce((acc, e) => { + acc[e.id] = { + queue: [], + timeout: null + }; + return acc; + }, {} as Accounts); + + return this; + } + + // プッシュ通知の既読をサーバーに送信 + public async read(data: pushNotificationDataMap[K]) { + if (data.type !== 'notification' || !(data.userId in this.accounts)) return; + + const account = this.accounts[data.userId]; + + account.queue.push(data.body.id as string); + + // 最後の呼び出しから200ms待ってまとめて処理する + if (account.timeout) clearTimeout(account.timeout); + account.timeout = setTimeout(() => { + account.timeout = null; + + api('notifications/read', data.userId, { notificationIds: account.queue }); + }, 200); + } +} + +export const swNotificationRead = (new SwNotificationReadManager()).construct(); diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts new file mode 100644 index 000000000..02cf0d96c --- /dev/null +++ b/packages/sw/src/scripts/operations.ts @@ -0,0 +1,70 @@ +/* + * Operations + * 各種操作 + */ +declare var self: ServiceWorkerGlobalScope; + +import * as Misskey from 'misskey-js'; +import { SwMessage, swMessageOrderType } from '@/types'; +import { acct as getAcct } from '@/filters/user'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { getUrlWithLoginId } from '@/scripts/login-id'; + +export const cli = new Misskey.api.APIClient({ origin, fetch: (...args) => fetch(...args) }); + +export async function api(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) { + const account = await getAccountFromId(userId); + if (!account) return; + + return cli.request(endpoint, options, account.token); +} + +// rendered acctからユーザーを開く +export function openUser(acct: string, loginId: string) { + return openClient('push', `/@${acct}`, loginId, { acct }); +} + +// noteIdからノートを開く +export function openNote(noteId: string, loginId: string) { + return openClient('push', `/notes/${noteId}`, loginId, { noteId }); +} + +export async function openChat(body: any, loginId: string) { + if (body.groupId === null) { + return openClient('push', `/my/messaging/${getAcct(body.user)}`, loginId, { body }); + } else { + return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body }); + } +} + +// post-formのオプションから投稿フォームを開く +export async function openPost(options: any, loginId: string) { + // クエリを作成しておく + let url = `/share?`; + if (options.initialText) url += `text=${options.initialText}&`; + if (options.reply) url += `replyId=${options.reply.id}&`; + if (options.renote) url += `renoteId=${options.renote.id}&`; + + return openClient('post', url, loginId, { options }); +} + +export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) { + const client = await findClient(); + + if (client) { + client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); + return client; + } + + return self.clients.openWindow(getUrlWithLoginId(url, loginId)); +} + +export async function findClient() { + const clients = await self.clients.matchAll({ + type: 'window' + }); + for (const c of clients) { + if (c.url.indexOf('?zen') < 0) return c; + } + return null; +} diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts new file mode 100644 index 000000000..0ba6a6e4a --- /dev/null +++ b/packages/sw/src/sw.ts @@ -0,0 +1,200 @@ +declare var self: ServiceWorkerGlobalScope; + +import { createEmptyNotification, createNotification } from '@/scripts/create-notification'; +import { swLang } from '@/scripts/lang'; +import { swNotificationRead } from '@/scripts/notification-read'; +import { pushNotificationDataMap } from '@/types'; +import * as swos from '@/scripts/operations'; +import { acct as getAcct } from '@/filters/user'; + +self.addEventListener('install', ev => { + ev.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', ev => { + ev.waitUntil( + caches.keys() + .then(cacheNames => Promise.all( + cacheNames + .filter((v) => v !== swLang.cacheName) + .map(name => caches.delete(name)) + )) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', ev => { + ev.respondWith( + fetch(ev.request) + .catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })) + ); +}); + +self.addEventListener('push', ev => { + // クライアント取得 + ev.waitUntil(self.clients.matchAll({ + includeUncontrolled: true, + type: 'window' + }).then(async (clients: readonly WindowClient[]) => { + const data: pushNotificationDataMap[K] = ev.data?.json(); + + switch (data.type) { + // case 'driveFileCreated': + case 'notification': + case 'unreadMessagingMessage': + // クライアントがあったらストリームに接続しているということなので通知しない + if (clients.length != 0) return; + return createNotification(data); + case 'readAllNotifications': + for (const n of await self.registration.getNotifications()) { + if (n?.data?.type === 'notification') n.close(); + } + break; + case 'readAllMessagingMessages': + for (const n of await self.registration.getNotifications()) { + if (n?.data?.type === 'unreadMessagingMessage') n.close(); + } + break; + case 'readNotifications': + for (const n of await self.registration.getNotifications()) { + if (data.body?.notificationIds?.includes(n.data.body.id)) { + n.close(); + } + } + break; + case 'readAllMessagingMessagesOfARoom': + for (const n of await self.registration.getNotifications()) { + if (n.data.type === 'unreadMessagingMessage' + && ('userId' in data.body + ? data.body.userId === n.data.body.userId + : data.body.groupId === n.data.body.groupId) + ) { + n.close(); + } + } + break; + } + + return createEmptyNotification(); + })); +}); + +self.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { + ev.waitUntil((async () => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } + + const { action, notification } = ev; + const data: pushNotificationDataMap[K] = notification.data; + const { userId: id } = data; + let client: WindowClient | null = null; + + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + if ('userId' in data.body) await swos.api('following/create', id, { userId: data.body.userId }); + break; + case 'showUser': + if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), id); + break; + case 'reply': + if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, id); + break; + case 'renote': + if ('note' in data.body) await swos.api('notes/create', id, { renoteId: data.body.note.id }); + break; + case 'accept': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/accept', id, { userId: data.body.userId }); + break; + case 'groupInvited': + await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id }); + break; + } + break; + case 'reject': + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/reject', id, { userId: data.body.userId }); + break; + case 'groupInvited': + await swos.api('users/groups/invitations/reject', id, { invitationId: data.body.invitation.id }); + break; + } + break; + case 'showFollowRequests': + client = await swos.openClient('push', '/my/follow-requests', id); + break; + default: + switch (data.body.type) { + case 'receiveFollowRequest': + client = await swos.openClient('push', '/my/follow-requests', id); + break; + case 'groupInvited': + client = await swos.openClient('push', '/my/groups', id); + break; + case 'reaction': + client = await swos.openNote(data.body.note.id, id); + break; + default: + if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, id); + } else if ('user' in data.body) { + client = await swos.openUser(getAcct(data.body.user), id); + } + break; + } + } + break; + case 'unreadMessagingMessage': + client = await swos.openChat(data.body, id); + break; + } + + if (client) { + client.focus(); + } + if (data.type === 'notification') { + swNotificationRead.then(that => that.read(data)); + } + + notification.close(); + + })()); +}); + +self.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { + const data: pushNotificationDataMap[K] = ev.notification.data; + + if (data.type === 'notification') { + swNotificationRead.then(that => that.read(data)); + } +}); + +self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { + ev.waitUntil((async () => { + switch (ev.data) { + case 'clear': + // Cache Storage全削除 + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)) + )); + return; // TODO + } + + if (typeof ev.data === 'object') { + // E.g. '[object Array]' → 'array' + const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + + if (otype === 'object') { + if (ev.data.msg === 'initialize') { + swLang.setLang(ev.data.lang); + } + } + } + })()); +}); diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts new file mode 100644 index 000000000..6aa3726ea --- /dev/null +++ b/packages/sw/src/types.ts @@ -0,0 +1,31 @@ +import * as Misskey from 'misskey-js'; + +export type swMessageOrderType = 'post' | 'push'; + +export type SwMessage = { + type: 'order'; + order: swMessageOrderType; + loginId: string; + url: string; + [x: string]: any; +}; + +// Defined also @/services/push-notification.ts#L7-L14 +type pushNotificationDataSourceMap = { + notification: Misskey.entities.Notification; + unreadMessagingMessage: Misskey.entities.MessagingMessage; + readNotifications: { notificationIds: string[] }; + readAllNotifications: undefined; + readAllMessagingMessages: undefined; + readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; +}; + +export type pushNotificationData = { + type: K; + body: pushNotificationDataSourceMap[K]; + userId: string; +}; + +export type pushNotificationDataMap = { + [K in keyof pushNotificationDataSourceMap]: pushNotificationData; +}; diff --git a/packages/sw/tsconfig.json b/packages/sw/tsconfig.json new file mode 100644 index 000000000..c3a845f12 --- /dev/null +++ b/packages/sw/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "es2017", + "module": "esnext", + "moduleResolution": "node", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + }, + "typeRoots": [ + "node_modules/@types", + "@types", + ], + "lib": [ + "esnext", + "webworker" + ] + }, + "compileOnSave": false, + "include": [ + "./**/*.ts" + ] +} diff --git a/packages/sw/webpack.config.js b/packages/sw/webpack.config.js new file mode 100644 index 000000000..a4bcf96dd --- /dev/null +++ b/packages/sw/webpack.config.js @@ -0,0 +1,71 @@ +/** + * webpack configuration + */ + +const fs = require('fs'); +const webpack = require('webpack'); + +class WebpackOnBuildPlugin { + constructor(callback) { + this.callback = callback; + } + + apply(compiler) { + compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback); + } +} + +const isProduction = process.env.NODE_ENV === 'production'; + +const locales = require('../../locales'); +const meta = require('../../package.json'); + +module.exports = { + target: 'webworker', + entry: { + ['sw-lib']: './src/lib.ts' + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [{ + loader: 'ts-loader', + options: { + happyPackMode: true, + transpileOnly: true, + configFile: __dirname + '/tsconfig.json', + } + }] + }] + }, + plugins: [ + new webpack.ProgressPlugin({}), + new webpack.DefinePlugin({ + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + }), + ], + output: { + path: __dirname + '/../../built/_sw_dist_', + filename: `[name].js`, + publicPath: `/`, + pathinfo: false, + }, + resolve: { + extensions: [ + '.js', '.ts', '.json' + ], + alias: { + '@': __dirname + '/src/', + } + }, + resolveLoader: { + modules: ['node_modules'] + }, + devtool: false, //'source-map', + mode: isProduction ? 'production' : 'development' +}; diff --git a/packages/sw/yarn.lock b/packages/sw/yarn.lock new file mode 100644 index 000000000..e6d683bc4 --- /dev/null +++ b/packages/sw/yarn.lock @@ -0,0 +1,710 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@eslint/eslintrc@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" + integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.2.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" + integrity sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +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" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +autobind-decorator@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c" + integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.1, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +esbuild-android-arm64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.17.tgz#7216810cb8d5b8cd03ce70bdc241dcdd90c34755" + integrity sha512-y7EJm8ADC9qKbo/dJ2zBXwNdIILJ76tTv7JDGvOkbLT8HJXIsgbpa0NJk7iFhyvP4GpsYvXTbvEQNn0DhyBhLA== + +esbuild-darwin-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.17.tgz#1419e020f41814f8a74ce92b2dcab29a6d47e510" + integrity sha512-V2JAP8yyVbW6qR4SVXsEDqRicYM0x5niUuB05IFiE5itPI45k8j2dA2l+DtirR2SGXr+LEqgX347+2VA6eyTiA== + +esbuild-darwin-arm64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.17.tgz#95acf1022066d48346a63ffc5e4d36a07b83c9b0" + integrity sha512-ENkSKpjF4SImyA2TdHhKiZqtYc1DkMykICe1KSBw0YNF1sentjFI6wu+CRiYMpC7REf/3TQXoems2XPqIqDMlQ== + +esbuild-freebsd-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.17.tgz#a3455199862110854937b05a0eecbed3e1aeec41" + integrity sha512-2i0nTNJM8ftNTvtR00vdqkru8XpHwAbkR2MBLoK2IDSzjsLStwCj+mxf6v83eVM9Abe3QA8xP+irqOdBlwDQ2g== + +esbuild-freebsd-arm64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.17.tgz#8a70f2a36f5b0da7d2efdd6fd02aa78611007fd0" + integrity sha512-QOmRi1n+uly2G7BbMbHb86YiFA5aM7B2T96A6OF1VG57LNwXwy8LPVM0PVjl7f9cV3pE3fy3VtXPJHJo8XggTA== + +esbuild-linux-32@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.17.tgz#b7123f6e4780687e017454604d909fbe558862e9" + integrity sha512-qG5NDk7FHHUVw01rjHESON0HvigF2X80b645TUlgTKsWRlrbzzHhMCmQguA01O5PiCimKnyoxti8aJIFNHpQnQ== + +esbuild-linux-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.17.tgz#47a6b510c2f7faef595a4d6257a629e65385fdc3" + integrity sha512-De8OcmNvfNyFfQRLWbfuZqau6NpYBJxNTLP7Ls/PqQcw0HAwfaYThutY8ozHpPbKFPa7wgqabXlIC4NVSWT0/A== + +esbuild-linux-arm64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.17.tgz#dfd9022b7215ca660d464fcb20597b88887c7e64" + integrity sha512-WDEOD/YRA4J1lxhETKZff3gRxGYqqZEiVwIOqNfvCh2YcwWU2y6UmNGZsxcuKk18wot4dAXCXQyNZgBkVUTCLw== + +esbuild-linux-arm@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.17.tgz#e6f6bb9fe52def5260d7d49b790fbec0e7c6d9cb" + integrity sha512-ZwsgFUk3gR2pEMJdh5z4Ds18fvGETgElPqmNdx1NtZTCOVlFMAwFB5u/tOR2FrXbMFv+LkGnNxPDh48PYPDz9A== + +esbuild-linux-mips64le@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.17.tgz#bceaad33ff18a822b6da0396c6497a231397b6c3" + integrity sha512-Lf4X9NB7r6imzp/11TaGs4kWL0DUn1JxI9gAAKotnKh6T8Y/0sLvZSvQS8WvSZcr0V8RRCrRZwiQqjOALUU/9g== + +esbuild-linux-ppc64le@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.17.tgz#9562f094d1e5e6c3b61b776b15a9bbd657042654" + integrity sha512-aExhxbrK7/Mh9FArdiC9MbvrQz2bGCDI8cBALKJbmhKg0h7LNt6y1E1S9GGBZ/ZXkHDvV9FFVrXXZKFVU5Qpiw== + +esbuild-linux-s390x@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.17.tgz#2963cfe62c227bbf1da64e36d4ca0b23db8008fe" + integrity sha512-b0T20rNcS7POi5YLw5dFlsiC+riobR5IfppQGn5NWer6QiIkdL1vOx9eC9CUD3z1itpkLboRAZYieZfKfhCA2Q== + +esbuild-netbsd-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.17.tgz#1d156023f9ae6be79b8627ab0cda2d7feb7f3a48" + integrity sha512-pFgTaAa2JF18nqNfCND9wOu1jbZ/mbDSaMxUp5fTkLlofyHhXeb5aChgXUkeipty2Pgq0OwOnxjHmiAxMI7N4g== + +esbuild-openbsd-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.17.tgz#3fc44102c9b65375385112f4ce5899ae5e38f349" + integrity sha512-K5+plb6gsAfBcFqB0EG4KvLbgBKslVAfEyJggicwt/QoDwQGJAzao4M6zOA4PG7LlXOwWSqv7VmSFbH+b6DyKw== + +esbuild-sunos-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.17.tgz#5bd24e7a7e863ea89d7e4eafd5364a155c9ea507" + integrity sha512-o1FINkbHRi9JB1YteOSXZdkDOmVUbmnCxRmTLkHvk8pfCFNpv/5/7ktt95teYKbEiJna2dEt3M4ckJ/+UVnW+w== + +esbuild-windows-32@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.17.tgz#8bda31c550fb6b425707114141d2c6ba034dab9b" + integrity sha512-Qutilz0I7OADWBtWrC/FD+2O/TNAkhwbZ+wIns7kF87lxIMtmqpBt3KnMk1e4F47aTrZRr0oH55Zhztd7m2PAA== + +esbuild-windows-64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.17.tgz#50b42c06908d3ce9fab8f0f9673199de5d0f9cbc" + integrity sha512-b21/oRV+PHrav0HkRpKjbM2yNRVe34gAfbdMppbZFea416wa8SrjcmVfSd7n4jgqoTQG0xe+MGgOpwXtjiB3DQ== + +esbuild-windows-arm64@0.14.17: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.17.tgz#62d3921a810b64a03fcace76dad4db51d2128b45" + integrity sha512-4HN9E1idllewYvptcrrdfTA6DIWgg11kK0Zrv6yjxstJZLJeKxfilGBEaksLGs4Pst2rAYMx3H2vbYq7AWLQNA== + +esbuild@^0.14.13: + version "0.14.17" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.17.tgz#6a634e56447aa0e90b34c42091d472d802d399e5" + integrity sha512-JLgyC6Uv31mv9T9Mm2xF1LntUMCNBSzvg2n32d8cTKZMwFr1wmMFY2FkVum98TSoEsDff0cR+Aj49H2sbBcjKQ== + optionalDependencies: + esbuild-android-arm64 "0.14.17" + esbuild-darwin-64 "0.14.17" + esbuild-darwin-arm64 "0.14.17" + esbuild-freebsd-64 "0.14.17" + esbuild-freebsd-arm64 "0.14.17" + esbuild-linux-32 "0.14.17" + esbuild-linux-64 "0.14.17" + esbuild-linux-arm "0.14.17" + esbuild-linux-arm64 "0.14.17" + esbuild-linux-mips64le "0.14.17" + esbuild-linux-ppc64le "0.14.17" + esbuild-linux-s390x "0.14.17" + esbuild-netbsd-64 "0.14.17" + esbuild-openbsd-64 "0.14.17" + esbuild-sunos-64 "0.14.17" + esbuild-windows-32 "0.14.17" + esbuild-windows-64 "0.14.17" + esbuild-windows-arm64 "0.14.17" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-scope@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" + integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" + integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== + +eslint@^8.2.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.8.0.tgz#9762b49abad0cb4952539ffdb0a046392e571a2d" + integrity sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ== + dependencies: + "@eslint/eslintrc" "^1.0.5" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.2.0" + espree "^9.3.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.2.0, espree@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" + integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.1.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.6.0, globals@^13.9.0: + version "13.12.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" + integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== + dependencies: + type-fest "^0.20.2" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +idb-keyval@^6.0.3: + version "6.1.0" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.1.0.tgz#e659cff41188e6097d7fadd69926f6adbbe70041" + integrity sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw== + dependencies: + safari-14-idb-fix "^3.0.0" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +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== + dependencies: + brace-expansion "^1.1.7" + +misskey-js@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" + integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== + dependencies: + autobind-decorator "^2.4.0" + eventemitter3 "^4.0.7" + reconnecting-websocket "^4.4.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safari-14-idb-fix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440" + integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/scripts/build.js b/scripts/build.js index 783af7827..608648b95 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -17,6 +17,14 @@ const execa = require('execa'); stderr: process.stderr, }); + console.log('building packages/sw ...'); + + await execa('npm', ['run', 'build'], { + cwd: __dirname + '/../packages/sw', + stdout: process.stdout, + stderr: process.stderr, + }); + console.log('build finishing ...'); await execa('npm', ['run', 'gulp'], { diff --git a/scripts/clean-all.js b/scripts/clean-all.js index 814ff3f25..456b88032 100644 --- a/scripts/clean-all.js +++ b/scripts/clean-all.js @@ -7,6 +7,9 @@ const fs = require('fs'); fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/client/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../node_modules', { recursive: true, force: true }); })(); diff --git a/scripts/clean.js b/scripts/clean.js index a14f1fb35..70b9d882b 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -3,5 +3,6 @@ const fs = require('fs'); (async () => { fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/client/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../built', { recursive: true, force: true }); })(); diff --git a/scripts/dev.js b/scripts/dev.js index b7dd870c4..c5dbb7b35 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -25,6 +25,12 @@ const execa = require('execa'); stderr: process.stderr, }); + execa('npm', ['run', 'watch'], { + cwd: __dirname + '/../packages/sw', + stdout: process.stdout, + stderr: process.stderr, + }); + const start = async () => { try { await execa('npm', ['run', 'start'], { diff --git a/scripts/install-packages.js b/scripts/install-packages.js index c25063b29..bc8e016a3 100644 --- a/scripts/install-packages.js +++ b/scripts/install-packages.js @@ -16,4 +16,12 @@ const execa = require('execa'); stdout: process.stdout, stderr: process.stderr, }); + + console.log('installing dependencies of packages/sw ...'); + + await execa('yarn', ['install'], { + cwd: __dirname + '/../packages/sw', + stdout: process.stdout, + stderr: process.stderr, + }); })(); diff --git a/scripts/lint.js b/scripts/lint.js index 11aa4909b..72a63f4ba 100644 --- a/scripts/lint.js +++ b/scripts/lint.js @@ -14,4 +14,11 @@ const execa = require('execa'); stdout: process.stdout, stderr: process.stderr, }); + + console.log('linting packages/sw ...'); + await execa('npm', ['run', 'lint'], { + cwd: __dirname + '/../packages/sw', + stdout: process.stdout, + stderr: process.stderr, + }); })();