2023-10-01 01:58:06 +02:00
|
|
|
import type { Config } from '@/config.js';
|
|
|
|
|
import { MfmService } from '@/core/MfmService.js';
|
|
|
|
|
import { DI } from '@/di-symbols.js';
|
2023-10-29 02:06:19 +02:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
2023-09-24 12:05:59 +02:00
|
|
|
import { Entity } from 'megalodon';
|
2023-10-29 00:50:00 +02:00
|
|
|
import mfm from 'mfm-js';
|
2023-10-01 01:58:06 +02:00
|
|
|
import { GetterService } from '../GetterService.js';
|
|
|
|
|
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
|
|
|
|
import type { MiUser } from '@/models/User.js';
|
2023-10-22 03:00:35 +02:00
|
|
|
import type { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
2023-10-01 01:58:06 +02:00
|
|
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
2023-10-29 00:50:00 +02:00
|
|
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
2023-09-23 18:49:47 +02:00
|
|
|
|
|
|
|
|
export enum IdConvertType {
|
|
|
|
|
MastodonId,
|
|
|
|
|
SharkeyId,
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-01 01:58:06 +02:00
|
|
|
export const escapeMFM = (text: string): string => text
|
2023-10-29 00:50:00 +02:00
|
|
|
.replace(/&/g, "&")
|
|
|
|
|
.replace(/</g, "<")
|
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
|
.replace(/"/g, """)
|
|
|
|
|
.replace(/'/g, "'")
|
|
|
|
|
.replace(/`/g, "`")
|
|
|
|
|
.replace(/\r?\n/g, "<br>");
|
2023-10-01 01:58:06 +02:00
|
|
|
|
2023-10-29 02:06:19 +02:00
|
|
|
@Injectable()
|
2023-10-01 01:58:06 +02:00
|
|
|
export class MastoConverters {
|
|
|
|
|
constructor(
|
|
|
|
|
@Inject(DI.config)
|
|
|
|
|
private config: Config,
|
|
|
|
|
|
2023-10-29 02:06:19 +02:00
|
|
|
private mfmService: MfmService,
|
|
|
|
|
private getterService: GetterService,
|
2023-10-01 01:58:06 +02:00
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-29 00:50:00 +02:00
|
|
|
private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
|
2023-10-01 01:58:06 +02:00
|
|
|
let acct = u.username;
|
|
|
|
|
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
|
|
|
|
|
let url: string | null = null;
|
|
|
|
|
if (u.host) {
|
|
|
|
|
const info = m.find(r => r.username === u.username && r.host === u.host);
|
|
|
|
|
acct = `${u.username}@${u.host}`;
|
|
|
|
|
acctUrl = `https://${u.host}/@${u.username}`;
|
|
|
|
|
if (info) url = info.url ?? info.uri;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: u.id,
|
|
|
|
|
username: u.username,
|
|
|
|
|
acct: acct,
|
|
|
|
|
url: url ?? acctUrl,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getUser(id: string): Promise<MiUser> {
|
2023-10-29 02:06:19 +02:00
|
|
|
return this.getterService.getUser(id).then(p => {
|
2023-10-01 01:58:06 +02:00
|
|
|
return p;
|
2023-10-01 02:06:04 +02:00
|
|
|
});
|
2023-10-01 01:58:06 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-29 00:50:00 +02:00
|
|
|
public async convertAccount(account: Entity.Account) {
|
|
|
|
|
return awaitAll({
|
|
|
|
|
id: account.id,
|
|
|
|
|
username: account.username,
|
|
|
|
|
acct: account.acct,
|
|
|
|
|
fqn: account.fqn,
|
|
|
|
|
display_name: account.display_name || account.username,
|
|
|
|
|
locked: account.locked,
|
|
|
|
|
created_at: account.created_at,
|
|
|
|
|
followers_count: account.followers_count,
|
|
|
|
|
following_count: account.following_count,
|
|
|
|
|
statuses_count: account.statuses_count,
|
|
|
|
|
note: account.note,
|
|
|
|
|
url: account.url,
|
|
|
|
|
avatar: account.avatar,
|
|
|
|
|
avatar_static: account.avatar,
|
|
|
|
|
header: account.header,
|
|
|
|
|
header_static: account.header,
|
|
|
|
|
emojis: account.emojis,
|
|
|
|
|
moved: null, //FIXME
|
|
|
|
|
fields: [],
|
|
|
|
|
bot: false,
|
|
|
|
|
discoverable: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-01 01:58:06 +02:00
|
|
|
public async convertStatus(status: Entity.Status) {
|
2023-10-29 00:50:00 +02:00
|
|
|
const convertedAccount = this.convertAccount(status.account);
|
2023-10-29 02:06:19 +02:00
|
|
|
const note = await this.getterService.getNote(status.id);
|
2023-10-29 00:50:00 +02:00
|
|
|
|
2023-10-01 01:58:06 +02:00
|
|
|
const mentions = Promise.all(note.mentions.map(p =>
|
|
|
|
|
this.getUser(p)
|
|
|
|
|
.then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
|
|
|
|
|
.catch(() => null)))
|
2023-10-29 00:50:00 +02:00
|
|
|
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
|
|
|
|
|
|
|
|
|
|
const content = note.text !== null
|
2023-10-29 02:06:19 +02:00
|
|
|
? this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, null)
|
2023-10-29 00:50:00 +02:00
|
|
|
.then(p => p ?? escapeMFM(note.text!))
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
const tags = note.tags.map(tag => {
|
|
|
|
|
return {
|
|
|
|
|
name: tag,
|
|
|
|
|
url: `${this.config.url}/tags/${tag}`,
|
|
|
|
|
} as Entity.Tag;
|
|
|
|
|
});
|
2023-09-23 18:49:47 +02:00
|
|
|
|
2023-10-29 00:50:00 +02:00
|
|
|
// noinspection ES6MissingAwait
|
|
|
|
|
return await awaitAll({
|
|
|
|
|
id: note.id,
|
|
|
|
|
uri: note.uri ?? `https://${this.config.host}/notes/${note.id}`,
|
|
|
|
|
url: note.url ?? note.uri ?? `https://${this.config.host}/notes/${note.id}`,
|
|
|
|
|
account: convertedAccount,
|
|
|
|
|
in_reply_to_id: note.replyId,
|
|
|
|
|
in_reply_to_account_id: note.replyUserId,
|
|
|
|
|
reblog: status.reblog,
|
|
|
|
|
content: content,
|
|
|
|
|
content_type: 'text/x.misskeymarkdown',
|
|
|
|
|
text: note.text,
|
|
|
|
|
created_at: status.created_at,
|
|
|
|
|
emojis: status.emojis,
|
|
|
|
|
replies_count: note.repliesCount,
|
|
|
|
|
reblogs_count: note.renoteCount,
|
|
|
|
|
favourites_count: status.favourites_count,
|
|
|
|
|
reblogged: false,
|
|
|
|
|
favourited: status.favourited,
|
|
|
|
|
muted: status.muted,
|
|
|
|
|
sensitive: status.sensitive,
|
|
|
|
|
spoiler_text: note.cw ? note.cw : '',
|
|
|
|
|
visibility: status.visibility,
|
|
|
|
|
media_attachments: status.media_attachments,
|
|
|
|
|
mentions: mentions,
|
|
|
|
|
tags: tags,
|
|
|
|
|
card: null, //FIXME
|
|
|
|
|
poll: status.poll ?? null,
|
|
|
|
|
application: null, //FIXME
|
|
|
|
|
language: null, //FIXME
|
|
|
|
|
pinned: null,
|
|
|
|
|
reactions: status.emoji_reactions,
|
|
|
|
|
emoji_reactions: status.emoji_reactions,
|
|
|
|
|
bookmarked: false,
|
|
|
|
|
quote: false,
|
|
|
|
|
edited_at: note.updatedAt?.toISOString(),
|
|
|
|
|
});
|
2023-09-24 17:36:13 +02:00
|
|
|
}
|
2023-09-23 18:49:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function simpleConvert(data: any) {
|
|
|
|
|
// copy the object to bypass weird pass by reference bugs
|
|
|
|
|
const result = Object.assign({}, data);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertAccount(account: Entity.Account) {
|
|
|
|
|
return simpleConvert(account);
|
|
|
|
|
}
|
|
|
|
|
export function convertAnnouncement(announcement: Entity.Announcement) {
|
|
|
|
|
return simpleConvert(announcement);
|
|
|
|
|
}
|
|
|
|
|
export function convertAttachment(attachment: Entity.Attachment) {
|
|
|
|
|
return simpleConvert(attachment);
|
|
|
|
|
}
|
|
|
|
|
export function convertFilter(filter: Entity.Filter) {
|
|
|
|
|
return simpleConvert(filter);
|
|
|
|
|
}
|
|
|
|
|
export function convertList(list: Entity.List) {
|
|
|
|
|
return simpleConvert(list);
|
|
|
|
|
}
|
|
|
|
|
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
|
|
|
|
return simpleConvert(tag);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertNotification(notification: Entity.Notification) {
|
|
|
|
|
notification.account = convertAccount(notification.account);
|
2023-09-24 17:36:13 +02:00
|
|
|
if (notification.status) notification.status = convertStatus(notification.status);
|
2023-09-23 18:49:47 +02:00
|
|
|
return notification;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertPoll(poll: Entity.Poll) {
|
|
|
|
|
return simpleConvert(poll);
|
|
|
|
|
}
|
|
|
|
|
export function convertReaction(reaction: Entity.Reaction) {
|
|
|
|
|
if (reaction.accounts) {
|
|
|
|
|
reaction.accounts = reaction.accounts.map(convertAccount);
|
|
|
|
|
}
|
|
|
|
|
return reaction;
|
|
|
|
|
}
|
|
|
|
|
export function convertRelationship(relationship: Entity.Relationship) {
|
|
|
|
|
return simpleConvert(relationship);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertStatus(status: Entity.Status) {
|
|
|
|
|
status.account = convertAccount(status.account);
|
|
|
|
|
status.media_attachments = status.media_attachments.map((attachment) =>
|
|
|
|
|
convertAttachment(attachment),
|
|
|
|
|
);
|
|
|
|
|
if (status.poll) status.poll = convertPoll(status.poll);
|
|
|
|
|
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
|
|
|
|
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 14:45:57 +02:00
|
|
|
export function convertStatusSource(status: Entity.StatusSource) {
|
|
|
|
|
return simpleConvert(status);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-23 18:49:47 +02:00
|
|
|
export function convertConversation(conversation: Entity.Conversation) {
|
|
|
|
|
conversation.accounts = conversation.accounts.map(convertAccount);
|
|
|
|
|
if (conversation.last_status) {
|
|
|
|
|
conversation.last_status = convertStatus(conversation.last_status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return conversation;
|
|
|
|
|
}
|