Merge branch 'develop' into fix/followers-only-boost

This commit is contained in:
naskya 2023-03-29 02:24:21 +00:00
commit 6b8be4ddb7
7 changed files with 164 additions and 0 deletions

View file

@ -0,0 +1,14 @@
export type Post = {
text: string | null;
cw: string | null;
localOnly: boolean;
createdAt: Date;
};
export function parse(acct: any): Post {
return { text: acct.text, cw: acct.cw, localOnly: acct.localOnly, createdAt: new Date(acct.createdAt) };
}
export function toJson(acct: Post): string {
return { text: acct.text, cw: acct.cw, localOnly: acct.localOnly }.toString();
}

View file

@ -314,6 +314,23 @@ export function createImportFollowingJob(
); );
} }
export function createImportPostsJob(
user: ThinUser,
fileId: DriveFile["id"],
) {
return dbQueue.add(
"importPosts",
{
user: user,
fileId: fileId,
},
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
export function createImportMutingJob(user: ThinUser, fileId: DriveFile["id"]) { export function createImportMutingJob(user: ThinUser, fileId: DriveFile["id"]) {
return dbQueue.add( return dbQueue.add(
"importMuting", "importMuting",

View file

@ -0,0 +1,76 @@
import { IsNull } from "typeorm";
import follow from "@/services/following/create.js";
import * as Post from "@/misc/post.js";
import create from "@/services/note/create.js";
import { downloadTextFile } from "@/misc/download-text-file.js";
import { Users, DriveFiles } from "@/models/index.js";
import type { DbUserImportJobData } from "@/queue/types.js";
import { queueLogger } from "../../logger.js";
import type Bull from "bull";
const logger = queueLogger.createSubLogger("import-posts");
export async function importPosts(
job: Bull.Job<DbUserImportJobData>,
done: any,
): Promise<void> {
logger.info(`Importing following 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 csv = await downloadTextFile(file.url);
let linenum = 0;
if (file.type.endsWith("json")) {
for (const post of JSON.parse(csv)) {
try {
if (post.replyId != null) {
continue;
}
if (post.renoteId != null) {
continue;
}
if (post.visibility !== "public") continue;
const { text, cw, localOnly, createdAt } = Post.parse(post);
logger.info(`Posting[${linenum}] ...`);
const note = await create(user, {
createdAt: createdAt,
files: undefined,
poll: undefined,
text: text || undefined,
reply: null,
renote: null,
cw: cw,
localOnly,
visibility: "public",
visibleUsers: [],
channel: null,
apMentions: undefined,
apHashtags: undefined,
apEmojis: undefined,
});
} catch (e) {
logger.warn(`Error in line:${linenum} ${e}`);
}
}
}
logger.succ("Imported");
done();
}

View file

@ -11,6 +11,7 @@ import { importFollowing } from "./import-following.js";
import { importUserLists } from "./import-user-lists.js"; import { importUserLists } from "./import-user-lists.js";
import { deleteAccount } from "./delete-account.js"; import { deleteAccount } from "./delete-account.js";
import { importMuting } from "./import-muting.js"; import { importMuting } from "./import-muting.js";
import { importPosts } from "./import-posts.js";
import { importBlocking } from "./import-blocking.js"; import { importBlocking } from "./import-blocking.js";
import { importCustomEmojis } from "./import-custom-emojis.js"; import { importCustomEmojis } from "./import-custom-emojis.js";
@ -26,6 +27,7 @@ const jobs = {
importMuting, importMuting,
importBlocking, importBlocking,
importUserLists, importUserLists,
importPosts,
importCustomEmojis, importCustomEmojis,
deleteAccount, deleteAccount,
} as Record< } as Record<

View file

@ -182,6 +182,7 @@ import * as ep___i_exportBlocking from "./endpoints/i/export-blocking.js";
import * as ep___i_exportFollowing from "./endpoints/i/export-following.js"; import * as ep___i_exportFollowing from "./endpoints/i/export-following.js";
import * as ep___i_exportMute from "./endpoints/i/export-mute.js"; 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_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_exportUserLists from "./endpoints/i/export-user-lists.js";
import * as ep___i_favorites from "./endpoints/i/favorites.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_likes from "./endpoints/i/gallery/likes.js";
@ -527,6 +528,7 @@ const eps = [
["i/export-following", ep___i_exportFollowing], ["i/export-following", ep___i_exportFollowing],
["i/export-mute", ep___i_exportMute], ["i/export-mute", ep___i_exportMute],
["i/export-notes", ep___i_exportNotes], ["i/export-notes", ep___i_exportNotes],
["i/import-posts", ep___i_importPosts],
["i/export-user-lists", ep___i_exportUserLists], ["i/export-user-lists", ep___i_exportUserLists],
["i/favorites", ep___i_favorites], ["i/favorites", ep___i_favorites],
["i/gallery/likes", ep___i_gallery_likes], ["i/gallery/likes", ep___i_gallery_likes],

View file

@ -0,0 +1,43 @@
import define from "../../define.js";
import { createImportPostsJob } from "@/queue/index.js";
import { ApiError } from "../../error.js";
import { DriveFiles } from "@/models/index.js";
import { DAY } from "@/const.js";
export const meta = {
secure: true,
requireCredential: true,
limit: {
duration: DAY,
max: 1,
},
errors: {
noSuchFile: {
message: "No such file.",
code: "NO_SUCH_FILE",
id: "e674141e-bd2a-ba85-e616-aefb187c9c2a",
},
emptyFile: {
message: "That file is empty.",
code: "EMPTY_FILE",
id: "d2f12af1-e7b4-feac-86a3-519548f2728e",
},
},
} 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 === 0) throw new ApiError(meta.errors.emptyFile);
createImportPostsJob(user, file.id);
});

View file

@ -7,6 +7,11 @@
<template #icon><i class="ph-download-simple ph-bold ph-lg"></i></template> <template #icon><i class="ph-download-simple ph-bold ph-lg"></i></template>
<MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ph-download-simple ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ph-download-simple ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton>
</FormFolder> </FormFolder>
<FormFolder class="_formBlock">
<template #label>{{ i18n.ts.import }}</template>
<template #icon><i class="ph-upload-simple ph-bold ph-lg"></i></template>
<MkButton primary :class="$style.button" inline @click="importPosts($event)"><i class="ph-upload-simple ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton>
</FormFolder>
</FormSection> </FormSection>
<FormSection> <FormSection>
<template #label>{{ i18n.ts._exportOrImport.followingList }}</template> <template #label>{{ i18n.ts._exportOrImport.followingList }}</template>
@ -108,6 +113,11 @@ const exportNotes = () => {
os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
}; };
const importPosts = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
os.api('i/import-posts', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const exportFollowing = () => { const exportFollowing = () => {
os.api('i/export-following', { os.api('i/export-following', {
excludeMuting: excludeMutingUsers.value, excludeMuting: excludeMutingUsers.value,