Merge branch 'fetch' into serve-stream

This commit is contained in:
tamaina 2023-01-06 15:20:59 +00:00
commit a58771c230
179 changed files with 1731 additions and 1930 deletions

View File

@ -122,10 +122,12 @@ id: 'aid'
# Proxy for HTTP/HTTPS # Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128 #proxy: http://127.0.0.1:3128
#proxyBypassHosts: [ proxyBypassHosts:
# 'example.com', - api.deepl.com
# '192.0.2.8' - api-free.deepl.com
#] - www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
# Proxy for SMTP/SMTPS # Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT #proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT

View File

@ -20,6 +20,9 @@ You should also include the user name that made the change.
- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic - You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
- 新たに動的なPagesを作ることはできなくなりました - 新たに動的なPagesを作ることはできなくなりました
- 代わりに今後AiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能の実装を予定しています。 - 代わりに今後AiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能の実装を予定しています。
- AiScriptが0.12.0にアップデートされました
- 0.12.0の変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
- 0.12.0未満のプラグインは読み込むことはできません
- iOS15以下のデバイスはサポートされなくなりました - iOS15以下のデバイスはサポートされなくなりました
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。 - 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
@ -27,6 +30,7 @@ You should also include the user name that made the change.
- remote: `https://p1.a9z.dev/emoji/syuilo_birth_present@mk.f72u.net.webp` - remote: `https://p1.a9z.dev/emoji/syuilo_birth_present@mk.f72u.net.webp`
- API: `user`および`note`エンティティに`emojis`プロパティが含まれなくなりました - API: `user`および`note`エンティティに`emojis`プロパティが含まれなくなりました
- API: `user`エンティティに`avatarColor`および`bannerColor`プロパティが含まれなくなりました - API: `user`エンティティに`avatarColor`および`bannerColor`プロパティが含まれなくなりました
- API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました
### Improvements ### Improvements
- Push notification of Antenna note @tamaina - Push notification of Antenna note @tamaina
@ -41,6 +45,7 @@ You should also include the user name that made the change.
- Server: delete outdated notifications regularly to improve db performance @syuilo - Server: delete outdated notifications regularly to improve db performance @syuilo
- Server: delete outdated hard-mutes regularly to improve db performance @syuilo - Server: delete outdated hard-mutes regularly to improve db performance @syuilo
- Server: delete outdated notes of antenna regularly to improve db performance @syuilo - Server: delete outdated notes of antenna regularly to improve db performance @syuilo
- Server: improve activitypub deliver performance @syuilo
- Client: use tabler-icons instead of fontawesome to better design @syuilo - Client: use tabler-icons instead of fontawesome to better design @syuilo
- Client: Add new gabber kick sounds (thanks for noizenecio) - Client: Add new gabber kick sounds (thanks for noizenecio)
- Client: Add link to user RSS feed in profile menu @ssmucny - Client: Add link to user RSS feed in profile menu @ssmucny
@ -74,6 +79,7 @@ You should also include the user name that made the change.
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa - Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
- Client: update emoji picker immediately on all input @saschanaz - Client: update emoji picker immediately on all input @saschanaz
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo - Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
- Client: fix wrong link in tutorial @syuilo
## 12.119.1 (2022/12/03) ## 12.119.1 (2022/12/03)
### Bugfixes ### Bugfixes

View File

@ -1382,6 +1382,7 @@ _profile:
changeBanner: "Banner ändern" changeBanner: "Banner ändern"
_exportOrImport: _exportOrImport:
allNotes: "Alle Notizen" allNotes: "Alle Notizen"
favoritedNotes: "Als Favorit markierte Notizen"
followingList: "Gefolgte Benutzer" followingList: "Gefolgte Benutzer"
muteList: "Stummschaltungen" muteList: "Stummschaltungen"
blockingList: "Blockierungen" blockingList: "Blockierungen"

View File

@ -1382,6 +1382,7 @@ _profile:
changeBanner: "Change banner" changeBanner: "Change banner"
_exportOrImport: _exportOrImport:
allNotes: "All notes" allNotes: "All notes"
favoritedNotes: "Favorite notes"
followingList: "Followed users" followingList: "Followed users"
muteList: "Muted users" muteList: "Muted users"
blockingList: "Blocked users" blockingList: "Blocked users"

View File

@ -167,7 +167,6 @@ annotation: "注釈"
federation: "連合" federation: "連合"
instances: "インスタンス" instances: "インスタンス"
registeredAt: "初観測" registeredAt: "初観測"
latestRequestSentAt: "直近のリクエスト送信"
latestRequestReceivedAt: "直近のリクエスト受信" latestRequestReceivedAt: "直近のリクエスト受信"
latestStatus: "直近のステータス" latestStatus: "直近のステータス"
storageUsage: "ストレージ使用量" storageUsage: "ストレージ使用量"
@ -916,6 +915,7 @@ caption: "キャプション"
loggedInAsBot: "Botアカウントでログイン中" loggedInAsBot: "Botアカウントでログイン中"
tools: "ツール" tools: "ツール"
cannotLoad: "読み込めません" cannotLoad: "読み込めません"
numberOfProfileView: "プロフィール表示回数"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"

View File

@ -915,6 +915,7 @@ windowRestore: "복구"
caption: "캡션" caption: "캡션"
loggedInAsBot: "봇 계정으로 로그인중" loggedInAsBot: "봇 계정으로 로그인중"
tools: "도구" tools: "도구"
cannotLoad: "불러오지 못했습니다"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
sensitivity: "탐지 민감도" sensitivity: "탐지 민감도"

View File

@ -1382,6 +1382,7 @@ _profile:
changeBanner: "เปลี่ยนแบนเนอร์" changeBanner: "เปลี่ยนแบนเนอร์"
_exportOrImport: _exportOrImport:
allNotes: "โน้ตทั้งหมด" allNotes: "โน้ตทั้งหมด"
favoritedNotes: "บันทึกที่ชื่นชอบ"
followingList: "กำลังติดตาม" followingList: "กำลังติดตาม"
muteList: "ปิดเสียง" muteList: "ปิดเสียง"
blockingList: "บล็อค" blockingList: "บล็อค"

View File

@ -915,6 +915,7 @@ windowRestore: "还原"
caption: "标题" caption: "标题"
loggedInAsBot: "已登录的Bot" loggedInAsBot: "已登录的Bot"
tools: "工具" tools: "工具"
cannotLoad: "无法加载"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度" sensitivity: "检测敏感度"

View File

@ -915,6 +915,8 @@ windowRestore: "復原"
caption: "標題" caption: "標題"
loggedInAsBot: "以機器人帳號登入中" loggedInAsBot: "以機器人帳號登入中"
tools: "工具" tools: "工具"
cannotLoad: "無法載入"
numberOfProfileView: "個人檔案檢視次數"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度" sensitivity: "檢測敏感度"
@ -1381,6 +1383,7 @@ _profile:
changeBanner: "變更橫幅圖像" changeBanner: "變更橫幅圖像"
_exportOrImport: _exportOrImport:
allNotes: "所有貼文" allNotes: "所有貼文"
favoritedNotes: "「我的最愛」貼文"
followingList: "追隨中" followingList: "追隨中"
muteList: "靜音" muteList: "靜音"
blockingList: "封鎖" blockingList: "封鎖"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-beta.14", "version": "13.0.0-beta.20",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -57,7 +57,7 @@
"@typescript-eslint/parser": "5.47.1", "@typescript-eslint/parser": "5.47.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.2.0", "cypress": "12.2.0",
"eslint": "^8.30.0", "eslint": "^8.31.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"typescript": "4.9.4" "typescript": "4.9.4"
} }

View File

@ -0,0 +1,11 @@
export class removeLatestRequestSentAt1672703171386 {
name = 'removeLatestRequestSentAt1672703171386'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "latestRequestSentAt"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "latestRequestSentAt" TIMESTAMP WITH TIME ZONE`);
}
}

View File

@ -0,0 +1,11 @@
export class removeLastCommunicatedAt1672704017999 {
name = 'removeLastCommunicatedAt1672704017999'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "lastCommunicatedAt"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
}
}

View File

@ -0,0 +1,11 @@
export class removeLatestStatus1672704136584 {
name = 'removeLatestStatus1672704136584'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "latestStatus"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "latestStatus" integer`);
}
}

View File

@ -21,9 +21,9 @@
"@tensorflow/tfjs-node": "4.1.0" "@tensorflow/tfjs-node": "4.1.0"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.9.0", "@bull-board/api": "^4.10.0",
"@bull-board/fastify": "^4.9.0", "@bull-board/fastify": "^4.10.0",
"@bull-board/ui": "^4.9.0", "@bull-board/ui": "^4.10.0",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0", "@fastify/accepts": "4.1.0",
"@fastify/cookie": "^8.3.0", "@fastify/cookie": "^8.3.0",
@ -37,12 +37,11 @@
"@nestjs/testing": "9.2.1", "@nestjs/testing": "9.2.1",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2", "@sinonjs/fake-timers": "10.0.2",
"@syuilo/aiscript": "0.11.1",
"accepts": "^1.3.8", "accepts": "^1.3.8",
"ajv": "8.11.2", "ajv": "8.11.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.1272.0", "aws-sdk": "2.1286.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"bull": "4.10.2", "bull": "4.10.2",
@ -57,7 +56,7 @@
"date-fns": "2.29.3", "date-fns": "2.29.3",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"fastify": "4.10.2", "fastify": "4.11.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.0.0", "file-type": "18.0.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -69,7 +68,7 @@
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "20.0.3", "jsdom": "20.0.3",
"json5": "2.2.2", "json5": "2.2.3",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "8.1.0", "jsonld": "8.1.0",
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
@ -78,7 +77,6 @@
"misskey-js": "0.0.14", "misskey-js": "0.0.14",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.0",
"nodemailer": "6.8.0", "nodemailer": "6.8.0",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "^0.10.0", "oauth": "^0.10.0",
@ -115,10 +113,11 @@
"tinycolor2": "1.5.1", "tinycolor2": "1.5.1",
"tmp": "0.2.1", "tmp": "0.2.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
"ulid": "2.3.0", "ulid": "2.3.0",
"undici": "^5.14.0",
"unzipper": "0.10.11", "unzipper": "0.10.11",
"uuid": "9.0.0", "uuid": "9.0.0",
"vary": "1.1.2", "vary": "1.1.2",
@ -141,7 +140,7 @@
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20", "@types/fluent-ffmpeg": "2.1.20",
"@types/ioredis": "4.28.10", "@types/ioredis": "4.28.10",
"@types/jest": "29.2.4", "@types/jest": "29.2.5",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1", "@types/jsdom": "20.0.1",
"@types/jsonld": "1.5.8", "@types/jsonld": "1.5.8",
@ -161,7 +160,7 @@
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.0", "@types/sanitize-html": "2.8.0",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sharp": "0.31.0", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/syslog-pro": "^1.0.0", "@types/syslog-pro": "^1.0.0",
@ -172,15 +171,16 @@
"@types/vary": "1.1.0", "@types/vary": "1.1.0",
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.47.1", "@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1", "@typescript-eslint/parser": "5.47.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.30.0", "eslint": "8.31.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"execa": "6.1.0", "execa": "6.1.0",
"jest": "29.3.1", "jest": "29.3.1",
"jest-mock": "^29.3.1", "jest-mock": "^29.3.1",
"node-fetch": "3.3.0",
"typescript": "4.9.4" "typescript": "4.9.4"
} }
} }

