Skip to content
This repository has been archived by the owner on Jan 31, 2024. It is now read-only.

Commit

Permalink
upd: add MFM to HTML support and Mentions parsing to mastodon api (#33)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Marie and Insert5StarName authored Sep 30, 2023
1 parent e5d9eb3 commit 54578f6
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 41 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Publish Docker image

on:
push:
branches:
- stable
paths:
- packages/**
- locales/**
release:
types: [published]
workflow_dispatch:
Expand Down
Binary file added packages/backend/assets/transparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/backend/src/core/MfmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/endpoints/ap/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const meta = {
requireCredential: true,

limit: {
duration: ms('1hour'),
duration: ms('1minute'),
max: 30,
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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];
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
95 changes: 95 additions & 0 deletions packages/backend/src/server/api/mastodon/converters.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
39 changes: 22 additions & 17 deletions packages/backend/src/server/api/mastodon/endpoints/status.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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() {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 54578f6

Please sign in to comment.