mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-12-04 07:38:08 -07:00
parent
ba0e5eec93
commit
4410989fa2
113 changed files with 9045 additions and 6611 deletions
195
.config/ci.yml
Normal file
195
.config/ci.yml
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
# Firefish configuration
|
||||||
|
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
# ┌─────┐
|
||||||
|
#───┘ URL └─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Final accessible URL seen by a user.
|
||||||
|
url: https://example.tld/
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
# ┌───────────────────────┐
|
||||||
|
#───┘ Port and TLS settings └───────────────────────────────────
|
||||||
|
|
||||||
|
#
|
||||||
|
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||||
|
#
|
||||||
|
# +----- https://example.tld/ ------------+
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||||
|
# +------+ |+-------------+ +----------------+|
|
||||||
|
# +---------------------------------------+
|
||||||
|
#
|
||||||
|
# You need to set up a reverse proxy. (e.g. nginx)
|
||||||
|
# An encrypted connection with HTTPS is highly recommended
|
||||||
|
# because tokens may be transferred in GET requests.
|
||||||
|
|
||||||
|
# The port that your Misskey server should listen on.
|
||||||
|
port: 3000
|
||||||
|
|
||||||
|
# ┌──────────────────────────┐
|
||||||
|
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||||
|
|
||||||
|
db:
|
||||||
|
host: database
|
||||||
|
port: 5432
|
||||||
|
|
||||||
|
# Database name
|
||||||
|
db: postgres
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
user: postgres
|
||||||
|
pass: test
|
||||||
|
|
||||||
|
# Whether disable Caching queries
|
||||||
|
#disableCache: true
|
||||||
|
|
||||||
|
# Extra Connection options
|
||||||
|
#extra:
|
||||||
|
# ssl: true
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Redis configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: redis
|
||||||
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
#pass: example-pass
|
||||||
|
#prefix: example-prefix
|
||||||
|
#db: 1
|
||||||
|
|
||||||
|
# ┌─────────────────────────────┐
|
||||||
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
#elasticsearch:
|
||||||
|
# host: localhost
|
||||||
|
# port: 9200
|
||||||
|
# ssl: false
|
||||||
|
# user:
|
||||||
|
# pass:
|
||||||
|
|
||||||
|
# ┌───────────────┐
|
||||||
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
||||||
|
# You can select the ID generation method.
|
||||||
|
# You don't usually need to change this setting, but you can
|
||||||
|
# change it according to your preferences.
|
||||||
|
|
||||||
|
# Available methods:
|
||||||
|
# aid ... Short, Millisecond accuracy
|
||||||
|
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||||
|
# ulid ... Millisecond accuracy
|
||||||
|
# objectid ... This is left for backward compatibility
|
||||||
|
|
||||||
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
|
# ID SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
id: 'aid'
|
||||||
|
|
||||||
|
# ┌─────────────────────┐
|
||||||
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# Max note length, should be < 8000.
|
||||||
|
#maxNoteLength: 3000
|
||||||
|
|
||||||
|
# Whether disable HSTS
|
||||||
|
#disableHsts: true
|
||||||
|
|
||||||
|
# Number of worker processes
|
||||||
|
#clusterLimit: 1
|
||||||
|
|
||||||
|
# Job concurrency per worker
|
||||||
|
# deliverJobConcurrency: 128
|
||||||
|
# inboxJobConcurrency: 16
|
||||||
|
|
||||||
|
# Job rate limiter
|
||||||
|
# deliverJobPerSec: 128
|
||||||
|
# inboxJobPerSec: 16
|
||||||
|
|
||||||
|
# Job attempts
|
||||||
|
# deliverJobMaxAttempts: 12
|
||||||
|
# inboxJobMaxAttempts: 8
|
||||||
|
|
||||||
|
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||||
|
#outgoingAddressFamily: ipv4
|
||||||
|
|
||||||
|
# Syslog option
|
||||||
|
#syslog:
|
||||||
|
# host: localhost
|
||||||
|
# port: 514
|
||||||
|
|
||||||
|
# Proxy for HTTP/HTTPS
|
||||||
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
|
#proxyBypassHosts: [
|
||||||
|
# 'example.com',
|
||||||
|
# '192.0.2.8'
|
||||||
|
#]
|
||||||
|
|
||||||
|
# Proxy for SMTP/SMTPS
|
||||||
|
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||||
|
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||||
|
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||||
|
|
||||||
|
# Media Proxy
|
||||||
|
#mediaProxy: https://example.com/proxy
|
||||||
|
|
||||||
|
# Proxy remote files (default: false)
|
||||||
|
#proxyRemoteFiles: true
|
||||||
|
|
||||||
|
#allowedPrivateNetworks: [
|
||||||
|
# '127.0.0.1/32'
|
||||||
|
#]
|
||||||
|
|
||||||
|
# Upload or download file size limits (bytes)
|
||||||
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# Managed hosting settings
|
||||||
|
# !!!!!!!!!!
|
||||||
|
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||||
|
# >>>>>> YOU DON'T NEED THIS! <<<<<<
|
||||||
|
# !!!!!!!!!!
|
||||||
|
# Each category is optional, but if each item in each category is mandatory!
|
||||||
|
# If you mess this up, that's on you, you've been warned...
|
||||||
|
|
||||||
|
#maxUserSignups: 100
|
||||||
|
#isManagedHosting: true
|
||||||
|
#deepl:
|
||||||
|
# managed: true
|
||||||
|
# authKey: ''
|
||||||
|
# isPro: false
|
||||||
|
#
|
||||||
|
#email:
|
||||||
|
# managed: true
|
||||||
|
# address: 'example@email.com'
|
||||||
|
# host: 'email.com'
|
||||||
|
# port: 587
|
||||||
|
# user: 'example@email.com'
|
||||||
|
# pass: ''
|
||||||
|
# useImplicitSslTls: false
|
||||||
|
#
|
||||||
|
#objectStorage:
|
||||||
|
# managed: true
|
||||||
|
# baseUrl: ''
|
||||||
|
# bucket: ''
|
||||||
|
# prefix: ''
|
||||||
|
# endpoint: ''
|
||||||
|
# region: ''
|
||||||
|
# accessKey: ''
|
||||||
|
# secretKey: ''
|
||||||
|
# useSsl: true
|
||||||
|
# connnectOverProxy: false
|
||||||
|
# setPublicReadOnUpload: true
|
||||||
|
# s3ForcePathStyle: true
|
||||||
|
|
||||||
|
# !!!!!!!!!!
|
||||||
|
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||||
|
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
|
||||||
|
# !!!!!!!!!!
|
||||||
|
|
||||||
|
# Seriously. Do NOT fill out the above settings if you're self-hosting.
|
||||||
|
# They're much better off being set from the control panel.
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"eg2.vscode-npm-script",
|
||||||
|
"vue.volar",
|
||||||
|
"vue.vscode-typescript-vue-plugin",
|
||||||
|
"arcanis.vscode-zipfs",
|
||||||
|
"orta.vscode-twoslash-queries",
|
||||||
|
"biomejs.biome"
|
||||||
|
]
|
||||||
|
}
|
BIN
.yarn/corepack.tgz
(Stored with Git LFS)
BIN
.yarn/corepack.tgz
(Stored with Git LFS)
Binary file not shown.
1
assets/branding
Submodule
1
assets/branding
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1c4e96bcfe61c981a1e8f23142082ac8ce7fc575
|
13
packages/backend/assets/LICENSE
Normal file
13
packages/backend/assets/LICENSE
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Copyright 2023 The Iceshrimp contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
103
packages/backend/src/queue/processors/db/delete-account.ts
Normal file
103
packages/backend/src/queue/processors/db/delete-account.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { DriveFiles, Notes, UserProfiles, Users } from "@/models/index.js";
|
||||||
|
import type { DbUserDeleteJobData } from "@/queue/types.js";
|
||||||
|
import type { Note } from "@/models/entities/note.js";
|
||||||
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
|
import { MoreThan } from "typeorm";
|
||||||
|
import { deleteFileSync } from "@/services/drive/delete-file.js";
|
||||||
|
import { sendEmail } from "@/services/send-email.js";
|
||||||
|
import { publishInternalEvent } from "@/services/stream.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("delete-account");
|
||||||
|
|
||||||
|
export async function deleteAccount(
|
||||||
|
job: Bull.Job<DbUserDeleteJobData>,
|
||||||
|
): Promise<string | void> {
|
||||||
|
logger.info(`Deleting account of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
|
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||||
|
if (!user) return;
|
||||||
|
const isLocal = Users.isLocalUser(user);
|
||||||
|
|
||||||
|
{
|
||||||
|
// Delete notes
|
||||||
|
let cursor: Note["id"] | null = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const notes = (await Notes.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
})) as Note[];
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = notes[notes.length - 1].id;
|
||||||
|
|
||||||
|
await Notes.delete(notes.map((note) => note.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("All of notes deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Delete files
|
||||||
|
let cursor: DriveFile["id"] | null = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const files = (await DriveFiles.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
})) as DriveFile[];
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = files[files.length - 1].id;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
await deleteFileSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("All of files deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Send email notification
|
||||||
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
if (profile.email && profile.emailVerified) {
|
||||||
|
sendEmail(
|
||||||
|
profile.email,
|
||||||
|
"Account deleted",
|
||||||
|
"Your account has been deleted.",
|
||||||
|
"Your account has been deleted.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// soft指定されている場合は物理削除しない
|
||||||
|
if (job.data.soft) {
|
||||||
|
// nop
|
||||||
|
} else {
|
||||||
|
await Users.delete(job.data.user.id);
|
||||||
|
publishInternalEvent(isLocal ? "localUserDeleted" : "remoteUserDeleted", { id: user.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Account deleted";
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { deleteFileSync } from "@/services/drive/delete-file.js";
|
||||||
|
import { Users, DriveFiles } from "@/models/index.js";
|
||||||
|
import { MoreThan } from "typeorm";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("delete-drive-files");
|
||||||
|
|
||||||
|
export async function deleteDriveFiles(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
|
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||||
|
if (user == null) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deletedCount = 0;
|
||||||
|
let cursor: any = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const files = await DriveFiles.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
job.progress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = files[files.length - 1].id;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
await deleteFileSync(file);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await DriveFiles.countBy({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
job.progress(deletedCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ(
|
||||||
|
`All drive files (${deletedCount}) of ${user.id} has been deleted.`,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
}
|
105
packages/backend/src/queue/processors/db/export-blocking.ts
Normal file
105
packages/backend/src/queue/processors/db/export-blocking.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
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 { getFullApAccount } from "@/misc/convert-host.js";
|
||||||
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
|
import { Users, Blockings } from "@/models/index.js";
|
||||||
|
import { MoreThan } from "typeorm";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-blocking");
|
||||||
|
|
||||||
|
export async function exportBlocking(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Exporting blocking 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" });
|
||||||
|
|
||||||
|
let exportedCount = 0;
|
||||||
|
let cursor: any = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const blockings = await Blockings.find({
|
||||||
|
where: {
|
||||||
|
blockerId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blockings.length === 0) {
|
||||||
|
job.progress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = blockings[blockings.length - 1].id;
|
||||||
|
|
||||||
|
for (const block of blockings) {
|
||||||
|
const u = await Users.findOneBy({ id: block.blockeeId });
|
||||||
|
if (u == null) {
|
||||||
|
exportedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = getFullApAccount(u.username, u.host);
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
stream.write(content + "\n", (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
exportedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await Blockings.countBy({
|
||||||
|
blockerId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
job.progress(exportedCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.end();
|
||||||
|
logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = `blocking-${dateFormat(
|
||||||
|
new Date(),
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
)}.csv`;
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path,
|
||||||
|
name: fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
132
packages/backend/src/queue/processors/db/export-custom-emojis.ts
Normal file
132
packages/backend/src/queue/processors/db/export-custom-emojis.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
|
import { ulid } from "ulid";
|
||||||
|
import mime from "mime-types";
|
||||||
|
import archiver from "archiver";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { addFile } from "@/services/drive/add-file.js";
|
||||||
|
import { format as dateFormat } from "date-fns";
|
||||||
|
import { Users, Emojis } from "@/models/index.js";
|
||||||
|
import {} from "@/queue/types.js";
|
||||||
|
import { createTemp, createTempDir } from "@/misc/create-temp.js";
|
||||||
|
import { downloadUrl } from "@/misc/download-url.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-custom-emojis");
|
||||||
|
|
||||||
|
export async function exportCustomEmojis(
|
||||||
|
job: Bull.Job,
|
||||||
|
done: () => void,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info("Exporting custom emojis ...");
|
||||||
|
|
||||||
|
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||||
|
if (user == null) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [path, cleanup] = await createTempDir();
|
||||||
|
|
||||||
|
logger.info(`Temp dir is ${path}`);
|
||||||
|
|
||||||
|
const metaPath = `${path}/meta.json`;
|
||||||
|
|
||||||
|
fs.writeFileSync(metaPath, "", "utf-8");
|
||||||
|
|
||||||
|
const metaStream = fs.createWriteStream(metaPath, { flags: "a" });
|
||||||
|
|
||||||
|
const writeMeta = (text: string): Promise<void> => {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
metaStream.write(text, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeMeta(
|
||||||
|
`{"metaVersion":2,"host":"${
|
||||||
|
config.host
|
||||||
|
}","exportedAt":"${new Date().toString()}","emojis":[`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const customEmojis = await Emojis.find({
|
||||||
|
where: {
|
||||||
|
host: IsNull(),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
id: "ASC",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const emoji of customEmojis) {
|
||||||
|
const ext = mime.extension(emoji.type);
|
||||||
|
// there are some restrictions on file names, so to be safe the files are
|
||||||
|
// named after their database id instead of the actual emoji name
|
||||||
|
const fileName = emoji.id + (ext ? '.' + ext : '');
|
||||||
|
const emojiPath = `${path}/${fileName}`;
|
||||||
|
fs.writeFileSync(emojiPath, "", "binary");
|
||||||
|
let downloaded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadUrl(emoji.originalUrl, emojiPath);
|
||||||
|
downloaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: 何度か再試行
|
||||||
|
logger.error(e instanceof Error ? e : new Error(e as string));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!downloaded) {
|
||||||
|
fs.unlinkSync(emojiPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = JSON.stringify({
|
||||||
|
fileName: fileName,
|
||||||
|
downloaded: downloaded,
|
||||||
|
emoji: emoji,
|
||||||
|
});
|
||||||
|
const isFirst = customEmojis.indexOf(emoji) === 0;
|
||||||
|
|
||||||
|
await writeMeta(isFirst ? content : ",\n" + content);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeMeta("]}");
|
||||||
|
|
||||||
|
metaStream.end();
|
||||||
|
|
||||||
|
// Create archive
|
||||||
|
const [archivePath, archiveCleanup] = await createTemp();
|
||||||
|
const archiveStream = fs.createWriteStream(archivePath);
|
||||||
|
const archive = archiver("zip", {
|
||||||
|
zlib: { level: 0 },
|
||||||
|
});
|
||||||
|
archiveStream.on("close", async () => {
|
||||||
|
logger.succ(`Exported to: ${archivePath}`);
|
||||||
|
|
||||||
|
const fileName = `custom-emojis-${dateFormat(
|
||||||
|
new Date(),
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
)}.zip`;
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path: archivePath,
|
||||||
|
name: fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
cleanup();
|
||||||
|
archiveCleanup();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
archive.pipe(archiveStream);
|
||||||
|
archive.directory(path, false);
|
||||||
|
archive.finalize();
|
||||||
|
}
|
113
packages/backend/src/queue/processors/db/export-following.ts
Normal file
113
packages/backend/src/queue/processors/db/export-following.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
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 { getFullApAccount } from "@/misc/convert-host.js";
|
||||||
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
|
import { Users, Followings, Mutings } from "@/models/index.js";
|
||||||
|
import { In, MoreThan, Not } from "typeorm";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
import type { Following } from "@/models/entities/following.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-following");
|
||||||
|
|
||||||
|
export async function exportFollowing(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: () => void,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Exporting following 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" });
|
||||||
|
|
||||||
|
let cursor: Following["id"] | null = null;
|
||||||
|
|
||||||
|
const mutings = job.data.excludeMuting
|
||||||
|
? await Mutings.findBy({
|
||||||
|
muterId: user.id,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const followings = (await Followings.find({
|
||||||
|
where: {
|
||||||
|
followerId: user.id,
|
||||||
|
...(mutings.length > 0
|
||||||
|
? { followeeId: Not(In(mutings.map((x) => x.muteeId))) }
|
||||||
|
: {}),
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
})) as Following[];
|
||||||
|
|
||||||
|
if (followings.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = followings[followings.length - 1].id;
|
||||||
|
|
||||||
|
for (const following of followings) {
|
||||||
|
const u = await Users.findOneBy({ id: following.followeeId });
|
||||||
|
if (u == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
job.data.excludeInactive &&
|
||||||
|
u.updatedAt &&
|
||||||
|
Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = getFullApAccount(u.username, u.host);
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
stream.write(content + "\n", (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.end();
|
||||||
|
logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = `following-${dateFormat(
|
||||||
|
new Date(),
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
)}.csv`;
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path,
|
||||||
|
name: fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
106
packages/backend/src/queue/processors/db/export-mute.ts
Normal file
106
packages/backend/src/queue/processors/db/export-mute.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { addFile } from "@/services/drive/add-file.js";
|
||||||
|
import { format as dateFormat } from "date-fns";
|
||||||
|
import { getFullApAccount } from "@/misc/convert-host.js";
|
||||||
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
|
import { Users, Mutings } from "@/models/index.js";
|
||||||
|
import { IsNull, MoreThan } from "typeorm";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-mute");
|
||||||
|
|
||||||
|
export async function exportMute(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Exporting mute 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" });
|
||||||
|
|
||||||
|
let exportedCount = 0;
|
||||||
|
let cursor: any = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const mutes = await Mutings.find({
|
||||||
|
where: {
|
||||||
|
muterId: user.id,
|
||||||
|
expiresAt: IsNull(),
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mutes.length === 0) {
|
||||||
|
job.progress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = mutes[mutes.length - 1].id;
|
||||||
|
|
||||||
|
for (const mute of mutes) {
|
||||||
|
const u = await Users.findOneBy({ id: mute.muteeId });
|
||||||
|
if (u == null) {
|
||||||
|
exportedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = getFullApAccount(u.username, u.host);
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
stream.write(content + "\n", (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
exportedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await Mutings.countBy({
|
||||||
|
muterId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
job.progress(exportedCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.end();
|
||||||
|
logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = `mute-${dateFormat(
|
||||||
|
new Date(),
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
)}.csv`;
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path,
|
||||||
|
name: fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
133
packages/backend/src/queue/processors/db/export-notes.ts
Normal file
133
packages/backend/src/queue/processors/db/export-notes.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
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 { Users, Notes, Polls, DriveFiles } from "@/models/index.js";
|
||||||
|
import { MoreThan } from "typeorm";
|
||||||
|
import type { Note } from "@/models/entities/note.js";
|
||||||
|
import type { Poll } from "@/models/entities/poll.js";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-notes");
|
||||||
|
|
||||||
|
export async function exportNotes(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Exporting notes 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(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await write("[");
|
||||||
|
|
||||||
|
let exportedNotesCount = 0;
|
||||||
|
let cursor: Note["id"] | null = null;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const notes = (await Notes.find({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
order: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
})) as Note[];
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
job.progress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = notes[notes.length - 1].id;
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
let poll: Poll | undefined;
|
||||||
|
if (note.hasPoll) {
|
||||||
|
poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||||
|
}
|
||||||
|
const content = JSON.stringify(await serialize(note, poll));
|
||||||
|
const isFirst = exportedNotesCount === 0;
|
||||||
|
await write(isFirst ? content : ",\n" + content);
|
||||||
|
exportedNotesCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await Notes.countBy({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
job.progress(exportedNotesCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
await write("]");
|
||||||
|
|
||||||
|
stream.end();
|
||||||
|
logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = `notes-${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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serialize(
|
||||||
|
note: Note,
|
||||||
|
poll: Poll | null = null,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return {
|
||||||
|
id: note.id,
|
||||||
|
text: note.text,
|
||||||
|
createdAt: note.createdAt,
|
||||||
|
fileIds: note.fileIds,
|
||||||
|
files: await DriveFiles.packMany(note.fileIds),
|
||||||
|
replyId: note.replyId,
|
||||||
|
renoteId: note.renoteId,
|
||||||
|
poll: poll,
|
||||||
|
cw: note.cw,
|
||||||
|
visibility: note.visibility,
|
||||||
|
visibleUserIds: note.visibleUserIds,
|
||||||
|
localOnly: note.localOnly,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
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 { getFullApAccount } from "@/misc/convert-host.js";
|
||||||
|
import { createTemp } from "@/misc/create-temp.js";
|
||||||
|
import { Users, UserLists, UserListJoinings } from "@/models/index.js";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
import type { DbUserJobData } from "@/queue/types.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("export-user-lists");
|
||||||
|
|
||||||
|
export async function exportUserLists(
|
||||||
|
job: Bull.Job<DbUserJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
|
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||||
|
if (user == null) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lists = await UserLists.findBy({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create temp file
|
||||||
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
|
logger.info(`Temp file is ${path}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = fs.createWriteStream(path, { flags: "a" });
|
||||||
|
|
||||||
|
for (const list of lists) {
|
||||||
|
const joinings = await UserListJoinings.findBy({ userListId: list.id });
|
||||||
|
const users = await Users.findBy({
|
||||||
|
id: In(joinings.map((j) => j.userId)),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const u of users) {
|
||||||
|
const acct = getFullApAccount(u.username, u.host);
|
||||||
|
const content = `${list.name},${acct}`;
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
stream.write(content + "\n", (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(err);
|
||||||
|
rej(err);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.end();
|
||||||
|
logger.succ(`Exported to: ${path}`);
|
||||||
|
|
||||||
|
const fileName = `user-lists-${dateFormat(
|
||||||
|
new Date(),
|
||||||
|
"yyyy-MM-dd-HH-mm-ss",
|
||||||
|
)}.csv`;
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path,
|
||||||
|
name: fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ(`Exported to: ${driveFile.id}`);
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
79
packages/backend/src/queue/processors/db/import-blocking.ts
Normal file
79
packages/backend/src/queue/processors/db/import-blocking.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import * as Acct from "@/misc/acct.js";
|
||||||
|
import { resolveUser } from "@/remote/resolve-user.js";
|
||||||
|
import { downloadTextFile } from "@/misc/download-text-file.js";
|
||||||
|
import { isSelfHost, toPuny } from "@/misc/convert-host.js";
|
||||||
|
import { Users, DriveFiles, Blockings } from "@/models/index.js";
|
||||||
|
import type { DbUserImportJobData } from "@/queue/types.js";
|
||||||
|
import block from "@/services/blocking/create.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-blocking");
|
||||||
|
|
||||||
|
export async function importBlocking(
|
||||||
|
job: Bull.Job<DbUserImportJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Importing blocking 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;
|
||||||
|
|
||||||
|
for (const line of csv.trim().split("\n")) {
|
||||||
|
linenum++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const acct = line.split(",")[0].trim();
|
||||||
|
const { username, host } = Acct.parse(acct);
|
||||||
|
|
||||||
|
let target = isSelfHost(host!)
|
||||||
|
? await Users.findOneBy({
|
||||||
|
host: IsNull(),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
})
|
||||||
|
: await Users.findOneBy({
|
||||||
|
host: toPuny(host!),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host == null && target == null) continue;
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
target = await resolveUser(username, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip myself
|
||||||
|
if (target.id === job.data.user.id) continue;
|
||||||
|
|
||||||
|
logger.info(`Block[${linenum}] ${target.id} ...`);
|
||||||
|
|
||||||
|
await block(user, target);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
150
packages/backend/src/queue/processors/db/import-custom-emojis.ts
Normal file
150
packages/backend/src/queue/processors/db/import-custom-emojis.ts
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import { createTempDir } from "@/misc/create-temp.js";
|
||||||
|
import { downloadUrl } from "@/misc/download-url.js";
|
||||||
|
import { DriveFiles, Emojis } from "@/models/index.js";
|
||||||
|
import type { DbUserImportJobData } from "@/queue/types.js";
|
||||||
|
import { addFile } from "@/services/drive/add-file.js";
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import { db } from "@/db/postgre.js";
|
||||||
|
import probeImageSize from "probe-image-size";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-custom-emojis");
|
||||||
|
|
||||||
|
// TODO: 名前衝突時の動作を選べるようにする
|
||||||
|
export async function importCustomEmojis(
|
||||||
|
job: Bull.Job<DbUserImportJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info("Importing custom emojis ...");
|
||||||
|
|
||||||
|
const file = await DriveFiles.findOneBy({
|
||||||
|
id: job.data.fileId,
|
||||||
|
});
|
||||||
|
if (file == null) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tempPath, cleanup] = await createTempDir();
|
||||||
|
|
||||||
|
logger.info(`Temp dir is ${tempPath}`);
|
||||||
|
|
||||||
|
const destPath = `${tempPath}/emojis.zip`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(destPath, "", "binary");
|
||||||
|
await downloadUrl(file.url, destPath);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: 何度か再試行
|
||||||
|
if (e instanceof Error || typeof e === "string") {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = `${tempPath}/emojis`;
|
||||||
|
const unzipStream = fs.createReadStream(destPath);
|
||||||
|
const zip = new AdmZip(destPath);
|
||||||
|
zip.extractAllToAsync(outputPath, true, false, async (error) => {
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (fs.existsSync(`${outputPath}/meta.json`)) {
|
||||||
|
logger.info("starting emoji import with metadata");
|
||||||
|
const metaRaw = fs.readFileSync(`${outputPath}/meta.json`, "utf-8");
|
||||||
|
const meta = JSON.parse(metaRaw);
|
||||||
|
|
||||||
|
for (const record of meta.emojis) {
|
||||||
|
if (!record.downloaded) continue;
|
||||||
|
const emojiInfo = record.emoji;
|
||||||
|
const emojiPath = `${outputPath}/${record.fileName}`;
|
||||||
|
await Emojis.delete({
|
||||||
|
name: emojiInfo.name,
|
||||||
|
});
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user: null,
|
||||||
|
path: emojiPath,
|
||||||
|
name: record.fileName,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
const file = fs.createReadStream(emojiPath);
|
||||||
|
const size = await probeImageSize(file);
|
||||||
|
file.destroy();
|
||||||
|
await Emojis.insert({
|
||||||
|
id: genId(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: emojiInfo.name,
|
||||||
|
category: emojiInfo.category,
|
||||||
|
host: null,
|
||||||
|
aliases: emojiInfo.aliases,
|
||||||
|
originalUrl: driveFile.url,
|
||||||
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
|
license: emojiInfo.license,
|
||||||
|
width: size.width || null,
|
||||||
|
height: size.height || null,
|
||||||
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("starting emoji import without metadata");
|
||||||
|
// Since we lack metadata, we import into a randomized category name instead
|
||||||
|
let categoryName = genId();
|
||||||
|
|
||||||
|
let containedEmojis = fs.readdirSync(outputPath);
|
||||||
|
|
||||||
|
// Filter out accidental JSON files
|
||||||
|
containedEmojis = containedEmojis.filter(
|
||||||
|
(emoji) => !emoji.match(/\.(json)$/i),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const emojiFilename of containedEmojis) {
|
||||||
|
// strip extension and get filename to use as name
|
||||||
|
const name = path.basename(emojiFilename, path.extname(emojiFilename));
|
||||||
|
const emojiPath = `${outputPath}/${emojiFilename}`;
|
||||||
|
|
||||||
|
logger.info(`importing ${name}`);
|
||||||
|
|
||||||
|
await Emojis.delete({
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user: null,
|
||||||
|
path: emojiPath,
|
||||||
|
name: path.basename(emojiFilename),
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
const file = fs.createReadStream(emojiPath);
|
||||||
|
const size = await probeImageSize(file);
|
||||||
|
file.destroy();
|
||||||
|
logger.info(`emoji size: ${size.width}x${size.height}`);
|
||||||
|
|
||||||
|
await Emojis.insert({
|
||||||
|
id: genId(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: name,
|
||||||
|
category: categoryName,
|
||||||
|
host: null,
|
||||||
|
aliases: [],
|
||||||
|
originalUrl: driveFile.url,
|
||||||
|
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||||
|
type: driveFile.webpublicType ?? driveFile.type,
|
||||||
|
license: null,
|
||||||
|
width: size.width || null,
|
||||||
|
height: size.height || null,
|
||||||
|
}).then((x) => Emojis.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.queryResultCache!.remove(["meta_emojis"]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
logger.succ(`Unzipping to ${outputPath}`);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as Post from "@/misc/post.js";
|
||||||
|
import create from "@/services/note/create.js";
|
||||||
|
import { Users } from "@/models/index.js";
|
||||||
|
import type { DbUserImportMastoPostJobData } from "@/queue/types.js";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-firefish-post");
|
||||||
|
|
||||||
|
export async function importCkPost(
|
||||||
|
job: Bull.Job<DbUserImportMastoPostJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
done();
|
||||||
|
}
|
116
packages/backend/src/queue/processors/db/import-following.ts
Normal file
116
packages/backend/src/queue/processors/db/import-following.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
import follow from "@/services/following/create.js";
|
||||||
|
|
||||||
|
import * as Acct from "@/misc/acct.js";
|
||||||
|
import { resolveUser } from "@/remote/resolve-user.js";
|
||||||
|
import { downloadTextFile } from "@/misc/download-text-file.js";
|
||||||
|
import { isSelfHost, toPuny } from "@/misc/convert-host.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-following");
|
||||||
|
|
||||||
|
export async function importFollowing(
|
||||||
|
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 acct of JSON.parse(csv)) {
|
||||||
|
try {
|
||||||
|
const { username, host } = Acct.parse(acct);
|
||||||
|
|
||||||
|
let target = isSelfHost(host!)
|
||||||
|
? await Users.findOneBy({
|
||||||
|
host: IsNull(),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
})
|
||||||
|
: await Users.findOneBy({
|
||||||
|
host: toPuny(host!),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host == null && target == null) continue;
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
target = await resolveUser(username, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip myself
|
||||||
|
if (target.id === job.data.user.id) continue;
|
||||||
|
|
||||||
|
logger.info(`Follow[${linenum}] ${target.id} ...`);
|
||||||
|
|
||||||
|
follow(user, target);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const line of csv.trim().split("\n")) {
|
||||||
|
linenum++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const acct = line.split(",")[0].trim();
|
||||||
|
const { username, host } = Acct.parse(acct);
|
||||||
|
|
||||||
|
let target = isSelfHost(host!)
|
||||||
|
? await Users.findOneBy({
|
||||||
|
host: IsNull(),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
})
|
||||||
|
: await Users.findOneBy({
|
||||||
|
host: toPuny(host!),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host == null && target == null) continue;
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
target = await resolveUser(username, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip myself
|
||||||
|
if (target.id === job.data.user.id) continue;
|
||||||
|
|
||||||
|
logger.info(`Follow[${linenum}] ${target.id} ...`);
|
||||||
|
|
||||||
|
follow(user, target);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import create from "@/services/note/create.js";
|
||||||
|
import { Users } from "@/models/index.js";
|
||||||
|
import type { DbUserImportMastoPostJobData } from "@/queue/types.js";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import type Bull from "bull";
|
||||||
|
import { htmlToMfm } from "@/remote/activitypub/misc/html-to-mfm.js";
|
||||||
|
import { resolveNote } from "@/remote/activitypub/models/note.js";
|
||||||
|
import { Note } from "@/models/entities/note.js";
|
||||||
|
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||||
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-masto-post");
|
||||||
|
|
||||||
|
export async function importMastoPost(
|
||||||
|
job: Bull.Job<DbUserImportMastoPostJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
done();
|
||||||
|
}
|
89
packages/backend/src/queue/processors/db/import-muting.ts
Normal file
89
packages/backend/src/queue/processors/db/import-muting.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import * as Acct from "@/misc/acct.js";
|
||||||
|
import { resolveUser } from "@/remote/resolve-user.js";
|
||||||
|
import { downloadTextFile } from "@/misc/download-text-file.js";
|
||||||
|
import { isSelfHost, toPuny } from "@/misc/convert-host.js";
|
||||||
|
import { Users, DriveFiles, Mutings } from "@/models/index.js";
|
||||||
|
import type { DbUserImportJobData } from "@/queue/types.js";
|
||||||
|
import type { User } from "@/models/entities/user.js";
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-muting");
|
||||||
|
|
||||||
|
export async function importMuting(
|
||||||
|
job: Bull.Job<DbUserImportJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Importing muting 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;
|
||||||
|
|
||||||
|
for (const line of csv.trim().split("\n")) {
|
||||||
|
linenum++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const acct = line.split(",")[0].trim();
|
||||||
|
const { username, host } = Acct.parse(acct);
|
||||||
|
|
||||||
|
let target = isSelfHost(host!)
|
||||||
|
? await Users.findOneBy({
|
||||||
|
host: IsNull(),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
})
|
||||||
|
: await Users.findOneBy({
|
||||||
|
host: toPuny(host!),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (host == null && target == null) continue;
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
target = await resolveUser(username, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
throw new Error(`cannot resolve user: @${username}@${host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip myself
|
||||||
|
if (target.id === job.data.user.id) continue;
|
||||||
|
|
||||||
|
logger.info(`Mute[${linenum}] ${target.id} ...`);
|
||||||
|
|
||||||
|
await mute(user, target);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mute(user: User, target: User) {
|
||||||
|
await Mutings.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
muterId: user.id,
|
||||||
|
muteeId: target.id,
|
||||||
|
});
|
||||||
|
}
|
76
packages/backend/src/queue/processors/db/import-posts.ts
Normal file
76
packages/backend/src/queue/processors/db/import-posts.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { downloadTextFile } from "@/misc/download-text-file.js";
|
||||||
|
import { processMastoNotes } from "@/misc/process-masto-notes.js";
|
||||||
|
import { Users, DriveFiles } from "@/models/index.js";
|
||||||
|
import type { DbUserImportPostsJobData } from "@/queue/types.js";
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import type Bull from "bull";
|
||||||
|
import {
|
||||||
|
createImportCkPostJob,
|
||||||
|
createImportMastoPostJob,
|
||||||
|
} from "@/queue/index.js";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-posts");
|
||||||
|
|
||||||
|
export async function importPosts(
|
||||||
|
job: Bull.Job<DbUserImportPostsJobData>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.name.endsWith("tar.gz") || file.name.endsWith("zip")) {
|
||||||
|
try {
|
||||||
|
logger.info("Reading Mastodon archive");
|
||||||
|
const outbox = await processMastoNotes(
|
||||||
|
file.name,
|
||||||
|
file.url,
|
||||||
|
job.data.user.id,
|
||||||
|
);
|
||||||
|
for (const post of outbox.orderedItems) {
|
||||||
|
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// handle error
|
||||||
|
logger.warn(`Failed reading Mastodon archive: ${e}`);
|
||||||
|
}
|
||||||
|
logger.succ("Mastodon archive imported");
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await downloadTextFile(file.url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(json);
|
||||||
|
if (parsed instanceof Array) {
|
||||||
|
logger.info("Parsing key style posts");
|
||||||
|
for (const post of JSON.parse(json)) {
|
||||||
|
createImportCkPostJob(job.data.user, post, job.data.signatureCheck);
|
||||||
|
}
|
||||||
|
} else if (parsed instanceof Object) {
|
||||||
|
logger.info("Parsing animal style posts");
|
||||||
|
for (const post of parsed.orderedItems) {
|
||||||
|
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// handle error
|
||||||
|
logger.warn(`Error reading: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
111
packages/backend/src/queue/processors/db/import-user-lists.ts
Normal file
111
packages/backend/src/queue/processors/db/import-user-lists.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
|
||||||
|
import { queueLogger } from "../../logger.js";
|
||||||
|
import * as Acct from "@/misc/acct.js";
|
||||||
|
import { resolveUser } from "@/remote/resolve-user.js";
|
||||||
|
import { pushUserToUserList } from "@/services/user-list/push.js";
|
||||||
|
import { downloadTextFile } from "@/misc/download-text-file.js";
|
||||||
|
import { isSelfHost, toPuny } from "@/misc/convert-host.js";
|
||||||
|
import {
|
||||||
|
DriveFiles,
|
||||||
|
Users,
|
||||||
|
UserLists,
|
||||||
|
UserListJoinings, Blockings, Followings,
|
||||||
|
} from "@/models/index.js";
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import type { DbUserImportJobData } from "@/queue/types.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger("import-user-lists");
|
||||||
|
|
||||||
|
export async function importUserLists(
|
||||||
|
job: Bull.Job<DbUserImportJobData>,
|
||||||
|
done: any,
|
||||||
|
): Promise<void> {
|
||||||
|
logger.info(`Importing user lists 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;
|
||||||
|
|
||||||
|
for (const line of csv.trim().split("\n")) {
|
||||||
|
linenum++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listName = line.split(",")[0].trim();
|
||||||
|
const { username, host } = Acct.parse(line.split(",")[1].trim());
|
||||||
|
|
||||||
|
let list = await UserLists.findOneBy({
|
||||||
|
userId: user.id,
|
||||||
|
name: listName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (list == null) {
|
||||||
|
list = await UserLists.insert({
|
||||||
|
id: genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: user.id,
|
||||||
|
name: listName,
|
||||||
|
}).then((x) => UserLists.findOneByOrFail(x.identifiers[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = isSelfHost(host!)
|
||||||
|
? await Users.findOneBy({
|
||||||
|
host: IsNull(),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
})
|
||||||
|
: await Users.findOneBy({
|
||||||
|
host: toPuny(host!),
|
||||||
|
usernameLower: username.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
target = await resolveUser(username, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBlocked = await Blockings.exist({
|
||||||
|
where: {
|
||||||
|
blockerId: target.id,
|
||||||
|
blockeeId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const isFollowed = await Followings.exist({
|
||||||
|
where: {
|
||||||
|
followerId: user.id,
|
||||||
|
followeeId: target.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBlocked || !isFollowed) continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(await UserListJoinings.findOneBy({
|
||||||
|
userListId: list!.id,
|
||||||
|
userId: target.id,
|
||||||
|
})) != null
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
pushUserToUserList(target, list!);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Error in line:${linenum} ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ("Imported");
|
||||||
|
done();
|
||||||
|
}
|
47
packages/backend/src/queue/processors/db/index.ts
Normal file
47
packages/backend/src/queue/processors/db/index.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import type Bull from "bull";
|
||||||
|
import type { DbJobData } from "@/queue/types.js";
|
||||||
|
import { deleteDriveFiles } from "./delete-drive-files.js";
|
||||||
|
import { exportCustomEmojis } from "./export-custom-emojis.js";
|
||||||
|
import { exportNotes } from "./export-notes.js";
|
||||||
|
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 { importFollowing } from "./import-following.js";
|
||||||
|
import { importUserLists } from "./import-user-lists.js";
|
||||||
|
import { deleteAccount } from "./delete-account.js";
|
||||||
|
import { importMuting } from "./import-muting.js";
|
||||||
|
import { importPosts } from "./import-posts.js";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const jobs = {
|
||||||
|
deleteDriveFiles,
|
||||||
|
exportCustomEmojis,
|
||||||
|
exportNotes,
|
||||||
|
exportFollowing,
|
||||||
|
exportMute,
|
||||||
|
exportBlocking,
|
||||||
|
exportUserLists,
|
||||||
|
importFollowing,
|
||||||
|
importMuting,
|
||||||
|
importBlocking,
|
||||||
|
importUserLists,
|
||||||
|
importPosts,
|
||||||
|
importMastoPost,
|
||||||
|
importCkPost,
|
||||||
|
importCustomEmojis,
|
||||||
|
deleteAccount,
|
||||||
|
} as Record<
|
||||||
|
string,
|
||||||
|
| Bull.ProcessCallbackFunction<DbJobData>
|
||||||
|
| Bull.ProcessPromiseFunction<DbJobData>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function (dbQueue: Bull.Queue<DbJobData>) {
|
||||||
|
for (const [k, v] of Object.entries(jobs)) {
|
||||||
|
dbQueue.process(k, v);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
import { DriveFiles, Notes } from "@/models/index.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive", "notes"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Find the notes to which the given file is attached.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "array",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "Note",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: "No such file.",
|
||||||
|
code: "NO_SUCH_FILE",
|
||||||
|
id: "c118ece3-2e4b-4296-99d1-51756e32d232",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} 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) => {
|
||||||
|
// Fetch file
|
||||||
|
const file = await DriveFiles.findOneBy({
|
||||||
|
id: ps.fileId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = await Notes.createQueryBuilder("note")
|
||||||
|
.where(":file = ANY(note.fileIds)", { file: file.id })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return await Notes.packMany(notes, user, {
|
||||||
|
detail: true,
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { createWorker } from "tesseract.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Return caption of image",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps) => {
|
||||||
|
const worker = createWorker({
|
||||||
|
logger: (m) => console.log(m),
|
||||||
|
});
|
||||||
|
|
||||||
|
await worker.load();
|
||||||
|
await worker.loadLanguage("eng");
|
||||||
|
await worker.initialize("eng");
|
||||||
|
const {
|
||||||
|
data: { text },
|
||||||
|
} = await worker.recognize(ps.url);
|
||||||
|
await worker.terminate();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { DriveFiles } from "@/models/index.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Check if a given file exists.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "boolean",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
md5: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["md5"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const exist = await DriveFiles.exist({
|
||||||
|
where: {
|
||||||
|
md5: ps.md5,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return exist;
|
||||||
|
});
|
129
packages/backend/src/server/api/endpoints/drive/files/create.ts
Normal file
129
packages/backend/src/server/api/endpoints/drive/files/create.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { addFile } from "@/services/drive/add-file.js";
|
||||||
|
import { DriveFiles } from "@/models/index.js";
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||||
|
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||||
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
|
import { HOUR } from "@/const.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { apiLogger } from "../../../logger.js";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: HOUR,
|
||||||
|
max: 120,
|
||||||
|
},
|
||||||
|
|
||||||
|
requireFile: true,
|
||||||
|
|
||||||
|
kind: "write:drive",
|
||||||
|
|
||||||
|
description: "Upload a new drive file.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "DriveFile",
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
invalidFileName: {
|
||||||
|
message: "Invalid file name.",
|
||||||
|
code: "INVALID_FILE_NAME",
|
||||||
|
id: "f449b209-0c60-4e51-84d5-29486263bfd4",
|
||||||
|
},
|
||||||
|
|
||||||
|
inappropriate: {
|
||||||
|
message:
|
||||||
|
"Cannot upload the file because it has been determined that it possibly contains inappropriate content.",
|
||||||
|
code: "INAPPROPRIATE",
|
||||||
|
id: "bec5bd69-fba3-43c9-b4fb-2894b66ad5d2",
|
||||||
|
},
|
||||||
|
|
||||||
|
noFreeSpace: {
|
||||||
|
message:
|
||||||
|
"Cannot upload the file because you have no free space of drive.",
|
||||||
|
code: "NO_FREE_SPACE",
|
||||||
|
id: "d08dbc37-a6a9-463a-8c47-96c32ab5f064",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
folderId: {
|
||||||
|
type: "string",
|
||||||
|
format: "misskey:id",
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
name: { type: "string", nullable: true, default: null },
|
||||||
|
comment: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
maxLength: DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isSensitive: { type: "boolean", default: false },
|
||||||
|
force: { type: "boolean", default: false },
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(
|
||||||
|
meta,
|
||||||
|
paramDef,
|
||||||
|
async (ps, user, _, file, cleanup, ip, headers) => {
|
||||||
|
// Get 'name' parameter
|
||||||
|
let name = ps.name || file.originalname;
|
||||||
|
if (name !== undefined && name !== null) {
|
||||||
|
name = name.trim();
|
||||||
|
if (name.length === 0) {
|
||||||
|
name = null;
|
||||||
|
} else if (name === "blob") {
|
||||||
|
name = null;
|
||||||
|
} else if (!DriveFiles.validateFileName(name)) {
|
||||||
|
throw new ApiError(meta.errors.invalidFileName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create file
|
||||||
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path: file.path,
|
||||||
|
name,
|
||||||
|
comment: ps.comment,
|
||||||
|
folderId: ps.folderId,
|
||||||
|
force: ps.force,
|
||||||
|
sensitive: ps.isSensitive,
|
||||||
|
requestIp: meta.enableIpLogging ? ip : null,
|
||||||
|
requestHeaders: meta.enableIpLogging ? headers : null,
|
||||||
|
});
|
||||||
|
return await DriveFiles.pack(driveFile, { self: true });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error || typeof e === "string") {
|
||||||
|
apiLogger.error(e);
|
||||||
|
}
|
||||||
|
if (e instanceof IdentifiableError) {
|
||||||
|
if (e.id === "282f77bf-5816-4f72-9264-aa14d8261a21")
|
||||||
|
throw new ApiError(meta.errors.inappropriate);
|
||||||
|
if (e.id === "c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6")
|
||||||
|
throw new ApiError(meta.errors.noFreeSpace);
|
||||||
|
}
|
||||||
|
throw new ApiError();
|
||||||
|
} finally {
|
||||||
|
cleanup!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { deleteFile } from "@/services/drive/delete-file.js";
|
||||||
|
import { publishDriveStream } from "@/services/stream.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
import { DriveFiles, Users } from "@/models/index.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "write:drive",
|
||||||
|
|
||||||
|
description: "Delete an existing drive file.",
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: "No such file.",
|
||||||
|
code: "NO_SUCH_FILE",
|
||||||
|
id: "908939ec-e52b-4458-b395-1025195cea58",
|
||||||
|
},
|
||||||
|
|
||||||
|
accessDenied: {
|
||||||
|
message: "Access denied.",
|
||||||
|
code: "ACCESS_DENIED",
|
||||||
|
id: "5eb8d909-2540-4970-90b8-dd6f86088121",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} 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 (!(user.isAdmin || user.isModerator) && file.userId !== user.id) {
|
||||||
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await deleteFile(file);
|
||||||
|
|
||||||
|
// Publish fileDeleted event
|
||||||
|
publishDriveStream(user.id, "fileDeleted", file.id);
|
||||||
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { DriveFiles } from "@/models/index.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Search for a drive file by a hash of the contents.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "array",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "DriveFile",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
md5: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["md5"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const files = await DriveFiles.findBy({
|
||||||
|
md5: ps.md5,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await DriveFiles.packMany(files, { self: true });
|
||||||
|
});
|
|
@ -0,0 +1,51 @@
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { DriveFiles } from "@/models/index.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Search for a drive file by the given parameters.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "array",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "DriveFile",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
folderId: {
|
||||||
|
type: "string",
|
||||||
|
format: "misskey:id",
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const files = await DriveFiles.findBy({
|
||||||
|
name: ps.name,
|
||||||
|
userId: user.id,
|
||||||
|
folderId: ps.folderId ?? IsNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
files.map((file) => DriveFiles.pack(file, { self: true })),
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||||
|
import { DriveFiles, Users } from "@/models/index.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "read:drive",
|
||||||
|
|
||||||
|
description: "Show the properties of a drive file.",
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "DriveFile",
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchFile: {
|
||||||
|
message: "No such file.",
|
||||||
|
code: "NO_SUCH_FILE",
|
||||||
|
id: "067bc436-2718-4795-b0fb-ecbe43949e31",
|
||||||
|
},
|
||||||
|
|
||||||
|
accessDenied: {
|
||||||
|
message: "Access denied.",
|
||||||
|
code: "ACCESS_DENIED",
|
||||||
|
id: "25b73c73-68b1-41d0-bad1-381cfdf6579f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
fileId: { type: "string", format: "misskey:id" },
|
||||||
|
},
|
||||||
|
required: ["fileId"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
url: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
let file: DriveFile | null = null;
|
||||||
|
|
||||||
|
if (ps.fileId) {
|
||||||
|
file = await DriveFiles.findOneBy({ id: ps.fileId });
|
||||||
|
} else if (ps.url) {
|
||||||
|
file = await DriveFiles.findOne({
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
url: ps.url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
webpublicUrl: ps.url,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
thumbnailUrl: ps.url,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(user.isAdmin || user.isModerator) && file.userId !== user.id) {
|
||||||
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await DriveFiles.pack(file, {
|
||||||
|
detail: true,
|
||||||
|
withUser: true,
|
||||||
|
self: true,
|
||||||
|
});
|
||||||
|
});
|
116
packages/backend/src/server/api/endpoints/drive/files/update.ts
Normal file
116
packages/backend/src/server/api/endpoints/drive/files/update.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { publishDriveStream } from "@/services/stream.js";
|
||||||
|
import { DriveFiles, DriveFolders, Users } from "@/models/index.js";
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { ApiError } from "../../../error.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "write:drive",
|
||||||
|
|
||||||
|
description: "Update the properties of a drive file.",
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
invalidFileName: {
|
||||||
|
message: "Invalid file name.",
|
||||||
|
code: "INVALID_FILE_NAME",
|
||||||
|
id: "395e7156-f9f0-475e-af89-53c3c23080c2",
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchFile: {
|
||||||
|
message: "No such file.",
|
||||||
|
code: "NO_SUCH_FILE",
|
||||||
|
id: "e7778c7e-3af9-49cd-9690-6dbc3e6c972d",
|
||||||
|
},
|
||||||
|
|
||||||
|
accessDenied: {
|
||||||
|
message: "Access denied.",
|
||||||
|
code: "ACCESS_DENIED",
|
||||||
|
id: "01a53b27-82fc-445b-a0c1-b558465a8ed2",
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchFolder: {
|
||||||
|
message: "No such folder.",
|
||||||
|
code: "NO_SUCH_FOLDER",
|
||||||
|
id: "ea8fb7a5-af77-4a08-b608-c0218176cd73",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "DriveFile",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
fileId: { type: "string", format: "misskey:id" },
|
||||||
|
folderId: { type: "string", format: "misskey:id", nullable: true },
|
||||||
|
name: { type: "string" },
|
||||||
|
isSensitive: { type: "boolean" },
|
||||||
|
comment: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
maxLength: DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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 (!(user.isAdmin || user.isModerator) && file.userId !== user.id) {
|
||||||
|
throw new ApiError(meta.errors.accessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.name) file.name = ps.name;
|
||||||
|
if (!DriveFiles.validateFileName(file.name)) {
|
||||||
|
throw new ApiError(meta.errors.invalidFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.comment !== undefined) file.comment = ps.comment;
|
||||||
|
|
||||||
|
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
|
||||||
|
|
||||||
|
if (ps.folderId !== undefined) {
|
||||||
|
if (ps.folderId === null) {
|
||||||
|
file.folderId = null;
|
||||||
|
} else {
|
||||||
|
const folder = await DriveFolders.findOneBy({
|
||||||
|
id: ps.folderId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (folder == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
file.folderId = folder.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await DriveFiles.update(file.id, {
|
||||||
|
name: file.name,
|
||||||
|
comment: file.comment,
|
||||||
|
folderId: file.folderId,
|
||||||
|
isSensitive: file.isSensitive,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileObj = await DriveFiles.pack(file, { self: true });
|
||||||
|
|
||||||
|
// Publish fileUpdated event
|
||||||
|
publishDriveStream(user.id, "fileUpdated", fileObj);
|
||||||
|
|
||||||
|
return fileObj;
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
|
||||||
|
import define from "../../../define.js";
|
||||||
|
import { DriveFiles } from "@/models/index.js";
|
||||||
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
|
import { HOUR } from "@/const.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["drive"],
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: HOUR,
|
||||||
|
max: 60,
|
||||||
|
},
|
||||||
|
|
||||||
|
description:
|
||||||
|
"Request the server to download a new drive file from the specified URL.",
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: "write:drive",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
url: { type: "string" },
|
||||||
|
folderId: {
|
||||||
|
type: "string",
|
||||||
|
format: "misskey:id",
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isSensitive: { type: "boolean", default: false },
|
||||||
|
comment: { type: "string", nullable: true, maxLength: 512, default: null },
|
||||||
|
marker: { type: "string", nullable: true, default: null },
|
||||||
|
force: { type: "boolean", default: false },
|
||||||
|
},
|
||||||
|
required: ["url"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
uploadFromUrl({
|
||||||
|
url: ps.url,
|
||||||
|
user,
|
||||||
|
folderId: ps.folderId,
|
||||||
|
sensitive: ps.isSensitive,
|
||||||
|
force: ps.force,
|
||||||
|
comment: ps.comment,
|
||||||
|
}).then((file) => {
|
||||||
|
DriveFiles.pack(file, { self: true }).then((packedFile) => {
|
||||||
|
publishMainStream(user.id, "urlUploadFinished", {
|
||||||
|
marker: ps.marker,
|
||||||
|
file: packedFile,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue