import type { Config } from '@/config.js';
import { MfmService } from '@/core/MfmService.js';
import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
import { Entity } from 'megalodon';
import mfm from 'mfm-js';
import { GetterService } from '../GetterService.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js';
import type { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
export enum IdConvertType {
MastodonId,
SharkeyId,
}
export const escapeMFM = (text: string): string => text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/`/g, "`")
.replace(/\r?\n/g, "
");
@Injectable()
export class MastoConverters {
constructor(
@Inject(DI.config)
private config: Config,
private mfmService: MfmService,
private getterService: GetterService,
) {
}
private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
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 {
return this.getterService.getUser(id).then(p => {
return p;
});
}
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,
});
}
public async convertStatus(status: Entity.Status) {
const convertedAccount = this.convertAccount(status.account);
const note = await this.getterService.getNote(status.id);
const mentions = Promise.all(note.mentions.map(p =>
this.getUser(p)
.then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
.catch(() => null)))
.then(p => p.filter(m => m)) as Promise;
const content = note.text !== null
? this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, null)
.then(p => p ?? escapeMFM(note.text!))
: '';
const tags = note.tags.map(tag => {
return {
name: tag,
url: `${this.config.url}/tags/${tag}`,
} as Entity.Tag;
});
// 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(),
});
}
}
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);
if (notification.status) notification.status = convertStatus(notification.status);
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;
}
export function convertStatusSource(status: Entity.StatusSource) {
return simpleConvert(status);
}
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;
}