Compare commits
17 Commits
d50c8d3479
...
ad2525760e
Author | SHA1 | Date |
---|---|---|
laozhoubuluo | ad2525760e | |
naskya | 971f196627 | |
naskya | 8cc0e40d35 | |
naskya | beeea86253 | |
naskya | 084a4bc63a | |
naskya | cda31d3dc7 | |
naskya | 907578e8f8 | |
naskya | 2923ea86de | |
naskya | 226c990385 | |
naskya | 769f52c8ee | |
naskya | 8a00d82f36 | |
naskya | 34ed877f57 | |
Lhcfl | f5074f35cc | |
naskya | a847dd55ad | |
naskya | 5382dc5da8 | |
naskya | 989e93f2a0 | |
老周部落 | 58d1ddb523 |
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1430,6 +1430,9 @@ _exportOrImport:
|
|||
muteList: "已静音用户"
|
||||
blockingList: "已屏蔽用户"
|
||||
userLists: "列表"
|
||||
antennas: "天线"
|
||||
favorites: "收藏"
|
||||
clips: "便签"
|
||||
excludeMutingUsers: "排除已静音用户"
|
||||
excludeInactiveUsers: "排除不活跃用户"
|
||||
_charts:
|
||||
|
|
|
@ -1422,6 +1422,9 @@ _exportOrImport:
|
|||
muteList: "靜音"
|
||||
blockingList: "封鎖"
|
||||
userLists: "清單"
|
||||
antennas: "天線"
|
||||
favorites: "最愛列表"
|
||||
clips: "摘錄"
|
||||
excludeMutingUsers: "排除被靜音的使用者"
|
||||
excludeInactiveUsers: "排除不活躍帳戶"
|
||||
_charts:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 } = {},
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 ""; // absoluteではrelativeを使わないので計算しない
|
||||
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>
|
||||
|
|
|
@ -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")}`,
|
||||
|
|
|
@ -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": {
|
||||
|
|
215
pnpm-lock.yaml
215
pnpm-lock.yaml
|
@ -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'}
|
||||
|
|
Loading…
Reference in New Issue