View File

@ -27,16 +27,19 @@ export class CaptchaService {
response, response,
}); });
const res = await fetch(url, { const res = await this.httpRequestService.fetch(
method: 'POST', url,
body: params, {
headers: { method: 'POST',
'User-Agent': this.config.userAgent, body: params,
headers: {
'User-Agent': this.config.userAgent,
},
}, },
// TODO {
//timeout: 10 * 1000, noOkError: true,
agent: (url, bypassProxy) => this.httpRequestService.getAgentByUrl(url, bypassProxy), }
}).catch(err => { ).catch(err => {
throw `${err.message ?? err}`; throw `${err.message ?? err}`;
}); });

View File

@ -8,11 +8,12 @@ import got, * as Got from 'got';
import chalk from 'chalk'; import chalk from 'chalk';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { buildConnector } from 'undici';
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -20,6 +21,7 @@ import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class DownloadService { export class DownloadService {
private logger: Logger; private logger: Logger;
private undiciFetcher: UndiciFetcher;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -29,69 +31,58 @@ export class DownloadService {
private loggerService: LoggerService, private loggerService: LoggerService,
) { ) {
this.logger = this.loggerService.getLogger('download'); this.logger = this.loggerService.getLogger('download');
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption(
{
connect: process.env.NODE_ENV === 'development' ?
this.httpRequestService.clientDefaults.connect
:
this.httpRequestService.getConnectorWithIpCheck(
buildConnector({
...this.httpRequestService.clientDefaults.connect,
}),
(ip) => !this.isPrivateIp(ip)
),
bodyTimeout: 30 * 1000,
},
{
connect: this.httpRequestService.clientDefaults.connect,
}
), this.logger);
} }
@bindThis @bindThis
public gotUrl(url: string): Got.Request { public fetchUrl(url: string): any {
this.logger.info(`Downloading ${chalk.cyan(url)} ...`); this.logger.info(`Downloading ${chalk.cyan(url)} ...`);
const timeout = 30 * 1000; const timeout = 30 * 1000;
const operationTimeout = 60 * 1000; const operationTimeout = 60 * 1000;
const maxSize = this.config.maxFileSize ?? 262144000; const maxSize = this.config.maxFileSize ?? 262144000;
const req = got.stream(url, { const response = await this.undiciFetcher.fetch(
headers: { url,
'User-Agent': this.config.userAgent, {
}, method: 'GET',
timeout: { headers: {
lookup: timeout, 'User-Agent': this.config.userAgent,
connect: timeout, },
secureConnect: timeout,
socket: timeout, // read timeout
response: timeout,
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: this.httpRequestService.httpAgent,
https: this.httpRequestService.httpsAgent,
},
http2: false, // default
retry: {
limit: 0,
},
}).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) {
this.logger.warn(`Blocked address: ${res.ip}`);
req.destroy();
}
} }
);
const contentLength = res.headers['content-length']; if (response.body === null) {
if (contentLength != null) { throw new StatusError('No body', 400, 'No body');
const size = Number(contentLength); }
if (size > maxSize) {
this.logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
req.destroy();
}
}
}).on('downloadProgress', (progress: Got.Progress) => {
if (progress.transferred > maxSize) {
this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
req.destroy();
}
});
return req; this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
return response;
} }
@bindThis @bindThis
public async pipeRequestToFile(req: Got.Request, path: string): Promise<void> { public async pipeRequestToFile(response: any, path: string): Promise<void> {
const copied = req.pipe(new stream.PassThrough());
try { try {
this.logger.info(`Saving File to ${chalk.cyanBright(path)} from downloading ...`); this.logger.info(`Saving File to ${chalk.cyanBright(path)} from downloading ...`);
await pipeline(copied, fs.createWriteStream(path)); await pipeline(stream.Readable.fromWeb(response.body), fs.createWriteStream(path));
} catch (e) { } catch (e) {
if (e instanceof Got.HTTPError) { if (e instanceof Got.HTTPError) {
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
@ -135,6 +126,6 @@ export class DownloadService {
} }
} }
return PrivateIp(ip); return PrivateIp(ip) ?? false;
} }
} }

View File

@ -22,7 +22,7 @@ export class FederatedInstanceService {
} }
@bindThis @bindThis
public async registerOrFetchInstanceDoc(host: string): Promise<Instance> { public async fetch(host: string): Promise<Instance> {
host = this.utilityService.toPuny(host); host = this.utilityService.toPuny(host);
const cached = this.cache.get(host); const cached = this.cache.get(host);
@ -35,7 +35,6 @@ export class FederatedInstanceService {
id: this.idService.genId(), id: this.idService.genId(),
host, host,
caughtAt: new Date(), caughtAt: new Date(),
lastCommunicatedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
this.cache.set(host, i); this.cache.set(host, i);
@ -45,4 +44,17 @@ export class FederatedInstanceService {
return index; return index;
} }
} }
@bindThis
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
host = this.utilityService.toPuny(host);
const cached = this.cache.get(host);
if (cached == null) return;
this.cache.set(host, {
...cached,
...data,
});
}
} }

View File

