Merge branch 'develop' of codeberg.org:calckey/calckey into develop
This commit is contained in:
commit
9a56764fe0
10 changed files with 237 additions and 7 deletions
14
packages/backend/src/misc/post.ts
Normal file
14
packages/backend/src/misc/post.ts
Normal 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();
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
132
packages/backend/src/queue/processors/db/import-posts.ts
Normal file
132
packages/backend/src/queue/processors/db/import-posts.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
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";
|
||||||
|
import { htmlToMfm } from "@/remote/activitypub/misc/html-to-mfm.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-posts");
|
||||||
|
|
||||||
|
export async function importPosts(
|
||||||
|
job: Bull.Job<DbUserImportJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Importing posts 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 json = await downloadTextFile(file.url);
|
||||||
|
|
||||||
|
let linenum = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(json);
|
||||||
|
if (parsed instanceof Array) {
|
||||||
|
logger.info("Parsing key style posts");
|
||||||
|
for (const post of JSON.parse(json)) {
|
||||||
|
try {
|
||||||
|
linenum++;
|
||||||
|
if (post.replyId != null) {
|
||||||
|
logger.info(`Is reply, skip [${linenum}] ...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (post.renoteId != null) {
|
||||||
|
logger.info(`Is boost, skip [${linenum}] ...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (post.visibility !== "public") {
|
||||||
|
logger.info(`Is non-public, skip [${linenum}] ...`);
|
||||||
|
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: null,
|
||||||
|
apHashtags: undefined,
|
||||||
|
apEmojis: undefined,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parsed instanceof Object) {
|
||||||
|
logger.info("Parsing animal style posts");
|
||||||
|
for (const post of parsed.orderedItems) {
|
||||||
|
try {
|
||||||
|
linenum++;
|
||||||
|
if (post.inReplyTo != null) {
|
||||||
|
logger.info(`Is reply, skip [${linenum}] ...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (post.directMessage) {
|
||||||
|
logger.info(`Is dm, skip [${linenum}] ...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
text = htmlToMfm(post.content, post.tag);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error while parsing text in line ${linenum}: ${e}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.info(`Posting[${linenum}] ...`);
|
||||||
|
|
||||||
|
const note = await create(user, {
|
||||||
|
createdAt: new Date(post.published),
|
||||||
|
files: undefined,
|
||||||
|
poll: undefined,
|
||||||
|
text: text || undefined,
|
||||||
|
reply: null,
|
||||||
|
renote: null,
|
||||||
|
cw: post.sensitive,
|
||||||
|
localOnly: false,
|
||||||
|
visibility: "public",
|
||||||
|
visibleUsers: [],
|
||||||
|
channel: null,
|
||||||
|
apMentions: null,
|
||||||
|
apHashtags: undefined,
|
||||||
|
apEmojis: undefined,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// handle error
|
||||||
|
logger.warn(`Error reading: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
|
@ -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<
|
||||||
|
|
|
@ -112,13 +112,13 @@ export async function createNote(
|
||||||
const note: IPost = object;
|
const note: IPost = object;
|
||||||
|
|
||||||
if (note.id && !note.id.startsWith("https://")) {
|
if (note.id && !note.id.startsWith("https://")) {
|
||||||
throw new Error(`unexpected shcema of note.id: ${note.id}`);
|
throw new Error(`unexpected schema of note.id: ${note.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getOneApHrefNullable(note.url);
|
const url = getOneApHrefNullable(note.url);
|
||||||
|
|
||||||
if (url && !url.startsWith("https://")) {
|
if (url && !url.startsWith("https://")) {
|
||||||
throw new Error(`unexpected shcema of note url: ${url}`);
|
throw new Error(`unexpected schema of note url: ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
|
@ -13,6 +13,9 @@ export default (object: any, note: Note) => {
|
||||||
} else if (note.visibility === "home") {
|
} else if (note.visibility === "home") {
|
||||||
to = [`${attributedTo}/followers`];
|
to = [`${attributedTo}/followers`];
|
||||||
cc = ["https://www.w3.org/ns/activitystreams#Public"];
|
cc = ["https://www.w3.org/ns/activitystreams#Public"];
|
||||||
|
} else if (note.visibility === 'followers') {
|
||||||
|
to = [`${attributedTo}/followers`];
|
||||||
|
cc = [];
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
43
packages/backend/src/server/api/endpoints/i/import-posts.ts
Normal file
43
packages/backend/src/server/api/endpoints/i/import-posts.ts
Normal 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: 9999999,
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
});
|
|
@ -24,7 +24,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, onActivated, provide, watch, ref } from 'vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
@ -39,7 +39,7 @@ import { useRouter } from '@/router';
|
||||||
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
const isEmpty = (x: string | null) => x == null || x === '';
|
const isEmpty = (x: string | null) => x == null || x === '';
|
||||||
|
const el = ref<HTMLElement | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const indexInfo = {
|
const indexInfo = {
|
||||||
|
@ -54,7 +54,6 @@ let INFO = $ref(indexInfo);
|
||||||
let childInfo = $ref(null);
|
let childInfo = $ref(null);
|
||||||
let narrow = $ref(false);
|
let narrow = $ref(false);
|
||||||
let view = $ref(null);
|
let view = $ref(null);
|
||||||
let el = $ref(null);
|
|
||||||
let pageProps = $ref({});
|
let pageProps = $ref({});
|
||||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||||
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha;
|
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha;
|
||||||
|
@ -207,14 +206,22 @@ watch(narrow, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ro.observe(el);
|
ro.observe(el.value);
|
||||||
|
|
||||||
narrow = el.offsetWidth < NARROW_THRESHOLD;
|
narrow = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||||
if (currentPage?.route.name == null && !narrow) {
|
if (currentPage?.route.name == null && !narrow) {
|
||||||
router.push('/admin/overview');
|
router.push('/admin/overview');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
narrow = el.value.offsetWidth < NARROW_THRESHOLD;
|
||||||
|
|
||||||
|
if (!narrow && currentPage?.route.name == null) {
|
||||||
|
router.replace('/admin/overview');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue