Compare commits

...

17 Commits

Author SHA1 Message Date
laozhoubuluo ad2525760e Merge branch 'feat/import_export' into 'develop'
feat: ports misskey antenna/favorites/clips exports and antenna imports


See merge request firefish/firefish!10686
2024-05-07 23:59:50 +00:00
naskya 971f196627
ci: yet another fix 2024-05-08 08:27:54 +09:00
naskya 8cc0e40d35
ci: remove more unneeded paths 2024-05-08 07:16:32 +09:00
naskya beeea86253
ci: remove unneeded steps from clippy check 2024-05-08 06:54:43 +09:00
naskya 084a4bc63a
ci: add pull_policy 2024-05-08 06:46:41 +09:00
naskya cda31d3dc7
Revert "refactor (backend): port publishNotesStream to backend-rs"
This reverts commit 5382dc5da8.

It turns out this sends an inccorect time info to the stream
since JavaScript's Date object doesn't have timezone info

I'll revisit this in the future
2024-05-08 06:08:26 +09:00
naskya 907578e8f8
ci: fix config error 2024-05-08 05:28:41 +09:00
naskya 2923ea86de
ci: update workflow rules 2024-05-08 05:26:59 +09:00
naskya 226c990385
ci: use buildah caches 2024-05-08 05:26:36 +09:00
naskya 769f52c8ee Merge branch 'fix/reactive' into 'develop'
fix: use reactive MkTime

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10796
2024-05-07 19:59:12 +00:00
naskya 8a00d82f36
ci: add firefish-js 2024-05-08 04:49:13 +09:00
naskya 34ed877f57
ci: don't build the backend on client-only changes 2024-05-08 04:41:20 +09:00
Lhcfl f5074f35cc fix: use reactive MkTime 2024-05-08 03:00:07 +08:00
naskya a847dd55ad
ci: fix cargo clippy task 2024-05-08 03:58:21 +09:00
naskya 5382dc5da8
refactor (backend): port publishNotesStream to backend-rs 2024-05-08 02:15:07 +09:00
naskya 989e93f2a0
fix: migrate back from happy-dom to JSDOM (closes #10924 #10914 #10842)
this reverts commit 4565867b8b.
2024-05-08 01:52:15 +09:00
老周部落 58d1ddb523
feat: ports misskey antenna/favorites/clips exports and antenna imports 2024-05-06 22:31:37 +08:00
25 changed files with 1205 additions and 75 deletions

4
.gitignore vendored
View File

@ -56,10 +56,6 @@ packages/backend/assets/instance.css
packages/backend/assets/sounds/None.mp3
packages/backend/assets/LICENSE
!/packages/backend/queue/processors/db
!/packages/backend/src/db
!/packages/backend/src/server/api/endpoints/drive/files
packages/megalodon/lib
packages/megalodon/.idea

View File

@ -3,8 +3,10 @@ image: docker.io/rust:slim-bookworm
services:
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
alias: postgres
pull_policy: if-not-present
- name: docker.io/redis:7-alpine
alias: redis
pull_policy: if-not-present
workflow:
rules:
@ -12,6 +14,11 @@ workflow:
when: always
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
when: always
- if: $CI_PROJECT_PATH != 'firefish/firefish'
changes:
paths:
- .gitlab-ci.yml
when: never
- when: never
cache:
@ -56,9 +63,11 @@ build_test:
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/**/*
- packages/backend/*
- packages/backend-rs/*
- packages/macro-rs/*
- packages/megalodon/*
- scripts/**/*
- locales/**/*
- package.json
- pnpm-lock.yaml
- Cargo.toml
@ -68,6 +77,39 @@ build_test:
- pnpm run build:debug
- pnpm run migrate
client_build_test:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/client/*
- packages/firefish-js/*
- packages/sw/*
- locales/**/*
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
changes:
paths:
- packages/backend/*
- packages/backend-rs/*
- packages/macro-rs/*
- packages/megalodon/*
- Cargo.toml
- Cargo.lock
when: never
services: []
before_script:
- apt-get update && apt-get -y upgrade
- apt-get -y --no-install-recommends install curl
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
- apt-get install -y --no-install-recommends build-essential python3 perl nodejs
- corepack enable
- corepack prepare pnpm@latest --activate
- cp .config/ci.yml .config/default.yml
script:
- pnpm install --frozen-lockfile
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
container_image_build:
stage: build
image: docker.io/debian:bookworm-slim
@ -90,8 +132,21 @@ container_image_build:
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
script:
- buildah build --isolation chroot --device /dev/fuse:rw --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add all --tag "${IMAGE_TAG}" --platform linux/amd64 .
- |-
buildah build \
--isolation chroot \
--device /dev/fuse:rw \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--cap-add all \
--platform linux/amd64 \
--layers \
--cache-to "${IMAGE_CACHE}" \
--cache-from "${IMAGE_CACHE}" \
--tag "${IMAGE_TAG}" \
.
- buildah inspect "${IMAGE_TAG}"
- buildah push "${IMAGE_TAG}"
@ -119,7 +174,7 @@ cargo_unit_test:
cargo_clippy:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_COMMIT_BRANCH == 'develop'
changes:
paths:
- packages/backend-rs/**/*
@ -128,6 +183,11 @@ cargo_clippy:
- Cargo.lock
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
services: []
before_script:
- apt-get install -y --no-install-recommends build-essential clang mold perl
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
- rustup component add clippy
script:
- cargo clippy -- -D warnings

View File

@ -28,6 +28,14 @@ Breaking changes are indicated by the :warning: icon.
## v20240405
- Added `notes/history` endpoint.
- With the addition of new features, the following endpoints are added:
- export/import antennas
- `i/export-antennas`
- `i/import-antennas`
- export favorites
- `i/export-favorites`
- export clips
- `i/export-clips`
## v20240319

View File

@ -1809,6 +1809,9 @@ _exportOrImport:
muteList: "Muted users"
blockingList: "Blocked users"
userLists: "User lists"
antennas: "Antennas"
favorites: "Favorites"
clips: "Clips"
excludeMutingUsers: "Exclude muted users"
excludeInactiveUsers: "Exclude inactive users"
_charts:

View File

@ -1430,6 +1430,9 @@ _exportOrImport:
muteList: "已静音用户"
blockingList: "已屏蔽用户"
userLists: "列表"
antennas: "天线"
favorites: "收藏"
clips: "便签"
excludeMutingUsers: "排除已静音用户"
excludeInactiveUsers: "排除不活跃用户"
_charts:

View File

@ -1422,6 +1422,9 @@ _exportOrImport:
muteList: "靜音"
blockingList: "封鎖"
userLists: "清單"
antennas: "天線"
favorites: "最愛列表"
clips: "摘錄"
excludeMutingUsers: "排除被靜音的使用者"
excludeInactiveUsers: "排除不活躍帳戶"
_charts:

View File

@ -59,11 +59,11 @@
"form-data": "^4.0.0",
"got": "14.2.1",
"gunzip-maybe": "^1.4.2",
"happy-dom": "^14.7.1",
"hpagent": "1.2.0",
"ioredis": "5.4.1",
"ip-cidr": "4.0.0",
"is-svg": "5.0.0",
"jsdom": "24.0.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
@ -131,6 +131,7 @@
"@types/content-disposition": "^0.5.8",
"@types/escape-regexp": "0.0.3",
"@types/fluent-ffmpeg": "2.1.24",
"@types/jsdom": "21.1.6",
"@types/jsonld": "1.5.13",
"@types/jsrsasign": "10.5.13",
"@types/katex": "0.16.7",

View File

@ -1,21 +1,17 @@
import { type HTMLElement, Window } from "happy-dom";
import { JSDOM } from "jsdom";
import type * as mfm from "mfm-js";
import katex from "katex";
import { config } from "@/config.js";
import { intersperse } from "@/prelude/array.js";
import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
function toMathMl(code: string, displayMode: boolean): HTMLElement | null {
const { window } = new Window();
const document = window.document;
document.body.innerHTML = katex.renderToString(code, {
function toMathMl(code: string, displayMode: boolean): MathMLElement | null {
const rendered = katex.renderToString(code, {
throwOnError: false,
output: "mathml",
displayMode,
});
return document.querySelector("math");
return JSDOM.fragment(rendered).querySelector("math");
}
export function toHtml(
@ -26,7 +22,7 @@ export function toHtml(
return null;
}
const { window } = new Window();
const { window } = new JSDOM("");
const doc = window.document;

View File

@ -295,6 +295,45 @@ export function createExportUserListsJob(user: ThinUser) {
);
}
export function createExportAntennasJob(user: ThinUser) {
return dbQueue.add(
"exportAntennas",
{
user: user,
},
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
export function createExportFavoritesJob(user: ThinUser) {
return dbQueue.add(
"exportFavorites",
{
user: user,
},
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
export function createExportClipsJob(user: ThinUser) {
return dbQueue.add(
"exportClips",
{
user: user,
},
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
export function createImportFollowingJob(
user: ThinUser,
fileId: DriveFile["id"],
@ -438,6 +477,23 @@ export function createImportCustomEmojisJob(
);
}
export function createImportAntennasJob(
user: ThinUser,
fileId: DriveFile["id"],
) {
return dbQueue.add(
"importAntennas",
{
user: user,
fileId: fileId,
},
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
export function createDeleteAccountJob(
user: ThinUser,
opts: { soft?: boolean } = {},

View File

@ -0,0 +1,106 @@
import type Bull from "bull";
import * as fs from "node:fs";
import { queueLogger } from "../../logger.js";
import { addFile } from "@/services/drive/add-file.js";
import { format as dateFormat } from "date-fns";
import { createTemp } from "@/misc/create-temp.js";
import { Users, UserListJoinings, Antennas } from "@/models/index.js";
import { In } from "typeorm";
import type { DbUserJobData } from "@/queue/types.js";
import type { User } from "@/models/entities/user.js";
import { getFullApAccount } from "@/misc/convert-host.js";
import { inspect } from "node:util";
const logger = queueLogger.createSubLogger("export-antennas");
export async function exportAntennas(
job: Bull.Job<DbUserJobData>,
done: any,
): Promise<void> {
logger.info(`Exporting antennas of ${job.data.user.id} ...`);
const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) {
done();
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
logger.info(`Temp file is ${path}`);
try {
const stream = fs.createWriteStream(path, { flags: "a" });
const write = (input: string): Promise<void> => {
return new Promise((resolve, reject) => {
stream.write(input, (err) => {
if (err) {
logger.error(inspect(err));
reject();
} else {
resolve();
}
});
});
};
const antennas = await Antennas.findBy({ userId: job.data.user.id });
write("[");
for (const [index, antenna] of antennas.entries()) {
let users: User[] | undefined;
if (antenna.userListId !== null) {
const joinings = await UserListJoinings.findBy({
userListId: antenna.userListId,
});
users = await Users.findBy({
id: In(joinings.map((j) => j.userId)),
});
}
write(
JSON.stringify({
name: antenna.name,
src: antenna.src,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
users: antenna.users,
userListAcct:
typeof users !== "undefined"
? users.map((u) => {
getFullApAccount(u.username, u.host);
})
: null,
caseSensitive: antenna.caseSensitive,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
notify: antenna.notify,
}),
);
if (antennas.length - 1 !== index) {
write(", ");
}
}
write("]");
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = `antennas-${dateFormat(
new Date(),
"yyyy-MM-dd-HH-mm-ss",
)}.json`;
const driveFile = await addFile({
user,
path,
name: fileName,
force: true,
});
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
}
done();
}

View File

@ -0,0 +1,184 @@
import type Bull from "bull";
import * as fs from "node:fs";
import { queueLogger } from "../../logger.js";
import { addFile } from "@/services/drive/add-file.js";
import { format as dateFormat } from "date-fns";
import { createTemp } from "@/misc/create-temp.js";
import { Users, Clips, ClipNotes, Polls } from "@/models/index.js";
import { MoreThan } from "typeorm";
import type { DbUserJobData } from "@/queue/types.js";
import type { Clip } from "@/models/entities/clip.js";
import type { ClipNote } from "@/models/entities/clip-note.js";
import type { User } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js";
import type { Poll } from "@/models/entities/poll.js";
import { config } from "@/config.js";
import { inspect } from "node:util";
const logger = queueLogger.createSubLogger("export-clips");
export async function exportClips(
job: Bull.Job<DbUserJobData>,
done: any,
): Promise<void> {
logger.info(`Exporting clips of ${job.data.user.id} ...`);
const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) {
done();
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
logger.info(`Temp file is ${path}`);
try {
const stream = fs.createWriteStream(path, { flags: "a" });
const write = (text: string): Promise<void> => {
return new Promise<void>((res, rej) => {
stream.write(text, (err) => {
if (err) {
logger.error(inspect(err));
rej(err);
} else {
res();
}
});
});
};
await write("[");
let exportedClipsCount = 0;
let cursor: Clip["id"] | null = null;
while (true) {
const clips = await Clips.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
if (clips.length === 0) {
job.progress(100);
break;
}
cursor = clips.at(-1)?.id ?? null;
for (const clip of clips) {
// Stringify but remove the last `]}`
const clip_export = {
id: clip.id,
name: clip.name,
description: clip.description,
clipNotes: [],
};
const content = JSON.stringify(clip_export).slice(0, -2);
const isFirst = exportedClipsCount === 0;
await write(isFirst ? content : `,\n${content}`);
let exportedClipNotesCount = 0;
let cursor: ClipNote["id"] | null = null;
while (true) {
const clipNotes = (await ClipNotes.find({
where: {
clipId: clip.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
relations: ["note", "note.user"],
})) as (ClipNote & { note: Note & { user: User } })[];
if (clipNotes.length === 0) {
break;
}
cursor = clipNotes.at(-1)?.id ?? null;
for (const clipNote of clipNotes) {
let poll: Poll | undefined;
if (clipNote.note.hasPoll) {
poll = await Polls.findOneByOrFail({ noteId: clipNote.note.id });
}
const clipnode_export = {
id: clipNote.id,
createdAt: new Date(clip.createdAt).toISOString(),
note: {
id: clipNote.note.id,
text: clipNote.note.text,
createdAt: new Date(clipNote.note.createdAt).toISOString(),
fileIds: clipNote.note.fileIds,
replyId: clipNote.note.replyId,
renoteId: clipNote.note.renoteId,
poll: poll,
cw: clipNote.note.cw,
visibility: clipNote.note.visibility,
visibleUserIds: clipNote.note.visibleUserIds,
localOnly: clipNote.note.localOnly,
objectUrl: `${config.url}/notes/${clipNote.note.id}`, // add objectUrl for future import, firefish only
uri: clipNote.note.uri,
url: clipNote.note.url,
user: {
id: clipNote.note.user.id,
name: clipNote.note.user.name,
username: clipNote.note.user.username,
host: clipNote.note.user.host,
uri: clipNote.note.user.uri,
},
},
};
const content = JSON.stringify(clipnode_export);
const isFirst = exportedClipNotesCount === 0;
await write(isFirst ? content : `,\n${content}`);
exportedClipNotesCount++;
}
}
await write("]}");
exportedClipsCount++;
}
const total = await Clips.countBy({
userId: user.id,
});
job.progress(exportedClipsCount / total);
}
await write("]");
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = `clips-${dateFormat(
new Date(),
"yyyy-MM-dd-HH-mm-ss",
)}.json`;
const driveFile = await addFile({
user,
path,
name: fileName,
force: true,
});
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
done();
}
}

View File

@ -0,0 +1,144 @@
import type Bull from "bull";
import * as fs from "node:fs";
import { queueLogger } from "../../logger.js";
import { addFile } from "@/services/drive/add-file.js";
import { format as dateFormat } from "date-fns";
import { createTemp } from "@/misc/create-temp.js";
import { Users, NoteFavorites, Polls } from "@/models/index.js";
import { MoreThan } from "typeorm";
import type { DbUserJobData } from "@/queue/types.js";
import type { NoteFavorite } from "@/models/entities/note-favorite.js";
import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js";
import type { Poll } from "@/models/entities/poll.js";
import { config } from "@/config.js";
import { inspect } from "node:util";
const logger = queueLogger.createSubLogger("export-favorites");
export async function exportFavorites(
job: Bull.Job<DbUserJobData>,
done: any,
): Promise<void> {
logger.info(`Exporting favorites of ${job.data.user.id} ...`);
const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) {
done();
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
logger.info(`Temp file is ${path}`);
try {
const stream = fs.createWriteStream(path, { flags: "a" });
const write = (text: string): Promise<void> => {
return new Promise<void>((res, rej) => {
stream.write(text, (err) => {
if (err) {
logger.error(inspect(err));
rej(err);
} else {
res();
}
});
});
};
await write("[");
let exportedFavoritesCount = 0;
let cursor: NoteFavorite["id"] | null = null;
while (true) {
const favorites = (await NoteFavorites.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
relations: ["note", "note.user"],
})) as (NoteFavorite & { note: Note & { user: User } })[];
if (favorites.length === 0) {
job.progress(100);
break;
}
cursor = favorites.at(-1)?.id ?? null;
for (const favorite of favorites) {
let poll: Poll | undefined;
if (favorite.note.hasPoll) {
poll = await Polls.findOneByOrFail({ noteId: favorite.note.id });
}
const favorite_export = {
id: favorite.id,
createdAt: new Date(favorite.createdAt).toISOString(),
note: {
id: favorite.note.id,
text: favorite.note.text,
createdAt: new Date(favorite.note.createdAt).toISOString(),
fileIds: favorite.note.fileIds,
replyId: favorite.note.replyId,
renoteId: favorite.note.renoteId,
poll: poll,
cw: favorite.note.cw,
visibility: favorite.note.visibility,
visibleUserIds: favorite.note.visibleUserIds,
localOnly: favorite.note.localOnly,
objectUrl: `${config.url}/notes/${favorite.note.id}`, // add objectUrl for future import, firefish only
uri: favorite.note.uri,
url: favorite.note.url,
user: {
id: favorite.note.user.id,
name: favorite.note.user.name,
username: favorite.note.user.username,
host: favorite.note.user.host,
uri: favorite.note.user.uri,
},
},
};
const content = JSON.stringify(favorite_export);
const isFirst = exportedFavoritesCount === 0;
await write(isFirst ? content : `,\n${content}`);
exportedFavoritesCount++;
}
const total = await NoteFavorites.countBy({
userId: user.id,
});
job.progress(exportedFavoritesCount / total);
}
await write("]");
stream.end();
logger.succ(`Exported to: ${path}`);
const fileName = `favorites-${dateFormat(
new Date(),
"yyyy-MM-dd-HH-mm-ss",
)}.json`;
const driveFile = await addFile({
user,
path,
name: fileName,
force: true,
});
logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
done();
}
}

View File

@ -0,0 +1,131 @@
import type Bull from "bull";
import Ajv from "ajv";
import { queueLogger } from "../../logger.js";
import { downloadTextFile } from "@/misc/download-text-file.js";
import { DriveFiles, Users, Antennas } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import type { DbUserImportJobData } from "@/queue/types.js";
import { publishInternalEvent } from "@/services/stream.js";
import { inspect } from "node:util";
const logger = queueLogger.createSubLogger("import-antennas");
export async function importAntennas(
job: Bull.Job<DbUserImportJobData>,
done: any,
): Promise<void> {
logger.info(`Importing antennas of ${job.data.user.id} ...`);
const user = await Users.findOneBy({ id: job.data.user.id });
if (user == null) {
done();
return;
}
const file = await DriveFiles.findOneBy({
id: job.data.fileId,
});
if (file == null) {
done();
return;
}
const antennas = JSON.parse(await downloadTextFile(file.url));
const now = new Date();
try {
const validate = new Ajv().compile({
type: "object",
properties: {
name: { type: "string", minLength: 1, maxLength: 100 },
src: { type: "string", enum: ["home", "all", "users", "list"] },
userListAcct: {
type: "array",
items: {
type: "string",
},
nullable: true,
},
keywords: {
type: "array",
items: {
type: "array",
items: {
type: "string",
},
},
},
excludeKeywords: {
type: "array",
items: {
type: "array",
items: {
type: "string",
},
},
},
users: {
type: "array",
items: {
type: "string",
},
},
caseSensitive: { type: "boolean" },
withReplies: { type: "boolean" },
withFile: { type: "boolean" },
notify: { type: "boolean" },
},
required: [
"name",
"src",
"keywords",
"excludeKeywords",
"users",
"caseSensitive",
"withReplies",
"withFile",
"notify",
],
});
for (const antenna of antennas) {
if (
antenna.keywords.length === 0 ||
antenna.keywords[0].every((x) => x === "")
)
continue;
if (!validate(antenna)) {
logger.warn("Validation Failed");
continue;
}
const result = await Antennas.insert({
id: genId(),
createdAt: now,
userId: job.data.user.id,
name: antenna.name,
src:
antenna.src === "list" && antenna.userListAcct
? "users"
: antenna.src,
userListId: null,
keywords: antenna.keywords,
excludeKeywords: antenna.excludeKeywords,
users: (antenna.src === "list" && antenna.userListAcct !== null
? antenna.userListAcct
: antenna.users
).filter(Boolean),
caseSensitive: antenna.caseSensitive,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
notify: antenna.notify,
}).then((x) => Antennas.findOneByOrFail(x.identifiers[0]));
logger.succ(`Antenna created: ${result.id}`);
publishInternalEvent("antennaCreated", result);
}
} catch (err: any) {
logger.error(inspect(err));
} finally {
logger.succ("Imported");
done();
}
}

View File

@ -7,6 +7,9 @@ import { exportFollowing } from "./export-following.js";
import { exportMute } from "./export-mute.js";
import { exportBlocking } from "./export-blocking.js";
import { exportUserLists } from "./export-user-lists.js";
import { exportAntennas } from "./export-antennas.js";
import { exportFavorites } from "./export-favorites.js";
import { exportClips } from "./export-clips.js";
import { importFollowing } from "./import-following.js";
import { importUserLists } from "./import-user-lists.js";
import { deleteAccount } from "./delete-account.js";
@ -16,6 +19,7 @@ import { importMastoPost } from "./import-masto-post.js";
import { importCkPost } from "./import-firefish-post.js";
import { importBlocking } from "./import-blocking.js";
import { importCustomEmojis } from "./import-custom-emojis.js";
import { importAntennas } from "./import-antennas.js";
const jobs = {
deleteDriveFiles,
@ -25,6 +29,9 @@ const jobs = {
exportMute,
exportBlocking,
exportUserLists,
exportAntennas,
exportFavorites,
exportClips,
importFollowing,
importMuting,
importBlocking,
@ -33,6 +40,7 @@ const jobs = {
importMastoPost,
importCkPost,
importCustomEmojis,
importAntennas,
deleteAccount,
} as Record<
string,

View File

@ -179,6 +179,9 @@ import * as ep___i_exportMute from "./endpoints/i/export-mute.js";
import * as ep___i_exportNotes from "./endpoints/i/export-notes.js";
import * as ep___i_importPosts from "./endpoints/i/import-posts.js";
import * as ep___i_exportUserLists from "./endpoints/i/export-user-lists.js";
import * as ep___i_exportAntennas from "./endpoints/i/export-antennas.js";
import * as ep___i_exportFavorites from "./endpoints/i/export-favorites.js";
import * as ep___i_exportClips from "./endpoints/i/export-clips.js";
import * as ep___i_favorites from "./endpoints/i/favorites.js";
import * as ep___i_gallery_likes from "./endpoints/i/gallery/likes.js";
import * as ep___i_gallery_posts from "./endpoints/i/gallery/posts.js";
@ -187,6 +190,7 @@ import * as ep___i_importBlocking from "./endpoints/i/import-blocking.js";
import * as ep___i_importFollowing from "./endpoints/i/import-following.js";
import * as ep___i_importMuting from "./endpoints/i/import-muting.js";
import * as ep___i_importUserLists from "./endpoints/i/import-user-lists.js";
import * as ep___i_importAntennas from "./endpoints/i/import-antennas.js";
import * as ep___i_notifications from "./endpoints/i/notifications.js";
import * as ep___i_pageLikes from "./endpoints/i/page-likes.js";
import * as ep___i_pages from "./endpoints/i/pages.js";
@ -528,6 +532,9 @@ const eps = [
["i/export-notes", ep___i_exportNotes],
["i/import-posts", ep___i_importPosts],
["i/export-user-lists", ep___i_exportUserLists],
["i/export-antennas", ep___i_exportAntennas],
["i/export-favorites", ep___i_exportFavorites],
["i/export-clips", ep___i_exportClips],
["i/favorites", ep___i_favorites],
["i/gallery/likes", ep___i_gallery_likes],
["i/gallery/posts", ep___i_gallery_posts],
@ -536,6 +543,7 @@ const eps = [
["i/import-following", ep___i_importFollowing],
["i/import-muting", ep___i_importMuting],
["i/import-user-lists", ep___i_importUserLists],
["i/import-antennas", ep___i_importAntennas],
["i/notifications", ep___i_notifications],
["i/page-likes", ep___i_pageLikes],
["i/pages", ep___i_pages],

View File

@ -0,0 +1,22 @@
import define from "@/server/api/define.js";
import { createExportAntennasJob } from "@/queue/index.js";
import { HOUR } from "@/const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1,
},
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
createExportAntennasJob(user);
});

View File

@ -0,0 +1,22 @@
import define from "@/server/api/define.js";
import { createExportClipsJob } from "@/queue/index.js";
import { HOUR } from "@/const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1,
},
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
createExportClipsJob(user);
});

View File

@ -0,0 +1,22 @@
import define from "@/server/api/define.js";
import { createExportFavoritesJob } from "@/queue/index.js";
import { HOUR } from "@/const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: HOUR,
max: 1,
},
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
createExportFavoritesJob(user);
});

View File

@ -0,0 +1,81 @@
import define from "@/server/api/define.js";
import { createImportAntennasJob } from "@/queue/index.js";
import { ApiError } from "@/server/api/error.js";
import { DriveFiles, Antennas } from "@/models/index.js";
import { downloadTextFile } from "@/misc/download-text-file.js";
import { HOUR } from "@/const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duratition: HOUR,
max: 1,
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "b98644cf-a5ac-4277-a502-0b8054a709a3",
},
unexpectedFileType: {
message: "Must be a JSON file.",
code: "UNEXPECTED_FILE_TYPE",
id: "660f3599-bce0-4f95-9dde-311fd841c183",
},
tooBigFile: {
message: "That file is too big.",
code: "TOO_BIG_FILE",
id: "dee9d4ed-ad07-43ed-8b34-b2856398bc60",
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "31a1b42c-06f7-42ae-8a38-a661c5c9f691",
},
tooManyAntennas: {
message: "You cannot create antenna any more.",
code: "TOO_MANY_ANTENNAS",
id: "c3a5a51e-04d4-11ee-be56-0242ac120002",
},
},
} as const;
export const paramDef = {
type: "object",
properties: {
fileId: { type: "string", format: "misskey:id" },
},
required: ["fileId"],
} as const;
export default define(meta, paramDef, async (ps, user) => {
const file = await DriveFiles.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
if (file.size > 2_000_000) throw new ApiError(meta.errors.tooBigFile);
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
let antennas;
try {
antennas = JSON.parse(await downloadTextFile(file.url));
} catch (e) {
throw new ApiError(meta.errors.unexpectedFileType);
}
const currentAntennasCount = await Antennas.findBy({
userId: user.id,
});
// In the future, should consider evolving to be configurable. Misskey v13 here is a configuration item.
if (currentAntennasCount + antennas.length > 5 && !user.isAdmin) {
throw new ApiError(meta.errors.tooManyAntennas);
}
createImportAntennasJob(user, file.id);
});

View File

@ -1,8 +1,8 @@
import { URL } from "node:url";
import { Window } from "happy-dom";
import { type DOMWindow, JSDOM } from "jsdom";
import fetch from "node-fetch";
import tinycolor from "tinycolor2";
import { getJson, getAgentByUrl } from "@/misc/fetch.js";
import { getJson, getHtml, getAgentByUrl } from "@/misc/fetch.js";
import {
type Instance,
MAX_LENGTH_INSTANCE,
@ -112,15 +112,13 @@ export async function fetchInstanceMetadata(
}
}
async function fetchDom(instance: Instance): Promise<Window["document"]> {
async function fetchDom(instance: Instance): Promise<DOMWindow["document"]> {
logger.info(`Fetching HTML of ${instance.host} ...`);
const window = new Window({
url: `https://${instance.host}`,
});
const doc = window.document;
const html = await getHtml(`https://${instance.host}`);
const { window } = new JSDOM(html);
return doc;
return window.document;
}
async function fetchManifest(
@ -137,7 +135,7 @@ async function fetchManifest(
async function fetchFaviconUrl(
instance: Instance,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
): Promise<string | null> {
const url = `https://${instance.host}`;
@ -169,7 +167,7 @@ async function fetchFaviconUrl(
async function fetchIconUrl(
instance: Instance,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {
if (manifest?.icons && manifest.icons.length > 0 && manifest.icons[0].src) {
@ -219,9 +217,9 @@ async function getThemeColor(
async function getSiteName(
info: Nodeinfo | null,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | undefined | null> {
): Promise<string | null> {
if (info?.metadata) {
if (info.metadata.nodeName || info.metadata.name) {
return info.metadata.nodeName || info.metadata.name;
@ -247,7 +245,7 @@ async function getSiteName(
async function getDescription(
info: Nodeinfo | null,
doc: Window["document"] | null,
doc: DOMWindow["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {
if (info?.metadata) {

View File

@ -1,12 +1,11 @@
import { Window } from "happy-dom";
import type { HTMLAnchorElement, HTMLLinkElement } from "happy-dom";
import { JSDOM } from "jsdom";
import { config } from "@/config.js";
import { getHtml } from "@/misc/fetch.js";
async function getRelMeLinks(url: string): Promise<string[]> {
try {
const dom = new Window({
url: url,
});
const html = await getHtml(url);
const dom = new JSDOM(html);
const allLinks = [...dom.window.document.querySelectorAll("a, link")];
const relMeLinks = allLinks
.filter((a) => {

View File

@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { i18n } from "@/i18n";
import { dateTimeFormat } from "@/scripts/intl-const";
@ -25,7 +25,7 @@ const props = withDefaults(
},
);
const _time =
const _time = computed(() =>
props.time == null
? Number.NaN
: typeof props.time === "number"
@ -33,16 +33,19 @@ const _time =
: (props.time instanceof Date
? props.time
: new Date(props.time)
).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
).getTime(),
);
const invalid = computed(() => Number.isNaN(_time.value));
const absolute = computed(() =>
!invalid.value ? dateTimeFormat.format(_time.value) : i18n.ts._ago.invalid,
);
const now = ref(props.origin?.getTime() ?? Date.now());
const relative = computed<string>(() => {
if (props.mode === "absolute") return ""; // absoluterelative使
if (invalid) return i18n.ts._ago.invalid;
if (invalid.value) return i18n.ts._ago.invalid;
const ago = (now.value - _time) / 1000; /* ms */
const ago = (now.value - _time.value) / 1000; /* ms */
return ago >= 31536000
? i18n.t("_ago.yearsAgo", { n: Math.floor(ago / 31536000).toString() })
: ago >= 2592000
@ -74,15 +77,25 @@ const relative = computed<string>(() => {
: i18n.ts._ago.future;
});
let tickId: number;
let tickId: number | undefined;
function tick() {
if (
invalid.value ||
props.origin ||
(props.mode !== "relative" && props.mode !== "detail")
) {
if (tickId) window.clearInterval(tickId);
tickId = undefined;
return;
}
const _now = Date.now();
const agoPrev = (now.value - _time) / 1000; /* ms */ // interval
const agoPrev = (now.value - _time.value) / 1000; /* ms */ // interval
now.value = _now;
const ago = (now.value - _time) / 1000; /* ms */ // interval
const ago = (now.value - _time.value) / 1000; /* ms */ // interval
const prev = agoPrev < 60 ? 10000 : agoPrev < 3600 ? 60000 : 180000;
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
@ -94,16 +107,13 @@ function tick() {
}
}
if (
!invalid &&
!props.origin &&
(props.mode === "relative" || props.mode === "detail")
) {
onMounted(() => {
tick();
});
onUnmounted(() => {
if (tickId) window.clearInterval(tickId);
});
}
watch(() => props.time, tick);
onMounted(() => {
tick();
});
onUnmounted(() => {
if (tickId) window.clearInterval(tickId);
});
</script>

View File

@ -172,6 +172,71 @@
>
</FormFolder>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._exportOrImport.antennas }}</template>
<FormFolder class="_formBlock">
<template #label>{{ i18n.ts.export }}</template>
<template #icon
><i :class="icon('ph-download-simple')"></i
></template>
<MkButton
primary
:class="$style.button"
inline
@click="exportAntennas()"
><i :class="icon('ph-download-simple')"></i>
{{ i18n.ts.export }}</MkButton
>
</FormFolder>
<FormFolder class="_formBlock">
<template #label>{{ i18n.ts.import }}</template>
<template #icon
><i :class="icon('ph-upload-simple')"></i
></template>
<MkButton
primary
:class="$style.button"
inline
@click="importAntennas($event)"
><i :class="icon('ph-upload-simple')"></i>
{{ i18n.ts.import }}</MkButton
>
</FormFolder>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._exportOrImport.favorites }}</template>
<FormFolder class="_formBlock">
<template #label>{{ i18n.ts.export }}</template>
<template #icon
><i :class="icon('ph-download-simple')"></i
></template>
<MkButton
primary
:class="$style.button"
inline
@click="exportFavorites()"
><i :class="icon('ph-download-simple')"></i>
{{ i18n.ts.export }}</MkButton
>
</FormFolder>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._exportOrImport.clips }}</template>
<FormFolder class="_formBlock">
<template #label>{{ i18n.ts.export }}</template>
<template #icon
><i :class="icon('ph-download-simple')"></i
></template>
<MkButton
primary
:class="$style.button"
inline
@click="exportClips()"
><i :class="icon('ph-download-simple')"></i>
{{ i18n.ts.export }}</MkButton
>
</FormFolder>
</FormSection>
</div>
</template>
@ -248,6 +313,18 @@ const exportMuting = () => {
os.api("i/export-mute", {}).then(onExportSuccess).catch(onError);
};
const exportAntennas = () => {
os.api("i/export-antennas", {}).then(onExportSuccess).catch(onError);
};
const exportFavorites = () => {
os.api("i/export-favorites", {}).then(onExportSuccess).catch(onError);
};
const exportClips = () => {
os.api("i/export-clips", {}).then(onExportSuccess).catch(onError);
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api("i/import-following", { fileId: file.id })
@ -276,6 +353,13 @@ const importBlocking = async (ev) => {
.catch(onError);
};
const importAntennas = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api("i/import-antennas", { fileId: file.id })
.then(onImportSuccess)
.catch(onError);
};
definePageMetadata({
title: i18n.ts.importAndExport,
icon: `${icon("ph-package")}`,

View File

@ -491,6 +491,9 @@ export type Endpoints = {
"i/export-mute": { req: TODO; res: TODO };
"i/export-notes": { req: TODO; res: TODO };
"i/export-user-lists": { req: TODO; res: TODO };
"i/export-antennas": { req: TODO; res: TODO };
"i/export-favorites": { req: TODO; res: TODO };
"i/export-clips": { req: TODO; res: TODO };
"i/favorites": {
req: {
limit?: number;
@ -511,6 +514,7 @@ export type Endpoints = {
"i/get-word-muted-notes-count": { req: TODO; res: TODO };
"i/import-following": { req: TODO; res: TODO };
"i/import-user-lists": { req: TODO; res: TODO };
"i/import-antennas": { req: TODO; res: TODO };
"i/move": { req: TODO; res: TODO };
"i/known-as": { req: TODO; res: TODO };
"i/notifications": {

View File

@ -153,9 +153,6 @@ importers:
gunzip-maybe:
specifier: ^1.4.2
version: 1.4.2
happy-dom:
specifier: ^14.7.1
version: 14.7.1
hpagent:
specifier: 1.2.0
version: 1.2.0
@ -168,6 +165,9 @@ importers:
is-svg:
specifier: 5.0.0
version: 5.0.0
jsdom:
specifier: 24.0.0
version: 24.0.0
json5:
specifier: 2.2.3
version: 2.2.3
@ -368,6 +368,9 @@ importers:
'@types/fluent-ffmpeg':
specifier: 2.1.24
version: 2.1.24
'@types/jsdom':
specifier: 21.1.6
version: 21.1.6
'@types/jsonld':
specifier: 1.5.13
version: 1.5.13
@ -4114,6 +4117,14 @@ packages:
pretty-format: 29.7.0
dev: true
/@types/jsdom@21.1.6:
resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==}
dependencies:
'@types/node': 20.12.7
'@types/tough-cookie': 4.0.5
parse5: 7.1.2
dev: true
/@types/json-schema@7.0.12:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
dev: true
@ -4416,6 +4427,10 @@ packages:
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
dev: true
/@types/tough-cookie@4.0.5:
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
dev: true
/@types/unist@2.0.7:
resolution: {integrity: sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==}
dev: true
@ -6712,6 +6727,13 @@ packages:
hasBin: true
dev: true
/cssstyle@4.0.1:
resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
engines: {node: '>=18'}
dependencies:
rrweb-cssom: 0.6.0
dev: false
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: true
@ -6743,6 +6765,14 @@ packages:
engines: {node: '>= 12'}
dev: false
/data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
dev: false
/date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
@ -6816,6 +6846,10 @@ packages:
engines: {node: '>=10'}
dev: true
/decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
dev: false
/decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@ -9019,15 +9053,6 @@ packages:
engines: {node: '>=0.8.0'}
dev: true
/happy-dom@14.7.1:
resolution: {integrity: sha512-v60Q0evZ4clvMcrAh5/F8EdxDdfHdFrtffz/CNe10jKD+nFweZVxM91tW+UyY2L4AtpgIaXdZ7TQmiO1pfcwbg==}
engines: {node: '>=16.0.0'}
dependencies:
entities: 4.5.0
webidl-conversions: 7.0.0
whatwg-mimetype: 3.0.0
dev: false
/hard-rejection@2.1.0:
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
engines: {node: '>=6'}
@ -9129,6 +9154,13 @@ packages:
engines: {node: '>=14'}
dev: false
/html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
dependencies:
whatwg-encoding: 3.1.1
dev: false
/html-entities@2.3.2:
resolution: {integrity: sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==}
dev: false
@ -9199,6 +9231,16 @@ packages:
toidentifier: 1.0.1
dev: false
/http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
debug: 4.3.4(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
dev: false
/http2-wrapper@1.0.3:
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
engines: {node: '>=10.19.0'}
@ -9239,6 +9281,16 @@ packages:
- supports-color
dev: false
/https-proxy-agent@7.0.4:
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
debug: 4.3.4(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
dev: false
/human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@ -9606,6 +9658,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
dev: false
/is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
@ -10375,6 +10431,42 @@ packages:
engines: {node: '>=12.0.0'}
dev: true
/jsdom@24.0.0:
resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==}
engines: {node: '>=18'}
peerDependencies:
canvas: ^2.11.2
peerDependenciesMeta:
canvas:
optional: true
dependencies:
cssstyle: 4.0.1
data-urls: 5.0.0
decimal.js: 10.4.3
form-data: 4.0.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.4
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.9
parse5: 7.1.2
rrweb-cssom: 0.6.0
saxes: 6.0.0
symbol-tree: 3.2.4
tough-cookie: 4.1.4
w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.16.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: false
/jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
hasBin: true
@ -11579,6 +11671,10 @@ packages:
boolbase: 1.0.0
dev: true
/nwsapi@2.2.9:
resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==}
dev: false
/oauth@0.10.0:
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
dev: false
@ -11885,7 +11981,6 @@ packages:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies:
entities: 4.5.0
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
@ -12365,6 +12460,10 @@ packages:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
/psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: false
/pug-attrs@3.0.0:
resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==}
dependencies:
@ -12524,6 +12623,10 @@ packages:
deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
dev: false
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -12760,6 +12863,10 @@ packages:
/require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
@ -12886,6 +12993,10 @@ packages:
fsevents: 2.3.3
dev: true
/rrweb-cssom@0.6.0:
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
dev: false
/rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
dependencies:
@ -12964,6 +13075,13 @@ packages:
/sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
/saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
dependencies:
xmlchars: 2.2.0
dev: false
/schema-utils@3.3.0:
resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==}
engines: {node: '>= 10.13.0'}
@ -13569,6 +13687,10 @@ packages:
engines: {node: '>= 4.7.0'}
dev: true
/symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: false
/synckit@0.6.2:
resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==}
engines: {node: '>=12.20'}
@ -13788,9 +13910,26 @@ packages:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
/tough-cookie@4.1.4:
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
engines: {node: '>=6'}
dependencies:
psl: 1.9.0
punycode: 2.3.1
universalify: 0.2.0
url-parse: 1.5.10
dev: false
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
/tr46@5.0.0:
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
engines: {node: '>=18'}
dependencies:
punycode: 2.3.1
dev: false
/trace-redirect@1.0.6:
resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
dev: false
@ -14270,6 +14409,11 @@ packages:
engines: {node: '>= 4.0.0'}
dev: false
/universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
dev: false
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
@ -14310,6 +14454,13 @@ packages:
dependencies:
punycode: 2.3.1
/url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/url-polyfill@1.1.12:
resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==}
dev: true
@ -14581,6 +14732,13 @@ packages:
typescript: 5.4.5
dev: true
/w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
dependencies:
xml-name-validator: 5.0.0
dev: false
/walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
dependencies:
@ -14691,9 +14849,24 @@ packages:
- supports-color
dev: false
/whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
engines: {node: '>=12'}
/whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
dependencies:
iconv-lite: 0.6.3
dev: false
/whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
dev: false
/whatwg-url@14.0.0:
resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
engines: {node: '>=18'}
dependencies:
tr46: 5.0.0
webidl-conversions: 7.0.0
dev: false
/whatwg-url@5.0.0:
@ -14823,7 +14996,6 @@ packages:
optional: true
utf-8-validate:
optional: true
dev: true
/xev@3.0.2:
resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==}
@ -14841,6 +15013,11 @@ packages:
engines: {node: '>=12'}
dev: true
/xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
dev: false
/xml2js@0.5.0:
resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==}
engines: {node: '>=4.0.0'}
@ -14862,6 +15039,10 @@ packages:
engines: {node: '>=4.0'}
dev: false
/xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}