@ -1,7 +1,6 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import fetch from 'node-fetch';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { InstancesRepository } from '@/models/index.js'; import type { InstancesRepository } from '@/models/index.js';
@ -191,11 +190,7 @@ export class FetchInstanceMetadataService {
const faviconUrl = url + '/favicon.ico'; const faviconUrl = url + '/favicon.ico';
const favicon = await fetch(faviconUrl, { const favicon = await this.httpRequestService.fetch(faviconUrl, {}, { noOkError: true });
// TODO
//timeout: 10000,
agent: url => this.httpRequestService.getAgentByUrl(url),
});
if (favicon.ok) { if (favicon.ok) {
return faviconUrl; return faviconUrl;

View File

@ -1,67 +1,255 @@
import * as http from 'node:http'; import * as http from 'node:http';
import * as https from 'node:https'; import * as https from 'node:https';
import CacheableLookup from 'cacheable-lookup'; import CacheableLookup from 'cacheable-lookup';
import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Response } from 'node-fetch'; import * as undici from 'undici';
import type { URL } from 'node:url'; import { LookupFunction } from 'node:net';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
// true to allow, false to deny
export type IpChecker = (ip: string) => boolean;
/*
* Child class to create and save Agent for fetch.
* You should construct this when you want
* to change timeout, size limit, socket connect function, etc.
*/
export class UndiciFetcher {
/**
* Get http non-proxy agent (undici)
*/
public nonProxiedAgent: undici.Agent;
/**
* Get http proxy or non-proxy agent (undici)
*/
public agent: undici.ProxyAgent | undici.Agent;
private proxyBypassHosts: string[];
private userAgent: string | undefined;
private logger: Logger | undefined;
constructor(
args: {
agentOptions: undici.Agent.Options;
proxy?: {
uri: string;
options?: undici.Agent.Options; // Override of agentOptions
},
proxyBypassHosts?: string[];
userAgent?: string;
},
logger?: Logger,
) {
this.logger = logger;
this.logger?.debug('UndiciFetcher constructor', args);
this.proxyBypassHosts = args.proxyBypassHosts ?? [];
this.userAgent = args.userAgent;
this.nonProxiedAgent = new undici.Agent({
...args.agentOptions,
connect: (process.env.NODE_ENV !== 'production' && typeof args.agentOptions.connect !== 'function')
? (options, cb) => {
// Custom connector for debug
undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => {
this.logger?.debug('Socket connector called', socket);
if (err) {
this.logger?.debug(`Socket error`, err);
cb(new Error(`Error while socket connecting\n${err}`), null);
return;
}
this.logger?.debug(`Socket connected: port ${socket.localPort} => remote ${socket.remoteAddress}`);
cb(null, socket);
});
} : args.agentOptions.connect,
});
this.agent = args.proxy
? new undici.ProxyAgent({
...args.agentOptions,
...args.proxy.options,
uri: args.proxy.uri,
connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy?.options?.connect ?? args.agentOptions.connect) !== 'function')
? (options, cb) => {
// Custom connector for debug
undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => {
this.logger?.debug('Socket connector called (secure)', socket);
if (err) {
this.logger?.debug(`Socket error`, err);
cb(new Error(`Error while socket connecting\n${err}`), null);
return;
}
this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`);
cb(null, socket);
});
} : (args.proxy?.options?.connect ?? args.agentOptions.connect),
})
: this.nonProxiedAgent;
}
/**
* Get agent by URL
* @param url URL
* @param bypassProxy Allways bypass proxy
*/
@bindThis
public getAgentByUrl(url: URL, bypassProxy = false): undici.Agent | undici.ProxyAgent {
if (bypassProxy || this.proxyBypassHosts.includes(url.hostname)) {
return this.nonProxiedAgent;
} else {
return this.agent;
}
}
@bindThis
public async fetch(
url: string | URL,
options: undici.RequestInit = {},
privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }
): Promise<undici.Response> {
const res = await undici.fetch(url, {
dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy),
...options,
}).catch((err) => {
this.logger?.error('fetch error', err);
throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable');
});
if (!res.ok && !privateOptions.noOkError) {
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
}
return res;
}
@bindThis
public async getJson<T extends unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
const res = await this.fetch(
url,
{
headers: Object.assign({
'User-Agent': this.userAgent,
Accept: accept,
}, headers ?? {}),
}
);
return await res.json() as T;
}
@bindThis
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
const res = await this.fetch(
url,
{
headers: Object.assign({
'User-Agent': this.userAgent,
Accept: accept,
}, headers ?? {}),
}
);
return await res.text();
}
}
@Injectable() @Injectable()
export class HttpRequestService { export class HttpRequestService {
/** public defaultFetcher: UndiciFetcher;
* Get http non-proxy agent public fetch: UndiciFetcher['fetch'];
*/ public getHtml: UndiciFetcher['getHtml'];
public defaultJsonFetcher: UndiciFetcher;
public getJson: UndiciFetcher['getJson'];
//#region for old http/https, only used in S3Service
// http non-proxy agent
private http: http.Agent; private http: http.Agent;
/** // https non-proxy agent
* Get https non-proxy agent
*/
private https: https.Agent; private https: https.Agent;
/** // http proxy or non-proxy agent
* Get http proxy or non-proxy agent
*/
public httpAgent: http.Agent; public httpAgent: http.Agent;
/** // https proxy or non-proxy agent
* Get https proxy or non-proxy agent
*/
public httpsAgent: https.Agent; public httpsAgent: https.Agent;
//#endregion
public readonly dnsCache: CacheableLookup;
public readonly clientDefaults: undici.Agent.Options;
private maxSockets: number;
private logger: Logger;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
private loggerService: LoggerService,
) { ) {
const cache = new CacheableLookup({ this.logger = this.loggerService.getLogger('http-request');
this.dnsCache = new CacheableLookup({
maxTtl: 3600, // 1hours maxTtl: 3600, // 1hours
errorTtl: 30, // 30secs errorTtl: 30, // 30secs
lookup: false, // nativeのdns.lookupにfallbackしない lookup: false, // nativeのdns.lookupにfallbackしない
}); });
this.clientDefaults = {
keepAliveTimeout: 30 * 1000,
keepAliveMaxTimeout: 10 * 60 * 1000,
keepAliveTimeoutThreshold: 1 * 1000,
strictContentLength: true,
headersTimeout: 10 * 1000,
bodyTimeout: 10 * 1000,
maxHeaderSize: 16364, // default
maxResponseSize: 10 * 1024 * 1024,
maxRedirections: 3,
connect: {
timeout: 10 * 1000, // コネクションが確立するまでのタイムアウト
maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80
lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98
},
}
this.maxSockets = Math.max(256, this.config.deliverJobConcurrency ?? 128);
this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger);
this.fetch = this.defaultFetcher.fetch;
this.getHtml = this.defaultFetcher.getHtml;
this.defaultJsonFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption({
maxResponseSize: 1024 * 256,
}), this.logger);
this.getJson = this.defaultJsonFetcher.getJson;
//#region for old http/https, only used in S3Service
this.http = new http.Agent({ this.http = new http.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: this.dnsCache.lookup,
} as http.AgentOptions); } as http.AgentOptions);
this.https = new https.Agent({ this.https = new https.Agent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
lookup: cache.lookup, lookup: this.dnsCache.lookup,
} as https.AgentOptions); } as https.AgentOptions);
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
this.httpAgent = config.proxy this.httpAgent = config.proxy
? new HttpProxyAgent({ ? new HttpProxyAgent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
maxSockets, maxSockets: this.maxSockets,
maxFreeSockets: 256, maxFreeSockets: 256,
scheduling: 'lifo', scheduling: 'lifo',
proxy: config.proxy, proxy: config.proxy,
@ -72,21 +260,46 @@ export class HttpRequestService {
? new HttpsProxyAgent({ ? new HttpsProxyAgent({
keepAlive: true, keepAlive: true,
keepAliveMsecs: 30 * 1000, keepAliveMsecs: 30 * 1000,
maxSockets, maxSockets: this.maxSockets,
maxFreeSockets: 256, maxFreeSockets: 256,
scheduling: 'lifo', scheduling: 'lifo',
proxy: config.proxy, proxy: config.proxy,
}) })
: this.https; : this.https;
//#endregion
} }
/** /**
* Get agent by URL * Get http agent by URL
* @param url URL * @param url URL
* @param bypassProxy Allways bypass proxy * @param bypassProxy Allways bypass proxy
*/ */
@bindThis @bindThis
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent { public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) {
return {
agentOptions: {
...this.clientDefaults,
...opts,
},
...(this.config.proxy ? {
proxy: {
uri: this.config.proxy,
options: {
connections: this.maxSockets,
...proxyOpts,
}
}
} : {}),
}
}
/**
* Get http agent by URL
* @param url URL
* @param bypassProxy Allways bypass proxy
*/
@bindThis
public getHttpAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) { if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
return url.protocol === 'http:' ? this.http : this.https; return url.protocol === 'http:' ? this.http : this.https;
} else { } else {
@ -94,67 +307,37 @@ export class HttpRequestService {
} }
} }
/**
* check ip
*/
@bindThis @bindThis
public async getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>): Promise<unknown> { public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync {
const res = await this.getResponse({ return (options, cb) => {
url, connector(options, (err, socket) => {
method: 'GET', this.logger.debug('Socket connector (with ip checker) called', socket);
headers: Object.assign({ if (err) {
'User-Agent': this.config.userAgent, this.logger.error(`Socket error`, err)
Accept: accept, cb(new Error(`Error while socket connecting\n${err}`), null);
}, headers ?? {}), return;
timeout, }
size: 1024 * 256,
});
return await res.json(); if (socket.remoteAddress == undefined) {
} this.logger.error(`Socket error: remoteAddress is undefined`);
cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null);
return;
}
@bindThis // allow
public async getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>): Promise<string> { if (checkIp(socket.remoteAddress)) {
const res = await this.getResponse({ this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`);
url, cb(null, socket);
method: 'GET', return;
headers: Object.assign({ }
'User-Agent': this.config.userAgent,
Accept: accept,
}, headers ?? {}),
timeout,
});
return await res.text(); this.logger.error('IP is not allowed', socket);
} cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null);
socket.destroy();
@bindThis });
public async getResponse(args: { };
url: string,
method: string,
body?: string,
headers: Record<string, string>,
timeout?: number,
size?: number,
}): Promise<Response> {
const timeout = args.timeout ?? 10 * 1000;
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, timeout * 6);
const res = await fetch(args.url, {
method: args.method,
headers: args.headers,
body: args.body,
timeout,
size: args.size ?? 10 * 1024 * 1024,
agent: (url) => this.getAgentByUrl(url),
signal: controller.signal,
});
if (!res.ok) {
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
}
return res;
} }
} }

View File

@ -428,7 +428,7 @@ export class NoteCreateService {
// Register host // Register host
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => { this.federatedInstanceService.fetch(user.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, true); this.instanceChart.updateNote(i.host, note, true);
}); });

View File

@ -100,7 +100,7 @@ export class NoteDeleteService {
this.perUserNotesChart.update(user, note, false); this.perUserNotesChart.update(user, note, false);
if (this.userEntityService.isRemoteUser(user)) { if (this.userEntityService.isRemoteUser(user)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => { this.federatedInstanceService.fetch(user.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
this.instanceChart.updateNote(i.host, note, false); this.instanceChart.updateNote(i.host, note, false);
}); });

View File

@ -33,7 +33,7 @@ export class S3Service {
? false ? false
: meta.objectStorageS3ForcePathStyle, : meta.objectStorageS3ForcePathStyle,
httpOptions: { httpOptions: {
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
}, },
}); });
} }

View File

@ -205,12 +205,12 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, true); this.instanceChart.updateFollowing(i.host, true);
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => { this.federatedInstanceService.fetch(followee.host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, true); this.instanceChart.updateFollowers(i.host, true);
}); });
@ -323,12 +323,12 @@ export class UserFollowingService {
//#region Update instance stats //#region Update instance stats
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => { this.federatedInstanceService.fetch(follower.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
this.instanceChart.updateFollowing(i.host, false); this.instanceChart.updateFollowing(i.host, false);
}); });
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => { this.federatedInstanceService.fetch(followee.host).then(i => {
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
this.instanceChart.updateFollowers(i.host, false); this.instanceChart.updateFollowers(i.host, false);
}); });

View File

