From 2f4c48bbe689fbc4344bef383395bbb9b47aa80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:58:03 +0900 Subject: [PATCH 01/13] =?UTF-8?q?spec(frontend):=20=E3=81=BF=E3=81=A4?= =?UTF-8?q?=E3=81=91=E3=82=8B=E3=81=AB=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E9=A0=85=E7=9B=AE=E3=81=AE=E8=AA=BF=E6=95=B4=20(Missk?= =?UTF-8?q?eyIO#783)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MkUserSetupDialog.vue | 2 +- packages/frontend/src/pages/explore.users.vue | 128 ++---------------- 2 files changed, 10 insertions(+), 120 deletions(-) diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 9c95991a7..0adc9ac6a 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - -
- -
-
- - - -
- {{ tag.tag }} - {{ tag.tag }} -
-
- - - - - - - -
+ + + + + + + +
From fcfd004c38053f71340b6bf29714da5386ee62e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:28:14 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat(analytics):=20Google=20Analytics?= =?UTF-8?q?=E3=83=BB=E5=90=8C=E6=84=8F=E3=83=A2=E3=83=BC=E3=83=89=E3=83=BB?= =?UTF-8?q?=E4=B8=80=E9=83=A8=E6=A9=9F=E8=83=BD=E3=81=AE=E3=83=88=E3=83=A9?= =?UTF-8?q?=E3=83=83=E3=82=AD=E3=83=B3=E3=82=B0=E5=AE=9F=E8=A3=85=20(Missk?= =?UTF-8?q?eyIO#784)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .config/docker_example.yml | 2 +- .config/example.yml | 6 +- locales/en-US.yml | 14 ++ locales/index.d.ts | 56 ++++++ locales/ja-JP.yml | 13 ++ locales/ko-KR.yml | 14 ++ .../1730629332694-GoogleAnalyticsId.js | 11 ++ packages/backend/src/NestLogger.ts | 5 +- packages/backend/src/boot/entry.ts | 15 +- packages/backend/src/boot/master.ts | 5 +- packages/backend/src/core/DriveService.ts | 4 +- .../src/core/FetchInstanceMetadataService.ts | 2 +- packages/backend/src/core/LoggerService.ts | 4 +- .../backend/src/core/NoteCreateService.ts | 10 +- .../backend/src/core/UserBlockingService.ts | 2 +- .../backend/src/core/UserFollowingService.ts | 12 +- .../src/core/activitypub/ApRequestService.ts | 3 +- .../src/core/activitypub/ApResolverService.ts | 2 +- .../src/core/entities/MetaEntityService.ts | 1 + packages/backend/src/logger.ts | 26 ++- packages/backend/src/models/Meta.ts | 6 + packages/backend/src/models/Poll.ts | 2 +- .../backend/src/models/json-schema/meta.ts | 4 + .../processors/UserSuspendProcessorService.ts | 2 +- .../src/server/api/SigninApiService.ts | 3 +- .../src/server/api/SignupApiService.ts | 3 +- .../src/server/api/endpoints/admin/meta.ts | 5 + .../server/api/endpoints/admin/update-meta.ts | 5 + .../src/server/api/endpoints/announcement.ts | 4 +- .../api/endpoints/federation/instances.ts | 2 +- .../backend/src/server/api/endpoints/i.ts | 28 ++- .../src/server/api/endpoints/i/update.ts | 6 +- .../src/server/web/ClientServerService.ts | 2 +- packages/backend/test/unit/chart.ts | 4 +- packages/frontend/@types/vue-gtag.d.ts | 14 ++ packages/frontend/package.json | 1 + packages/frontend/src/account.ts | 12 +- packages/frontend/src/boot/common.ts | 38 ++++ packages/frontend/src/boot/main-boot.ts | 27 ++- .../src/components/MkTrackingConsent.vue | 177 ++++++++++++++++++ .../frontend/src/components/global/MkAd.vue | 35 +++- packages/frontend/src/index.html | 10 +- packages/frontend/src/local-storage.ts | 6 +- packages/frontend/src/nirax.ts | 31 +++ .../src/pages/admin/external-services.vue | 13 ++ .../frontend/src/pages/settings/privacy.vue | 108 ++++++++++- packages/frontend/src/router/main.ts | 14 +- packages/frontend/src/scripts/aiscript/api.ts | 2 +- packages/frontend/src/scripts/misskey-api.ts | 62 +++--- packages/frontend/src/scripts/usage-report.ts | 61 ++++++ packages/frontend/vue-shims.d.ts | 2 +- packages/misskey-js/src/autogen/types.ts | 3 + pnpm-lock.yaml | 19 +- 53 files changed, 805 insertions(+), 113 deletions(-) create mode 100644 packages/backend/migration/1730629332694-GoogleAnalyticsId.js create mode 100644 packages/frontend/@types/vue-gtag.d.ts create mode 100644 packages/frontend/src/components/MkTrackingConsent.vue create mode 100644 packages/frontend/src/scripts/usage-report.ts diff --git a/.config/docker_example.yml b/.config/docker_example.yml index d80516ce2..9e9c050e1 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -195,4 +195,4 @@ signToActivityPubGet: true #maxFileSize: 262144000 # Value of Content-Security-Policy header -#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';" +#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/; base-uri 'self'; object-src 'self';" diff --git a/.config/example.yml b/.config/example.yml index 7612c063e..6c1a15161 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -118,7 +118,7 @@ redis: # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -214,7 +214,7 @@ proxyRemoteFiles: true signToActivityPubGet: true # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' @@ -227,4 +227,4 @@ signToActivityPubGet: true #pidFile: /tmp/misskey.pid # Value of Content-Security-Policy header -#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/; base-uri 'self'; object-src 'self';" +#contentSecurityPolicy: "script-src 'self' 'unsafe-eval' https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/; base-uri 'self'; object-src 'self';" diff --git a/locales/en-US.yml b/locales/en-US.yml index 5e25dee5d..bd8e60e6a 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1273,6 +1273,20 @@ here: "here" mutualLink: "Mutual Link" saveThisFile: "Save this file to Drive" changeUserName: "Change name" +gtagConsentCustomize: "Data Collection and Privacy Settings" +gtagConsentCustomizeDescription: "You can customize the scope of data collected by {host}.\nHowever, you cannot disable the collection of security-related information such as authentication features, fraud prevention, and other user protections." +gtagConsentAnalytics: "Collection of Statistical Information" +gtagConsentAnalyticsDescription: "Enable the storage (cookies, etc.) of analytics-related information such as site visit duration." +gtagConsentFunctionality: "Collection of Feature and Setting Usage" +gtagConsentFunctionalityDescription: "Enable the storage of information that supports website or app features, such as language settings." +gtagConsentPersonalization: "Collection of Personalized Information" +gtagConsentPersonalizationDescription: "Enable the storage of personalization-related information such as recommended posts." +helpUsImproveUserExperience: "To build the future of Misskey,\nplease help us by agreeing to data collection!" +pleaseConsentToTracking: "{host} may collect information that may include personal data such as your IP address, usage data, and device information during your use, based on our [Privacy Policy]({privacyPolicyUrl}), for the purpose of providing and operating the service and improving the user experience.\n\nThe collected data will be used for future feature development, operational policy decisions, and identifying areas for service improvement." +consentEssential: "Allow Essential Items" +consentAll: "Allow All Items" +consentSelected: "Allow Selected Items" + _bubbleGame: howToPlay: "How to play" hold: "Hold" diff --git a/locales/index.d.ts b/locales/index.d.ts index 482457d78..3c2838b77 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5150,6 +5150,62 @@ export interface Locale extends ILocale { * 名前を変更 */ "changeUserName": string; + /** + * データ収集とプライバシー設定 + */ + "gtagConsentCustomize": string; + /** + * {host}が収集するデータの範囲をカスタマイズできます。 + * ただし、認証機能、不正行為防止、その他のユーザー保護など、セキュリティに関連する情報の収集は無効化できません。 + */ + "gtagConsentCustomizeDescription": ParameterizedString<"host">; + /** + * 統計情報の収集 + */ + "gtagConsentAnalytics": string; + /** + * サイトの滞在時間など、分析に関連する情報の保存(Cookie など)を有効にします。 + */ + "gtagConsentAnalyticsDescription": string; + /** + * 機能・設定の利用状況の収集 + */ + "gtagConsentFunctionality": string; + /** + * 言語設定など、ウェブサイトやアプリの機能をサポートする情報の保存を有効にします。 + */ + "gtagConsentFunctionalityDescription": string; + /** + * パーソナライズされた情報の収集 + */ + "gtagConsentPersonalization": string; + /** + * おすすめの投稿など、パーソナライズに関連する情報の保存を有効にします。 + */ + "gtagConsentPersonalizationDescription": string; + /** + * Misskeyの明日を作るために、 + * データ収集にご協力ください! + */ + "helpUsImproveUserExperience": string; + /** + * {host}は[プライバシーポリシー]({privacyPolicyUrl})に基づき、サービスの提供・運営・ユーザー体験の向上のためにご利用中のIPアドレス、利用状況、デバイス情報等、個人情報を含む可能性のある情報を収集することがあります。 + * + * 収集されたデータは今後の機能の開発、運営の方針の決定、サービスの改善点の特定に利用されます。 + */ + "pleaseConsentToTracking": ParameterizedString<"host" | "privacyPolicyUrl">; + /** + * 必須項目のみ許可 + */ + "consentEssential": string; + /** + * 全て許可 + */ + "consentAll": string; + /** + * 選択した項目のみ許可 + */ + "consentSelected": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1693d9c7b..9e0663d58 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1282,6 +1282,19 @@ here: "こちら" mutualLink: "相互リンク" saveThisFile: "このファイルをドライブに保存する" changeUserName: "名前を変更" +gtagConsentCustomize: "データ収集とプライバシー設定" +gtagConsentCustomizeDescription: "{host}が収集するデータの範囲をカスタマイズできます。\nただし、認証機能、不正行為防止、その他のユーザー保護など、セキュリティに関連する情報の収集は無効化できません。" +gtagConsentAnalytics: "統計情報の収集" +gtagConsentAnalyticsDescription: "サイトの滞在時間など、分析に関連する情報の保存(Cookie など)を有効にします。" +gtagConsentFunctionality: "機能・設定の利用状況の収集" +gtagConsentFunctionalityDescription: "言語設定など、ウェブサイトやアプリの機能をサポートする情報の保存を有効にします。" +gtagConsentPersonalization: "パーソナライズされた情報の収集" +gtagConsentPersonalizationDescription: "おすすめの投稿など、パーソナライズに関連する情報の保存を有効にします。" +helpUsImproveUserExperience: "Misskeyの明日を作るために、\nデータ収集にご協力ください!" +pleaseConsentToTracking: "{host}は[プライバシーポリシー]({privacyPolicyUrl})に基づき、サービスの提供・運営・ユーザー体験の向上のためにご利用中のIPアドレス、利用状況、デバイス情報等、個人情報を含む可能性のある情報を収集することがあります。\n\n収集されたデータは今後の機能の開発、運営の方針の決定、サービスの改善点の特定に利用されます。" +consentEssential: "必須項目のみ許可" +consentAll: "全て許可" +consentSelected: "選択した項目のみ許可" _bubbleGame: howToPlay: "遊び方" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 0cdfb2d7b..097c71a12 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1266,6 +1266,20 @@ here: "여기" mutualLink: "서로링크" saveThisFile: "이 파일을 드라이브에 저장" changeUserName: "이름 변경" +gtagConsentCustomize: "데이터 수집 및 개인정보 설정" +gtagConsentCustomizeDescription: "{host}에서 수집하는 데이터 범위를 사용자 지정할 수 있습니다.\n다만, 인증 기능, 부정 행위 방지, 기타 사용자 보호 등 보안과 관련된 정보 수집은 비활성화할 수 없습니다." +gtagConsentAnalytics: "통계 정보 수집" +gtagConsentAnalyticsDescription: "사이트 체류 시간 등 분석 관련 정보 저장(쿠키 등)을 활성화합니다." +gtagConsentFunctionality: "기능 및 설정 사용 정보 수집" +gtagConsentFunctionalityDescription: "언어 설정 등 웹사이트나 앱의 기능을 지원하는 정보 저장을 활성화합니다." +gtagConsentPersonalization: "개인 맞춤형 정보 수집" +gtagConsentPersonalizationDescription: "추천 게시물 등 개인화 관련 정보 저장을 활성화합니다." +helpUsImproveUserExperience: "Misskey의 미래를 위해,\n데이터 수집에 협조해 주세요!" +pleaseConsentToTracking: "{host}는 [개인정보 처리방침]({privacyPolicyUrl})에 따라 서비스 제공, 운영, 사용자 경험 향상을 위해 사용 중인 IP 주소, 이용 현황, 디바이스 정보 등 개인 정보를 포함할 수 있는 정보를 수집할 수 있습니다.\n\n수집된 데이터는 향후 기능 개발, 운영 방침 결정, 서비스 개선점 파악에 활용됩니다." +consentEssential: "필수 항목만 허용" +consentAll: "모두 허용" +consentSelected: "선택한 항목만 허용" + _bubbleGame: howToPlay: "설명" hold: "홀드" diff --git a/packages/backend/migration/1730629332694-GoogleAnalyticsId.js b/packages/backend/migration/1730629332694-GoogleAnalyticsId.js new file mode 100644 index 000000000..0f8cbf87f --- /dev/null +++ b/packages/backend/migration/1730629332694-GoogleAnalyticsId.js @@ -0,0 +1,11 @@ +export class GoogleAnalyticsId1730629332694 { + name = 'GoogleAnalyticsId1730629332694' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsId" character varying(32)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsId"`); + } +} diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts index 80f1f7a02..8e03dcd7d 100644 --- a/packages/backend/src/NestLogger.ts +++ b/packages/backend/src/NestLogger.ts @@ -4,10 +4,9 @@ */ import { LoggerService } from '@nestjs/common'; -import Logger from '@/logger.js'; +import { coreLogger } from '@/logger.js'; -const logger = new Logger('core', 'cyan'); -const nestLogger = logger.createSubLogger('nest', 'green', false); +const nestLogger = coreLogger.createSubLogger('nest', 'green', false); export class NestLogger implements LoggerService { /** diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 9acb89929..dbfb53878 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -12,7 +12,7 @@ import { EventEmitter } from 'node:events'; import process from 'node:process'; import chalk from 'chalk'; import Xev from 'xev'; -import Logger from '@/logger.js'; +import { coreLogger } from '@/logger.js'; import { envOption } from '../env.js'; import { masterMain } from './master.js'; import { workerMain } from './worker.js'; @@ -24,8 +24,7 @@ process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`; Error.stackTraceLimit = Infinity; EventEmitter.defaultMaxListeners = 128; -const logger = new Logger('core', 'cyan'); -const clusterLogger = logger.createSubLogger('cluster', 'orange', false); +const clusterLogger = coreLogger.createSubLogger('cluster', 'orange', false); const ev = new Xev(); //#region Events @@ -53,12 +52,12 @@ if (cluster.isPrimary && !envOption.disableClustering) { }); process.on('SIGINT', () => { - logger.warn(chalk.yellow('Process received SIGINT')); + coreLogger.warn(chalk.yellow('Process received SIGINT')); isShuttingDown = true; }); process.on('SIGTERM', () => { - logger.warn(chalk.yellow('Process received SIGTERM')); + coreLogger.warn(chalk.yellow('Process received SIGTERM')); isShuttingDown = true; }); } @@ -71,18 +70,18 @@ if (!envOption.quiet) { // Display detail of uncaught exception process.on('uncaughtException', err => { try { - logger.error(`Uncaught exception: ${err.message}`, { error: err }); + coreLogger.error(`Uncaught exception: ${err.message}`, { error: err }); } catch { } }); // Dying away... process.on('exit', code => { - logger.warn(chalk.yellow(`The process is going to exit with code ${code}`)); + coreLogger.warn(chalk.yellow(`The process is going to exit with code ${code}`)); }); process.on('warning', warning => { if ((warning as never)['code'] !== 'MISSKEY_SHUTDOWN') return; - logger.warn(chalk.yellow(`${warning.message}: ${(warning as never)['detail']}`)); + coreLogger.warn(chalk.yellow(`${warning.message}: ${(warning as never)['detail']}`)); for (const id in cluster.workers) cluster.workers[id]?.process.kill('SIGTERM'); process.exit(); }); diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index b442e1aa4..b14a8f4dc 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -10,7 +10,7 @@ import * as os from 'node:os'; import cluster from 'node:cluster'; import chalk from 'chalk'; import chalkTemplate from 'chalk-template'; -import Logger from '@/logger.js'; +import { coreLogger } from '@/logger.js'; import { loadConfig } from '@/config.js'; import type { Config } from '@/config.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; @@ -22,8 +22,7 @@ const _dirname = dirname(_filename); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); -const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta', false); +const bootLogger = coreLogger.createSubLogger('boot', 'magenta', false); const themeColor = chalk.hex('#86b300'); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index b15fbeaab..08ea127de 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { LoggerService } from '@/core/LoggerService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -123,12 +124,13 @@ export class DriveService { private globalEventService: GlobalEventService, private queueService: QueueService, private roleService: RoleService, + private loggerService: LoggerService, private moderationLogService: ModerationLogService, private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, ) { - const logger = new Logger('drive', 'blue'); + const logger = this.loggerService.getLogger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); this.downloaderLogger = logger.createSubLogger('downloader'); this.deleteLogger = logger.createSubLogger('delete'); diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index ceca6a27d..231e09af1 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -159,7 +159,7 @@ export class FetchInstanceMetadataService { throw err.statusCode ?? err.message; }); - this.logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); + this.logger.succ(`Successfully fetched nodeinfo of ${instance.host}`); return info as NodeInfo; } catch (err) { diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index a3d371a30..cd5d097e9 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -4,7 +4,7 @@ */ import { Injectable } from '@nestjs/common'; -import Logger from '@/logger.js'; +import { rootLogger } from '@/logger.js'; import { bindThis } from '@/decorators.js'; import type { KEYWORD } from 'color-convert/conversions.js'; @@ -16,6 +16,6 @@ export class LoggerService { @bindThis public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { - return new Logger(domain); + return rootLogger.createSubLogger(domain, color, store); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 66acd6b22..20c7f3e2b 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -257,7 +257,7 @@ export class NoteCreateService implements OnApplicationShutdown { const policies = await this.roleService.getUserPolicies(user.id); if (!policies.canCreateContent) { - this.logger.error('Request rejected because user has no permission to create content', { user: user.id, note: data }); + this.logger.error('Request rejected because user has no permission to create content', { userId: user.id, note: data }); throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.'); } @@ -265,7 +265,7 @@ export class NoteCreateService implements OnApplicationShutdown { const sensitiveWords = meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) { data.visibility = 'home'; - this.logger.warn('Visibility changed to home because sensitive words are included', { user: user.id, note: data }); + this.logger.warn('Visibility changed to home because sensitive words are included', { userId: user.id, note: data }); } else if (!policies.canPublicNote) { data.visibility = 'home'; } @@ -281,7 +281,7 @@ export class NoteCreateService implements OnApplicationShutdown { ); if (hasProhibitedWords) { - this.logger.error('Request rejected because prohibited words are included', { user: user.id, note: data }); + this.logger.error('Request rejected because prohibited words are included', { userId: user.id, note: data }); throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Notes including prohibited words are not allowed.'); } @@ -384,7 +384,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (process.env.MISSKEY_BLOCK_MENTIONS_FROM_UNFAMILIAR_REMOTE_USERS === 'true' && user.host !== null && willCauseNotification) { const userEntity = await this.usersRepository.findOneBy({ id: user.id }); if ((userEntity?.followersCount ?? 0) === 0) { - this.logger.error('Request rejected because user has no local followers', { user: user.id, note: data }); + this.logger.error('Request rejected because user has no local followers', { userId: user.id, note: data }); throw new IdentifiableError('e11b3a16-f543-4885-8eb1-66cad131dbfd', 'Notes including mentions, replies, or renotes from remote users are not allowed until user has at least one local follower.'); } } @@ -396,7 +396,7 @@ export class NoteCreateService implements OnApplicationShutdown { || (data.visibility === 'specified' && data.visibleUsers?.some(u => u.id !== user.id)) || (this.isQuote(data) && data.renote.userId !== user.id) ) { - this.logger.error('Request rejected because user has no permission to initiate conversation', { user: user.id, note: data }); + this.logger.error('Request rejected because user has no permission to initiate conversation', { userId: user.id, note: data }); throw new IdentifiableError('332dd91b-6a00-430a-ac39-620cf60ad34b', 'Notes including mentions, replies, or renotes are not allowed.'); } } diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 96f389b54..9ab2c0271 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -50,7 +50,7 @@ export class UserBlockingService implements OnModuleInit { private apRendererService: ApRendererService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('user-block'); + this.logger = this.loggerService.getLogger('user:block'); } onModuleInit() { diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 5949c70a5..753eb3592 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -30,9 +30,8 @@ import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { ThinUser } from '@/queue/types.js'; -import Logger from '../logger.js'; - -const logger = new Logger('following/create'); +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; type Local = MiLocalUser | { id: MiLocalUser['id']; @@ -50,6 +49,7 @@ type Both = Local | Remote; @Injectable() export class UserFollowingService implements OnModuleInit { private userBlockingService: UserBlockingService; + private readonly logger: Logger; constructor( private moduleRef: ModuleRef, @@ -73,6 +73,7 @@ export class UserFollowingService implements OnModuleInit { private instancesRepository: InstancesRepository, private cacheService: CacheService, + private loggerService: LoggerService, private utilityService: UtilityService, private userEntityService: UserEntityService, private idService: IdService, @@ -88,6 +89,7 @@ export class UserFollowingService implements OnModuleInit { private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { + this.logger = this.loggerService.getLogger('user:following'); } onModuleInit() { @@ -255,7 +257,7 @@ export class UserFollowingService implements OnModuleInit { followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null, }).catch(err => { if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { - logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); + this.logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); alreadyFollowed = true; } else { throw err; @@ -378,7 +380,7 @@ export class UserFollowingService implements OnModuleInit { }); if (following === null || !following.follower || !following.followee) { - logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + this.logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); return; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 93ac8ce9a..b008a3ec5 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -145,8 +145,7 @@ export class ApRequestService { private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる + this.logger = this.loggerService.getLogger('ap:request'); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bb3c40f09..fbe32ba18 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -45,7 +45,7 @@ export class Resolver { private recursionLimit = 100, ) { this.history = new Set(); - this.logger = this.loggerService.getLogger('ap-resolve'); + this.logger = this.loggerService.getLogger('ap:resolve'); } @bindThis diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 7fbde9008..28780b8d4 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -77,6 +77,7 @@ export class MetaEntityService { recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + googleAnalyticsId: instance.googleAnalyticsId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index b2ffbea98..1e506596b 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -22,13 +22,14 @@ const pinoPrettyStream = pinoPretty({ // eslint-disable-next-line import/no-default-export export default class Logger { - private readonly domain: string; - private logger: pino.Logger; + private readonly domain: string | undefined; + private readonly logger: pino.Logger; private context: Record = {}; - constructor(domain: string, _color?: KEYWORD, _store = true, parentLogger?: Logger) { + constructor(domain: string | undefined, _color?: KEYWORD, _store = true, parentLogger?: Logger) { if (parentLogger) { - this.domain = parentLogger.domain + '.' + domain; + this.domain = [parentLogger.domain, domain].filter(x => x).join('.') || undefined; + this.context = { ...JSON.parse(JSON.stringify(parentLogger.context)) }; } else { this.domain = domain; } @@ -50,18 +51,20 @@ export default class Logger { formatters: { level: (label, number) => ({ severity: label, level: number }), }, - mixin: () => ({ cluster: cluster.isPrimary ? 'primary' : `worker#${cluster.worker!.id}`, ...this.context }), + mixin: () => this.mixin(), }, !envOption.logJson ? pinoPrettyStream : undefined); } - @bindThis - public createSubLogger(domain: string, _color?: KEYWORD, _store = true): Logger { - return new Logger(domain, undefined, false, this); + private mixin(): Record { + return { cluster: cluster.isPrimary ? 'primary' : `worker#${cluster.worker!.id}`, ...this.context }; + } + + public createSubLogger(domain?: string, _color?: KEYWORD, _store = true): Logger { + return new Logger(domain, _color, _store, this); } - @bindThis public setContext(context: Record): void { - this.context = context; + this.context = { ...this.context, ...JSON.parse(JSON.stringify(context)) }; } @bindThis @@ -130,3 +133,6 @@ export default class Logger { this.logger.info({ context, important }, message); } } + +export const rootLogger = new Logger(undefined, undefined, false, undefined); +export const coreLogger = rootLogger.createSubLogger('core', 'cyan'); diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index d346b7afb..649b2c2f9 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -260,6 +260,12 @@ export class MiMeta { // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること + @Column('varchar', { + length: 32, + nullable: true, + }) + public googleAnalyticsId: string | null; + @Column('enum', { enum: ['none', 'all', 'local', 'remote'], default: 'none', diff --git a/packages/backend/src/models/Poll.ts b/packages/backend/src/models/Poll.ts index ca985c8b2..b3d33a726 100644 --- a/packages/backend/src/models/Poll.ts +++ b/packages/backend/src/models/Poll.ts @@ -8,7 +8,7 @@ import { noteVisibilities } from '@/types.js'; import { id } from './util/id.js'; import { MiNote } from './Note.js'; import type { MiUser } from './User.js'; -import type { MiChannel } from "@/models/Channel.js"; +import type { MiChannel } from '@/models/Channel.js'; @Entity('poll') export class MiPoll { diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index bff146114..7314a224d 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -111,6 +111,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + googleAnalyticsId: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/queue/processors/UserSuspendProcessorService.ts b/packages/backend/src/queue/processors/UserSuspendProcessorService.ts index 9f4c2576a..f2312863b 100644 --- a/packages/backend/src/queue/processors/UserSuspendProcessorService.ts +++ b/packages/backend/src/queue/processors/UserSuspendProcessorService.ts @@ -13,7 +13,7 @@ import type { } from '@/models/_.js'; import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; import type * as Bull from "bullmq"; -import type { DbUserSuspendJobData } from "@/queue/types.js"; +import type { DbUserSuspendJobData } from '@/queue/types.js'; @Injectable() export class UserSuspendProcessorService { diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 58becdbc3..ed661a1fa 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -27,6 +27,7 @@ import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { FastifyReply, FastifyRequest } from 'fastify'; +import { randomUUID } from 'node:crypto'; @Injectable() export class SigninApiService { @@ -71,7 +72,7 @@ export class SigninApiService { reply: FastifyReply, ) { const logger = this.loggerService.getLogger('api:signin'); - logger.setContext({ username: request.body.username, ip: request.ip, headers: request.headers }); + logger.setContext({ username: request.body.username, ip: request.ip, headers: request.headers, span: request.headers['x-client-transaction-id'] ?? randomUUID() }); logger.info('Requested to sign in.'); reply.header('Access-Control-Allow-Origin', this.config.url); diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 2b2ece6c4..a0f909d93 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -22,6 +22,7 @@ import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { LoggerService } from '@/core/LoggerService.js'; import { SigninService } from './SigninService.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { randomUUID } from 'node:crypto'; @Injectable() export class SignupApiService { @@ -73,7 +74,7 @@ export class SignupApiService { reply: FastifyReply, ) { const logger = this.loggerService.getLogger('api:signup'); - logger.setContext({ username: request.body.username, email: request.body.emailAddress, ip: request.ip, headers: request.headers }); + logger.setContext({ username: request.body.username, email: request.body.emailAddress, ip: request.ip, headers: request.headers, span: request.headers['x-client-transaction-id'] ?? randomUUID() }); logger.info('Requested to create user account.'); const body = request.body; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 3081faccc..2b9f5c6e3 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -69,6 +69,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + googleAnalyticsId: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -557,6 +561,7 @@ export default class extends Endpoint { // eslint- recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + googleAnalyticsId: instance.googleAnalyticsId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 9948e06f8..73b20bf42 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -78,6 +78,7 @@ export const paramDef = { enableTurnstile: { type: 'boolean' }, turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true }, + googleAnalyticsId: { type: 'string', nullable: true }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, @@ -374,6 +375,10 @@ export default class extends Endpoint { // eslint- set.turnstileSecretKey = ps.turnstileSecretKey; } + if (ps.googleAnalyticsId !== undefined) { + set.googleAnalyticsId = ps.googleAnalyticsId; + } + if (ps.sensitiveMediaDetection !== undefined) { set.sensitiveMediaDetection = ps.sensitiveMediaDetection; } diff --git a/packages/backend/src/server/api/endpoints/announcement.ts b/packages/backend/src/server/api/endpoints/announcement.ts index 98b1746ae..1d333c184 100644 --- a/packages/backend/src/server/api/endpoints/announcement.ts +++ b/packages/backend/src/server/api/endpoints/announcement.ts @@ -6,8 +6,8 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; -import { EntityNotFoundError } from "typeorm"; -import { ApiError } from "../error.js"; +import { EntityNotFoundError } from 'typeorm'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['meta'], diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 76c785fd9..4ce0f299b 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -80,7 +80,7 @@ export default class extends Endpoint { // eslint- ) { super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => { const logger = this.loggerService.getLogger('api:federation:instances'); - logger.setContext({ params: ps, user: me?.id, ip, headers }); + logger.setContext({ params: ps, userId: me?.id, ip, headers }); logger.info('Requested to fetch federated instances.'); const query = this.instancesRepository.createQueryBuilder('instance'); diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts index 3f4008a06..4d6c60bad 100644 --- a/packages/backend/src/server/api/endpoints/i.ts +++ b/packages/backend/src/server/api/endpoints/i.ts @@ -8,7 +8,9 @@ import type { UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../error.js'; +import { ApiError } from '@/server/api/error.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { randomUUID } from 'node:crypto'; export const meta = { tags: ['account'], @@ -44,11 +46,16 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + private loggerService: LoggerService, private userEntityService: UserEntityService, ) { - super(meta, paramDef, async (ps, user, token) => { + super(meta, paramDef, async (ps, user, token, _file, _cleanup, ip, headers) => { const isSecure = token == null; + const logger = this.loggerService.getLogger('api:account:i'); + logger.setContext({ userId: user?.id, username: user?.username, client: isSecure ? 'misskey' : 'app', ip, headers, span: (headers ? headers['x-client-transaction-id'] : undefined) ?? randomUUID() }); + logger.info('Fetching account information'); + const now = new Date(); const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; @@ -71,11 +78,18 @@ export default class extends Endpoint { // eslint- userProfile.loggedInDates = [...userProfile.loggedInDates, today]; } - return await this.userEntityService.pack(userProfile.user!, userProfile.user!, { - schema: 'MeDetailed', - includeSecrets: isSecure, - userProfile, - }); + try { + const result = await this.userEntityService.pack(userProfile.user!, userProfile.user!, { + schema: 'MeDetailed', + includeSecrets: isSecure, + userProfile, + }); + logger.info('Returning account information'); + return result; + } catch (error) { + logger.error('Failed to pack user entity', { error }); + throw error; + } }); } } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c055ff3e3..fb3ea38f8 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -33,9 +33,9 @@ import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; -import { ApiLoggerService } from '../../ApiLoggerService.js'; -import { ApiError } from '../../error.js'; -import { IdService } from "@/core/IdService.js"; +import { ApiLoggerService } from '@/server/api/ApiLoggerService.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; export const meta = { tags: ['account'], diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index f6a57d8fe..deb30e3ce 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -282,7 +282,7 @@ export class ClientServerService { }; const csp = this.config.contentSecurityPolicy ?? 'script-src \'self\' ' + - 'https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ {scriptNonce}; ' + + 'https://challenges.cloudflare.com https://hcaptcha.com https://*.hcaptcha.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.recaptcha.net/recaptcha/ https://www.googletagmanager.com/ {scriptNonce}; ' + 'worker-src blob: \'self\'; ' + 'base-uri \'self\'; object-src \'self\'; report-uri /csp-error'; reply.header('Content-Security-Policy-Report-Only', csp.replace('{scriptNonce}', `'nonce-${scriptNonce}'`)); diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 9dedd3a79..d54f5f431 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -19,7 +19,7 @@ import { entity as TestUniqueChartEntity } from '@/core/chart/charts/entities/te import { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js'; import { loadConfig } from '@/config.js'; import type { AppLockService } from '@/core/AppLockService.js'; -import Logger from '@/logger.js'; +import { coreLogger } from '@/logger.js'; describe('Chart', () => { const config = loadConfig(); @@ -63,7 +63,7 @@ describe('Chart', () => { await db.initialize(); - const logger = new Logger('chart'); // TODO: モックにする + const logger = coreLogger.createSubLogger('chart'); // TODO: モックにする testChart = new TestChart(db, appLockService, logger); testGroupedChart = new TestGroupedChart(db, appLockService, logger); testUniqueChart = new TestUniqueChart(db, appLockService, logger); diff --git a/packages/frontend/@types/vue-gtag.d.ts b/packages/frontend/@types/vue-gtag.d.ts new file mode 100644 index 000000000..c300d516c --- /dev/null +++ b/packages/frontend/@types/vue-gtag.d.ts @@ -0,0 +1,14 @@ +declare module 'vue-gtag' { + export type GtagConsent = (command: 'consent', arg: 'default' | 'update', params: GtagConsentParams): void; + + export interface GtagConsentParams { + ad_storage?: 'granted' | 'denied', + ad_user_data?: 'granted' | 'denied', + ad_personalization?: 'granted' | 'denied', + analytics_storage?: 'granted' | 'denied', + functionality_storage?: 'granted' | 'denied', + personalization_storage?: 'granted' | 'denied', + security_storage?: 'granted' | 'denied', + wait_for_update?: number + } +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2f3f07a48..2f3ba0262 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -75,6 +75,7 @@ "v-code-diff": "1.13.1", "vite": "5.4.9", "vue": "3.5.12", + "vue-gtag": "2.0.1", "vuedraggable": "next" }, "devDependencies": { diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 7f20e0b1a..3ac9da773 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -12,8 +12,10 @@ import { MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; import { apiUrl } from '@/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; +import { generateClientTransactionId, misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; +import { set as gtagSet } from 'vue-gtag'; +import { instance } from '@/instance.js'; // TODO: 他のタブと永続化されたstateを同期 @@ -59,6 +61,7 @@ export async function signout() { }), headers: { 'Content-Type': 'application/json', + 'X-Client-Transaction-Id': generateClientTransactionId('misskey'), }, }); } @@ -109,6 +112,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr }), headers: { 'Content-Type': 'application/json', + 'X-Client-Transaction-Id': generateClientTransactionId('misskey'), }, }) .then(res => new Promise }>((done2, fail2) => { @@ -171,6 +175,12 @@ export function updateAccount(accountData: Partial) { $i[key] = value; } miLocalStorage.setItem('account', JSON.stringify($i)); + if (instance.googleAnalyticsId) { + gtagSet({ + 'client_id': miLocalStorage.getItem('id'), + 'user_id': $i.id, + }); + } } export async function refreshAccount() { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 1f459bb86..aab787540 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -23,6 +23,8 @@ import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { setupRouter } from '@/router/definition.js'; +import { mainRouter } from '@/router/main.js'; +import VueGtag, { bootstrap as gtagBootstrap, GtagConsent, GtagConsentParams } from 'vue-gtag'; export async function common(createVue: () => App) { console.info(`Misskey v${version}`); @@ -60,6 +62,10 @@ export async function common(createVue: () => App) { }); } + if (miLocalStorage.getItem('id') === null) { + miLocalStorage.setItem('id', crypto.randomUUID()); + } + let isClientUpdated = false; //#region クライアントが更新されたかチェック @@ -260,6 +266,38 @@ export async function common(createVue: () => App) { directives(app); components(app); + if (instance.googleAnalyticsId) { + app.use(VueGtag, { + bootstrap: false, + appName: `Misskey v${version}`, + config: { + id: instance.googleAnalyticsId, + params: { + anonymize_ip: false, + send_page_view: true, + }, + }, + }, mainRouter); + + const gtagConsent = miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams ?? { + ad_storage: 'denied', + ad_user_data: 'denied', + ad_personalization: 'denied', + analytics_storage: 'denied', + functionality_storage: 'denied', + personalization_storage: 'denied', + security_storage: 'granted', + }; + miLocalStorage.setItemAsJson('gtagConsent', gtagConsent); + + if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'default', gtagConsent); + + if (miLocalStorage.getItem('gaConsent') === 'true') { + // noinspection ES6MissingAwait + gtagBootstrap(); + } + } + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 // なぜか2回実行されることがあるため、mountするdivを1つに制限する const rootEl = ((): HTMLElement => { diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 825213d3e..bf91e1d1e 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -20,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; +import { instance } from '@/instance.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -234,19 +235,25 @@ export async function mainBoot() { } miLocalStorage.setItem('lastUsed', Date.now().toString()); - const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); - const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); - if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { - if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { - popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + if (!location.pathname.startsWith('/miauth') && !location.pathname.startsWith('/sso') && !location.pathname.startsWith('/oauth')) { + const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); + const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); + if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) { + if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { + popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + } + } + + // const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); + // if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { + // popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); + // } + + if (instance.googleAnalyticsId && miLocalStorage.getItem('gaConsent') === null) { + popup(defineAsyncComponent(() => import('@/components/MkTrackingConsent.vue')), {}, {}, 'closed'); } } - // const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); - // if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { - // popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); - // } - if ('Notification' in window) { // 許可を得ていなかったらリクエスト if (Notification.permission === 'default') { diff --git a/packages/frontend/src/components/MkTrackingConsent.vue b/packages/frontend/src/components/MkTrackingConsent.vue new file mode 100644 index 000000000..77228a611 --- /dev/null +++ b/packages/frontend/src/components/MkTrackingConsent.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 04c50c9c4..4161be2a3 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only rel: 'nofollow noopener', target: '_blank', }" + @click="onAdClicked" > @@ -42,7 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index 935461be9..233c52e6e 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -26,12 +26,12 @@ import { onMounted, onUnmounted, shallowRef, ref } from 'vue'; import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ - withOkButton: boolean; - withCloseButton: boolean; - okButtonDisabled: boolean; - escKeyDisabled: boolean; - width: number; - height: number; + withOkButton?: boolean; + withCloseButton?: boolean; + okButtonDisabled?: boolean; + escKeyDisabled?: boolean; + width?: number; + height?: number; }>(), { withOkButton: false, withCloseButton: true, diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue index 97310d32a..4f75a36fb 100644 --- a/packages/frontend/src/components/MkSignupDialog.vue +++ b/packages/frontend/src/components/MkSignupDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="dialog" :width="500" :height="600" - @close="dialog?.close()" + @close="onClose" @closed="$emit('closed')" > @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="$style.transition_x_leaveTo" > From 0f21232828413c6c77d04d578c39cc41e91b0f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:37:01 +0900 Subject: [PATCH 07/13] :art: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/misskey-dev/misskey/pull/14828 のデザイン修正 --- packages/frontend/src/pages/miauth.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index e65c8a11b..611530c77 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only @deny="onDeny" > diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index ce1d4e48d..a39a7ff26 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.update }} {{ i18n.ts.detach }} - {{ i18n.ts.attach }} + {{ i18n.ts.attach }}
@@ -61,6 +61,7 @@ const props = defineProps<{ id: string; url: string; name: string; + roleIdsThatCanBeUsedThisDecoration: string[]; }; }>(); @@ -83,6 +84,7 @@ const emit = defineEmits<{ const dialog = shallowRef>(); const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); +const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))); const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 9d66cee2d..8009d2739 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -119,6 +119,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat // @public (undocumented) type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json']; @@ -1242,6 +1245,7 @@ declare namespace entities { AdminAbuseReportResolverDeleteRequest, AdminAbuseReportResolverUpdateRequest, AdminAvatarDecorationsCreateRequest, + AdminAvatarDecorationsCreateResponse, AdminAvatarDecorationsDeleteRequest, AdminAvatarDecorationsListRequest, AdminAvatarDecorationsListResponse, diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 7024a456f..5bb885681 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -31,6 +31,7 @@ import type { AdminAbuseReportResolverDeleteRequest, AdminAbuseReportResolverUpdateRequest, AdminAvatarDecorationsCreateRequest, + AdminAvatarDecorationsCreateResponse, AdminAvatarDecorationsDeleteRequest, AdminAvatarDecorationsListRequest, AdminAvatarDecorationsListResponse, @@ -607,7 +608,7 @@ export type Endpoints = { 'admin/abuse-report-resolver/list': { req: AdminAbuseReportResolverListRequest; res: AdminAbuseReportResolverListResponse }; 'admin/abuse-report-resolver/delete': { req: AdminAbuseReportResolverDeleteRequest; res: EmptyResponse }; 'admin/abuse-report-resolver/update': { req: AdminAbuseReportResolverUpdateRequest; res: EmptyResponse }; - 'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse }; + 'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse }; 'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse }; 'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse }; 'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index bfa72c1da..0441ee865 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -34,6 +34,7 @@ export type AdminAbuseReportResolverListResponse = operations['admin___abuse-rep export type AdminAbuseReportResolverDeleteRequest = operations['admin___abuse-report-resolver___delete']['requestBody']['content']['application/json']; export type AdminAbuseReportResolverUpdateRequest = operations['admin___abuse-report-resolver___update']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json']; +export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json']; export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json']; export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5e52060b9..bf31b09dc 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6573,9 +6573,22 @@ export type operations = { }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + }; + }; }; /** @description Client error */ 400: { From 45faba3567a0379d3de0ff840c828ed00703e8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 27 May 2024 17:15:42 +0900 Subject: [PATCH 11/13] =?UTF-8?q?fix(backend):=20`/@`=20=E3=81=AB=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=82=BB=E3=82=B9=E3=81=99=E3=82=8B=E3=81=A8=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=83=BC=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C?= =?UTF-8?q?=E7=99=BA=E7=94=9F=E3=81=99=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(misskey-dev#13884)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from 1df8ea824e5dace883f0d6855d7342984c8032d0 Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- .../backend/src/server/web/ClientServerService.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index deb30e3ce..d5c3ef3b0 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -478,7 +478,9 @@ export class ClientServerService { }; // Atom - fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => { + fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => { + if (request.params.user == null) return await renderBase(reply); + const feed = await getFeed(request.params.user); if (feed) { @@ -491,7 +493,9 @@ export class ClientServerService { }); // RSS - fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => { + fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => { + if (request.params.user == null) return await renderBase(reply); + const feed = await getFeed(request.params.user); if (feed) { @@ -504,7 +508,9 @@ export class ClientServerService { }); // JSON - fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => { + fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => { + if (request.params.user == null) return await renderBase(reply); + const feed = await getFeed(request.params.user); if (feed) { From 57753225ea7194018ecd7c5999bf625336950d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 6 Nov 2024 09:06:44 +0900 Subject: [PATCH 12/13] =?UTF-8?q?fix(backend):=20=E3=83=8E=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=82=92=E9=80=A3=E5=90=88=E3=81=99=E3=82=8B=E9=9A=9B?= =?UTF-8?q?=E3=81=AB=E3=83=AA=E3=83=A2=E3=83=BC=E3=83=88=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AEacct=E3=81=AE=E5=A4=A7=E5=B0=8F?= =?UTF-8?q?=E6=96=87=E5=AD=97=E3=82=92=E5=8C=BA=E5=88=A5=E3=81=97=E3=81=A6?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=81=97=E3=81=A6=E3=81=84=E3=82=8B=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(misskey-dev#14880)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from 6718a54f6fce29edbe2755c31a119e4468fc56e2 Co-authored-by: Laura Hausmann --- packages/backend/src/core/MfmService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 4e1e8d902..3df8aaef1 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -410,7 +410,7 @@ export class MfmService { mention: (node) => { const a = doc.createElement('a'); const { username, host, acct } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); + const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.className = 'u-url mention'; a.textContent = acct; From 410b36b5a085bfddcd63d640619f8c984dd1361a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=82=8F=E3=82=8F=E3=82=8F=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:37:46 +0900 Subject: [PATCH 13/13] Bump up version to 2024.5.0-io.4 (MisskeyIO#789) --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e32299222..d67614966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.5.0-io.3d", + "version": "2024.5.0-io.4", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 354f4d427..3d98a9652 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.5.0-io.3d", + "version": "2024.5.0-io.4", "description": "Misskey SDK for JavaScript", "types": "./built/dts/index.d.ts", "exports": {