2023-11-12 15:07:32 +01:00
|
|
|
import * as fs from 'node:fs';
|
2024-02-03 12:55:56 +00:00
|
|
|
import * as fsp from 'node:fs/promises';
|
2023-12-30 20:44:31 -05:00
|
|
|
import * as crypto from 'node:crypto';
|
2023-11-12 15:07:32 +01:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
|
|
|
import { ZipReader } from 'slacc';
|
|
|
|
|
import { DI } from '@/di-symbols.js';
|
2023-11-28 22:46:10 +01:00
|
|
|
import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser, DriveFoldersRepository, MiDriveFolder } from '@/models/_.js';
|
2023-11-12 15:07:32 +01:00
|
|
|
import type Logger from '@/logger.js';
|
|
|
|
|
import { DownloadService } from '@/core/DownloadService.js';
|
|
|
|
|
import { bindThis } from '@/decorators.js';
|
|
|
|
|
import { QueueService } from '@/core/QueueService.js';
|
|
|
|
|
import { createTemp, createTempDir } from '@/misc/create-temp.js';
|
|
|
|
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
|
|
|
|
import { DriveService } from '@/core/DriveService.js';
|
|
|
|
|
import { MfmService } from '@/core/MfmService.js';
|
|
|
|
|
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
|
|
|
|
import { extractApHashtagObjects } from '@/core/activitypub/models/tag.js';
|
2023-11-28 22:46:10 +01:00
|
|
|
import { IdService } from '@/core/IdService.js';
|
2023-11-12 15:07:32 +01:00
|
|
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
|
|
|
|
import type * as Bull from 'bullmq';
|
2023-11-30 12:23:09 +00:00
|
|
|
import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js';
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ImportNotesProcessorService {
|
|
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
@Inject(DI.usersRepository)
|
|
|
|
|
private usersRepository: UsersRepository,
|
|
|
|
|
|
|
|
|
|
@Inject(DI.driveFilesRepository)
|
|
|
|
|
private driveFilesRepository: DriveFilesRepository,
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
@Inject(DI.driveFoldersRepository)
|
|
|
|
|
private driveFoldersRepository: DriveFoldersRepository,
|
|
|
|
|
|
2023-11-13 13:07:49 +01:00
|
|
|
@Inject(DI.notesRepository)
|
|
|
|
|
private notesRepository: NotesRepository,
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
private queueService: QueueService,
|
|
|
|
|
private noteCreateService: NoteCreateService,
|
|
|
|
|
private mfmService: MfmService,
|
|
|
|
|
private apNoteService: ApNoteService,
|
|
|
|
|
private driveService: DriveService,
|
|
|
|
|
private downloadService: DownloadService,
|
2023-11-28 22:46:10 +01:00
|
|
|
private idService: IdService,
|
2023-11-12 15:07:32 +01:00
|
|
|
private queueLoggerService: QueueLoggerService,
|
|
|
|
|
) {
|
|
|
|
|
this.logger = this.queueLoggerService.logger.createSubLogger('import-notes');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
2023-11-28 22:46:10 +01:00
|
|
|
private async uploadFiles(dir: string, user: MiUser, folder?: MiDriveFolder['id']) {
|
2024-02-03 12:55:56 +00:00
|
|
|
const fileList = await fsp.readdir(dir);
|
2023-11-22 16:00:46 +01:00
|
|
|
for await (const file of fileList) {
|
2023-11-12 15:07:32 +01:00
|
|
|
const name = `${dir}/${file}`;
|
|
|
|
|
if (fs.statSync(name).isDirectory()) {
|
2023-11-28 22:46:10 +01:00
|
|
|
await this.uploadFiles(name, user, folder);
|
2023-11-12 15:07:32 +01:00
|
|
|
} else {
|
2023-11-28 22:46:10 +01:00
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id, folderId: folder });
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
if (file.endsWith('.srt')) return;
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
await this.driveService.addFile({
|
|
|
|
|
user: user,
|
|
|
|
|
path: name,
|
|
|
|
|
name: file,
|
2023-11-28 22:46:10 +01:00
|
|
|
folderId: folder,
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 13:07:49 +01:00
|
|
|
@bindThis
|
2023-11-30 13:13:41 +00:00
|
|
|
private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> {
|
2023-11-13 13:07:49 +01:00
|
|
|
type NotesMap = {
|
|
|
|
|
[id: string]: any;
|
|
|
|
|
};
|
|
|
|
|
const notesTree: any[] = [];
|
2023-11-18 12:38:13 +01:00
|
|
|
const noteById: NotesMap = {};
|
|
|
|
|
const notesWaitingForParent: NotesMap = {};
|
|
|
|
|
|
2023-11-13 13:07:49 +01:00
|
|
|
for await (const note of arr) {
|
2023-11-30 13:13:41 +00:00
|
|
|
const noteId = idFieldPath.reduce(
|
|
|
|
|
(obj, step) => obj[step],
|
|
|
|
|
note,
|
|
|
|
|
);
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
|
|
|
|
|
noteById[noteId] = note;
|
2023-11-13 13:07:49 +01:00
|
|
|
note.childNotes = [];
|
2023-11-18 12:38:13 +01:00
|
|
|
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
const children = notesWaitingForParent[noteId];
|
2023-11-18 12:38:13 +01:00
|
|
|
if (children) {
|
|
|
|
|
note.childNotes.push(...children);
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
delete notesWaitingForParent[noteId];
|
2023-11-13 13:07:49 +01:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 13:13:41 +00:00
|
|
|
const noteReplyId = replyFieldPath.reduce(
|
|
|
|
|
(obj, step) => obj[step],
|
|
|
|
|
note,
|
|
|
|
|
);
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
if (noteReplyId == null) {
|
2023-11-18 12:38:13 +01:00
|
|
|
notesTree.push(note);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2023-11-13 13:07:49 +01:00
|
|
|
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
const parent = noteById[noteReplyId];
|
2023-11-18 12:38:13 +01:00
|
|
|
if (parent) {
|
|
|
|
|
parent.childNotes.push(note);
|
2023-11-17 20:10:16 +01:00
|
|
|
} else {
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
notesWaitingForParent[noteReplyId] ||= [];
|
|
|
|
|
notesWaitingForParent[noteReplyId].push(note);
|
2023-11-17 20:10:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-11-18 12:38:13 +01:00
|
|
|
|
prepare to import more notes
`recreateChain` converts a list of notes into a forest of notes, using
notes that are not replies as roots, and replies as child nodes,
recursively.
Previously, notes that are replies to notes not included in the
export, and their children, were never put in the forest, and
therefore wheren't imported.
This can be fine when importing from Twitter, since we can't really
link a note to a tweet.
And, for the moment, it's acceptable when importing from *key, because
the export doesn't contain the instance URL, so we can't resolve ids
to remote notes.
It's less fine when importing from Mastodon / Pleroma / Akkoma,
because in those cases we _can_ link to the remote note that the user
was replying to.
This commit makes `recreateChain` optionally return "orphaned" note
trees, so in the (near) future we can use it to properly thread
imported notes from those services.
2023-11-28 09:45:51 +00:00
|
|
|
if (includeOrphans) {
|
|
|
|
|
notesTree.push(...Object.values(notesWaitingForParent).flat(1));
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-18 12:38:13 +01:00
|
|
|
return notesTree;
|
2023-11-17 20:10:16 +01:00
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
@bindThis
|
|
|
|
|
private isIterable(obj: any) {
|
|
|
|
|
if (obj == null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return typeof obj[Symbol.iterator] === 'function';
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 11:37:20 +00:00
|
|
|
@bindThis
|
2024-02-03 12:55:46 +00:00
|
|
|
private parseTwitterFile(str : string) : { tweet: object }[] {
|
2024-02-03 11:29:46 +00:00
|
|
|
const jsonStr = str.replace(/^\s*window\.YTD\.tweets\.part0\s*=\s*/, '');
|
|
|
|
|
|
2024-02-01 15:58:50 +01:00
|
|
|
try {
|
2024-02-03 11:29:46 +00:00
|
|
|
return JSON.parse(jsonStr);
|
2024-02-01 15:58:50 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
//The format is not what we expected. Either this file was tampered with or twitters exports changed
|
2024-02-03 11:37:20 +00:00
|
|
|
this.logger.warn('Failed to import twitter notes due to malformed file');
|
|
|
|
|
throw error;
|
2024-02-01 15:58:50 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
@bindThis
|
|
|
|
|
public async process(job: Bull.Job<DbNoteImportJobData>): Promise<void> {
|
2023-11-12 18:01:39 +01:00
|
|
|
this.logger.info(`Starting note import of ${job.data.user.id} ...`);
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const file = await this.driveFilesRepository.findOneBy({
|
|
|
|
|
id: job.data.fileId,
|
|
|
|
|
});
|
|
|
|
|
if (file == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
if (folder == null) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 17:32:24 +01:00
|
|
|
const type = job.data.type;
|
2023-11-12 15:37:36 +01:00
|
|
|
|
|
|
|
|
if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) {
|
2023-11-12 15:07:32 +01:00
|
|
|
const [path, cleanup] = await createTempDir();
|
|
|
|
|
|
|
|
|
|
this.logger.info(`Temp dir is ${path}`);
|
|
|
|
|
|
|
|
|
|
const destPath = path + '/twitter.zip';
|
|
|
|
|
|
|
|
|
|
try {
|
2024-02-03 12:55:56 +00:00
|
|
|
await fsp.writeFile(destPath, '', 'binary');
|
2023-11-12 15:07:32 +01:00
|
|
|
await this.downloadService.downloadUrl(file.url, destPath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
if (e instanceof Error || typeof e === 'string') {
|
|
|
|
|
this.logger.error(e);
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const outputPath = path + '/twitter';
|
|
|
|
|
try {
|
|
|
|
|
this.logger.succ(`Unzipping to ${outputPath}`);
|
2024-02-03 12:55:56 +00:00
|
|
|
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
2024-02-01 15:58:50 +01:00
|
|
|
|
2024-02-03 12:55:56 +00:00
|
|
|
const unprocessedTweets = this.parseTwitterFile(await fsp.readFile(outputPath + '/data/tweets.js', 'utf-8'));
|
2024-02-01 15:58:50 +01:00
|
|
|
|
2024-02-03 12:55:46 +00:00
|
|
|
const tweets = unprocessedTweets.map(e => e.tweet);
|
2024-02-03 11:37:20 +00:00
|
|
|
const processedTweets = await this.recreateChain(['id_str'], ['in_reply_to_status_id_str'], tweets, false);
|
|
|
|
|
this.queueService.createImportTweetsToDbJob(job.data.user, processedTweets, null);
|
2023-11-12 15:07:32 +01:00
|
|
|
} finally {
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
2023-11-22 16:00:46 +01:00
|
|
|
} else if (type === 'Facebook' || file.name.startsWith('facebook-') && file.name.endsWith('.zip')) {
|
|
|
|
|
const [path, cleanup] = await createTempDir();
|
|
|
|
|
|
|
|
|
|
this.logger.info(`Temp dir is ${path}`);
|
|
|
|
|
|
|
|
|
|
const destPath = path + '/facebook.zip';
|
|
|
|
|
|
|
|
|
|
try {
|
2024-02-03 12:55:56 +00:00
|
|
|
await fsp.writeFile(destPath, '', 'binary');
|
2023-11-22 16:00:46 +01:00
|
|
|
await this.downloadService.downloadUrl(file.url, destPath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
if (e instanceof Error || typeof e === 'string') {
|
|
|
|
|
this.logger.error(e);
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const outputPath = path + '/facebook';
|
|
|
|
|
try {
|
|
|
|
|
this.logger.succ(`Unzipping to ${outputPath}`);
|
2024-02-03 12:55:56 +00:00
|
|
|
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
|
|
|
|
const postsJson = await fsp.readFile(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8');
|
2023-11-22 16:00:46 +01:00
|
|
|
const posts = JSON.parse(postsJson);
|
2023-11-28 22:46:10 +01:00
|
|
|
const facebookFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder?.id });
|
|
|
|
|
if (facebookFolder == null && folder) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
if (createdFolder) await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user, createdFolder.id);
|
|
|
|
|
}
|
2023-11-22 16:00:46 +01:00
|
|
|
this.queueService.createImportFBToDbJob(job.data.user, posts);
|
|
|
|
|
} finally {
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
2023-11-12 15:07:32 +01:00
|
|
|
} else if (file.name.endsWith('.zip')) {
|
|
|
|
|
const [path, cleanup] = await createTempDir();
|
|
|
|
|
|
|
|
|
|
this.logger.info(`Temp dir is ${path}`);
|
|
|
|
|
|
|
|
|
|
const destPath = path + '/unknown.zip';
|
|
|
|
|
|
|
|
|
|
try {
|
2024-02-03 12:55:56 +00:00
|
|
|
await fsp.writeFile(destPath, '', 'binary');
|
2023-11-12 15:07:32 +01:00
|
|
|
await this.downloadService.downloadUrl(file.url, destPath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
if (e instanceof Error || typeof e === 'string') {
|
|
|
|
|
this.logger.error(e);
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const outputPath = path + '/unknown';
|
|
|
|
|
try {
|
|
|
|
|
this.logger.succ(`Unzipping to ${outputPath}`);
|
2024-02-03 12:55:56 +00:00
|
|
|
ZipReader.withDestinationPath(outputPath).viaBuffer(await fsp.readFile(destPath));
|
2023-11-12 15:37:36 +01:00
|
|
|
const isInstagram = type === 'Instagram' || fs.existsSync(outputPath + '/instagram_live') || fs.existsSync(outputPath + '/instagram_ads_and_businesses');
|
|
|
|
|
const isOutbox = type === 'Mastodon' || fs.existsSync(outputPath + '/outbox.json');
|
2023-11-12 15:07:32 +01:00
|
|
|
if (isInstagram) {
|
2024-02-03 12:55:56 +00:00
|
|
|
const postsJson = await fsp.readFile(outputPath + '/content/posts_1.json', 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
const posts = JSON.parse(postsJson);
|
2023-11-28 22:46:10 +01:00
|
|
|
const igFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder?.id });
|
|
|
|
|
if (igFolder == null && folder) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
if (createdFolder) await this.uploadFiles(outputPath + '/media/posts', user, createdFolder.id);
|
|
|
|
|
}
|
2023-11-12 15:48:28 +01:00
|
|
|
this.queueService.createImportIGToDbJob(job.data.user, posts);
|
2023-11-12 15:07:32 +01:00
|
|
|
} else if (isOutbox) {
|
2024-02-03 12:55:56 +00:00
|
|
|
const actorJson = await fsp.readFile(outputPath + '/actor.json', 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
const actor = JSON.parse(actorJson);
|
|
|
|
|
const isPleroma = actor['@context'].some((v: any) => typeof v === 'string' && v.match(/litepub(.*)/));
|
|
|
|
|
if (isPleroma) {
|
2024-02-03 12:55:56 +00:00
|
|
|
const outboxJson = await fsp.readFile(outputPath + '/outbox.json', 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
const outbox = JSON.parse(outboxJson);
|
2023-11-30 13:24:57 +00:00
|
|
|
const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
|
|
|
|
|
this.queueService.createImportPleroToDbJob(job.data.user, processedToots, null);
|
2023-11-12 15:07:32 +01:00
|
|
|
} else {
|
2024-02-03 12:55:56 +00:00
|
|
|
const outboxJson = await fsp.readFile(outputPath + '/outbox.json', 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
const outbox = JSON.parse(outboxJson);
|
2023-11-28 22:46:10 +01:00
|
|
|
let mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder?.id });
|
|
|
|
|
if (mastoFolder == null && folder) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
}
|
|
|
|
|
if (fs.existsSync(outputPath + '/media_attachments/files') && mastoFolder) {
|
|
|
|
|
await this.uploadFiles(outputPath + '/media_attachments/files', user, mastoFolder.id);
|
|
|
|
|
}
|
2023-11-30 13:13:41 +00:00
|
|
|
const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true);
|
2023-11-30 12:23:09 +00:00
|
|
|
this.queueService.createImportMastoToDbJob(job.data.user, processedToots, null);
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
} else if (job.data.type === 'Misskey' || file.name.startsWith('notes-') && file.name.endsWith('.json')) {
|
|
|
|
|
const [path, cleanup] = await createTemp();
|
|
|
|
|
|
|
|
|
|
this.logger.info(`Temp dir is ${path}`);
|
|
|
|
|
|
|
|
|
|
try {
|
2024-02-03 12:55:56 +00:00
|
|
|
await fsp.writeFile(path, '', 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
await this.downloadService.downloadUrl(file.url, path);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
if (e instanceof Error || typeof e === 'string') {
|
|
|
|
|
this.logger.error(e);
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 12:55:56 +00:00
|
|
|
const notesJson = await fsp.readFile(path, 'utf-8');
|
2023-11-12 15:07:32 +01:00
|
|
|
const notes = JSON.parse(notesJson);
|
2023-11-30 13:13:41 +00:00
|
|
|
const processedNotes = await this.recreateChain(['id'], ['replyId'], notes, false);
|
2023-11-13 13:07:49 +01:00
|
|
|
this.queueService.createImportKeyNotesToDbJob(job.data.user, processedNotes, null);
|
2023-11-12 15:07:32 +01:00
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.logger.succ('Import jobs created');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
2023-11-30 12:23:09 +00:00
|
|
|
public async processKeyNotesToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
2023-11-12 15:07:32 +01:00
|
|
|
const note = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 10:25:39 +01:00
|
|
|
if (note.renoteId) return;
|
|
|
|
|
|
2023-11-13 13:07:49 +01:00
|
|
|
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
if (folder == null) return;
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
const files: MiDriveFile[] = [];
|
|
|
|
|
const date = new Date(note.createdAt);
|
|
|
|
|
|
|
|
|
|
if (note.files && this.isIterable(note.files)) {
|
2023-11-28 22:46:10 +01:00
|
|
|
let keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
if (keyFolder == null) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
for await (const file of note.files) {
|
|
|
|
|
const [filePath, cleanup] = await createTemp();
|
|
|
|
|
const slashdex = file.url.lastIndexOf('/');
|
|
|
|
|
const name = file.url.substring(slashdex + 1);
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: keyFolder?.id });
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
try {
|
|
|
|
|
await this.downloadService.downloadUrl(file.url, filePath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
|
|
|
|
}
|
|
|
|
|
const driveFile = await this.driveService.addFile({
|
|
|
|
|
user: user,
|
|
|
|
|
path: filePath,
|
|
|
|
|
name: name,
|
2023-11-28 22:46:10 +01:00
|
|
|
folderId: keyFolder?.id,
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
files.push(driveFile);
|
|
|
|
|
} else {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 13:07:49 +01:00
|
|
|
const createdNote = await this.noteCreateService.import(user, { createdAt: date, reply: parentNote, text: note.text, apMentions: new Array(0), visibility: note.visibility, localOnly: note.localOnly, files: files, cw: note.cw });
|
|
|
|
|
if (note.childNotes) this.queueService.createImportKeyNotesToDbJob(user, note.childNotes, createdNote.id);
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
2023-11-30 12:23:09 +00:00
|
|
|
public async processMastoToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
2023-11-12 15:07:32 +01:00
|
|
|
const toot = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-31 03:48:51 +01:00
|
|
|
const followers = toot.to.some((str: string) => str.includes('/followers'));
|
|
|
|
|
|
2023-12-31 04:09:44 +01:00
|
|
|
if (toot.directMessage || !toot.to.includes('https://www.w3.org/ns/activitystreams#Public') && !followers) return;
|
2023-12-31 03:48:51 +01:00
|
|
|
|
2023-12-31 22:41:35 +01:00
|
|
|
const visibility = followers ? toot.cc.includes('https://www.w3.org/ns/activitystreams#Public') ? 'home' : 'followers' : 'public';
|
2023-11-30 12:23:09 +00:00
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
const date = new Date(toot.object.published);
|
|
|
|
|
let text = undefined;
|
|
|
|
|
const files: MiDriveFile[] = [];
|
|
|
|
|
let reply: MiNote | null = null;
|
|
|
|
|
|
|
|
|
|
if (toot.object.inReplyTo != null) {
|
2023-11-30 12:23:09 +00:00
|
|
|
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
|
|
|
|
if (parentNote) {
|
|
|
|
|
reply = parentNote;
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
reply = await this.apNoteService.resolveNote(toot.object.inReplyTo);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
reply = null;
|
|
|
|
|
}
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hashtags = extractApHashtagObjects(toot.object.tag).map((x) => x.name).filter((x): x is string => x != null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
text = await this.mfmService.fromHtml(toot.object.content, hashtags);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
text = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toot.object.attachment && this.isIterable(toot.object.attachment)) {
|
|
|
|
|
for await (const file of toot.object.attachment) {
|
|
|
|
|
const slashdex = file.url.lastIndexOf('/');
|
|
|
|
|
const name = file.url.substring(slashdex + 1);
|
|
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
|
|
|
|
if (exists) {
|
2024-02-13 22:01:53 +00:00
|
|
|
if (file.name) {
|
|
|
|
|
this.driveService.updateFile(exists, { comment: file.name }, user);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-31 03:48:51 +01:00
|
|
|
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, visibility: visibility, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply });
|
2023-11-30 12:23:09 +00:00
|
|
|
if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id);
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
2023-11-30 13:24:57 +00:00
|
|
|
public async processPleroToDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
2023-11-12 15:07:32 +01:00
|
|
|
const post = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-30 13:24:57 +00:00
|
|
|
if (post.directMessage) return;
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
const date = new Date(post.object.published);
|
|
|
|
|
let text = undefined;
|
|
|
|
|
const files: MiDriveFile[] = [];
|
|
|
|
|
let reply: MiNote | null = null;
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
if (folder == null) return;
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
if (post.object.inReplyTo != null) {
|
2023-11-30 13:24:57 +00:00
|
|
|
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
|
|
|
|
if (parentNote) {
|
|
|
|
|
reply = parentNote;
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
reply = await this.apNoteService.resolveNote(post.object.inReplyTo);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
reply = null;
|
|
|
|
|
}
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hashtags = extractApHashtagObjects(post.object.tag).map((x) => x.name).filter((x): x is string => x != null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
text = await this.mfmService.fromHtml(post.object.content, hashtags);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
text = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (post.object.attachment && this.isIterable(post.object.attachment)) {
|
2023-11-28 22:46:10 +01:00
|
|
|
let pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
if (pleroFolder == null) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
for await (const file of post.object.attachment) {
|
|
|
|
|
const slashdex = file.url.lastIndexOf('/');
|
2023-12-30 20:44:31 -05:00
|
|
|
const filename = file.url.substring(slashdex + 1);
|
2023-12-31 11:14:41 -05:00
|
|
|
const hash = crypto.createHash('md5').update(file.url).digest('base64url');
|
|
|
|
|
const name = `${hash}-${filename}`;
|
2023-11-12 15:07:32 +01:00
|
|
|
const [filePath, cleanup] = await createTemp();
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id });
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
try {
|
|
|
|
|
await this.downloadService.downloadUrl(file.url, filePath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
|
|
|
|
}
|
|
|
|
|
const driveFile = await this.driveService.addFile({
|
|
|
|
|
user: user,
|
|
|
|
|
path: filePath,
|
|
|
|
|
name: name,
|
2023-12-30 20:44:31 -05:00
|
|
|
comment: file.name,
|
2023-11-28 22:46:10 +01:00
|
|
|
folderId: pleroFolder?.id,
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
files.push(driveFile);
|
|
|
|
|
} else {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-30 13:24:57 +00:00
|
|
|
const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply });
|
|
|
|
|
if (post.childNotes) this.queueService.createImportPleroToDbJob(user, post.childNotes, createdNote.id);
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
|
|
|
|
public async processIGDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> {
|
|
|
|
|
const post = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let date;
|
|
|
|
|
let title;
|
|
|
|
|
const files: MiDriveFile[] = [];
|
|
|
|
|
|
2023-11-12 22:16:47 +01:00
|
|
|
function decodeIGString(str: string) {
|
2023-11-12 18:19:44 +01:00
|
|
|
const arr = [];
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
arr.push(str.charCodeAt(i));
|
|
|
|
|
}
|
|
|
|
|
return Buffer.from(arr).toString('utf8');
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
if (post.media && this.isIterable(post.media) && post.media.length > 1) {
|
|
|
|
|
date = new Date(post.creation_timestamp * 1000);
|
2023-11-12 18:19:44 +01:00
|
|
|
title = decodeIGString(post.title);
|
2023-11-12 15:07:32 +01:00
|
|
|
for await (const file of post.media) {
|
|
|
|
|
const slashdex = file.uri.lastIndexOf('/');
|
|
|
|
|
const name = file.uri.substring(slashdex + 1);
|
|
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: `${name}.jpg`, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: `${name}.mp4`, userId: user.id });
|
|
|
|
|
if (exists) {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (post.media && this.isIterable(post.media) && !(post.media.length > 1)) {
|
|
|
|
|
date = new Date(post.media[0].creation_timestamp * 1000);
|
2023-11-12 18:19:44 +01:00
|
|
|
title = decodeIGString(post.media[0].title);
|
2023-11-12 15:07:32 +01:00
|
|
|
const slashdex = post.media[0].uri.lastIndexOf('/');
|
|
|
|
|
const name = post.media[0].uri.substring(slashdex + 1);
|
|
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: `${name}.jpg`, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: `${name}.mp4`, userId: user.id });
|
|
|
|
|
if (exists) {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.noteCreateService.import(user, { createdAt: date, text: title, files: files });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@bindThis
|
2023-11-30 12:23:09 +00:00
|
|
|
public async processTwitterDb(job: Bull.Job<DbNoteWithParentImportToDbJobData>): Promise<void> {
|
2023-11-12 15:07:32 +01:00
|
|
|
const tweet = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id });
|
|
|
|
|
if (folder == null) return;
|
|
|
|
|
|
2023-11-17 20:10:16 +01:00
|
|
|
const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null;
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
async function replaceTwitterUrls(full_text: string, urls: any) {
|
|
|
|
|
let full_textedit = full_text;
|
|
|
|
|
urls.forEach((url: any) => {
|
|
|
|
|
full_textedit = full_textedit.replaceAll(url.url, url.expanded_url);
|
|
|
|
|
});
|
|
|
|
|
return full_textedit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function replaceTwitterMentions(full_text: string, mentions: any) {
|
|
|
|
|
let full_textedit = full_text;
|
|
|
|
|
mentions.forEach((mention: any) => {
|
2024-01-28 16:06:16 +00:00
|
|
|
full_textedit = full_textedit.replaceAll(`@${mention.screen_name}`, `[@${mention.screen_name}](https://twitter.com/${mention.screen_name})`);
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
return full_textedit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(tweet.created_at);
|
2024-02-03 12:55:46 +00:00
|
|
|
const decodedText = tweet.full_text.replaceAll('>', '>').replaceAll('<', '<').replaceAll('&', '&');
|
2024-02-03 12:05:08 +00:00
|
|
|
const textReplaceURLs = tweet.entities.urls && tweet.entities.urls.length > 0 ? await replaceTwitterUrls(decodedText, tweet.entities.urls) : decodedText;
|
2023-11-18 12:38:13 +01:00
|
|
|
const text = tweet.entities.user_mentions && tweet.entities.user_mentions.length > 0 ? await replaceTwitterMentions(textReplaceURLs, tweet.entities.user_mentions) : textReplaceURLs;
|
2023-11-12 15:07:32 +01:00
|
|
|
const files: MiDriveFile[] = [];
|
2023-11-18 12:38:13 +01:00
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
if (tweet.extended_entities && this.isIterable(tweet.extended_entities.media)) {
|
2023-11-28 22:46:10 +01:00
|
|
|
let twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
if (twitFolder == null) {
|
|
|
|
|
await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id });
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 15:07:32 +01:00
|
|
|
for await (const file of tweet.extended_entities.media) {
|
|
|
|
|
if (file.video_info) {
|
|
|
|
|
const [filePath, cleanup] = await createTemp();
|
|
|
|
|
const slashdex = file.video_info.variants[0].url.lastIndexOf('/');
|
|
|
|
|
const name = file.video_info.variants[0].url.substring(slashdex + 1);
|
|
|
|
|
|
2023-11-28 22:46:10 +01:00
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: twitFolder?.id });
|
2023-11-12 15:07:32 +01:00
|
|
|
|
|
|
|
|
const videos = file.video_info.variants.filter((x: any) => x.content_type === 'video/mp4');
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
try {
|
|
|
|
|
await this.downloadService.downloadUrl(videos[0].url, filePath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
|
|
|
|
}
|
|
|
|
|
const driveFile = await this.driveService.addFile({
|
|
|
|
|
user: user,
|
|
|
|
|
path: filePath,
|
|
|
|
|
name: name,
|
2023-11-28 22:46:10 +01:00
|
|
|
folderId: twitFolder?.id,
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
files.push(driveFile);
|
|
|
|
|
} else {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup();
|
|
|
|
|
} else if (file.media_url_https) {
|
|
|
|
|
const [filePath, cleanup] = await createTemp();
|
|
|
|
|
const slashdex = file.media_url_https.lastIndexOf('/');
|
|
|
|
|
const name = file.media_url_https.substring(slashdex + 1);
|
|
|
|
|
|
|
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
|
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
|
try {
|
|
|
|
|
await this.downloadService.downloadUrl(file.media_url_https, filePath);
|
|
|
|
|
} catch (e) { // TODO: 何度か再試行
|
|
|
|
|
this.logger.error(e instanceof Error ? e : new Error(e as string));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const driveFile = await this.driveService.addFile({
|
|
|
|
|
user: user,
|
|
|
|
|
path: filePath,
|
|
|
|
|
name: name,
|
2023-11-28 22:46:10 +01:00
|
|
|
folderId: twitFolder?.id,
|
2023-11-12 15:07:32 +01:00
|
|
|
});
|
|
|
|
|
files.push(driveFile);
|
|
|
|
|
} else {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
cleanup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-17 20:10:16 +01:00
|
|
|
const createdNote = await this.noteCreateService.import(user, { createdAt: date, reply: parentNote, text: text, files: files });
|
2023-11-18 12:38:13 +01:00
|
|
|
if (tweet.childNotes) this.queueService.createImportTweetsToDbJob(user, tweet.childNotes, createdNote.id);
|
2023-11-12 15:07:32 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
this.logger.warn(`Error: ${e}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-22 16:00:46 +01:00
|
|
|
|
|
|
|
|
@bindThis
|
|
|
|
|
public async processFBDb(job: Bull.Job<DbNoteImportToDbJobData>): Promise<void> {
|
|
|
|
|
const post = job.data.target;
|
|
|
|
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
|
|
|
|
if (user == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 19:02:43 +01:00
|
|
|
if (!this.isIterable(post.data) || this.isIterable(post.data) && post.data[0].post === undefined) return;
|
2023-11-22 16:00:46 +01:00
|
|
|
|
|
|
|
|
const date = new Date(post.timestamp * 1000);
|
|
|
|
|
const title = decodeFBString(post.data[0].post);
|
|
|
|
|
const files: MiDriveFile[] = [];
|
|
|
|
|
|
|
|
|
|
function decodeFBString(str: string) {
|
|
|
|
|
const arr = [];
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
arr.push(str.charCodeAt(i));
|
|
|
|
|
}
|
|
|
|
|
return Buffer.from(arr).toString('utf8');
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-22 19:02:43 +01:00
|
|
|
if (post.attachments && this.isIterable(post.attachments)) {
|
|
|
|
|
const media = [];
|
|
|
|
|
for await (const data of post.attachments[0].data) {
|
|
|
|
|
if (data.media) {
|
|
|
|
|
media.push(data.media);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for await (const file of media) {
|
|
|
|
|
const slashdex = file.uri.lastIndexOf('/');
|
|
|
|
|
const name = file.uri.substring(slashdex + 1);
|
|
|
|
|
const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id });
|
2023-11-22 16:00:46 +01:00
|
|
|
if (exists) {
|
|
|
|
|
files.push(exists);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.noteCreateService.import(user, { createdAt: date, text: title, files: files });
|
|
|
|
|
}
|
2023-11-12 15:07:32 +01:00
|
|
|
}
|