@ -30,7 +30,7 @@ export class WebfingerService {
public async webfinger(query: string): Promise<IWebFinger> { public async webfinger(query: string): Promise<IWebFinger> {
const url = this.genUrl(query); const url = this.genUrl(query);
return await this.httpRequestService.getJson(url, 'application/jrd+json, application/json') as IWebFinger; return await this.httpRequestService.getJson<IWebFinger>(url, 'application/jrd+json, application/json');
} }
@bindThis @bindThis

View File

@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js'; import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
type Request = { type Request = {
@ -28,6 +28,8 @@ type PrivateKey = {
@Injectable() @Injectable()
export class ApRequestService { export class ApRequestService {
private undiciFetcher: UndiciFetcher;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@ -35,6 +37,9 @@ export class ApRequestService {
private userKeypairStoreService: UserKeypairStoreService, private userKeypairStoreService: UserKeypairStoreService,
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
) { ) {
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
maxRedirections: 0,
}));
} }
@bindThis @bindThis
@ -152,12 +157,14 @@ export class ApRequestService {
}, },
}); });
await this.httpRequestService.getResponse({ await this.undiciFetcher.fetch(
url, url,
method: req.request.method, {
headers: req.request.headers, method: req.request.method,
body, headers: req.request.headers,
}); body,
}
);
} }
/** /**
@ -180,11 +187,13 @@ export class ApRequestService {
}, },
}); });
const res = await this.httpRequestService.getResponse({ const res = await this.httpRequestService.fetch(
url, url,
method: req.request.method, {
headers: req.request.headers, method: req.request.method,
}); headers: req.request.headers,
}
);
return await res.json(); return await res.json();
} }

View File

@ -4,7 +4,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -17,6 +17,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js';
export class Resolver { export class Resolver {
private history: Set<string>; private history: Set<string>;
private user?: ILocalUser; private user?: ILocalUser;
private undiciFetcher: UndiciFetcher;
constructor( constructor(
private config: Config, private config: Config,
@ -34,6 +35,9 @@ export class Resolver {
private recursionLimit = 100, private recursionLimit = 100,
) { ) {
this.history = new Set(); this.history = new Set();
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
maxRedirections: 0,
}));
} }
@bindThis @bindThis
@ -96,8 +100,8 @@ export class Resolver {
} }
const object = (this.user const object = (this.user
? await this.apRequestService.signedGet(value, this.user) ? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; : await this.undiciFetcher.getJson<IObject>(value, 'application/activity+json, application/ld+json'));
if (object == null || ( if (object == null || (
Array.isArray(object['@context']) ? Array.isArray(object['@context']) ?

View File

@ -1,6 +1,5 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import fetch from 'node-fetch';
import { HttpRequestService } from '@/core/HttpRequestService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { CONTEXTS } from './misc/contexts.js'; import { CONTEXTS } from './misc/contexts.js';
@ -116,14 +115,19 @@ class LdSignature {
@bindThis @bindThis
private async fetchDocument(url: string) { private async fetchDocument(url: string) {
const json = await fetch(url, { const json = await this.httpRequestService.fetch(
headers: { url,
Accept: 'application/ld+json, application/json', {
headers: {
Accept: 'application/ld+json, application/json',
},
// TODO
//timeout: this.loderTimeout,
}, },
// TODO {
//timeout: this.loderTimeout, noOkError: true,
agent: u => u.protocol === 'http:' ? this.httpRequestService.httpAgent : this.httpRequestService.httpsAgent, }
}).then(res => { ).then(res => {
if (!res.ok) { if (!res.ok) {
throw `${res.status} ${res.statusText}`; throw `${res.status} ${res.statusText}`;
} else { } else {

View File

@ -348,7 +348,7 @@ export class ApPersonService implements OnModuleInit {
} }
// Register host // Register host
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
this.instanceChart.newUser(i.host); this.instanceChart.newUser(i.host);
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);

View File

@ -86,7 +86,7 @@ export default class FederationChart extends Chart<typeof schema> {
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`) .where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
.andWhere('instance.isSuspended = false') .andWhere('instance.isSuspended = false')
.andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) .andWhere('instance.isNotResponding = false')
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)), .then(x => parseInt(x.count, 10)),
this.instancesRepository.createQueryBuilder('instance') this.instancesRepository.createQueryBuilder('instance')
@ -94,7 +94,7 @@ export default class FederationChart extends Chart<typeof schema> {
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts }) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
.andWhere('instance.isSuspended = false') .andWhere('instance.isSuspended = false')
.andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) }) .andWhere('instance.isNotResponding = false')
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)), .then(x => parseInt(x.count, 10)),
]); ]);

View File

@ -7,8 +7,8 @@ import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {
@ -33,8 +33,6 @@ export class InstanceEntityService {
notesCount: instance.notesCount, notesCount: instance.notesCount,
followingCount: instance.followingCount, followingCount: instance.followingCount,
followersCount: instance.followersCount, followersCount: instance.followersCount,
latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null,
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended, isSuspended: instance.isSuspended,
isBlocked: meta.blockedHosts.includes(instance.host), isBlocked: meta.blockedHosts.includes(instance.host),

View File

@ -59,22 +59,6 @@ export class Instance {
}) })
public followersCount: number; public followersCount: number;
/**
*
*/
@Column('timestamp with time zone', {
nullable: true,
})
public latestRequestSentAt: Date | null;
/**
* HTTPステータスコード
*/
@Column('integer', {
nullable: true,
})
public latestStatus: number | null;
/** /**
* *
*/ */
@ -83,12 +67,6 @@ export class Instance {
}) })
public latestRequestReceivedAt: Date | null; public latestRequestReceivedAt: Date | null;
/**
*
*/
@Column('timestamp with time zone')
public lastCommunicatedAt: Date;
/** /**
* *
*/ */

View File

@ -32,16 +32,6 @@ export const packedFederationInstanceSchema = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
latestRequestSentAt: {
type: 'string',
optional: false, nullable: true,
format: 'date-time',
},
lastCommunicatedAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
isNotResponding: { isNotResponding: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -15,10 +15,10 @@ import ApRequestChart from '@/core/chart/charts/ap-request.js';
import FederationChart from '@/core/chart/charts/federation.js'; import FederationChart from '@/core/chart/charts/federation.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull'; import type Bull from 'bull';
import type { DeliverJobData } from '../types.js'; import type { DeliverJobData } from '../types.js';
import { bindThis } from '@/decorators.js';
@Injectable() @Injectable()
export class DeliverProcessorService { export class DeliverProcessorService {
@ -48,7 +48,6 @@ export class DeliverProcessorService {
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60); this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
this.latest = null;
} }
@bindThis @bindThis
@ -76,20 +75,18 @@ export class DeliverProcessorService {
} }
try { try {
if (this.latest !== (this.latest = JSON.stringify(job.data.content, null, 2))) {
this.logger.debug(`delivering ${this.latest}`);
}
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content); await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
// Update stats // Update stats
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.update(i.id, { if (i.isNotResponding) {
latestRequestSentAt: new Date(), this.instancesRepository.update(i.id, {
latestStatus: 200, isNotResponding: false,
lastCommunicatedAt: new Date(), });
isNotResponding: false, this.federatedInstanceService.updateCachePartial(host, {
}); isNotResponding: false,
});
}
this.fetchInstanceMetadataService.fetchInstanceMetadata(i); this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
@ -100,13 +97,16 @@ export class DeliverProcessorService {
return 'Success'; return 'Success';
} catch (res) { } catch (res) {
// Update stats // Update stats
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => { this.federatedInstanceService.fetch(host).then(i => {
this.instancesRepository.update(i.id, { if (!i.isNotResponding) {
latestRequestSentAt: new Date(), this.instancesRepository.update(i.id, {
latestStatus: res instanceof StatusError ? res.statusCode : null, isNotResponding: true,
isNotResponding: true, });
}); this.federatedInstanceService.updateCachePartial(host, {
isNotResponding: true,
});
}
this.instanceChart.requestSent(i.host, false); this.instanceChart.requestSent(i.host, false);
this.apRequestChart.deliverFail(); this.apRequestChart.deliverFail();
@ -114,17 +114,17 @@ export class DeliverProcessorService {
}); });
if (res instanceof StatusError) { if (res instanceof StatusError) {
// 4xx // 4xx
if (res.isClientError) { if (res.isClientError) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり // HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく // 何回再送しても成功することはないということなのでエラーにはしないでおく
return `${res.statusCode} ${res.statusMessage}`; return `${res.statusCode} ${res.statusMessage}`;
} }
// 5xx etc. // 5xx etc.
throw `${res.statusCode} ${res.statusMessage}`; throw `${res.statusCode} ${res.statusMessage}`;
} else { } else {
// DNS error, socket error, timeout ... // DNS error, socket error, timeout ...
throw res; throw res;
} }
} }

View File

@ -176,10 +176,12 @@ export class InboxProcessorService {
} }
// Update stats // Update stats
this.federatedInstanceService.registerOrFetchInstanceDoc(authUser.user.host).then(i => { this.federatedInstanceService.fetch(authUser.user.host).then(i => {
this.instancesRepository.update(i.id, { this.instancesRepository.update(i.id, {
latestRequestReceivedAt: new Date(), latestRequestReceivedAt: new Date(),
lastCommunicatedAt: new Date(), isNotResponding: false,
});
this.federatedInstanceService.updateCachePartial(host, {
isNotResponding: false, isNotResponding: false,
}); });

View File

@ -33,24 +33,26 @@ export class WebhookDeliverProcessorService {
try { try {
this.logger.debug(`delivering ${job.data.webhookId}`); this.logger.debug(`delivering ${job.data.webhookId}`);
const res = await this.httpRequestService.getResponse({ const res = await this.httpRequestService.fetch(
url: job.data.to, job.data.to,
method: 'POST', {
headers: { method: 'POST',
'User-Agent': 'Misskey-Hooks', headers: {
'X-Misskey-Host': this.config.host, 'User-Agent': 'Misskey-Hooks',
'X-Misskey-Hook-Id': job.data.webhookId, 'X-Misskey-Host': this.config.host,
'X-Misskey-Hook-Secret': job.data.secret, 'X-Misskey-Hook-Id': job.data.webhookId,
}, 'X-Misskey-Hook-Secret': job.data.secret,
body: JSON.stringify({ },
hookId: job.data.webhookId, body: JSON.stringify({
userId: job.data.userId, hookId: job.data.webhookId,
eventId: job.data.eventId, userId: job.data.userId,
createdAt: job.data.createdAt, eventId: job.data.eventId,
type: job.data.type, createdAt: job.data.createdAt,
body: job.data.content, type: job.data.type,
}), body: job.data.content,
}); }),
}
);
this.webhooksRepository.update({ id: job.data.webhookId }, { this.webhooksRepository.update({ id: job.data.webhookId }, {
latestSentAt: new Date(), latestSentAt: new Date(),

View File

@ -64,8 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
case '-followers': query.orderBy('instance.followersCount', 'ASC'); break; case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC'); break;
case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC'); break;
default: query.orderBy('instance.id', 'DESC'); break; default: query.orderBy('instance.id', 'DESC'); break;
} }

View File

@ -33,15 +33,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private httpRequestService: HttpRequestService, private httpRequestService: HttpRequestService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const res = await this.httpRequestService.getResponse({ const res = await this.httpRequestService.fetch(
url: ps.url, ps.url,
method: 'GET', {
headers: Object.assign({ method: 'GET',
'User-Agent': config.userAgent, headers: Object.assign({
Accept: 'application/rss+xml, */*', 'User-Agent': config.userAgent,
}), Accept: 'application/rss+xml, */*',
timeout: 5000, }),
}); // timeout: 5000,
}
);
const text = await res.text(); const text = await res.text();

View File

@ -1,5 +1,4 @@
import { URLSearchParams } from 'node:url'; import { URLSearchParams } from 'node:url';
import fetch from 'node-fetch';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/index.js'; import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
@ -84,25 +83,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
const res = await fetch(endpoint, { const res = await this.httpRequestService.fetch(
method: 'POST', endpoint,
headers: { {
'Content-Type': 'application/x-www-form-urlencoded', method: 'POST',
'User-Agent': config.userAgent, headers: {
Accept: 'application/json, */*', 'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': config.userAgent,
Accept: 'application/json, */*',
},
body: params.toString(),
}, },
body: params, {
// TODO noOkError: false,
//timeout: 10000, }
agent: (url) => this.httpRequestService.getAgentByUrl(url), );
});
const json = (await res.json()) as { const json = (await res.json()) as {
translations: { translations: {
detected_source_language: string; detected_source_language: string;
text: string; text: string;
}[]; }[];
}; };
return { return {
sourceLang: json.translations[0].detected_source_language, sourceLang: json.translations[0].detected_source_language,

View File

@ -139,10 +139,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchUser); throw new ApiError(meta.errors.noSuchUser);
} }
if (me == null && ip != null) { if (user.host == null) {
this.perUserPvChart.commitByVisitor(user, ip); if (me == null && ip != null) {
} else if (me && me.id !== user.id) { this.perUserPvChart.commitByVisitor(user, ip);
this.perUserPvChart.commitByUser(user, me.id); } else if (me && me.id !== user.id) {
this.perUserPvChart.commitByUser(user, me.id);
}
} }
return await this.userEntityService.pack(user, me, { return await this.userEntityService.pack(user, me, {

View File

@ -181,7 +181,7 @@ export class DiscordServerService {
} }
})); }));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>; })) as Record<string, unknown>;
@ -249,7 +249,7 @@ export class DiscordServerService {
} }
})); }));
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, { const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
})) as Record<string, unknown>; })) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') { if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {

