mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-23 14:46:40 +09:00
feat(analytics): Google Analytics・同意モード・一部機能のトラッキング実装 (MisskeyIO#784)
This commit is contained in:
parent
2f4c48bbe6
commit
fcfd004c38
@ -195,4 +195,4 @@ signToActivityPubGet: true
|
|||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
# Value of Content-Security-Policy header
|
# 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';"
|
||||||
|
@ -227,4 +227,4 @@ signToActivityPubGet: true
|
|||||||
#pidFile: /tmp/misskey.pid
|
#pidFile: /tmp/misskey.pid
|
||||||
|
|
||||||
# Value of Content-Security-Policy header
|
# 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';"
|
||||||
|
@ -1273,6 +1273,20 @@ here: "here"
|
|||||||
mutualLink: "Mutual Link"
|
mutualLink: "Mutual Link"
|
||||||
saveThisFile: "Save this file to Drive"
|
saveThisFile: "Save this file to Drive"
|
||||||
changeUserName: "Change name"
|
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:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
hold: "Hold"
|
hold: "Hold"
|
||||||
|
56
locales/index.d.ts
vendored
56
locales/index.d.ts
vendored
@ -5150,6 +5150,62 @@ export interface Locale extends ILocale {
|
|||||||
* 名前を変更
|
* 名前を変更
|
||||||
*/
|
*/
|
||||||
"changeUserName": string;
|
"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": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -1282,6 +1282,19 @@ here: "こちら"
|
|||||||
mutualLink: "相互リンク"
|
mutualLink: "相互リンク"
|
||||||
saveThisFile: "このファイルをドライブに保存する"
|
saveThisFile: "このファイルをドライブに保存する"
|
||||||
changeUserName: "名前を変更"
|
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:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -1266,6 +1266,20 @@ here: "여기"
|
|||||||
mutualLink: "서로링크"
|
mutualLink: "서로링크"
|
||||||
saveThisFile: "이 파일을 드라이브에 저장"
|
saveThisFile: "이 파일을 드라이브에 저장"
|
||||||
changeUserName: "이름 변경"
|
changeUserName: "이름 변경"
|
||||||
|
gtagConsentCustomize: "데이터 수집 및 개인정보 설정"
|
||||||
|
gtagConsentCustomizeDescription: "{host}에서 수집하는 데이터 범위를 사용자 지정할 수 있습니다.\n다만, 인증 기능, 부정 행위 방지, 기타 사용자 보호 등 보안과 관련된 정보 수집은 비활성화할 수 없습니다."
|
||||||
|
gtagConsentAnalytics: "통계 정보 수집"
|
||||||
|
gtagConsentAnalyticsDescription: "사이트 체류 시간 등 분석 관련 정보 저장(쿠키 등)을 활성화합니다."
|
||||||
|
gtagConsentFunctionality: "기능 및 설정 사용 정보 수집"
|
||||||
|
gtagConsentFunctionalityDescription: "언어 설정 등 웹사이트나 앱의 기능을 지원하는 정보 저장을 활성화합니다."
|
||||||
|
gtagConsentPersonalization: "개인 맞춤형 정보 수집"
|
||||||
|
gtagConsentPersonalizationDescription: "추천 게시물 등 개인화 관련 정보 저장을 활성화합니다."
|
||||||
|
helpUsImproveUserExperience: "Misskey의 미래를 위해,\n데이터 수집에 협조해 주세요!"
|
||||||
|
pleaseConsentToTracking: "{host}는 [개인정보 처리방침]({privacyPolicyUrl})에 따라 서비스 제공, 운영, 사용자 경험 향상을 위해 사용 중인 IP 주소, 이용 현황, 디바이스 정보 등 개인 정보를 포함할 수 있는 정보를 수집할 수 있습니다.\n\n수집된 데이터는 향후 기능 개발, 운영 방침 결정, 서비스 개선점 파악에 활용됩니다."
|
||||||
|
consentEssential: "필수 항목만 허용"
|
||||||
|
consentAll: "모두 허용"
|
||||||
|
consentSelected: "선택한 항목만 허용"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
hold: "홀드"
|
hold: "홀드"
|
||||||
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
@ -4,10 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { LoggerService } from '@nestjs/common';
|
import { LoggerService } from '@nestjs/common';
|
||||||
import Logger from '@/logger.js';
|
import { coreLogger } from '@/logger.js';
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const nestLogger = coreLogger.createSubLogger('nest', 'green', false);
|
||||||
const nestLogger = logger.createSubLogger('nest', 'green', false);
|
|
||||||
|
|
||||||
export class NestLogger implements LoggerService {
|
export class NestLogger implements LoggerService {
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +12,7 @@ import { EventEmitter } from 'node:events';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import Xev from 'xev';
|
import Xev from 'xev';
|
||||||
import Logger from '@/logger.js';
|
import { coreLogger } from '@/logger.js';
|
||||||
import { envOption } from '../env.js';
|
import { envOption } from '../env.js';
|
||||||
import { masterMain } from './master.js';
|
import { masterMain } from './master.js';
|
||||||
import { workerMain } from './worker.js';
|
import { workerMain } from './worker.js';
|
||||||
@ -24,8 +24,7 @@ process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`;
|
|||||||
Error.stackTraceLimit = Infinity;
|
Error.stackTraceLimit = Infinity;
|
||||||
EventEmitter.defaultMaxListeners = 128;
|
EventEmitter.defaultMaxListeners = 128;
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const clusterLogger = coreLogger.createSubLogger('cluster', 'orange', false);
|
||||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
|
|
||||||
//#region Events
|
//#region Events
|
||||||
@ -53,12 +52,12 @@ if (cluster.isPrimary && !envOption.disableClustering) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
logger.warn(chalk.yellow('Process received SIGINT'));
|
coreLogger.warn(chalk.yellow('Process received SIGINT'));
|
||||||
isShuttingDown = true;
|
isShuttingDown = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
logger.warn(chalk.yellow('Process received SIGTERM'));
|
coreLogger.warn(chalk.yellow('Process received SIGTERM'));
|
||||||
isShuttingDown = true;
|
isShuttingDown = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -71,18 +70,18 @@ if (!envOption.quiet) {
|
|||||||
// Display detail of uncaught exception
|
// Display detail of uncaught exception
|
||||||
process.on('uncaughtException', err => {
|
process.on('uncaughtException', err => {
|
||||||
try {
|
try {
|
||||||
logger.error(`Uncaught exception: ${err.message}`, { error: err });
|
coreLogger.error(`Uncaught exception: ${err.message}`, { error: err });
|
||||||
} catch { }
|
} catch { }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dying away...
|
// Dying away...
|
||||||
process.on('exit', code => {
|
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 => {
|
process.on('warning', warning => {
|
||||||
if ((warning as never)['code'] !== 'MISSKEY_SHUTDOWN') return;
|
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');
|
for (const id in cluster.workers) cluster.workers[id]?.process.kill('SIGTERM');
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ import * as os from 'node:os';
|
|||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import chalkTemplate from 'chalk-template';
|
import chalkTemplate from 'chalk-template';
|
||||||
import Logger from '@/logger.js';
|
import { coreLogger } from '@/logger.js';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { showMachineInfo } from '@/misc/show-machine-info.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 meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||||
|
|
||||||
const logger = new Logger('core', 'cyan');
|
const bootLogger = coreLogger.createSubLogger('boot', 'magenta', false);
|
||||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
|
||||||
|
|
||||||
const themeColor = chalk.hex('#86b300');
|
const themeColor = chalk.hex('#86b300');
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
|
|||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
@ -123,12 +124,13 @@ export class DriveService {
|
|||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private driveChart: DriveChart,
|
private driveChart: DriveChart,
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
const logger = new Logger('drive', 'blue');
|
const logger = this.loggerService.getLogger('drive', 'blue');
|
||||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||||
this.downloaderLogger = logger.createSubLogger('downloader');
|
this.downloaderLogger = logger.createSubLogger('downloader');
|
||||||
this.deleteLogger = logger.createSubLogger('delete');
|
this.deleteLogger = logger.createSubLogger('delete');
|
||||||
|
@ -159,7 +159,7 @@ export class FetchInstanceMetadataService {
|
|||||||
throw err.statusCode ?? err.message;
|
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;
|
return info as NodeInfo;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import Logger from '@/logger.js';
|
import { rootLogger } from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { KEYWORD } from 'color-convert/conversions.js';
|
import type { KEYWORD } from 'color-convert/conversions.js';
|
||||||
|
|
||||||
@ -16,6 +16,6 @@ export class LoggerService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) {
|
||||||
return new Logger(domain);
|
return rootLogger.createSubLogger(domain, color, store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
const policies = await this.roleService.getUserPolicies(user.id);
|
const policies = await this.roleService.getUserPolicies(user.id);
|
||||||
|
|
||||||
if (!policies.canCreateContent) {
|
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.');
|
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;
|
const sensitiveWords = meta.sensitiveWords;
|
||||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
|
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
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) {
|
} else if (!policies.canPublicNote) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
}
|
}
|
||||||
@ -281,7 +281,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hasProhibitedWords) {
|
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.');
|
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) {
|
if (process.env.MISSKEY_BLOCK_MENTIONS_FROM_UNFAMILIAR_REMOTE_USERS === 'true' && user.host !== null && willCauseNotification) {
|
||||||
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
|
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
|
||||||
if ((userEntity?.followersCount ?? 0) === 0) {
|
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.');
|
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))
|
|| (data.visibility === 'specified' && data.visibleUsers?.some(u => u.id !== user.id))
|
||||||
|| (this.isQuote(data) && data.renote.userId !== 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.');
|
throw new IdentifiableError('332dd91b-6a00-430a-ac39-620cf60ad34b', 'Notes including mentions, replies, or renotes are not allowed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('user-block');
|
this.logger = this.loggerService.getLogger('user:block');
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
@ -30,9 +30,8 @@ import { AccountMoveService } from '@/core/AccountMoveService.js';
|
|||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import type { ThinUser } from '@/queue/types.js';
|
import type { ThinUser } from '@/queue/types.js';
|
||||||
import Logger from '../logger.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
const logger = new Logger('following/create');
|
|
||||||
|
|
||||||
type Local = MiLocalUser | {
|
type Local = MiLocalUser | {
|
||||||
id: MiLocalUser['id'];
|
id: MiLocalUser['id'];
|
||||||
@ -50,6 +49,7 @@ type Both = Local | Remote;
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFollowingService implements OnModuleInit {
|
export class UserFollowingService implements OnModuleInit {
|
||||||
private userBlockingService: UserBlockingService;
|
private userBlockingService: UserBlockingService;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
@ -73,6 +73,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
@ -88,6 +89,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
) {
|
) {
|
||||||
|
this.logger = this.loggerService.getLogger('user:following');
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
@ -255,7 +257,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
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;
|
alreadyFollowed = true;
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
@ -378,7 +380,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (following === null || !following.follower || !following.followee) {
|
if (following === null || !following.follower || !following.followee) {
|
||||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
this.logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +145,7 @@ export class ApRequestService {
|
|||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
this.logger = this.loggerService.getLogger('ap:request');
|
||||||
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -45,7 +45,7 @@ export class Resolver {
|
|||||||
private recursionLimit = 100,
|
private recursionLimit = 100,
|
||||||
) {
|
) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
this.logger = this.loggerService.getLogger('ap-resolve');
|
this.logger = this.loggerService.getLogger('ap:resolve');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -77,6 +77,7 @@ export class MetaEntityService {
|
|||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
turnstileSiteKey: instance.turnstileSiteKey,
|
turnstileSiteKey: instance.turnstileSiteKey,
|
||||||
|
googleAnalyticsId: instance.googleAnalyticsId,
|
||||||
swPublickey: instance.swPublicKey,
|
swPublickey: instance.swPublicKey,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
||||||
|
@ -22,13 +22,14 @@ const pinoPrettyStream = pinoPretty({
|
|||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private readonly domain: string;
|
private readonly domain: string | undefined;
|
||||||
private logger: pino.Logger;
|
private readonly logger: pino.Logger;
|
||||||
private context: Record<string, any> = {};
|
private context: Record<string, any> = {};
|
||||||
|
|
||||||
constructor(domain: string, _color?: KEYWORD, _store = true, parentLogger?: Logger) {
|
constructor(domain: string | undefined, _color?: KEYWORD, _store = true, parentLogger?: Logger) {
|
||||||
if (parentLogger) {
|
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 {
|
} else {
|
||||||
this.domain = domain;
|
this.domain = domain;
|
||||||
}
|
}
|
||||||
@ -50,18 +51,20 @@ export default class Logger {
|
|||||||
formatters: {
|
formatters: {
|
||||||
level: (label, number) => ({ severity: label, level: number }),
|
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);
|
}, !envOption.logJson ? pinoPrettyStream : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
private mixin(): Record<string, any> {
|
||||||
public createSubLogger(domain: string, _color?: KEYWORD, _store = true): Logger {
|
return { cluster: cluster.isPrimary ? 'primary' : `worker#${cluster.worker!.id}`, ...this.context };
|
||||||
return new Logger(domain, undefined, false, this);
|
}
|
||||||
|
|
||||||
|
public createSubLogger(domain?: string, _color?: KEYWORD, _store = true): Logger {
|
||||||
|
return new Logger(domain, _color, _store, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public setContext(context: Record<string, any>): void {
|
public setContext(context: Record<string, any>): void {
|
||||||
this.context = context;
|
this.context = { ...this.context, ...JSON.parse(JSON.stringify(context)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@ -130,3 +133,6 @@ export default class Logger {
|
|||||||
this.logger.info({ context, important }, message);
|
this.logger.info({ context, important }, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const rootLogger = new Logger(undefined, undefined, false, undefined);
|
||||||
|
export const coreLogger = rootLogger.createSubLogger('core', 'cyan');
|
||||||
|
@ -260,6 +260,12 @@ export class MiMeta {
|
|||||||
|
|
||||||
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 32,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public googleAnalyticsId: string | null;
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('enum', {
|
||||||
enum: ['none', 'all', 'local', 'remote'],
|
enum: ['none', 'all', 'local', 'remote'],
|
||||||
default: 'none',
|
default: 'none',
|
||||||
|
@ -8,7 +8,7 @@ import { noteVisibilities } from '@/types.js';
|
|||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiNote } from './Note.js';
|
import { MiNote } from './Note.js';
|
||||||
import type { MiUser } from './User.js';
|
import type { MiUser } from './User.js';
|
||||||
import type { MiChannel } from "@/models/Channel.js";
|
import type { MiChannel } from '@/models/Channel.js';
|
||||||
|
|
||||||
@Entity('poll')
|
@Entity('poll')
|
||||||
export class MiPoll {
|
export class MiPoll {
|
||||||
|
@ -111,6 +111,10 @@ export const packedMetaLiteSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
googleAnalyticsId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
swPublickey: {
|
swPublickey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -13,7 +13,7 @@ import type {
|
|||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||||
import type * as Bull from "bullmq";
|
import type * as Bull from "bullmq";
|
||||||
import type { DbUserSuspendJobData } from "@/queue/types.js";
|
import type { DbUserSuspendJobData } from '@/queue/types.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserSuspendProcessorService {
|
export class UserSuspendProcessorService {
|
||||||
|
@ -27,6 +27,7 @@ import { RateLimiterService } from './RateLimiterService.js';
|
|||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SigninApiService {
|
export class SigninApiService {
|
||||||
@ -71,7 +72,7 @@ export class SigninApiService {
|
|||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const logger = this.loggerService.getLogger('api:signin');
|
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.');
|
logger.info('Requested to sign in.');
|
||||||
|
|
||||||
reply.header('Access-Control-Allow-Origin', this.config.url);
|
reply.header('Access-Control-Allow-Origin', this.config.url);
|
||||||
|
@ -22,6 +22,7 @@ import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
|
|||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { SigninService } from './SigninService.js';
|
import { SigninService } from './SigninService.js';
|
||||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SignupApiService {
|
export class SignupApiService {
|
||||||
@ -73,7 +74,7 @@ export class SignupApiService {
|
|||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const logger = this.loggerService.getLogger('api:signup');
|
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.');
|
logger.info('Requested to create user account.');
|
||||||
|
|
||||||
const body = request.body;
|
const body = request.body;
|
||||||
|
@ -69,6 +69,10 @@ export const meta = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
googleAnalyticsId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
swPublickey: {
|
swPublickey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
@ -557,6 +561,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
turnstileSiteKey: instance.turnstileSiteKey,
|
turnstileSiteKey: instance.turnstileSiteKey,
|
||||||
|
googleAnalyticsId: instance.googleAnalyticsId,
|
||||||
swPublickey: instance.swPublicKey,
|
swPublickey: instance.swPublicKey,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
mascotImageUrl: instance.mascotImageUrl,
|
mascotImageUrl: instance.mascotImageUrl,
|
||||||
|
@ -78,6 +78,7 @@ export const paramDef = {
|
|||||||
enableTurnstile: { type: 'boolean' },
|
enableTurnstile: { type: 'boolean' },
|
||||||
turnstileSiteKey: { type: 'string', nullable: true },
|
turnstileSiteKey: { type: 'string', nullable: true },
|
||||||
turnstileSecretKey: { type: 'string', nullable: true },
|
turnstileSecretKey: { type: 'string', nullable: true },
|
||||||
|
googleAnalyticsId: { type: 'string', nullable: true },
|
||||||
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
|
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
|
||||||
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
||||||
setSensitiveFlagAutomatically: { type: 'boolean' },
|
setSensitiveFlagAutomatically: { type: 'boolean' },
|
||||||
@ -374,6 +375,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.turnstileSecretKey = ps.turnstileSecretKey;
|
set.turnstileSecretKey = ps.turnstileSecretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.googleAnalyticsId !== undefined) {
|
||||||
|
set.googleAnalyticsId = ps.googleAnalyticsId;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.sensitiveMediaDetection !== undefined) {
|
if (ps.sensitiveMediaDetection !== undefined) {
|
||||||
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
|
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||||
import { EntityNotFoundError } from "typeorm";
|
import { EntityNotFoundError } from 'typeorm';
|
||||||
import { ApiError } from "../error.js";
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
super(meta, paramDef, async (ps, me, _token, _file, _cleanup, ip, headers) => {
|
||||||
const logger = this.loggerService.getLogger('api:federation:instances');
|
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.');
|
logger.info('Requested to fetch federated instances.');
|
||||||
|
|
||||||
const query = this.instancesRepository.createQueryBuilder('instance');
|
const query = this.instancesRepository.createQueryBuilder('instance');
|
||||||
|
@ -8,7 +8,9 @@ import type { UserProfilesRepository } from '@/models/_.js';
|
|||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DI } from '@/di-symbols.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 = {
|
export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
@ -44,11 +46,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
private loggerService: LoggerService,
|
||||||
private userEntityService: UserEntityService,
|
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 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 now = new Date();
|
||||||
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
const today = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`;
|
||||||
|
|
||||||
@ -71,11 +78,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
userProfile.loggedInDates = [...userProfile.loggedInDates, today];
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
|
try {
|
||||||
|
const result = await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
|
||||||
schema: 'MeDetailed',
|
schema: 'MeDetailed',
|
||||||
includeSecrets: isSecure,
|
includeSecrets: isSecure,
|
||||||
userProfile,
|
userProfile,
|
||||||
});
|
});
|
||||||
|
logger.info('Returning account information');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to pack user entity', { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ import type { Config } from '@/config.js';
|
|||||||
import { safeForSql } from '@/misc/safe-for-sql.js';
|
import { safeForSql } from '@/misc/safe-for-sql.js';
|
||||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { IdService } from "@/core/IdService.js";
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account'],
|
tags: ['account'],
|
||||||
|
@ -282,7 +282,7 @@ export class ClientServerService {
|
|||||||
};
|
};
|
||||||
const csp = this.config.contentSecurityPolicy
|
const csp = this.config.contentSecurityPolicy
|
||||||
?? 'script-src \'self\' ' +
|
?? '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\'; ' +
|
'worker-src blob: \'self\'; ' +
|
||||||
'base-uri \'self\'; object-src \'self\'; report-uri /csp-error';
|
'base-uri \'self\'; object-src \'self\'; report-uri /csp-error';
|
||||||
reply.header('Content-Security-Policy-Report-Only', csp.replace('{scriptNonce}', `'nonce-${scriptNonce}'`));
|
reply.header('Content-Security-Policy-Report-Only', csp.replace('{scriptNonce}', `'nonce-${scriptNonce}'`));
|
||||||
|
@ -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 { entity as TestIntersectionChartEntity } from '@/core/chart/charts/entities/test-intersection.js';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import type { AppLockService } from '@/core/AppLockService.js';
|
import type { AppLockService } from '@/core/AppLockService.js';
|
||||||
import Logger from '@/logger.js';
|
import { coreLogger } from '@/logger.js';
|
||||||
|
|
||||||
describe('Chart', () => {
|
describe('Chart', () => {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
@ -63,7 +63,7 @@ describe('Chart', () => {
|
|||||||
|
|
||||||
await db.initialize();
|
await db.initialize();
|
||||||
|
|
||||||
const logger = new Logger('chart'); // TODO: モックにする
|
const logger = coreLogger.createSubLogger('chart'); // TODO: モックにする
|
||||||
testChart = new TestChart(db, appLockService, logger);
|
testChart = new TestChart(db, appLockService, logger);
|
||||||
testGroupedChart = new TestGroupedChart(db, appLockService, logger);
|
testGroupedChart = new TestGroupedChart(db, appLockService, logger);
|
||||||
testUniqueChart = new TestUniqueChart(db, appLockService, logger);
|
testUniqueChart = new TestUniqueChart(db, appLockService, logger);
|
||||||
|
14
packages/frontend/@types/vue-gtag.d.ts
vendored
Normal file
14
packages/frontend/@types/vue-gtag.d.ts
vendored
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -75,6 +75,7 @@
|
|||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "5.4.9",
|
"vite": "5.4.9",
|
||||||
"vue": "3.5.12",
|
"vue": "3.5.12",
|
||||||
|
"vue-gtag": "2.0.1",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -12,8 +12,10 @@ import { MenuButton } from '@/types/menu.js';
|
|||||||
import { del, get, set } from '@/scripts/idb-proxy.js';
|
import { del, get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@/config.js';
|
||||||
import { waiting, popup, popupMenu, success, alert } from '@/os.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 { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
|
||||||
|
import { set as gtagSet } from 'vue-gtag';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ export async function signout() {
|
|||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -109,6 +112,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
|||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
||||||
@ -171,6 +175,12 @@ export function updateAccount(accountData: Partial<Account>) {
|
|||||||
$i[key] = value;
|
$i[key] = value;
|
||||||
}
|
}
|
||||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||||
|
if (instance.googleAnalyticsId) {
|
||||||
|
gtagSet({
|
||||||
|
'client_id': miLocalStorage.getItem('id'),
|
||||||
|
'user_id': $i.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAccount() {
|
export async function refreshAccount() {
|
||||||
|
@ -23,6 +23,8 @@ import { deckStore } from '@/ui/deck/deck-store.js';
|
|||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
import { setupRouter } from '@/router/definition.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<Element>) {
|
export async function common(createVue: () => App<Element>) {
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
@ -60,6 +62,10 @@ export async function common(createVue: () => App<Element>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (miLocalStorage.getItem('id') === null) {
|
||||||
|
miLocalStorage.setItem('id', crypto.randomUUID());
|
||||||
|
}
|
||||||
|
|
||||||
let isClientUpdated = false;
|
let isClientUpdated = false;
|
||||||
|
|
||||||
//#region クライアントが更新されたかチェック
|
//#region クライアントが更新されたかチェック
|
||||||
@ -260,6 +266,38 @@ export async function common(createVue: () => App<Element>) {
|
|||||||
directives(app);
|
directives(app);
|
||||||
components(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
|
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
|
||||||
// なぜか2回実行されることがあるため、mountするdivを1つに制限する
|
// なぜか2回実行されることがあるため、mountするdivを1つに制限する
|
||||||
const rootEl = ((): HTMLElement => {
|
const rootEl = ((): HTMLElement => {
|
||||||
|
@ -20,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
|
|||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mainRouter } from '@/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
@ -234,9 +235,10 @@ export async function mainBoot() {
|
|||||||
}
|
}
|
||||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||||
|
|
||||||
|
if (!location.pathname.startsWith('/miauth') && !location.pathname.startsWith('/sso') && !location.pathname.startsWith('/oauth')) {
|
||||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||||
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
|
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)))) {
|
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
||||||
}
|
}
|
||||||
@ -247,6 +249,11 @@ export async function mainBoot() {
|
|||||||
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (instance.googleAnalyticsId && miLocalStorage.getItem('gaConsent') === null) {
|
||||||
|
popup(defineAsyncComponent(() => import('@/components/MkTrackingConsent.vue')), {}, {}, 'closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ('Notification' in window) {
|
if ('Notification' in window) {
|
||||||
// 許可を得ていなかったらリクエスト
|
// 許可を得ていなかったらリクエスト
|
||||||
if (Notification.permission === 'default') {
|
if (Notification.permission === 'default') {
|
||||||
|
177
packages/frontend/src/components/MkTrackingConsent.vue
Normal file
177
packages/frontend/src/components/MkTrackingConsent.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="_panel _shadow" :class="$style.root">
|
||||||
|
<div :class="$style.main">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<div :class="$style.headerIcon">
|
||||||
|
<i class="ti ti-report-analytics"></i>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.headerTitle"><Mfm :text="i18n.ts.helpUsImproveUserExperience" /></div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<Mfm
|
||||||
|
:text="i18n.tsx.pleaseConsentToTracking({
|
||||||
|
host: instance.name ?? host,
|
||||||
|
privacyPolicyUrl: instance.privacyPolicyUrl,
|
||||||
|
})"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<div class="_buttons" style="justify-content: right;">
|
||||||
|
<MkButton @click="consentEssential">{{ i18n.ts.consentEssential }}</MkButton>
|
||||||
|
<MkButton primary @click="consentAll">{{ i18n.ts.consentAll }}</MkButton>
|
||||||
|
</div>
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-lock-square"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.gtagConsentCustomize }}</template>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkInfo>{{ i18n.tsx.gtagConsentCustomizeDescription({ host: instance.name ?? host }) }}</MkInfo>
|
||||||
|
<MkSwitch v-model="gtagConsentAnalytics">
|
||||||
|
{{ i18n.ts.gtagConsentAnalytics }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentAnalyticsDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="gtagConsentFunctionality">
|
||||||
|
{{ i18n.ts.gtagConsentFunctionality }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentFunctionalityDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="gtagConsentPersonalization">
|
||||||
|
{{ i18n.ts.gtagConsentPersonalization }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentPersonalizationDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<div class="_buttons" style="justify-content: right;">
|
||||||
|
<MkButton @click="consentSelected">{{ i18n.ts.consentSelected }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import {
|
||||||
|
bootstrap as gtagBootstrap,
|
||||||
|
GtagConsent,
|
||||||
|
GtagConsentParams,
|
||||||
|
set as gtagSet
|
||||||
|
} from 'vue-gtag';
|
||||||
|
|
||||||
|
const emit = defineEmits<(ev: 'closed') => void>();
|
||||||
|
|
||||||
|
const zIndex = os.claimZIndex('middle');
|
||||||
|
|
||||||
|
const gtagConsentAnalytics = ref(false);
|
||||||
|
const gtagConsentFunctionality = ref(false);
|
||||||
|
const gtagConsentPersonalization = ref(false);
|
||||||
|
|
||||||
|
function consentAll() {
|
||||||
|
miLocalStorage.setItem('gaConsent', 'true');
|
||||||
|
const gtagConsent = <GtagConsentParams>{
|
||||||
|
ad_storage: 'granted',
|
||||||
|
ad_user_data: 'granted',
|
||||||
|
ad_personalization: 'granted',
|
||||||
|
analytics_storage: 'granted',
|
||||||
|
functionality_storage: 'granted',
|
||||||
|
personalization_storage: 'granted',
|
||||||
|
security_storage: 'granted',
|
||||||
|
};
|
||||||
|
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
|
||||||
|
|
||||||
|
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent);
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function consentEssential() {
|
||||||
|
miLocalStorage.setItem('gaConsent', 'true');
|
||||||
|
const gtagConsent = <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', 'update', gtagConsent);
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function consentSelected() {
|
||||||
|
miLocalStorage.setItem('gaConsent', 'true');
|
||||||
|
const gtagConsent = <GtagConsentParams>{
|
||||||
|
ad_storage: gtagConsentAnalytics.value ? 'granted' : 'denied',
|
||||||
|
ad_user_data: gtagConsentFunctionality.value ? 'granted' : 'denied',
|
||||||
|
ad_personalization: gtagConsentPersonalization.value ? 'granted' : 'denied',
|
||||||
|
analytics_storage: gtagConsentAnalytics.value ? 'granted' : 'denied',
|
||||||
|
functionality_storage: gtagConsentFunctionality.value ? 'granted' : 'denied',
|
||||||
|
personalization_storage: gtagConsentPersonalization.value ? 'granted' : 'denied',
|
||||||
|
security_storage: 'granted',
|
||||||
|
};
|
||||||
|
miLocalStorage.setItemAsJson('gtagConsent', gtagConsent);
|
||||||
|
|
||||||
|
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent);
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
emit('closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrap() {
|
||||||
|
gtagBootstrap();
|
||||||
|
gtagSet({
|
||||||
|
'client_id': miLocalStorage.getItem('id'),
|
||||||
|
'user_id': $i?.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
z-index: v-bind(zIndex);
|
||||||
|
bottom: var(--margin);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: calc(100% - (var(--margin) * 2));
|
||||||
|
max-width: 500px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 25px 25px 25px 25px;
|
||||||
|
width: inherit;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerIcon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 40px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitle {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0.7em 0 1em 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
rel: 'nofollow noopener',
|
rel: 'nofollow noopener',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
}"
|
}"
|
||||||
|
@click="onAdClicked"
|
||||||
>
|
>
|
||||||
<img :src="chosen.imageUrl" :class="$style.img">
|
<img :src="chosen.imageUrl" :class="$style.img">
|
||||||
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
|
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
|
||||||
@ -42,7 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
/* eslint-disable id-denylist */
|
||||||
|
import { ref, computed, onActivated, onMounted } from 'vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { url as local, host } from '@/config.js';
|
import { url as local, host } from '@/config.js';
|
||||||
@ -50,6 +52,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { usageReport } from '@/scripts/usage-report.js';
|
||||||
|
|
||||||
type Ad = (typeof instance)['ads'][number];
|
type Ad = (typeof instance)['ads'][number];
|
||||||
|
|
||||||
@ -123,6 +126,36 @@ function reduceFrequency(): void {
|
|||||||
chosen.value = choseAd();
|
chosen.value = choseAd();
|
||||||
showMenu.value = false;
|
showMenu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onAdClicked(): void {
|
||||||
|
if (chosen.value == null) return;
|
||||||
|
usageReport({
|
||||||
|
t: Math.floor(Date.now() / 1000),
|
||||||
|
e: 'a',
|
||||||
|
i: chosen.value.id,
|
||||||
|
a: 'c',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (chosen.value == null) return;
|
||||||
|
usageReport({
|
||||||
|
t: Math.floor(Date.now() / 1000),
|
||||||
|
e: 'a',
|
||||||
|
i: chosen.value.id,
|
||||||
|
a: 'v',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
if (chosen.value == null) return;
|
||||||
|
usageReport({
|
||||||
|
t: Math.floor(Date.now() / 1000),
|
||||||
|
e: 'a',
|
||||||
|
i: chosen.value.id,
|
||||||
|
a: 'v',
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -16,13 +16,13 @@
|
|||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/ https://fonts.gstatic.com/ https://www.google-analytics.com/ https://www.googletagmanager.com/;
|
||||||
worker-src 'self';
|
worker-src 'self';
|
||||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
|
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://www.googletagmanager.com https://esm.sh;
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://www.googletagmanager.com;
|
||||||
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://fonts.gstatic.com https://www.googletagmanager.com;
|
||||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com;
|
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.pwnedpasswords.com https://www.google-analytics.com https://analytics.google.com;
|
||||||
frame-src *;"
|
frame-src *;"
|
||||||
/>
|
/>
|
||||||
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type Keys =
|
type Keys =
|
||||||
|
'id' |
|
||||||
'v' |
|
'v' |
|
||||||
'lastVersion' |
|
'lastVersion' |
|
||||||
'instance' |
|
'instance' |
|
||||||
@ -39,7 +40,10 @@ type Keys =
|
|||||||
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
|
'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
|
||||||
'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
|
'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
|
||||||
`channelLastReadedAt:${string}` |
|
`channelLastReadedAt:${string}` |
|
||||||
'kawaii'
|
'kawaii' |
|
||||||
|
'gaConsent' |
|
||||||
|
'gtagConsent'
|
||||||
|
;
|
||||||
|
|
||||||
export const miLocalStorage = {
|
export const miLocalStorage = {
|
||||||
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
|
getItem: (key: Keys): string | null => window.localStorage.getItem(key),
|
||||||
|
@ -71,6 +71,8 @@ export type Resolved = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AfterNavigationHook = (to: RouteDef, from: RouteDef) => any;
|
||||||
|
|
||||||
function parsePath(path: string): ParsedPath {
|
function parsePath(path: string): ParsedPath {
|
||||||
const res = [] as ParsedPath;
|
const res = [] as ParsedPath;
|
||||||
|
|
||||||
@ -101,6 +103,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||||||
currentRef: ShallowRef<Resolved>;
|
currentRef: ShallowRef<Resolved>;
|
||||||
currentRoute: ShallowRef<RouteDef>;
|
currentRoute: ShallowRef<RouteDef>;
|
||||||
navHook: ((path: string, flag?: any) => boolean) | null;
|
navHook: ((path: string, flag?: any) => boolean) | null;
|
||||||
|
afterHooks: Array<AfterNavigationHook | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
||||||
@ -109,10 +112,14 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||||||
|
|
||||||
resolve(path: string): Resolved | null;
|
resolve(path: string): Resolved | null;
|
||||||
|
|
||||||
|
isReady(): Promise<boolean>;
|
||||||
|
|
||||||
getCurrentPath(): any;
|
getCurrentPath(): any;
|
||||||
|
|
||||||
getCurrentKey(): string;
|
getCurrentKey(): string;
|
||||||
|
|
||||||
|
afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined;
|
||||||
|
|
||||||
push(path: string, flag?: any): void;
|
push(path: string, flag?: any): void;
|
||||||
|
|
||||||
replace(path: string, key?: string | null): void;
|
replace(path: string, key?: string | null): void;
|
||||||
@ -191,6 +198,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||||||
private redirectCount = 0;
|
private redirectCount = 0;
|
||||||
|
|
||||||
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
||||||
|
public afterHooks: Array<AfterNavigationHook | null> = [];
|
||||||
|
|
||||||
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
||||||
super();
|
super();
|
||||||
@ -339,6 +347,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||||||
|
|
||||||
private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
|
private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
|
||||||
const beforePath = this.currentPath;
|
const beforePath = this.currentPath;
|
||||||
|
const beforeRoute = this.currentRoute.value;
|
||||||
this.currentPath = path;
|
this.currentPath = path;
|
||||||
|
|
||||||
const res = this.resolve(this.currentPath);
|
const res = this.resolve(this.currentPath);
|
||||||
@ -382,6 +391,12 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.afterHooks.length > 0) {
|
||||||
|
for (const hook of this.afterHooks) {
|
||||||
|
if (hook) hook(res.route, beforeRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.redirectCount = 0;
|
this.redirectCount = 0;
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
@ -389,6 +404,10 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isReady() {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
public getCurrentPath() {
|
public getCurrentPath() {
|
||||||
return this.currentPath;
|
return this.currentPath;
|
||||||
}
|
}
|
||||||
@ -397,6 +416,18 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||||||
return this.currentKey;
|
return this.currentKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined {
|
||||||
|
this.afterHooks.push(hook);
|
||||||
|
return () => {
|
||||||
|
const index = this.afterHooks.indexOf(hook);
|
||||||
|
if (index !== -1) {
|
||||||
|
return this.afterHooks.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public push(path: string, flag?: any) {
|
public push(path: string, flag?: any) {
|
||||||
const beforePath = this.currentPath;
|
const beforePath = this.currentPath;
|
||||||
if (path === beforePath) {
|
if (path === beforePath) {
|
||||||
|
@ -21,6 +21,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>Google Analytics</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkInput v-model="googleAnalyticsId">
|
||||||
|
<template #prefix><i class="ti ti-report-analytics"></i></template>
|
||||||
|
<template #label>Google Analytics ID</template>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -49,17 +59,20 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||||||
|
|
||||||
const deeplAuthKey = ref<string>('');
|
const deeplAuthKey = ref<string>('');
|
||||||
const deeplIsPro = ref<boolean>(false);
|
const deeplIsPro = ref<boolean>(false);
|
||||||
|
const googleAnalyticsId = ref<string>('');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
deeplAuthKey.value = meta.deeplAuthKey;
|
deeplAuthKey.value = meta.deeplAuthKey;
|
||||||
deeplIsPro.value = meta.deeplIsPro;
|
deeplIsPro.value = meta.deeplIsPro;
|
||||||
|
googleAnalyticsId.value = meta.googleAnalyticsId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
deeplAuthKey: deeplAuthKey.value,
|
deeplAuthKey: deeplAuthKey.value,
|
||||||
deeplIsPro: deeplIsPro.value,
|
deeplIsPro: deeplIsPro.value,
|
||||||
|
googleAnalyticsId: googleAnalyticsId.value,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
});
|
});
|
||||||
|
@ -68,20 +68,49 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
<MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
|
<MkSwitch v-model="keepCw" @update:modelValue="save()">{{ i18n.ts.keepCw }}</MkSwitch>
|
||||||
|
|
||||||
|
<FormSection v-if="instance.googleAnalyticsId">
|
||||||
|
<MkFolder :defaultOpen="true">
|
||||||
|
<template #icon><i class="ti ti-lock-square"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.gtagConsentCustomize }}</template>
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkInfo>{{ i18n.tsx.gtagConsentCustomizeDescription({ host: instance.name ?? host }) }}</MkInfo>
|
||||||
|
<MkSwitch v-model="gtagConsentAnalytics">
|
||||||
|
{{ i18n.ts.gtagConsentAnalytics }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentAnalyticsDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="gtagConsentFunctionality">
|
||||||
|
{{ i18n.ts.gtagConsentFunctionality }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentFunctionalityDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="gtagConsentPersonalization">
|
||||||
|
{{ i18n.ts.gtagConsentPersonalization }}
|
||||||
|
<template #caption>{{ i18n.ts.gtagConsentPersonalizationDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { signinRequired } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
import { host } from '@/config.js';
|
||||||
|
import { GtagConsent, GtagConsentParams } from 'vue-gtag';
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
@ -100,6 +129,81 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
|
|||||||
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
|
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
|
||||||
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
|
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
|
||||||
|
|
||||||
|
const gaConsentInternal = ref(miLocalStorage.getItem('gaConsent') === 'true');
|
||||||
|
const gaConsent = computed({
|
||||||
|
get: () => gaConsentInternal.value,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
miLocalStorage.setItem('gaConsent', value ? 'true' : 'false');
|
||||||
|
gaConsentInternal.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gtagConsentInternal = ref(
|
||||||
|
(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',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const gtagConsent = computed({
|
||||||
|
get: () => gtagConsentInternal.value,
|
||||||
|
set: (value: GtagConsentParams) => {
|
||||||
|
miLocalStorage.setItemAsJson('gtagConsent', value);
|
||||||
|
gtagConsentInternal.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gtagConsentAnalytics = computed({
|
||||||
|
get: () => gtagConsent.value.analytics_storage === 'granted',
|
||||||
|
set: (value: boolean) => {
|
||||||
|
gtagConsent.value = {
|
||||||
|
...gtagConsent.value,
|
||||||
|
ad_storage: value ? 'granted' : 'denied',
|
||||||
|
ad_user_data: value ? 'granted' : 'denied',
|
||||||
|
analytics_storage: value ? 'granted' : 'denied',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gtagConsentFunctionality = computed({
|
||||||
|
get: () => gtagConsent.value.functionality_storage === 'granted',
|
||||||
|
set: (value: boolean) => {
|
||||||
|
gtagConsent.value = {
|
||||||
|
...gtagConsent.value,
|
||||||
|
functionality_storage: value ? 'granted' : 'denied',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gtagConsentPersonalization = computed({
|
||||||
|
get: () => gtagConsent.value.personalization_storage === 'granted',
|
||||||
|
set: (value: boolean) => {
|
||||||
|
gtagConsent.value = {
|
||||||
|
...gtagConsent.value,
|
||||||
|
ad_personalization: value ? 'granted' : 'denied',
|
||||||
|
personalization_storage: value ? 'granted' : 'denied',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function reloadAsk() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.ts.reloadToApplySetting,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(gaConsent, async () => {
|
||||||
|
await reloadAsk();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(gtagConsent, async () => {
|
||||||
|
if (typeof window['gtag'] === 'function') (window['gtag'] as GtagConsent)('consent', 'update', gtagConsent.value);
|
||||||
|
});
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
misskeyApi('i/update', {
|
misskeyApi('i/update', {
|
||||||
isLocked: !!isLocked.value,
|
isLocked: !!isLocked.value,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { ShallowRef } from 'vue';
|
import { ShallowRef } from 'vue';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
import { AfterNavigationHook, IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
||||||
|
|
||||||
function getMainRouter(): IRouter {
|
function getMainRouter(): IRouter {
|
||||||
const router = mainRouterHolder;
|
const router = mainRouterHolder;
|
||||||
@ -40,6 +40,10 @@ class MainRouterProxy implements IRouter {
|
|||||||
this.supplier = supplier;
|
this.supplier = supplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get options(): { [key: string]: any } {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
get current(): Resolved {
|
get current(): Resolved {
|
||||||
return this.supplier().current;
|
return this.supplier().current;
|
||||||
}
|
}
|
||||||
@ -60,6 +64,10 @@ class MainRouterProxy implements IRouter {
|
|||||||
this.supplier().navHook = value;
|
this.supplier().navHook = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isReady(): Promise<boolean> {
|
||||||
|
return this.supplier().isReady();
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentKey(): string {
|
getCurrentKey(): string {
|
||||||
return this.supplier().getCurrentKey();
|
return this.supplier().getCurrentKey();
|
||||||
}
|
}
|
||||||
@ -68,6 +76,10 @@ class MainRouterProxy implements IRouter {
|
|||||||
return this.supplier().getCurrentPath();
|
return this.supplier().getCurrentPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEach(hook: AfterNavigationHook): AfterNavigationHook | undefined {
|
||||||
|
return this.supplier().afterEach(hook);
|
||||||
|
}
|
||||||
|
|
||||||
push(path: string, flag?: any): void {
|
push(path: string, flag?: any): void {
|
||||||
this.supplier().push(path, flag);
|
this.supplier().push(path, flag);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export function createAiScriptEnv(opts) {
|
|||||||
}
|
}
|
||||||
const actualToken: string|null = token?.value ?? opts.token ?? null;
|
const actualToken: string|null = token?.value ?? opts.token ?? null;
|
||||||
if (!rateLimiter.hit(ep.value)) return values.ERROR('rate_limited', values.NULL);
|
if (!rateLimiter.hit(ep.value)) return values.ERROR('rate_limited', values.NULL);
|
||||||
return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
|
return misskeyApi(ep.value, utils.valToJs(param), actualToken, undefined, 'aiscript').then(res => {
|
||||||
return utils.jsToVal(res);
|
return utils.jsToVal(res);
|
||||||
}, err => {
|
}, err => {
|
||||||
return values.ERROR('request_failed', utils.jsToVal(err));
|
return values.ERROR('request_failed', utils.jsToVal(err));
|
||||||
|
@ -7,8 +7,39 @@ import * as Misskey from 'misskey-js';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { apiUrl } from '@/config.js';
|
import { apiUrl } from '@/config.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
export const pendingApiRequestsCount = ref(0);
|
export const pendingApiRequestsCount = ref(0);
|
||||||
|
|
||||||
|
let id: string | null = miLocalStorage.getItem('id');
|
||||||
|
export function generateClientTransactionId(initiator: string) {
|
||||||
|
if (id === null) {
|
||||||
|
id = crypto.randomUUID();
|
||||||
|
miLocalStorage.setItem('id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${id}-${initiator}-${crypto.randomUUID()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResponse<_ResT>(
|
||||||
|
resolve: (value: (_ResT | PromiseLike<_ResT>)) => void,
|
||||||
|
reject: (reason?: any) => void,
|
||||||
|
): ((value: Response) => (void | PromiseLike<void>)) {
|
||||||
|
return async (res) => {
|
||||||
|
if (res.ok && res.status !== 204) {
|
||||||
|
const body = await res.json();
|
||||||
|
resolve(body);
|
||||||
|
} else if (res.status === 204) {
|
||||||
|
resolve(undefined as _ResT); // void -> undefined
|
||||||
|
} else {
|
||||||
|
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
|
||||||
|
const body = await res
|
||||||
|
.json()
|
||||||
|
.catch(() => ({ statusCode: res.status, message: res.statusText }));
|
||||||
|
reject(typeof body.error === 'object' ? body.error : body);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Implements Misskey.api.ApiClient.request
|
// Implements Misskey.api.ApiClient.request
|
||||||
export function misskeyApi<
|
export function misskeyApi<
|
||||||
ResT = void,
|
ResT = void,
|
||||||
@ -20,6 +51,7 @@ export function misskeyApi<
|
|||||||
data: P = {} as any,
|
data: P = {} as any,
|
||||||
token?: string | null | undefined,
|
token?: string | null | undefined,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
|
initiator: string = 'misskey',
|
||||||
): Promise<_ResT> {
|
): Promise<_ResT> {
|
||||||
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
@ -41,20 +73,10 @@ export function misskeyApi<
|
|||||||
cache: 'no-cache',
|
cache: 'no-cache',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
}).then(async (res) => {
|
}).then(handleResponse(resolve, reject)).catch(reject);
|
||||||
if (res.ok && res.status !== 204) {
|
|
||||||
const body = await res.json();
|
|
||||||
resolve(body);
|
|
||||||
} else if (res.status === 204) {
|
|
||||||
resolve(undefined as _ResT); // void -> undefined
|
|
||||||
} else {
|
|
||||||
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
|
|
||||||
const body = await res.json().catch(() => ({ statusCode: res.status, message: res.statusText }));
|
|
||||||
reject(typeof body.error === 'object' ? body.error : body);
|
|
||||||
}
|
|
||||||
}).catch(reject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.then(onFinally, onFinally);
|
promise.then(onFinally, onFinally);
|
||||||
@ -71,6 +93,7 @@ export function misskeyApiGet<
|
|||||||
>(
|
>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
data: P = {} as any,
|
data: P = {} as any,
|
||||||
|
initiator: string = 'misskey',
|
||||||
): Promise<_ResT> {
|
): Promise<_ResT> {
|
||||||
pendingApiRequestsCount.value++;
|
pendingApiRequestsCount.value++;
|
||||||
|
|
||||||
@ -86,17 +109,10 @@ export function misskeyApiGet<
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
cache: 'default',
|
cache: 'default',
|
||||||
}).then(async (res) => {
|
headers: {
|
||||||
const body = res.status === 204 ? null : await res.json();
|
'X-Client-Transaction-Id': generateClientTransactionId(initiator),
|
||||||
|
},
|
||||||
if (res.status === 200) {
|
}).then(handleResponse(resolve, reject)).catch(reject);
|
||||||
resolve(body);
|
|
||||||
} else if (res.status === 204) {
|
|
||||||
resolve(undefined as _ResT); // void -> undefined
|
|
||||||
} else {
|
|
||||||
reject(body.error);
|
|
||||||
}
|
|
||||||
}).catch(reject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.then(onFinally, onFinally);
|
promise.then(onFinally, onFinally);
|
||||||
|
61
packages/frontend/src/scripts/usage-report.ts
Normal file
61
packages/frontend/src/scripts/usage-report.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable id-denylist */
|
||||||
|
// buffering usage report data for 1 minute, then sending it to the server
|
||||||
|
// POST /api/usage [ { t: number, e: string, i: string, a: string } ]
|
||||||
|
// t: timestamp
|
||||||
|
// e: event type
|
||||||
|
// i: event initiator
|
||||||
|
// a: action
|
||||||
|
|
||||||
|
import { generateClientTransactionId } from '@/scripts/misskey-api.js';
|
||||||
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { GtagConsentParams } from 'vue-gtag';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
|
export interface UsageReport {
|
||||||
|
t: number;
|
||||||
|
e: string;
|
||||||
|
i: string;
|
||||||
|
a: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let disableUsageReport = !instance.googleAnalyticsId;
|
||||||
|
const usageReportBuffer: UsageReport[] = [];
|
||||||
|
let usageReportBufferTimer: number | null = null;
|
||||||
|
|
||||||
|
export function usageReport(data: UsageReport) {
|
||||||
|
if (disableUsageReport) return;
|
||||||
|
|
||||||
|
if (usageReportBuffer.length > 0) {
|
||||||
|
const last = usageReportBuffer[usageReportBuffer.length - 1];
|
||||||
|
if (last.t === data.t && last.e === data.e && last.a === data.a) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usageReportBuffer.push(data);
|
||||||
|
if (usageReportBufferTimer === null) {
|
||||||
|
usageReportBufferTimer = window.setTimeout(() => {
|
||||||
|
sendUsageReport();
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendUsageReport() {
|
||||||
|
if (usageReportBuffer.length === 0) return;
|
||||||
|
const data = usageReportBuffer.splice(0, usageReportBuffer.length);
|
||||||
|
usageReportBufferTimer = null;
|
||||||
|
|
||||||
|
if ((miLocalStorage.getItemAsJson('gtagConsent') as GtagConsentParams)?.ad_user_data !== 'granted') {
|
||||||
|
console.log('Usage report is not sent because the user has not consented to sharing data about ad interactions.');
|
||||||
|
disableUsageReport = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.fetch('/api/usage', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
cache: 'no-cache',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-Transaction-Id': generateClientTransactionId('misskey'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
2
packages/frontend/vue-shims.d.ts
vendored
2
packages/frontend/vue-shims.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from 'vue';
|
||||||
const component: ReturnType<typeof defineComponent>;
|
const component: ReturnType<typeof defineComponent>;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
@ -5095,6 +5095,7 @@ export type components = {
|
|||||||
recaptchaSiteKey: string | null;
|
recaptchaSiteKey: string | null;
|
||||||
enableTurnstile: boolean;
|
enableTurnstile: boolean;
|
||||||
turnstileSiteKey: string | null;
|
turnstileSiteKey: string | null;
|
||||||
|
googleAnalyticsId: string | null;
|
||||||
swPublickey: string | null;
|
swPublickey: string | null;
|
||||||
/** @default /assets/ai.png */
|
/** @default /assets/ai.png */
|
||||||
mascotImageUrl: string;
|
mascotImageUrl: string;
|
||||||
@ -5241,6 +5242,7 @@ export type operations = {
|
|||||||
recaptchaSiteKey: string | null;
|
recaptchaSiteKey: string | null;
|
||||||
enableTurnstile: boolean;
|
enableTurnstile: boolean;
|
||||||
turnstileSiteKey: string | null;
|
turnstileSiteKey: string | null;
|
||||||
|
googleAnalyticsId: string | null;
|
||||||
swPublickey: string | null;
|
swPublickey: string | null;
|
||||||
/** @default /assets/ai.png */
|
/** @default /assets/ai.png */
|
||||||
mascotImageUrl: string | null;
|
mascotImageUrl: string | null;
|
||||||
@ -10053,6 +10055,7 @@ export type operations = {
|
|||||||
enableTurnstile?: boolean;
|
enableTurnstile?: boolean;
|
||||||
turnstileSiteKey?: string | null;
|
turnstileSiteKey?: string | null;
|
||||||
turnstileSecretKey?: string | null;
|
turnstileSecretKey?: string | null;
|
||||||
|
googleAnalyticsId?: string | null;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
|
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
|
@ -875,6 +875,9 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: 3.5.12
|
specifier: 3.5.12
|
||||||
version: 3.5.12(typescript@5.6.3)
|
version: 3.5.12(typescript@5.6.3)
|
||||||
|
vue-gtag:
|
||||||
|
specifier: 2.0.1
|
||||||
|
version: 2.0.1(vue@3.5.12(typescript@5.6.3))
|
||||||
vuedraggable:
|
vuedraggable:
|
||||||
specifier: next
|
specifier: next
|
||||||
version: 4.1.0(vue@3.5.12(typescript@5.6.3))
|
version: 4.1.0(vue@3.5.12(typescript@5.6.3))
|
||||||
@ -9765,6 +9768,9 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.1.10:
|
||||||
|
resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
|
||||||
|
|
||||||
vue-component-type-helpers@2.1.6:
|
vue-component-type-helpers@2.1.6:
|
||||||
resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
|
resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
|
||||||
|
|
||||||
@ -9790,6 +9796,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=6.0.0'
|
eslint: '>=6.0.0'
|
||||||
|
|
||||||
|
vue-gtag@2.0.1:
|
||||||
|
resolution: {integrity: sha512-aM4A58FVL0wV2ptYi+xzAjeg+pQVRyUcfBc5UkXAwQrR4t3WBhor50Izp2I+3Oo7+l+vWJ7u78DGcNzReb8S/A==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-inbrowser-compiler-independent-utils@4.71.1:
|
vue-inbrowser-compiler-independent-utils@4.71.1:
|
||||||
resolution: {integrity: sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==}
|
resolution: {integrity: sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -12893,7 +12904,7 @@ snapshots:
|
|||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
vue: 3.5.12(typescript@5.6.3)
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
vue-component-type-helpers: 2.1.6
|
vue-component-type-helpers: 2.1.10
|
||||||
|
|
||||||
'@swc/cli@0.4.0(@swc/core@1.7.39)(chokidar@4.0.1)':
|
'@swc/cli@0.4.0(@swc/core@1.7.39)(chokidar@4.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -20310,6 +20321,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
|
|
||||||
|
vue-component-type-helpers@2.1.10: {}
|
||||||
|
|
||||||
vue-component-type-helpers@2.1.6: {}
|
vue-component-type-helpers@2.1.6: {}
|
||||||
|
|
||||||
vue-demi@0.14.10(vue@3.5.12(typescript@5.6.3)):
|
vue-demi@0.14.10(vue@3.5.12(typescript@5.6.3)):
|
||||||
@ -20345,6 +20358,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
vue-gtag@2.0.1(vue@3.5.12(typescript@5.6.3)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
|
|
||||||
vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.12(typescript@5.6.3)):
|
vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.12(typescript@5.6.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.12(typescript@5.6.3)
|
vue: 3.5.12(typescript@5.6.3)
|
||||||
|
Loading…
Reference in New Issue
Block a user