From 54578f69656ed534dfb28c4bf67b03d7b30bd261 Mon Sep 17 00:00:00 2001
From: Marie <marie@kaifa.ch>
Date: Sun, 1 Oct 2023 01:58:06 +0200
Subject: [PATCH] upd: add MFM to HTML support and Mentions parsing to mastodon
 api (#33)

* upd: attempt to turn MFM to html on mastodon

* revert: recent change until better implementation later

* chore: remove unused packages

* Update docker.yml

* upd: add MFM to HTML for timelines and status view

* chore: lint

* upd: megalodon resolve urls

* upd: add spliting

* test: local user mention

* test: change local user url in mention

* upd: change check

* test: megalodon changes

* upd: edit resolving of local users

This is starting to drive me nuts

* upd: remove the @ symbol in query

* fix: make renderPerson return host instead of null for local

* upd: change url for local user

* upd: change limit

* upd: add url to output

* upd: add mastodon boolean

* test: test different format

* fix: test of different format

* test: change up resolving

* fix: forgot to provide url

* upd: change lookup function a bit

* test: substring

* test: regex

* upd: remove substr

* test: new regexs

* dirty test

* test: one last attempt for today

* upd: fix build error

* upd: take input from iceshrimp dev

* upd: parse remote statuses

* upd: fix pleroma users misformatted urls

* upd: add uri to normal user

* fix: forgot to push updated types

* fix: resolving broke

* fix: html not converting correctly

* fix: return default img if no banner

* upd: swap out img used for no header, set fallback avatar

* fix: html escaped & and ' symbols

* upd: fix ' converting into 39; and get profile fields

* upd: resolve fields on lookup

---------

Co-authored-by: Amelia Yukii <123300075+Insert5StarName@users.noreply.github.com>
---
 .github/workflows/docker.yml                  |   6 ++
 packages/backend/assets/transparent.png       | Bin 0 -> 68 bytes
 packages/backend/src/core/MfmService.ts       |   7 ++
 .../src/server/api/endpoints/ap/show.ts       |   2 +-
 .../api/mastodon/MastodonApiServerService.ts  |  20 +++-
 .../src/server/api/mastodon/converters.ts     |  95 ++++++++++++++++++
 .../server/api/mastodon/endpoints/account.ts  |   2 +-
 .../server/api/mastodon/endpoints/status.ts   |  39 +++----
 .../server/api/mastodon/endpoints/timeline.ts |  17 ++--
 packages/megalodon/src/misskey.ts             |  68 ++++++++++++-
 packages/megalodon/src/misskey/api_client.ts  |  20 ++--
 .../megalodon/src/misskey/entities/user.ts    |   1 +
 .../src/misskey/entities/userDetail.ts        |   1 +
 13 files changed, 237 insertions(+), 41 deletions(-)
 create mode 100644 packages/backend/assets/transparent.png

diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 8140d8675e..db2bc7faa4 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -1,6 +1,12 @@
 name: Publish Docker image
 
 on:
+  push:
+    branches:
+      - stable
+    paths:
+      - packages/**
+      - locales/**
   release:
     types: [published]
   workflow_dispatch:
diff --git a/packages/backend/assets/transparent.png b/packages/backend/assets/transparent.png
new file mode 100644
index 0000000000000000000000000000000000000000..240ca4f8d4edca6d5905acf71bdd9f88d4bd3127
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kP61+gajamnSrtAhou#e
O#o+1c=d#Wzp$Py;d<%g9

literal 0
HcmV?d00001

diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index b275d1b142..19e2288100 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -339,6 +339,13 @@ export class MfmService {
 			mention: (node) => {
 				const a = doc.createElement('a');
 				const { username, host, acct } = node.props;
+/* 				if (mastodon) {
+					const splitacct = acct.split("@");
+					a.setAttribute('href', splitacct[2] !== this.config.host && splitacct[2] !== undefined ? `https://${splitacct[2]}/@${splitacct[1]}` : `https://${this.config.host}/${acct}`);
+					a.className = 'u-url mention';
+					a.textContent = acct;
+					return a;
+				} */
 				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
 				a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`);
 				a.className = 'u-url mention';
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index f442fbdd2f..6cdd617561 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -27,7 +27,7 @@ export const meta = {
 	requireCredential: true,
 
 	limit: {
-		duration: ms('1hour'),
+		duration: ms('1minute'),
 		max: 30,
 	},
 
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index fe9f1fc871..2653bbdbdf 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -3,7 +3,7 @@ import megalodon, { Entity, MegalodonInterface } from 'megalodon';
 import querystring from 'querystring';
 import { IsNull } from 'typeorm';
 import multer from 'fastify-multer';
-import type { UsersRepository } from '@/models/_.js';
+import type { NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 import { DI } from '@/di-symbols.js';
 import { bindThis } from '@/decorators.js';
 import type { Config } from '@/config.js';
@@ -12,6 +12,7 @@ import { convertId, IdConvertType as IdType, convertAccount, convertAnnouncement
 import { getInstance } from './endpoints/meta.js';
 import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
 import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
 	const accessTokenArr = authorization?.split(' ') ?? [null];
@@ -26,9 +27,14 @@ export class MastodonApiServerService {
 	constructor(
         @Inject(DI.usersRepository)
         private usersRepository: UsersRepository,
+		@Inject(DI.notesRepository)
+        private notesRepository: NotesRepository,
+		@Inject(DI.userProfilesRepository)
+		private userProfilesRepository: UserProfilesRepository,
         @Inject(DI.config)
         private config: Config,
         private metaService: MetaService,
+		private userEntityService: UserEntityService,
 	) { }
 
 	@bindThis
@@ -256,8 +262,10 @@ export class MastodonApiServerService {
 			const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
 			// displayed without being logged in
 			try {
-				const account = new ApiAccountMastodon(_request, client, BASE_URL);
-				reply.send(await account.lookup());
+				const data = await client.search((_request.query as any).acct, { type: 'accounts' });
+				const profile = await this.userProfilesRepository.findOneBy({userId: data.data.accounts[0].id});
+				data.data.accounts[0].fields = profile?.fields.map(f => ({...f, verified_at: null})) || [];
+				reply.send(convertAccount(data.data.accounts[0]));
 			} catch (e: any) {
 				/* console.error(e); */
 				reply.code(401).send(e.response.data);
@@ -294,6 +302,8 @@ export class MastodonApiServerService {
 			try {
 				const sharkId = convertId(_request.params.id, IdType.SharkeyId);
 				const data = await client.getAccount(sharkId);
+				const profile = await this.userProfilesRepository.findOneBy({userId: sharkId});
+				data.data.fields = profile?.fields.map(f => ({...f, verified_at: null})) || [];
 				reply.send(convertAccount(data.data));
 			} catch (e: any) {
 				/* console.error(e);
@@ -744,7 +754,7 @@ export class MastodonApiServerService {
 		//#endregion
 
 		//#region Timelines
-		const TLEndpoint = new ApiTimelineMastodon(fastify);
+		const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.userEntityService);
 
 		// GET Endpoints
 		TLEndpoint.getTL();
@@ -769,7 +779,7 @@ export class MastodonApiServerService {
 		//#endregion
 
 		//#region Status
-		const NoteEndpoint = new ApiStatusMastodon(fastify);
+		const NoteEndpoint = new ApiStatusMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.userEntityService);
 
 		// GET Endpoints
 		NoteEndpoint.getStatus();
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 58b8dc23ca..4f4524736d 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -1,4 +1,14 @@
+import type { Config } from '@/config.js';
+import { MfmService } from '@/core/MfmService.js';
+import { DI } from '@/di-symbols.js';
+import { Inject } from '@nestjs/common';
 import { Entity } from 'megalodon';
+import { parse } from 'mfm-js';
+import { GetterService } from '../GetterService.js';
+import type { IMentionedRemoteUsers } from '@/models/Note.js';
+import type { MiUser } from '@/models/User.js';
+import type { NotesRepository, UsersRepository } from '@/models/_.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 const CHAR_COLLECTION = '0123456789abcdefghijklmnopqrstuvwxyz';
 
@@ -7,6 +17,91 @@ export enum IdConvertType {
     SharkeyId,
 }
 
+export const escapeMFM = (text: string): string => text
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#39;")
+    .replace(/`/g, "&#x60;")
+    .replace(/\r?\n/g, "<br>");
+
+export class MastoConverters {
+	private MfmService: MfmService;
+	private GetterService: GetterService;
+
+	constructor(
+		@Inject(DI.config)
+		private config: Config,
+
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+
+		@Inject(DI.notesRepository)
+		private notesRepository: NotesRepository,
+		
+		private userEntityService: UserEntityService
+	) {
+		this.MfmService = new MfmService(this.config);
+		this.GetterService = new GetterService(this.usersRepository, this.notesRepository, this.userEntityService);
+	}
+
+	private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.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<MiUser> {
+		return this.GetterService.getUser(id).then(p => {
+			return p;
+		})
+	}
+
+	public async convertStatus(status: Entity.Status) {
+		status.account = convertAccount(status.account);
+		const note = await this.GetterService.getNote(status.id);
+		status.id = convertId(status.id, IdConvertType.MastodonId);
+		if (status.in_reply_to_account_id) status.in_reply_to_account_id = convertId(
+			status.in_reply_to_account_id,
+			IdConvertType.MastodonId,
+		);
+		if (status.in_reply_to_id) status.in_reply_to_id = convertId(status.in_reply_to_id, IdConvertType.MastodonId);
+		status.media_attachments = status.media_attachments.map((attachment) =>
+			convertAttachment(attachment),
+		);
+		// This will eventually be improved with a rewrite of this file
+		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<MastodonEntity.Mention[]>;
+		status.mentions = await mentions;
+		status.mentions = status.mentions.map((mention) => ({
+			...mention,
+			id: convertId(mention.id, IdConvertType.MastodonId),
+		}));
+		const convertedMFM = this.MfmService.toHtml(parse(status.content), JSON.parse(note.mentionedRemoteUsers));
+		status.content = status.content ? convertedMFM?.replace(/&amp;/g , "&").replaceAll(`<span>&</span><a href="${this.config.url}/tags/39;" rel="tag">#39;</a>` , "<span>\'</span>")! : status.content;
+		if (status.poll) status.poll = convertPoll(status.poll);
+		if (status.reblog) status.reblog = convertStatus(status.reblog);
+	
+		return status;
+	}
+}
+
 export function convertId(in_id: string, id_convert_type: IdConvertType): string {
 	switch (id_convert_type) {
 		case IdConvertType.MastodonId: {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 4abb5fff19..24ebe0c48b 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -63,7 +63,7 @@ export class ApiAccountMastodon {
 			const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
 			return convertAccount(data.data.accounts[0]);
 		} catch (e: any) {
-			/* console.error(e);
+			/* console.error(e)
 			console.error(e.response.data); */
 			return e.response;
 		}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index a295564b90..46dce65081 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -1,10 +1,13 @@
 import querystring from 'querystring';
 import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
-import { convertId, IdConvertType as IdType, convertAccount, convertAttachment, convertPoll, convertStatus, convertStatusSource } from '../converters.js';
+import { convertId, IdConvertType as IdType, convertAccount, convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
 import { getClient } from '../MastodonApiServerService.js';
 import { convertTimelinesArgsId, limitToInt } from './timeline.js';
 import type { Entity } from 'megalodon';
 import type { FastifyInstance } from 'fastify';
+import type { Config } from '@/config.js';
+import { NotesRepository, UsersRepository } from '@/models/_.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 function normalizeQuery(data: any) {
 	const str = querystring.stringify(data);
@@ -13,9 +16,11 @@ function normalizeQuery(data: any) {
 
 export class ApiStatusMastodon {
 	private fastify: FastifyInstance;
+	private mastoconverter: MastoConverters;
 
-	constructor(fastify: FastifyInstance) {
+	constructor(fastify: FastifyInstance, config: Config, usersrepo: UsersRepository, notesrepo: NotesRepository, userentity: UserEntityService) {
 		this.fastify = fastify;
+		this.mastoconverter = new MastoConverters(config, usersrepo, notesrepo, userentity);
 	}
 
 	public async getStatus() {
@@ -25,7 +30,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.getStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(_request.is404 ? 404 : 401).send(e.response.data);
@@ -59,8 +64,8 @@ export class ApiStatusMastodon {
 					convertId(_request.params.id, IdType.SharkeyId),
 					convertTimelinesArgsId(limitToInt(query)),
 				);
-				data.data.ancestors = data.data.ancestors.map((status: Entity.Status) => convertStatus(status));
-				data.data.descendants = data.data.descendants.map((status: Entity.Status) => convertStatus(status));
+				data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
+				data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
 				reply.send(data.data);
 			} catch (e: any) {
 				console.error(e);
@@ -219,7 +224,7 @@ export class ApiStatusMastodon {
 				}
 
 				const data = await client.postStatus(text, body);
-				reply.send(convertStatus(data.data as Entity.Status));
+				reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -240,7 +245,7 @@ export class ApiStatusMastodon {
 					body.media_ids = (body.media_ids as string[]).map((p) => convertId(p, IdType.SharkeyId));
 				}
 				const data = await client.editStatus(convertId(_request.params.id, IdType.SharkeyId), body);
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(_request.is404 ? 404 : 401).send(e.response.data);
@@ -258,7 +263,7 @@ export class ApiStatusMastodon {
 					convertId(_request.params.id, IdType.SharkeyId),
 					'❤',
 				)) as any;
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -276,7 +281,7 @@ export class ApiStatusMastodon {
 					convertId(_request.params.id, IdType.SharkeyId),
 					'❤',
 				);
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -291,7 +296,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.reblogStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -306,7 +311,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.unreblogStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -321,7 +326,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.bookmarkStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -336,7 +341,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.unbookmarkStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -351,7 +356,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.pinStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -366,7 +371,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.unpinStatus(convertId(_request.params.id, IdType.SharkeyId));
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -381,7 +386,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.createEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name);
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
@@ -396,7 +401,7 @@ export class ApiStatusMastodon {
 			const client = getClient(BASE_URL, accessTokens);
 			try {
 				const data = await client.deleteEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name);
-				reply.send(convertStatus(data.data));
+				reply.send(await this.mastoconverter.convertStatus(data.data));
 			} catch (e: any) {
 				console.error(e);
 				reply.code(401).send(e.response.data);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index a171205161..bb66a7707c 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -1,8 +1,11 @@
 import { ParsedUrlQuery } from 'querystring';
-import { convertId, IdConvertType as IdType, convertAccount, convertConversation, convertList, convertStatus } from '../converters.js';
+import { convertId, IdConvertType as IdType, convertAccount, convertConversation, convertList, MastoConverters } from '../converters.js';
 import { getClient } from '../MastodonApiServerService.js';
 import type { Entity } from 'megalodon';
 import type { FastifyInstance } from 'fastify';
+import type { Config } from '@/config.js';
+import { NotesRepository, UsersRepository } from '@/models/_.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
 
 export function limitToInt(q: ParsedUrlQuery) {
 	const object: any = q;
@@ -38,9 +41,11 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) {
 
 export class ApiTimelineMastodon {
 	private fastify: FastifyInstance;
+	private mastoconverter: MastoConverters;
 
-	constructor(fastify: FastifyInstance) {
+	constructor(fastify: FastifyInstance, config: Config, usersRepository: UsersRepository, notesRepository: NotesRepository, userEntityService: UserEntityService) {
 		this.fastify = fastify;
+		this.mastoconverter = new MastoConverters(config, usersRepository, notesRepository, userEntityService);
 	}
 
 	public async getTL() {
@@ -53,7 +58,7 @@ export class ApiTimelineMastodon {
 				const data = query.local === 'true'
 					? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
 					: await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
-				reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
+				reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -70,7 +75,7 @@ export class ApiTimelineMastodon {
 			try {
 				const query: any = _request.query;
 				const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(query)));
-				reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
+				reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -88,7 +93,7 @@ export class ApiTimelineMastodon {
 				const query: any = _request.query;
 				const params: any = _request.params;
 				const data = await client.getTagTimeline(params.hashtag, convertTimelinesArgsId(limitToInt(query)));
-				reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
+				reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
@@ -106,7 +111,7 @@ export class ApiTimelineMastodon {
 				const query: any = _request.query;
 				const params: any = _request.params;
 				const data = await client.getListTimeline(convertId(params.id, IdType.SharkeyId), convertTimelinesArgsId(limitToInt(query)));
-				reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
+				reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
 			} catch (e: any) {
 				console.error(e);
 				console.error(e.response.data);
diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts
index 104d27a5d3..de2b6e2e78 100644
--- a/packages/megalodon/src/misskey.ts
+++ b/packages/megalodon/src/misskey.ts
@@ -2122,6 +2122,40 @@ export default class Misskey implements MegalodonInterface {
   ): Promise<Response<Entity.Results>> {
     switch (options.type) {
       case 'accounts': {
+        if (q.startsWith("http://") || q.startsWith("https://")) {
+					return this.client
+						.post("/api/ap/show", { uri: q })
+						.then(async (res) => {
+							if (res.status != 200 || res.data.type != "User") {
+								res.status = 200;
+								res.statusText = "OK";
+								res.data = {
+									accounts: [],
+									statuses: [],
+									hashtags: [],
+								};
+
+								return res;
+							}
+
+							const account = await MisskeyAPI.Converter.userDetail(
+								res.data.object as MisskeyAPI.Entity.UserDetail,
+								this.baseUrl,
+							);
+
+							return {
+								...res,
+								data: {
+									accounts:
+										options?.max_id && options?.max_id >= account.id
+											? []
+											: [account],
+									statuses: [],
+									hashtags: [],
+								},
+							};
+						});
+				}
         let params = {
           query: q
         }
@@ -2151,7 +2185,7 @@ export default class Misskey implements MegalodonInterface {
 					});
 				}
         try {
-          const match = q.match(/^@?(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)$/);
+          const match = params.query.match(/^@?(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)$/);
           if (match) {
             const lookupQuery = {
               username: match.groups?.user,
@@ -2195,6 +2229,38 @@ export default class Misskey implements MegalodonInterface {
         }))
       }
       case 'statuses': {
+        if (q.startsWith("http://") || q.startsWith("https://")) {
+					return this.client
+						.post("/api/ap/show", { uri: q })
+						.then(async (res) => {
+							if (res.status != 200 || res.data.type != "Note") {
+								res.status = 200;
+								res.statusText = "OK";
+								res.data = {
+									accounts: [],
+									statuses: [],
+									hashtags: [],
+								};
+
+								return res;
+							}
+
+							const post = await MisskeyAPI.Converter.note(
+								res.data.object as MisskeyAPI.Entity.Note,
+								this.baseUrl
+							);
+
+							return {
+								...res,
+								data: {
+									accounts: [],
+									statuses:
+										options?.max_id && options.max_id >= post.id ? [] : [post],
+									hashtags: [],
+								},
+							};
+						});
+				}
         let params = {
           query: q
         }
diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts
index 6582cf3e77..c30886f903 100644
--- a/packages/megalodon/src/misskey/api_client.ts
+++ b/packages/megalodon/src/misskey/api_client.ts
@@ -93,11 +93,11 @@ namespace MisskeyAPI {
         following_count: u.followingCount ? u.followingCount : 0,
         statuses_count: u.notesCount ? u.notesCount : 0,
         note: u.description ? u.description : '',
-        url: acctUrl,
-        avatar: u.avatarUrl,
-        avatar_static: u.avatarUrl,
-        header: u.bannerUrl ? u.bannerUrl : '',
-        header_static: u.bannerUrl ? u.bannerUrl : '',
+        url: u.uri ?? acctUrl,
+        avatar: u.avatarUrl ? u.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
+        avatar_static: u.avatarUrl ? u.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
+        header: u.bannerUrl ? u.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
+        header_static: u.bannerUrl ? u.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
         emojis: mapEmojis(u.emojis),
         moved: null,
         fields: [],
@@ -128,11 +128,11 @@ namespace MisskeyAPI {
         following_count: u.followingCount,
         statuses_count: u.notesCount,
         note: u.description ? u.description.replace(/\n|\\n/g, "<br>") : '',
-        url: acctUrl,
-        avatar: u.avatarUrl,
-        avatar_static: u.avatarUrl,
-        header: u.bannerUrl,
-        header_static: u.bannerUrl,
+        url: u.uri ?? acctUrl,
+        avatar: u.avatarUrl ? u.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
+        avatar_static: u.avatarUrl ? u.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png',
+        header: u.bannerUrl ? u.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
+        header_static: u.bannerUrl ? u.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png',
         emojis: mapEmojis(u.emojis),
         moved: null,
         fields: [],
diff --git a/packages/megalodon/src/misskey/entities/user.ts b/packages/megalodon/src/misskey/entities/user.ts
index 782e04d795..e5e5592701 100644
--- a/packages/megalodon/src/misskey/entities/user.ts
+++ b/packages/megalodon/src/misskey/entities/user.ts
@@ -12,6 +12,7 @@ namespace MisskeyEntity {
     notesCount?: number
     host: string | null
     avatarUrl: string
+    uri?: string
     bannerUrl?: string | null
     avatarColor: string
     emojis: Array<Emoji> | { [key: string]: string }
diff --git a/packages/megalodon/src/misskey/entities/userDetail.ts b/packages/megalodon/src/misskey/entities/userDetail.ts
index 607d9a511e..bf0e3c2c29 100644
--- a/packages/megalodon/src/misskey/entities/userDetail.ts
+++ b/packages/megalodon/src/misskey/entities/userDetail.ts
@@ -16,6 +16,7 @@ namespace MisskeyEntity {
     emojis: Array<Emoji> | { [key: string]: string }
     createdAt: string
     bannerUrl: string
+    uri: string
     bannerColor: string
     isLocked: boolean
     isSilenced: boolean