View File

@ -174,7 +174,7 @@ export class GithubServerService {
} }
})); }));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`, 'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>; })) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'string') { if (typeof login !== 'string' || typeof id !== 'string') {
@ -223,7 +223,7 @@ export class GithubServerService {
} }
})); }));
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, { const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
'Authorization': `bearer ${accessToken}`, 'Authorization': `bearer ${accessToken}`,
})) as Record<string, unknown>; })) as Record<string, unknown>;

View File

@ -63,9 +63,8 @@ export class UrlPreviewService {
this.logger.info(meta.summalyProxy this.logger.info(meta.summalyProxy
? `(Proxy) Getting preview of ${url}@${lang} ...` ? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`); : `Getting preview of ${url}@${lang} ...`);
try { try {
const summary = meta.summalyProxy ? await this.httpRequestService.getJson(`${meta.summalyProxy}?${query({ const summary = meta.summalyProxy ? await this.httpRequestService.getJson<ReturnType<typeof summaly.default>>(`${meta.summalyProxy}?${query({
url: url, url: url,
lang: lang ?? 'ja-JP', lang: lang ?? 'ja-JP',
})}`) : await summaly.default(url, { })}`) : await summaly.default(url, {

View File

@ -11,14 +11,14 @@
"@rollup/plugin-alias": "4.0.2", "@rollup/plugin-alias": "4.0.2",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2", "@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.12.0",
"@tabler/icons": "^1.118.0", "@tabler/icons": "^1.118.0",
"@vitejs/plugin-vue": "4.0.0", "@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.45", "@vue/compiler-sfc": "3.2.45",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "2.0.4", "blurhash": "2.0.4",
"broadcast-channel": "4.18.1", "broadcast-channel": "4.19.1",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"chart.js": "4.1.1", "chart.js": "4.1.1",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
@ -34,7 +34,7 @@
"idb-keyval": "6.2.0", "idb-keyval": "6.2.0",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.2", "json5": "2.2.3",
"katex": "0.15.6", "katex": "0.15.6",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.0", "mfm-js": "0.23.0",
@ -57,7 +57,7 @@
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.5.1", "tinycolor2": "1.5.1",
"tsc-alias": "1.8.2", "tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.1", "tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "4.9.4", "typescript": "4.9.4",
"uuid": "9.0.0", "uuid": "9.0.0",
@ -76,22 +76,22 @@
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0", "@types/sanitize-html": "^2.8.0",
"@types/seedrandom": "3.0.3", "@types/seedrandom": "3.0.4",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "9.0.0", "@types/uuid": "9.0.0",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.47.1", "@typescript-eslint/eslint-plugin": "5.47.1",
"@typescript-eslint/parser": "5.47.1", "@typescript-eslint/parser": "5.47.1",
"@vue/runtime-core": "3.2.45", "@vue/runtime-core": "3.2.45",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "12.2.0", "cypress": "12.2.0",
"eslint": "8.30.0", "eslint": "8.31.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.8.0", "eslint-plugin-vue": "9.8.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"vue-eslint-parser": "^9.1.0", "vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.18" "vue-tsc": "^1.0.19"
} }
} }

View File

@ -23,7 +23,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XWindow from '@/components/MkWindow.vue'; import XWindow from '@/components/MkWindow.vue';
import MkTextarea from '@/components/form/textarea.vue'; import MkTextarea from '@/components/form/textarea.vue';
@ -40,7 +40,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const uiWindow = ref<InstanceType<typeof XWindow>>(); const uiWindow = shallowRef<InstanceType<typeof XWindow>>();
const comment = ref(props.initialComment || ''); const comment = ref(props.initialComment || '');
function send() { function send() {

View File

@ -16,9 +16,9 @@
</li> </li>
</ol> </ol>
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> <ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
<li v-for="emoji in emojis" tabindex="-1" :key="emoji.emoji" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> <li v-for="emoji in emojis" :key="emoji.emoji" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<div class="emoji"> <div class="emoji">
<MkEmoji :emoji="emoji.emoji" /> <MkEmoji :emoji="emoji.emoji"/>
</div> </div>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" class="name" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span> <span v-if="q" class="name" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
@ -35,7 +35,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, ref, shallowRef, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import sanitizeHtml from 'sanitize-html';
import contains from '@/scripts/contains'; import contains from '@/scripts/contains';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
@ -45,7 +46,6 @@ import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist'; import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import sanitizeHtml from 'sanitize-html';
type EmojiDef = { type EmojiDef = {
emoji: string; emoji: string;
@ -136,7 +136,7 @@ const emit = defineEmits<{
}>(); }>();
const suggests = ref<Element>(); const suggests = ref<Element>();
const rootEl = ref<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement>();
const fetching = ref(true); const fetching = ref(true);
const users = ref<any[]>([]); const users = ref<any[]>([]);
@ -384,7 +384,7 @@ onBeforeUnmount(() => {
position: fixed; position: fixed;
max-width: 100%; max-width: 100%;
margin-top: calc(1em + 8px); margin-top: calc(1em + 8px);
overflow: hidden; overflow: clip;
transition: top 0.1s ease, left 0.1s ease; transition: top 0.1s ease, left 0.1s ease;
> ol { > ol {
@ -401,7 +401,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
padding: 4px 12px; padding: 4px 12px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: clip;
font-size: 0.9em; font-size: 0.9em;
cursor: default; cursor: default;

View File

@ -47,8 +47,8 @@ const emit = defineEmits<{
(ev: 'click', payload: MouseEvent): void; (ev: 'click', payload: MouseEvent): void;
}>(); }>();
let el = $ref<HTMLElement | null>(null); let el = $shallowRef<HTMLElement | null>(null);
let ripples = $ref<HTMLElement | null>(null); let ripples = $shallowRef<HTMLElement | null>(null);
onMounted(() => { onMounted(() => {
if (props.autofocus) { if (props.autofocus) {
@ -207,7 +207,7 @@ function onMousedown(evt: MouseEvent): void {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: clip;
::v-deep(div) { ::v-deep(div) {
position: absolute; position: absolute;

View File

@ -6,7 +6,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@ -42,7 +42,7 @@ const emit = defineEmits<{
const available = ref(false); const available = ref(false);
const captchaEl = ref<HTMLDivElement | undefined>(); const captchaEl = shallowRef<HTMLDivElement | undefined>();
const variable = computed(() => { const variable = computed(() => {
switch (props.provider) { switch (props.provider) {
@ -62,7 +62,7 @@ const src = computed(() => {
} }
}); });
const scriptId = computed(() => `script-${props.provider}`) const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="cbbedffa"> <div class="cbbedffa">
<canvas ref="chartEl"></canvas> <canvas ref="chartEl"></canvas>
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
<div v-if="fetching" class="fetching"> <div v-if="fetching" class="fetching">
<MkLoading/> <MkLoading/>
</div> </div>
@ -13,27 +14,9 @@
id-denylist violation when setting it. This is causing about 60+ lint issues. id-denylist violation when setting it. This is causing about 60+ lint issues.
As this is part of Chart.js's API it makes sense to disable the check here. As this is part of Chart.js's API it makes sense to disable the check here.
*/ */
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue'; import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import zoomPlugin from 'chartjs-plugin-zoom';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import * as os from '@/os'; import * as os from '@/os';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
@ -41,6 +24,11 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import date from '@/filters/date'; import date from '@/filters/date';
import { initChart } from '@/scripts/init-chart';
import { chartLegend } from '@/scripts/chart-legend';
import MkChartLegend from '@/components/MkChartLegend.vue';
initChart();
const props = defineProps({ const props = defineProps({
src: { src: {
@ -82,24 +70,7 @@ const props = defineProps({
}, },
}); });
Chart.register( let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
zoomPlugin,
gradient,
);
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x); const negate = arr => arr.map(x => -x);
@ -135,7 +106,7 @@ let chartData: {
}[]; }[];
} = null; } = null;
const chartEl = ref<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement>(null);
const fetching = ref(true); const fetching = ref(true);
const getDate = (ago: number) => { const getDate = (ago: number) => {
@ -161,12 +132,8 @@ const render = () => {
chartInstance.destroy(); chartInstance.destroy();
} }
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
//
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
@ -221,8 +188,6 @@ const render = () => {
unit: props.span === 'day' ? 'month' : 'day', unit: props.span === 'day' ? 'month' : 'day',
}, },
grid: { grid: {
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
display: props.detailed, display: props.detailed,
@ -241,8 +206,6 @@ const render = () => {
stacked: props.stacked, stacked: props.stacked,
suggestedMax: 50, suggestedMax: 50,
grid: { grid: {
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
display: props.detailed, display: props.detailed,
@ -260,14 +223,9 @@ const render = () => {
hoverBorderWidth: 2, hoverBorderWidth: 2,
}, },
}, },
animation: false,
plugins: { plugins: {
legend: { legend: {
display: props.detailed, display: false,
position: 'bottom',
labels: {
boxWidth: 16,
},
}, },
tooltip: { tooltip: {
enabled: false, enabled: false,
@ -307,7 +265,7 @@ const render = () => {
gradient, gradient,
}, },
}, },
plugins: [chartVLine(vLineColor)], plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])],
}); });
}; };
@ -742,6 +700,33 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
}; };
}; };
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
const raw = await os.apiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span });
return {
series: [{
name: 'Unique PV (user)',
type: 'area',
data: format(raw.upv.user),
color: colors.purple,
}, {
name: 'PV (user)',
type: 'area',
data: format(raw.pv.user),
color: colors.green,
}, {
name: 'Unique PV (visitor)',
type: 'area',
data: format(raw.upv.visitor),
color: colors.yellow,
}, {
name: 'PV (visitor)',
type: 'area',
data: format(raw.pv.visitor),
color: colors.blue,
}],
};
};
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => { const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
return { return {
@ -814,6 +799,7 @@ const fetchAndRender = async () => {
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
case 'per-user-notes': return fetchPerUserNotesChart(); case 'per-user-notes': return fetchPerUserNotesChart();
case 'per-user-pv': return fetchPerUserPvChart();
case 'per-user-following': return fetchPerUserFollowingChart(); case 'per-user-following': return fetchPerUserFollowingChart();
case 'per-user-followers': return fetchPerUserFollowersChart(); case 'per-user-followers': return fetchPerUserFollowersChart();
case 'per-user-drive': return fetchPerUserDriveChart(); case 'per-user-drive': return fetchPerUserDriveChart();

View File

@ -0,0 +1,75 @@
<template>
<div :class="$style.root">
<button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)">
<span class="box" :style="{ background: chart.config.type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
{{ item.text }}
</button>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
import { Chart, LegendItem } from 'chart.js';
const props = defineProps({
});
let chart = $shallowRef<Chart>();
let items = $shallowRef<LegendItem[]>([]);
function update(_chart: Chart, _items: LegendItem[]) {
chart = _chart,
items = _items;
}
function onClick(item: LegendItem) {
if (chart == null) return;
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
chart.toggleDataVisibility(item.index);
} else {
chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex));
}
chart.update();
}
defineExpose({
update,
});
</script>
<style lang="scss" module>
.root {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
&:global {
> .item {
font-size: 85%;
padding: 4px 12px 4px 8px;
border: solid 1px var(--divider);
border-radius: 999px;
&:hover {
border-color: var(--inputBorderHover);
}
&.disabled {
text-decoration: line-through;
opacity: 0.6;
}
> .box {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 100%;
vertical-align: -10%;
}
}
}
}
</style>

View File

@ -22,7 +22,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let rootEl = $ref<HTMLDivElement>(); let rootEl = $shallowRef<HTMLDivElement>();
let zIndex = $ref<number>(os.claimZIndex('high')); let zIndex = $ref<number>(os.claimZIndex('high'));

View File

@ -50,8 +50,8 @@ const props = defineProps<{
}>(); }>();
const imgUrl = getProxiedImageUrl(props.file.url); const imgUrl = getProxiedImageUrl(props.file.url);
let dialogEl = $ref<InstanceType<typeof XModalWindow>>(); let dialogEl = $shallowRef<InstanceType<typeof XModalWindow>>();
let imgEl = $ref<HTMLImageElement>(); let imgEl = $shallowRef<HTMLImageElement>();
let cropper: Cropper | null = null; let cropper: Cropper | null = null;
let loading = $ref(true); let loading = $ref(true);

View File

@ -39,7 +39,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/form/input.vue'; import MkInput from '@/components/form/input.vue';
@ -94,7 +94,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const modal = ref<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const inputValue = ref(props.input?.default || null); const inputValue = ref(props.input?.default || null);
const selectedValue = ref(props.select?.default || null); const selectedValue = ref(props.select?.default || null);

View File

@ -88,7 +88,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XNavFolder from '@/components/MkDrive.navFolder.vue';
@ -118,8 +118,8 @@ const emit = defineEmits<{
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void; (ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
}>(); }>();
const loadMoreFiles = ref<InstanceType<typeof MkButton>>(); const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
const fileInput = ref<HTMLInputElement>(); const fileInput = shallowRef<HTMLInputElement>();
const folder = ref<Misskey.entities.DriveFolder | null>(null); const folder = ref<Misskey.entities.DriveFolder | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]); const files = ref<Misskey.entities.DriveFile[]>([]);

