feat: ports misskey antenna/favorites/clips exports and antenna imports
This commit is contained in:
parent
7c712df731
commit
f0ab65966f
|
@ -53,10 +53,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
|
||||
|
||||
|
|
|
@ -5,6 +5,14 @@ Breaking changes are indicated by the :warning: icon.
|
|||
## Unreleased
|
||||
|
||||
- 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
|
||||
|
||||
|
|
|
@ -1803,6 +1803,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:
|
||||
|
|
|
@ -1428,6 +1428,9 @@ _exportOrImport:
|
|||
muteList: "已静音用户"
|
||||
blockingList: "已屏蔽用户"
|
||||
userLists: "列表"
|
||||
antennas: "天线"
|
||||
favorites: "收藏"
|
||||
clips: "便签"
|
||||
excludeMutingUsers: "排除已静音用户"
|
||||
excludeInactiveUsers: "排除不活跃用户"
|
||||
_charts:
|
||||
|
|
|
@ -1424,6 +1424,9 @@ _exportOrImport:
|
|||
muteList: "靜音"
|
||||
blockingList: "封鎖"
|
||||
userLists: "清單"
|
||||
antennas: "天線"
|
||||
favorites: "最愛列表"
|
||||
clips: "摘錄"
|
||||
excludeMutingUsers: "排除被靜音的使用者"
|
||||
excludeInactiveUsers: "排除不活躍帳戶"
|
||||
_charts:
|
||||
|
|
|
@ -297,6 +297,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"],
|
||||
|
@ -440,6 +479,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/index.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/index.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";
|
||||
|
@ -530,6 +534,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],
|
||||
|
@ -538,6 +545,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);
|
||||
});
|
|
@ -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")}`,
|
||||
|
|
|
@ -443,6 +443,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;
|
||||
|
@ -456,6 +459,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": {
|
||||
|
|
Loading…
Reference in New Issue