View File

@ -19,7 +19,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XDrive from '@/components/MkDrive.vue'; import XDrive from '@/components/MkDrive.vue';
import XModalWindow from '@/components/MkModalWindow.vue'; import XModalWindow from '@/components/MkModalWindow.vue';
@ -38,7 +38,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const dialog = ref<InstanceType<typeof XModalWindow>>(); const dialog = shallowRef<InstanceType<typeof XModalWindow>>();
const selected = ref<Misskey.entities.DriveFile[]>([]); const selected = ref<Misskey.entities.DriveFile[]>([]);

View File

@ -77,7 +77,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue'; import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue'; import XSection from '@/components/MkEmojiPicker.section.vue';
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
@ -102,8 +102,8 @@ const emit = defineEmits<{
(ev: 'chosen', v: string): void; (ev: 'chosen', v: string): void;
}>(); }>();
const search = ref<HTMLInputElement>(); const search = shallowRef<HTMLInputElement>();
const emojis = ref<HTMLDivElement>(); const emojis = shallowRef<HTMLDivElement>();
const { const {
reactions: pinned, reactions: pinned,

View File

@ -26,7 +26,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
@ -48,8 +48,8 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const modal = ref<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const picker = ref<InstanceType<typeof MkEmojiPicker>>(); const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) { function chosen(emoji: any) {
emit('done', emoji); emit('done', emoji);

View File

@ -37,7 +37,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
let caption = $ref(props.default); let caption = $ref(props.default);

View File

@ -9,57 +9,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import * as os from '@/os'; import * as os from '@/os';
import 'chartjs-adapter-date-fns';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
MatrixController, MatrixElement,
);
const props = defineProps<{ const props = defineProps<{
src: string; src: string;
}>(); }>();
const rootEl = $ref<HTMLDivElement>(null); const rootEl = $shallowRef<HTMLDivElement>(null);
const chartEl = $ref<HTMLCanvasElement>(null); const chartEl = $shallowRef<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null;
let fetching = $ref(true); let fetching = $ref(true);
@ -123,11 +91,6 @@ async function renderChart() {
await nextTick(); await nextTick();
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
//
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
// 3 // 3
@ -191,8 +154,6 @@ async function renderChart() {
}, },
grid: { grid: {
display: false, display: false,
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
display: true, display: true,
@ -206,8 +167,6 @@ async function renderChart() {
position: 'right', position: 'right',
grid: { grid: {
display: false, display: false,
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
maxRotation: 0, maxRotation: 0,
@ -220,7 +179,6 @@ async function renderChart() {
}, },
}, },
}, },
animation: false,
plugins: { plugins: {
legend: { legend: {
display: false, display: false,

View File

@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const modal = $ref<InstanceType<typeof MkModal>>(); const modal = $shallowRef<InstanceType<typeof MkModal>>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
cover: true, cover: true,
}); });
const canvas = $ref<HTMLCanvasElement>(); const canvas = $shallowRef<HTMLCanvasElement>();
let loaded = $ref(false); let loaded = $ref(false);
function draw() { function draw() {

View File

@ -77,24 +77,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
DoughnutController,
} from 'chart.js';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
@ -103,31 +86,16 @@ import { i18n } from '@/i18n';
import MkHeatmap from '@/components/MkHeatmap.vue'; import MkHeatmap from '@/components/MkHeatmap.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
DoughnutController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
);
const chartLimit = 500; const chartLimit = 500;
let chartSpan = $ref<'hour' | 'day'>('hour'); let chartSpan = $ref<'hour' | 'day'>('hour');
let chartSrc = $ref('active-users'); let chartSrc = $ref('active-users');
let heatmapSrc = $ref('active-users'); let heatmapSrc = $ref('active-users');
let subDoughnutEl = $ref<HTMLCanvasElement>(); let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
let pubDoughnutEl = $ref<HTMLCanvasElement>(); let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
const { handler: externalTooltipHandler1 } = useChartTooltip({ const { handler: externalTooltipHandler1 } = useChartTooltip({
position: 'middle', position: 'middle',

View File

@ -44,7 +44,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
deviceKind === 'smartphone' ? 'drawer' : deviceKind === 'smartphone' ? 'drawer' :
'dialog'; 'dialog';
const modal = $ref<InstanceType<typeof MkModal>>(); const modal = $shallowRef<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu; const menu = defaultStore.state.menu;

View File

@ -38,7 +38,7 @@ const props = withDefaults(defineProps<{
}>(), { }>(), {
}); });
const audioEl = $ref<HTMLAudioElement | null>(); const audioEl = $shallowRef<HTMLAudioElement | null>();
let hide = $ref(true); let hide = $ref(true);
function volumechange() { function volumechange() {

View File

@ -6,7 +6,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { on } from 'events'; import { on } from 'events';
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'; import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import MkMenu from './MkMenu.vue'; import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu'; import { MenuItem } from '@/types/menu';
import * as os from '@/os'; import * as os from '@/os';
@ -24,7 +24,7 @@ const emit = defineEmits<{
(ev: 'actioned'): void; (ev: 'actioned'): void;
}>(); }>();
const el = ref<HTMLElement>(); const el = shallowRef<HTMLElement>();
const align = 'left'; const align = 'left';
function setPosition() { function setPosition() {

View File

@ -78,11 +78,11 @@ const emit = defineEmits<{
(ev: 'close', actioned?: boolean): void; (ev: 'close', actioned?: boolean): void;
}>(); }>();
let itemsEl = $ref<HTMLDivElement>(); let itemsEl = $shallowRef<HTMLDivElement>();
let items2: InnerMenuItem[] = $ref([]); let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>(); let child = $shallowRef<InstanceType<typeof XChild>>();
let keymap = $computed(() => ({ let keymap = $computed(() => ({
'up|k|shift+tab': focusUp, 'up|k|shift+tab': focusUp,
@ -112,7 +112,7 @@ watch(() => props.items, () => {
}); });
let childMenu = $ref<MenuItem[] | null>(); let childMenu = $ref<MenuItem[] | null>();
let childTarget = $ref<HTMLElement | null>(); let childTarget = $shallowRef<HTMLElement | null>();
function closeChild() { function closeChild() {
childMenu = null; childMenu = null;
@ -203,7 +203,7 @@ onBeforeUnmount(() => {
> .item { > .item {
display: block; display: block;
position: relative; position: relative;
padding: 6px 16px; padding: 5px 16px;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap; white-space: nowrap;
@ -226,10 +226,6 @@ onBeforeUnmount(() => {
border-radius: 6px; border-radius: 6px;
} }
> * {
position: relative;
}
&:not(:disabled):hover { &:not(:disabled):hover {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;

View File

@ -61,9 +61,9 @@ let maxHeight = $ref<number>();
let fixed = $ref(false); let fixed = $ref(false);
let transformOrigin = $ref('center'); let transformOrigin = $ref('center');
let showing = $ref(true); let showing = $ref(true);
let content = $ref<HTMLElement>(); let content = $shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority); const zIndex = os.claimZIndex(props.zPriority);
const type = $computed(() => { const type = $computed<ModalTypes>(() => {
if (props.preferType === 'auto') { if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
return 'drawer'; return 'drawer';
@ -383,6 +383,7 @@ defineExpose({
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%); mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
overflow: auto; overflow: auto;
display: flex; display: flex;
container-type: inline-size;
@media (max-width: 500px) { @media (max-width: 500px) {
padding: 16px; padding: 16px;

View File

@ -49,7 +49,7 @@ router.addListener('push', ctx => {
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let rootEl = $ref(); let rootEl = $ref();
let modal = $ref<InstanceType<typeof MkModal>>(); let modal = $shallowRef<InstanceType<typeof MkModal>>();
let path = $ref(props.initialPath); let path = $ref(props.initialPath);
let width = $ref(860); let width = $ref(860);
let height = $ref(660); let height = $ref(660);

View File

@ -41,9 +41,9 @@ const emit = defineEmits<{
(event: 'ok'): void; (event: 'ok'): void;
}>(); }>();
let modal = $ref<InstanceType<typeof MkModal>>(); let modal = $shallowRef<InstanceType<typeof MkModal>>();
let rootEl = $ref<HTMLElement>(); let rootEl = $shallowRef<HTMLElement>();
let headerEl = $ref<HTMLElement>(); let headerEl = $shallowRef<HTMLElement>();
let bodyWidth = $ref(0); let bodyWidth = $ref(0);
let bodyHeight = $ref(0); let bodyHeight = $ref(0);

View File

@ -59,7 +59,7 @@
</div> </div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote"/></div> <div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false"> <button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
<span>{{ i18n.ts.showMore }}</span> <span>{{ i18n.ts.showMore }}</span>
</button> </button>
@ -101,7 +101,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue'; import { computed, inject, onMounted, onUnmounted, reactive, ref, shallowRef, Ref } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
@ -156,11 +156,11 @@ const isRenote = (
note.poll == null note.poll == null
); );
const el = ref<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = ref<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
const renoteTime = ref<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = ref<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
@ -468,7 +468,7 @@ function readPromo() {
&.collapsed { &.collapsed {
position: relative; position: relative;
max-height: 9em; max-height: 9em;
overflow: hidden; overflow: clip;
> .fade { > .fade {
display: block; display: block;
@ -529,7 +529,7 @@ function readPromo() {
> .renote { > .renote {
padding: 8px 0; padding: 8px 0;
> * { > .note {
padding: 16px; padding: 16px;
border: dashed 1px var(--renote); border: dashed 1px var(--renote);
border-radius: 8px; border-radius: 8px;

View File

@ -70,7 +70,7 @@
</div> </div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote"/></div> <div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
@ -112,7 +112,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue'; import { computed, inject, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
@ -166,11 +166,11 @@ const isRenote = (
note.poll == null note.poll == null
); );
const el = ref<HTMLElement>(); const el = shallowRef<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = ref<InstanceType<typeof MkRenoteButton>>(); const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
const renoteTime = ref<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
const reactButton = ref<HTMLElement>(); const reactButton = shallowRef<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId); const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false); const showContent = ref(false);
@ -298,7 +298,7 @@ if (appearNote.replyId) {
.lxwezrsl { .lxwezrsl {
position: relative; position: relative;
transition: box-shadow 0.1s ease; transition: box-shadow 0.1s ease;
overflow: hidden; overflow: clip;
contain: content; contain: content;
&:focus-visible { &:focus-visible {
@ -491,7 +491,7 @@ if (appearNote.replyId) {
> .renote { > .renote {
padding: 8px 0; padding: 8px 0;
> * { > .note {
padding: 16px; padding: 16px;
border: dashed 1px var(--renote); border: dashed 1px var(--renote);
border-radius: 8px; border-radius: 8px;

View File

@ -18,7 +18,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { shallowRef } from 'vue';
import XNote from '@/components/MkNote.vue'; import XNote from '@/components/MkNote.vue';
import XList from '@/components/MkDateSeparatedList.vue'; import XList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
@ -29,7 +29,7 @@ const props = defineProps<{
noGap?: boolean; noGap?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
defineExpose({ defineExpose({
pagingComponent, pagingComponent,

View File

@ -73,7 +73,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'; import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import XReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';
@ -95,7 +95,7 @@ const props = withDefaults(defineProps<{
full: false, full: false,
}); });
const elRef = ref<HTMLElement>(null); const elRef = shallowRef<HTMLElement>(null);
const reactionRef = ref(null); const reactionRef = ref(null);
let readObserver: IntersectionObserver | undefined; let readObserver: IntersectionObserver | undefined;

View File

@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
let includingTypes = $computed(() => props.includingTypes || []); let includingTypes = $computed(() => props.includingTypes || []);
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({}); let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle); let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);

View File

@ -17,7 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue'; import { defineComponent, markRaw, onUnmounted, onMounted, computed, shallowRef } from 'vue';
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue'; import XNotification from '@/components/MkNotification.vue';
@ -33,7 +33,7 @@ const props = defineProps<{
unreadOnly?: boolean; unreadOnly?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>(); const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
const pagination: Paging = { const pagination: Paging = {
endpoint: 'i/notifications' as const, endpoint: 'i/notifications' as const,

View File

@ -47,7 +47,7 @@ defineEmits<{
const router = new Router(routes, props.initialPath); const router = new Router(routes, props.initialPath);
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
let windowEl = $ref<InstanceType<typeof XWindow>>(); let windowEl = $shallowRef<InstanceType<typeof XWindow>>();
const history = $ref<{ path: string; key: any; }[]>([{ const history = $ref<{ path: string; key: any; }[]>([{
path: router.getCurrentPath(), path: router.getCurrentPath(),
key: router.getCurrentKey(), key: router.getCurrentKey(),

View File

@ -32,7 +32,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue'; import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import * as os from '@/os'; import * as os from '@/os';
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
@ -65,7 +65,7 @@ const props = withDefaults(defineProps<{
disableAutoLoad?: boolean; disableAutoLoad?: boolean;
displayLimit?: number; displayLimit?: number;
}>(), { }>(), {
displayLimit: 30, displayLimit: 20,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -74,7 +74,7 @@ const emit = defineEmits<{
type Item = { id: string; [another: string]: unknown; }; type Item = { id: string; [another: string]: unknown; };
const rootEl = ref<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const items = ref<Item[]>([]); const items = ref<Item[]>([]);
const queue = ref<Item[]>([]); const queue = ref<Item[]>([]);
const offset = ref(0); const offset = ref(0);

View File

@ -19,7 +19,7 @@ const emit = defineEmits<{
}>(); }>();
let up = $ref(false); let up = $ref(false);
const zIndex = os.claimZIndex('high'); const zIndex = os.claimZIndex('veryLow');
onMounted(() => { onMounted(() => {
window.setTimeout(() => { window.setTimeout(() => {

View File

@ -102,7 +102,7 @@ const vote = async (id) => {
//border: solid 0.5px var(--divider); //border: solid 0.5px var(--divider);
background: var(--accentedBg); background: var(--accentedBg);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: clip;
cursor: pointer; cursor: pointer;
> .backdrop { > .backdrop {

View File

@ -22,7 +22,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let modal = $ref<InstanceType<typeof MkModal>>(); let modal = $shallowRef<InstanceType<typeof MkModal>>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -128,10 +128,10 @@ const emit = defineEmits<{
(ev: 'esc'): void; (ev: 'esc'): void;
}>(); }>();
const textareaEl = $ref<HTMLTextAreaElement | null>(null); const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
const cwInputEl = $ref<HTMLInputElement | null>(null); const cwInputEl = $shallowRef<HTMLInputElement | null>(null);
const hashtagsInputEl = $ref<HTMLInputElement | null>(null); const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null);
const visibilityButton = $ref<HTMLElement | null>(null); const visibilityButton = $shallowRef<HTMLElement | null>(null);
let posting = $ref(false); let posting = $ref(false);
let posted = $ref(false); let posted = $ref(false);

View File

@ -31,8 +31,8 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
let modal = $ref<InstanceType<typeof MkModal>>(); let modal = $shallowRef<InstanceType<typeof MkModal>>();
let form = $ref<InstanceType<typeof MkPostForm>>(); let form = $shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() { function onPosted() {
modal.close({ modal.close({

View File

@ -12,7 +12,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import XReactionIcon from '@/components/MkReactionIcon.vue';
@ -28,7 +28,7 @@ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);

View File

@ -14,7 +14,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref, shallowRef } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import XDetails from '@/components/MkUsersTooltip.vue'; import XDetails from '@/components/MkUsersTooltip.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
@ -28,7 +28,7 @@ const props = defineProps<{
count: number; count: number;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = shallowRef<HTMLElement>();
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);

View File

@ -9,53 +9,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { import { Chart } from 'chart.js';
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
} from 'chart.js';
import { enUS } from 'date-fns/locale'; import { enUS } from 'date-fns/locale';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import * as os from '@/os'; import * as os from '@/os';
import 'chartjs-adapter-date-fns';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { chartVLine } from '@/scripts/chart-vline'; import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color'; import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
Chart.register( initChart();
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
LineController,
CategoryScale,
LinearScale,
TimeScale,
Legend,
Title,
Tooltip,
SubTitle,
Filler,
MatrixController, MatrixElement,
);
const rootEl = $ref<HTMLDivElement>(null); const rootEl = $shallowRef<HTMLDivElement>(null);
const chartEl = $ref<HTMLCanvasElement>(null); const chartEl = $shallowRef<HTMLCanvasElement>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart = null;
let fetching = $ref(true); let fetching = $ref(true);
@ -95,11 +63,6 @@ async function renderChart() {
await nextTick(); await nextTick();
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
//
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
// 3 // 3
@ -150,8 +113,6 @@ async function renderChart() {
suggestedMax: maxDays, suggestedMax: maxDays,
grid: { grid: {
display: false, display: false,
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
display: true, display: true,
@ -174,8 +135,6 @@ async function renderChart() {
}, },
grid: { grid: {
display: false, display: false,
color: gridColor,
borderColor: 'rgb(0, 0, 0, 0)',
}, },
ticks: { ticks: {
maxRotation: 0, maxRotation: 0,
@ -187,7 +146,6 @@ async function renderChart() {
}, },
}, },
}, },
animation: false,
plugins: { plugins: {
legend: { legend: {
display: false, display: false,

View File

@ -32,7 +32,7 @@ const emit = defineEmits<{
(ev: 'cancelled'): void; (ev: 'cancelled'): void;
}>(); }>();
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
function onClose() { function onClose() {
emit('cancelled'); emit('cancelled');

View File

@ -33,7 +33,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
function onSignup(res) { function onSignup(res) {
emit('done', res); emit('done', res);

View File

@ -64,10 +64,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
const particles = ref([]); const particles = ref([]);
const el = ref<HTMLElement>(); const el = shallowRef<HTMLElement>();
const width = ref(0); const width = ref(0);
const height = ref(0); const height = ref(0);
const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202']; const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202'];

View File

@ -59,7 +59,7 @@ const collapsed = $ref(
&.collapsed { &.collapsed {
position: relative; position: relative;
max-height: 9em; max-height: 9em;
overflow: hidden; overflow: clip;
> .fade { > .fade {
display: block; display: block;

View File

@ -19,9 +19,9 @@ const computedStyle = getComputedStyle(document.documentElement);
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join(''); const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
let available = $ref(false); let available = $ref(false);
let rootEl = $ref<HTMLElement | null>(null); let rootEl = $shallowRef<HTMLElement | null>(null);
let canvasEl = $ref<HTMLCanvasElement | null>(null); let canvasEl = $shallowRef<HTMLCanvasElement | null>(null);
let tagsEl = $ref<HTMLElement | null>(null); let tagsEl = $shallowRef<HTMLElement | null>(null);
let width = $ref(300); let width = $ref(300);
watch($$(available), () => { watch($$(available), () => {

View File

@ -54,7 +54,7 @@ const emit = defineEmits<{
(ev: 'done', result: { name: string | null, permissions: string[] }): void; (ev: 'done', result: { name: string | null, permissions: string[] }): void;
}>(); }>();
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
let name = $ref(props.initialName); let name = $ref(props.initialName);
let permissions = $ref({}); let permissions = $ref({});

View File

@ -10,7 +10,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, ref } from 'vue'; import { nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import * as os from '@/os'; import * as os from '@/os';
import { calcPopupPosition } from '@/scripts/popup-position'; import { calcPopupPosition } from '@/scripts/popup-position';
@ -34,7 +34,7 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const el = ref<HTMLElement>(); const el = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex('high'); const zIndex = os.claimZIndex('high');
function setPosition() { function setPosition() {

View File

@ -10,14 +10,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSparkle from '@/components/MkSparkle.vue'; import MkSparkle from '@/components/MkSparkle.vue';
import { version } from '@/config'; import { version } from '@/config';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const modal = ref<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const whatIsNew = () => { const whatIsNew = () => {
modal.value.close(); modal.value.close();

View File

@ -175,7 +175,7 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
box-shadow: 0 0 0 1px var(--divider); box-shadow: 0 0 0 1px var(--divider);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: clip;
&:hover { &:hover {
text-decoration: none; text-decoration: none;

View File

@ -1,5 +1,5 @@
<template> <template>
<MkPagination ref="pagingComponent" :pagination="pagination"> <MkPagination :pagination="pagination">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@ -16,7 +16,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { shallowRef } from 'vue';
import MkUserInfo from '@/components/MkUserInfo.vue'; import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
@ -26,8 +26,6 @@ const props = defineProps<{
pagination: Paging; pagination: Paging;
noGap?: boolean; noGap?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -22,7 +22,7 @@ const props = defineProps<{
}, },
}>(); }>();
const specified = $ref<HTMLElement>(); const specified = $shallowRef<HTMLElement>();
if (props.note.visibility === 'specified') { if (props.note.visibility === 'specified') {
useTooltip($$(specified), async (showing) => { useTooltip($$(specified), async (showing) => {

View File

@ -1,42 +1,42 @@
<template> <template>
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="gqyayizv _popup"> <div class="gqyayizv _popup">
<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')"> <button key="public" class="_button item" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
<div><i class="ti ti-world"></i></div> <div class="icon"><i class="ti ti-world"></i></div>
<div> <div class="body">
<span>{{ i18n.ts._visibility.public }}</span> <span>{{ i18n.ts._visibility.public }}</span>
<span>{{ i18n.ts._visibility.publicDescription }}</span> <span>{{ i18n.ts._visibility.publicDescription }}</span>
</div> </div>
</button> </button>
<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')"> <button key="home" class="_button item" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
<div><i class="ti ti-home"></i></div> <div class="icon"><i class="ti ti-home"></i></div>
<div> <div class="body">
<span>{{ i18n.ts._visibility.home }}</span> <span>{{ i18n.ts._visibility.home }}</span>
<span>{{ i18n.ts._visibility.homeDescription }}</span> <span>{{ i18n.ts._visibility.homeDescription }}</span>
</div> </div>
</button> </button>
<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> <button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
<div><i class="ti ti-lock-open"></i></div> <div class="icon"><i class="ti ti-lock-open"></i></div>
<div> <div class="body">
<span>{{ i18n.ts._visibility.followers }}</span> <span>{{ i18n.ts._visibility.followers }}</span>
<span>{{ i18n.ts._visibility.followersDescription }}</span> <span>{{ i18n.ts._visibility.followersDescription }}</span>
</div> </div>
</button> </button>
<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')"> <button key="specified" :disabled="localOnly" class="_button item" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
<div><i class="ti ti-mail"></i></div> <div class="icon"><i class="ti ti-mail"></i></div>
<div> <div class="body">
<span>{{ i18n.ts._visibility.specified }}</span> <span>{{ i18n.ts._visibility.specified }}</span>
<span>{{ i18n.ts._visibility.specifiedDescription }}</span> <span>{{ i18n.ts._visibility.specifiedDescription }}</span>
</div> </div>
</button> </button>
<div class="divider"></div> <div class="divider"></div>
<button key="localOnly" class="_button localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly"> <button key="localOnly" class="_button item localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly">
<div><i class="ti ti-world-off"></i></div> <div class="icon"><i class="ti ti-world-off"></i></div>
<div> <div class="body">
<span>{{ i18n.ts._visibility.localOnly }}</span> <span>{{ i18n.ts._visibility.localOnly }}</span>
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span> <span>{{ i18n.ts._visibility.localOnlyDescription }}</span>
</div> </div>
<div><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div> <div class="toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
</button> </button>
</div> </div>
</MkModal> </MkModal>
@ -48,7 +48,7 @@ import * as misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const modal = $ref<InstanceType<typeof MkModal>>(); const modal = $shallowRef<InstanceType<typeof MkModal>>();
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
currentVisibility: typeof misskey.noteVisibilities[number]; currentVisibility: typeof misskey.noteVisibilities[number];
@ -89,7 +89,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
} }
> button { > .item {
display: flex; display: flex;
padding: 8px 14px; padding: 8px 14px;
font-size: 12px; font-size: 12px;
@ -115,7 +115,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
background: inherit; background: inherit;
} }
> *:nth-child(1) { > .icon {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -127,7 +127,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
margin-bottom: auto; margin-bottom: auto;
} }
> *:nth-child(2) { > .body {
flex: 1 1 auto; flex: 1 1 auto;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -143,7 +143,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
} }
} }
> *:nth-child(3) { > .toggle {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -9,10 +9,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref } from 'vue'; import { watch, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
const modal = ref<InstanceType<typeof MkModal>>(); const modal = shallowRef<InstanceType<typeof MkModal>>();
const props = defineProps<{ const props = defineProps<{
success: boolean; success: boolean;

View File

@ -88,7 +88,7 @@ const emit = defineEmits<{
provide('inWindow', true); provide('inWindow', true);
let rootEl = $ref<HTMLElement | null>(); let rootEl = $shallowRef<HTMLElement | null>();
let showing = $ref(true); let showing = $ref(true);
let beforeClickedAt = 0; let beforeClickedAt = 0;
let maximized = $ref(false); let maximized = $ref(false);

View File

@ -35,7 +35,7 @@ const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void; (ev: 'update:modelValue', v: boolean): void;
}>(); }>();
let button = $ref<HTMLElement>(); let button = $shallowRef<HTMLElement>();
const checked = toRefs(props).modelValue; const checked = toRefs(props).modelValue;
const toggle = () => { const toggle = () => {
if (props.disabled) return; if (props.disabled) return;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="dwzlatin" :class="{ opened }" ref="root"> <div class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle"> <div class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span> <span class="text"><slot name="label"></slot></span>
@ -19,7 +19,7 @@
> >
<KeepAlive> <KeepAlive>
<div v-show="opened"> <div v-show="opened">
<MkSpacer :margin-min="14" :margin-max="22" :container="root"> <MkSpacer :margin-min="14" :margin-max="22">
<slot></slot> <slot></slot>
</MkSpacer> </MkSpacer>
</div> </div>
@ -40,7 +40,6 @@ const props = withDefaults(defineProps<{
let opened = $ref(props.defaultOpen); let opened = $ref(props.defaultOpen);
let openedAtLeastOnce = $ref(props.defaultOpen); let openedAtLeastOnce = $ref(props.defaultOpen);
let root = $ref<HTMLElement>();
function enter(el) { function enter(el) {
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
@ -142,6 +141,7 @@ function toggle() {
> .body { > .body {
background: var(--panel); background: var(--panel);
border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px;
container-type: inline-size;
} }
&.opened { &.opened {

Some files were not shown because too many files have changed in this diff Show More