diff options
| author | Marie <marie@kaifa.ch> | 2023-10-31 12:31:48 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-31 12:31:48 +0100 |
| commit | 20ec6300f5f6512be724c72a21475812cb95cdc2 (patch) | |
| tree | 7fd20b05a42a2d56362221c6cfd8fd20a16599f0 /packages/backend/src/server/api | |
| parent | Merge pull request #115 from transfem-org/dependabot/github_actions/actions/s... (diff) | |
| parent | chore: remove console log on verify_credentials (diff) | |
| download | sharkey-20ec6300f5f6512be724c72a21475812cb95cdc2.tar.gz sharkey-20ec6300f5f6512be724c72a21475812cb95cdc2.tar.bz2 sharkey-20ec6300f5f6512be724c72a21475812cb95cdc2.zip | |
merge: mastodon api changes (#120)
Diffstat (limited to 'packages/backend/src/server/api')
8 files changed, 471 insertions, 364 deletions
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index a8c45b98f7..c0e4ea80dc 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -3,16 +3,17 @@ import megalodon, { Entity, MegalodonInterface } from 'megalodon'; import querystring from 'querystring'; import { IsNull } from 'typeorm'; import multer from 'fastify-multer'; -import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; -import { convertId, IdConvertType as IdType, convertAccount, convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList } from './converters.js'; +import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js'; 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'; +import { DriveService } from '@/core/DriveService.js'; export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface { const accessTokenArr = authorization?.split(' ') ?? [null]; @@ -33,10 +34,14 @@ export class MastodonApiServerService { private userProfilesRepository: UserProfilesRepository, @Inject(DI.noteEditRepository) private noteEditRepository: NoteEditRepository, + @Inject(DI.accessTokensRepository) + private accessTokensRepository: AccessTokensRepository, @Inject(DI.config) private config: Config, private metaService: MetaService, private userEntityService: UserEntityService, + private driveService: DriveService, + private mastoConverter: MastoConverters, ) { } @bindThis @@ -101,8 +106,8 @@ export class MastodonApiServerService { }, order: { id: 'ASC' }, }); - const contact = admin == null ? null : convertAccount((await client.getAccount(admin.id)).data); - reply.send(await getInstance(data.data, contact, this.config, await this.metaService.fetch())); + const contact = admin == null ? null : await this.mastoConverter.convertAccount((await client.getAccount(admin.id)).data); + reply.send(await getInstance(data.data, contact as Entity.Account, this.config, await this.metaService.fetch())); } catch (e: any) { /* console.error(e); */ reply.code(401).send(e.response.data); @@ -128,7 +133,7 @@ export class MastodonApiServerService { const client = getClient(BASE_URL, accessTokens); try { const data = await client.dismissInstanceAnnouncement( - convertId(_request.body['id'], IdType.SharkeyId), + _request.body['id'], ); reply.send(data.data); } catch (e: any) { @@ -202,6 +207,25 @@ export class MastodonApiServerService { } }); + fastify.get('/v1/trends/tags', async (_request, reply) => { + const BASE_URL = `${_request.protocol}://${_request.hostname}`; + const accessTokens = _request.headers.authorization; + 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 data = await client.getInstanceTrends(); + reply.send(data.data); + } catch (e: any) { + /* console.error(e); */ + reply.code(401).send(e.response.data); + } + }); + + fastify.get('/v1/trends/links', async (_request, reply) => { + // As we do not have any system for news/links this will just return empty + reply.send([]); + }); + fastify.post('/v1/apps', async (_request, reply) => { const BASE_URL = `${_request.protocol}://${_request.hostname}`; const client = getClient(BASE_URL, ''); // we are using this here, because in private mode some info isnt @@ -236,7 +260,7 @@ 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); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.verifyCredentials()); } catch (e: any) { /* console.error(e); */ @@ -244,16 +268,69 @@ export class MastodonApiServerService { } }); - fastify.patch('/v1/accounts/update_credentials', { preHandler: upload.none() }, async (_request, reply) => { + fastify.patch('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => { const BASE_URL = `${_request.protocol}://${_request.hostname}`; const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt // displayed without being logged in try { + // Check if there is an Header or Avatar being uploaded, if there is proceed to upload it to the drive of the user and then set it. + if (_request.files.length > 0 && accessTokens) { + const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const avatar = (_request.files as any).find((obj: any) => { + return obj.fieldname === 'avatar'; + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const header = (_request.files as any).find((obj: any) => { + return obj.fieldname === 'header'; + }); + + if (tokeninfo && avatar) { + const upload = await this.driveService.addFile({ + user: { id: tokeninfo.userId, host: null }, + path: avatar.path, + name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined, + sensitive: false, + }); + if (upload.type.startsWith('image/')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_request.body as any).avatar = upload.id; + } + } else if (tokeninfo && header) { + const upload = await this.driveService.addFile({ + user: { id: tokeninfo.userId, host: null }, + path: header.path, + name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined, + sensitive: false, + }); + if (upload.type.startsWith('image/')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_request.body as any).header = upload.id; + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((_request.body as any).fields_attributes) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fields = (_request.body as any).fields_attributes.map((field: any) => { + if (!(field.name.trim() === '' && field.value.trim() === '')) { + if (field.name.trim() === '') return reply.code(400).send('Field name can not be empty'); + if (field.value.trim() === '') return reply.code(400).send('Field value can not be empty'); + } + return { + ...field, + }; + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0); + } + const data = await client.updateCredentials(_request.body!); - reply.send(convertAccount(data.data)); + reply.send(await this.mastoConverter.convertAccount(data.data)); } catch (e: any) { - /* console.error(e); */ + //console.error(e); reply.code(401).send(e.response.data); } }); @@ -267,7 +344,7 @@ export class MastodonApiServerService { 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])); + reply.send(await this.mastoConverter.convertAccount(data.data.accounts[0])); } catch (e: any) { /* console.error(e); */ reply.code(401).send(e.response.data); @@ -281,12 +358,12 @@ export class MastodonApiServerService { // displayed without being logged in let users; try { - let ids = _request.query ? (_request.query as any)['id[]'] : null; + let ids = _request.query ? (_request.query as any)['id[]'] ?? (_request.query as any)['id'] : null; if (typeof ids === 'string') { ids = [ids]; } users = ids; - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getRelationships(users)); } catch (e: any) { /* console.error(e); */ @@ -302,11 +379,10 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const sharkId = convertId(_request.params.id, IdType.SharkeyId); + const sharkId = _request.params.id; 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)); + const account = await this.mastoConverter.convertAccount(data.data); + reply.send(account); } catch (e: any) { /* console.error(e); console.error(e.response.data); */ @@ -319,7 +395,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getStatuses()); } catch (e: any) { /* console.error(e); @@ -347,7 +423,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getFollowers()); } catch (e: any) { /* console.error(e); @@ -361,7 +437,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getFollowing()); } catch (e: any) { /* console.error(e); @@ -375,7 +451,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getAccountLists(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.getAccountLists(_request.params.id); reply.send(data.data.map((list) => convertList(list))); } catch (e: any) { /* console.error(e); @@ -389,7 +465,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.addFollow()); } catch (e: any) { /* console.error(e); @@ -403,7 +479,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.rmFollow()); } catch (e: any) { /* console.error(e); @@ -417,7 +493,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.addBlock()); } catch (e: any) { /* console.error(e); @@ -431,7 +507,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.rmBlock()); } catch (e: any) { /* console.error(e); @@ -445,7 +521,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.addMute()); } catch (e: any) { /* console.error(e); @@ -459,7 +535,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.rmMute()); } catch (e: any) { /* console.error(e); @@ -487,7 +563,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getBookmarks()); } catch (e: any) { /* console.error(e); @@ -501,7 +577,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getFavourites()); } catch (e: any) { /* console.error(e); @@ -515,7 +591,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getMutes()); } catch (e: any) { /* console.error(e); @@ -529,7 +605,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.getBlocks()); } catch (e: any) { /* console.error(e); @@ -544,7 +620,7 @@ export class MastodonApiServerService { const client = getClient(BASE_URL, accessTokens); try { const data = await client.getFollowRequests( ((_request.query as any) || { limit: 20 }).limit ); - reply.send(data.data.map((account) => convertAccount(account as Entity.Account))); + reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverter.convertAccount(account as Entity.Account)))); } catch (e: any) { /* console.error(e); console.error(e.response.data); */ @@ -557,7 +633,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.acceptFollow()); } catch (e: any) { /* console.error(e); @@ -571,7 +647,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const account = new ApiAccountMastodon(_request, client, BASE_URL); + const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await account.rejectFollow()); } catch (e: any) { /* console.error(e); @@ -587,7 +663,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const search = new ApiSearchMastodon(_request, client, BASE_URL); + const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await search.SearchV1()); } catch (e: any) { /* console.error(e); @@ -601,7 +677,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const search = new ApiSearchMastodon(_request, client, BASE_URL); + const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await search.SearchV2()); } catch (e: any) { /* console.error(e); @@ -615,7 +691,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const search = new ApiSearchMastodon(_request, client, BASE_URL); + const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await search.getStatusTrends()); } catch (e: any) { /* console.error(e); @@ -629,7 +705,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const search = new ApiSearchMastodon(_request, client, BASE_URL); + const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter); reply.send(await search.getSuggestions()); } catch (e: any) { /* console.error(e); @@ -756,7 +832,7 @@ export class MastodonApiServerService { //#endregion //#region Timelines - const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); + const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.mastoConverter); // GET Endpoints TLEndpoint.getTL(); @@ -781,7 +857,7 @@ export class MastodonApiServerService { //#endregion //#region Status - const NoteEndpoint = new ApiStatusMastodon(fastify, this.config, this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); + const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverter); // GET Endpoints NoteEndpoint.getStatus(); @@ -813,7 +889,7 @@ export class MastodonApiServerService { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.updateMedia(convertId(_request.params.id, IdType.SharkeyId), _request.body!); + const data = await client.updateMedia(_request.params.id, _request.body!); reply.send(convertAttachment(data.data)); } catch (e: any) { /* console.error(e); */ diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index cbd2550f92..0383191399 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -1,16 +1,17 @@ -import type { Config } from '@/config.js'; -import { MfmService } from '@/core/MfmService.js'; -import { DI } from '@/di-symbols.js'; -import { Inject } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Entity } from 'megalodon'; -import { parse } from 'mfm-js'; -import { GetterService } from '../GetterService.js'; +import mfm from 'mfm-js'; +import { DI } from '@/di-symbols.js'; +import { MfmService } from '@/core/MfmService.js'; +import type { Config } from '@/config.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'; - -const CHAR_COLLECTION = '0123456789abcdefghijklmnopqrstuvwxyz'; +import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { GetterService } from '../GetterService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { IdService } from '@/core/IdService.js'; export enum IdConvertType { MastodonId, @@ -18,18 +19,16 @@ export enum IdConvertType { } export const escapeMFM = (text: string): string => text - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/`/g, "`") - .replace(/\r?\n/g, "<br>"); + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`') + .replace(/\r?\n/g, '<br>'); +@Injectable() export class MastoConverters { - private MfmService: MfmService; - private GetterService: GetterService; - constructor( @Inject(DI.config) private config: Config, @@ -37,19 +36,21 @@ export class MastoConverters { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, @Inject(DI.noteEditRepository) private noteEditRepository: NoteEditRepository, - - private userEntityService: UserEntityService + + private mfmService: MfmService, + private getterService: GetterService, + private customEmojiService: CustomEmojiService, + private idService: IdService, + private driveFileEntityService: DriveFileEntityService, ) { - this.MfmService = new MfmService(this.config); - this.GetterService = new GetterService(this.usersRepository, this.notesRepository, this.noteEditRepository, this.userEntityService); } - private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention { + 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; @@ -67,95 +68,223 @@ export class MastoConverters { }; } + public fileType(s: string): 'unknown' | 'image' | 'gifv' | 'video' | 'audio' { + if (s === 'image/gif') { + return 'gifv'; + } + if (s.includes('image')) { + return 'image'; + } + if (s.includes('video')) { + return 'video'; + } + if (s.includes('audio')) { + return 'audio'; + } + return 'unknown'; + } + + public encodeFile(f: any): Entity.Attachment { + return { + id: f.id, + type: this.fileType(f.type), + url: f.url, + remote_url: f.url, + preview_url: f.thumbnailUrl, + text_url: f.url, + meta: { + width: f.properties.width, + height: f.properties.height + }, + description: f.comment ? f.comment : null, + blurhash: f.blurhash ? f.blurhash : null + }; + } + public async getUser(id: string): Promise<MiUser> { - return this.GetterService.getUser(id).then(p => { + 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(/&/g, "&").replaceAll(`<span>&</span><a href="${this.config.url}/tags/39;" rel="tag">#39;</a>`, "<span>\'</span>") as string : status.content; - if (status.poll) status.poll = convertPoll(status.poll); - if (status.reblog) status.reblog = convertStatus(status.reblog); - - return status; + private async encodeField(f: Entity.Field): Promise<Entity.Field> { + return { + name: f.name, + value: await this.mfmService.toMastoHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), + verified_at: null, + }; } -} -export function convertId(in_id: string, id_convert_type: IdConvertType): string { - switch (id_convert_type) { - case IdConvertType.MastodonId: { - let out = BigInt(0); - const lowerCaseId = in_id.toLowerCase(); - for (let i = 0; i < lowerCaseId.length; i++) { - const charValue = numFromChar(lowerCaseId.charAt(i)); - out += BigInt(charValue) * BigInt(36) ** BigInt(i); - } - return out.toString(); - } - - case IdConvertType.SharkeyId: { - let input = BigInt(in_id); - let outStr = ''; - while (input > BigInt(0)) { - const remainder = Number(input % BigInt(36)); - outStr = charFromNum(remainder) + outStr; - input /= BigInt(36); - } - const ReversedoutStr = outStr.split('').reduce((acc, char) => char + acc, ''); - return ReversedoutStr; + public async convertAccount(account: Entity.Account | MiUser) { + const user = await this.getUser(account.id); + const profile = await this.userProfilesRepository.findOneBy({ userId: user.id }); + const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host); + const emoji: Entity.Emoji[] = []; + Object.entries(emojis).forEach(entry => { + const [key, value] = entry; + emoji.push({ + shortcode: key, + static_url: value, + url: value, + visible_in_picker: true, + category: undefined, + }); + }); + const fqn = `${user.username}@${user.host ?? this.config.hostname}`; + let acct = user.username; + let acctUrl = `https://${user.host || this.config.host}/@${user.username}`; + if (user.host) { + acct = `${user.username}@${user.host}`; + acctUrl = `https://${user.host}/@${user.username}`; } - - default: - throw new Error('Invalid ID conversion type'); + return awaitAll({ + id: account.id, + username: user.username, + acct: acct, + fqn: fqn, + display_name: user.name ?? user.username, + locked: user.isLocked, + created_at: this.idService.parse(user.id).date.toISOString(), + followers_count: user.followersCount, + following_count: user.followingCount, + statuses_count: user.notesCount, + note: profile?.description ?? '', + url: user.uri ?? acctUrl, + avatar: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png', + avatar_static: user.avatarUrl ? user.avatarUrl : 'https://dev.joinsharkey.org/static-assets/avatar.png', + header: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png', + header_static: user.bannerUrl ? user.bannerUrl : 'https://dev.joinsharkey.org/static-assets/transparent.png', + emojis: emoji, + moved: null, //FIXME + fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []), + bot: user.isBot, + discoverable: user.isExplorable, + }); } -} -function numFromChar(character: string): number { - for (let i = 0; i < CHAR_COLLECTION.length; i++) { - if (CHAR_COLLECTION.charAt(i) === character) { - return i; + public async getEdits(id: string) { + const note = await this.getterService.getNote(id); + if (!note) { + return {}; + } + + const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p)); + const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } }); + const history: Promise<any>[] = []; + + let lastDate = this.idService.parse(note.id).date; + for (const edit of edits) { + const files = this.driveFileEntityService.packManyByIds(edit.fileIds); + const item = { + account: noteUser, + content: this.mfmService.toMastoHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''), + created_at: lastDate.toISOString(), + emojis: [], + sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false), + spoiler_text: edit.cw ?? '', + poll: null, + media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []) + }; + lastDate = edit.updatedAt; + history.push(awaitAll(item)); } + + return await Promise.all(history); } - throw new Error('Invalid character in parsed base36 id'); -} + private async convertReblog(status: Entity.Status | null): Promise<any> { + if (!status) return null; + return await this.convertStatus(status); + } + + public async convertStatus(status: Entity.Status) { + const convertedAccount = this.convertAccount(status.account); + const note = await this.getterService.getNote(status.id); + const noteUser = await this.getUser(status.account.id); + + const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host); + const emoji: Entity.Emoji[] = []; + Object.entries(emojis).forEach(entry => { + const [key, value] = entry; + emoji.push({ + shortcode: key, + static_url: value, + url: value, + visible_in_picker: true, + category: undefined, + }); + }); -function charFromNum(number: number): string { - if (number >= 0 && number < CHAR_COLLECTION.length) { - return CHAR_COLLECTION.charAt(number); - } else { - throw new Error('Invalid number for base-36 encoding'); + 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<Entity.Mention[]>; + + const tags = note.tags.map(tag => { + return { + name: tag, + url: `${this.config.url}/tags/${tag}`, + } as Entity.Tag; + }); + + const isQuote = note.renoteId && note.text ? true : false; + + const renote = note.renoteId ? this.getterService.getNote(note.renoteId) : null; + + const quoteUri = Promise.resolve(renote).then(renote => { + if (!renote || !isQuote) return null; + return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`; + }); + + const content = note.text !== null + ? quoteUri.then(quoteUri => this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri)) + .then(p => p ?? escapeMFM(note.text!)) + : ''; + + // 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: !isQuote ? await this.convertReblog(status.reblog) : null, + content: content, + content_type: 'text/x.misskeymarkdown', + text: note.text, + created_at: status.created_at, + emojis: emoji, + 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: isQuote ? await this.convertReblog(status.reblog) : null, + edited_at: note.updatedAt?.toISOString(), + }); } } function simpleConvert(data: any) { // copy the object to bypass weird pass by reference bugs const result = Object.assign({}, data); - result.id = convertId(data.id, IdConvertType.MastodonId); return result; } @@ -180,7 +309,6 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) { export function convertNotification(notification: Entity.Notification) { notification.account = convertAccount(notification.account); - notification.id = convertId(notification.id, IdConvertType.MastodonId); if (notification.status) notification.status = convertStatus(notification.status); return notification; } @@ -200,19 +328,9 @@ export function convertRelationship(relationship: Entity.Relationship) { export function convertStatus(status: Entity.Status) { status.account = convertAccount(status.account); - 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), ); - status.mentions = status.mentions.map((mention) => ({ - ...mention, - id: convertId(mention.id, IdConvertType.MastodonId), - })); if (status.poll) status.poll = convertPoll(status.poll); if (status.reblog) status.reblog = convertStatus(status.reblog); @@ -224,7 +342,6 @@ export function convertStatusSource(status: Entity.StatusSource) { } export function convertConversation(conversation: Entity.Conversation) { - conversation.id = convertId(conversation.id, IdConvertType.MastodonId); conversation.accounts = conversation.accounts.map(convertAccount); if (conversation.last_status) { conversation.last_status = convertStatus(conversation.last_status); diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 24ebe0c48b..07d9efb8c1 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -1,7 +1,11 @@ -import { convertId, IdConvertType as IdType, convertAccount, convertRelationship, convertStatus } from '../converters.js'; -import { argsToBools, convertTimelinesArgsId, limitToInt } from './timeline.js'; +import { MastoConverters, convertRelationship } from '../converters.js'; +import { argsToBools, limitToInt } from './timeline.js'; import type { MegalodonInterface } from 'megalodon'; import type { FastifyRequest } from 'fastify'; +import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { Config } from '@/config.js'; +import { Injectable } from '@nestjs/common'; const relationshipModel = { id: '', @@ -20,12 +24,13 @@ const relationshipModel = { note: '', }; +@Injectable() export class ApiAccountMastodon { private request: FastifyRequest; private client: MegalodonInterface; private BASE_URL: string; - constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string) { + constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) { this.request = request; this.client = client; this.BASE_URL = BASE_URL; @@ -34,23 +39,17 @@ export class ApiAccountMastodon { public async verifyCredentials() { try { const data = await this.client.verifyAccountCredentials(); - const acct = data.data; - acct.id = convertId(acct.id, IdType.MastodonId); - acct.display_name = acct.display_name || acct.username; - acct.url = `${this.BASE_URL}/@${acct.url}`; - acct.note = acct.note || ''; - acct.avatar_static = acct.avatar; - acct.header = acct.header || '/static-assets/transparent.png'; - acct.header_static = acct.header || '/static-assets/transparent.png'; - acct.source = { - note: acct.note, - fields: acct.fields, - privacy: '', - sensitive: false, - language: '', - }; - console.log(acct); - return acct; + const acct = await this.mastoconverter.convertAccount(data.data); + const newAcct = Object.assign({}, acct, { + source: { + note: acct.note, + fields: acct.fields, + privacy: '', + sensitive: false, + language: '', + }, + }); + return newAcct; } catch (e: any) { /* console.error(e); console.error(e.response.data); */ @@ -61,7 +60,7 @@ export class ApiAccountMastodon { public async lookup() { try { const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' }); - return convertAccount(data.data.accounts[0]); + return this.mastoconverter.convertAccount(data.data.accounts[0]); } catch (e: any) { /* console.error(e) console.error(e.response.data); */ @@ -79,7 +78,7 @@ export class ApiAccountMastodon { const reqIds = []; for (let i = 0; i < users.length; i++) { - reqIds.push(convertId(users[i], IdType.SharkeyId)); + reqIds.push(users[i]); } const data = await this.client.getRelationships(reqIds); @@ -93,11 +92,8 @@ export class ApiAccountMastodon { public async getStatuses() { try { - const data = await this.client.getAccountStatuses( - convertId((this.request.params as any).id, IdType.SharkeyId), - convertTimelinesArgsId(argsToBools(limitToInt(this.request.query as any))) - ); - return data.data.map((status) => convertStatus(status)); + const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any))); + return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status))); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -108,10 +104,10 @@ export class ApiAccountMastodon { public async getFollowers() { try { const data = await this.client.getAccountFollowers( - convertId((this.request.params as any).id, IdType.SharkeyId), - convertTimelinesArgsId(limitToInt(this.request.query as any)), + (this.request.params as any).id, + limitToInt(this.request.query as any), ); - return data.data.map((account) => convertAccount(account)); + return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account))); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -122,10 +118,10 @@ export class ApiAccountMastodon { public async getFollowing() { try { const data = await this.client.getAccountFollowing( - convertId((this.request.params as any).id, IdType.SharkeyId), - convertTimelinesArgsId(limitToInt(this.request.query as any)), + (this.request.params as any).id, + limitToInt(this.request.query as any), ); - return data.data.map((account) => convertAccount(account)); + return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account))); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -135,7 +131,7 @@ export class ApiAccountMastodon { public async addFollow() { try { - const data = await this.client.followAccount( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.followAccount( (this.request.params as any).id ); const acct = convertRelationship(data.data); acct.following = true; return acct; @@ -148,7 +144,7 @@ export class ApiAccountMastodon { public async rmFollow() { try { - const data = await this.client.unfollowAccount( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.unfollowAccount( (this.request.params as any).id ); const acct = convertRelationship(data.data); acct.following = false; return acct; @@ -161,7 +157,7 @@ export class ApiAccountMastodon { public async addBlock() { try { - const data = await this.client.blockAccount( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.blockAccount( (this.request.params as any).id ); return convertRelationship(data.data); } catch (e: any) { console.error(e); @@ -172,7 +168,7 @@ export class ApiAccountMastodon { public async rmBlock() { try { - const data = await this.client.unblockAccount( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.unblockAccount( (this.request.params as any).id ); return convertRelationship(data.data); } catch (e: any) { console.error(e); @@ -184,7 +180,7 @@ export class ApiAccountMastodon { public async addMute() { try { const data = await this.client.muteAccount( - convertId((this.request.params as any).id, IdType.SharkeyId), + (this.request.params as any).id, this.request.body as any, ); return convertRelationship(data.data); @@ -197,7 +193,7 @@ export class ApiAccountMastodon { public async rmMute() { try { - const data = await this.client.unmuteAccount( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.unmuteAccount( (this.request.params as any).id ); return convertRelationship(data.data); } catch (e: any) { console.error(e); @@ -208,8 +204,8 @@ export class ApiAccountMastodon { public async getBookmarks() { try { - const data = await this.client.getBookmarks( convertTimelinesArgsId(limitToInt(this.request.query as any)) ); - return data.data.map((status) => convertStatus(status)); + const data = await this.client.getBookmarks( limitToInt(this.request.query as any) ); + return data.data.map((status) => this.mastoconverter.convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -219,8 +215,8 @@ export class ApiAccountMastodon { public async getFavourites() { try { - const data = await this.client.getFavourites( convertTimelinesArgsId(limitToInt(this.request.query as any)) ); - return data.data.map((status) => convertStatus(status)); + const data = await this.client.getFavourites( limitToInt(this.request.query as any) ); + return data.data.map((status) => this.mastoconverter.convertStatus(status)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -230,8 +226,8 @@ export class ApiAccountMastodon { public async getMutes() { try { - const data = await this.client.getMutes( convertTimelinesArgsId(limitToInt(this.request.query as any)) ); - return data.data.map((account) => convertAccount(account)); + const data = await this.client.getMutes( limitToInt(this.request.query as any) ); + return data.data.map((account) => this.mastoconverter.convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -241,8 +237,8 @@ export class ApiAccountMastodon { public async getBlocks() { try { - const data = await this.client.getBlocks( convertTimelinesArgsId(limitToInt(this.request.query as any)) ); - return data.data.map((account) => convertAccount(account)); + const data = await this.client.getBlocks( limitToInt(this.request.query as any) ); + return data.data.map((account) => this.mastoconverter.convertAccount(account)); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -252,7 +248,7 @@ export class ApiAccountMastodon { public async acceptFollow() { try { - const data = await this.client.acceptFollowRequest( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.acceptFollowRequest( (this.request.params as any).id ); return convertRelationship(data.data); } catch (e: any) { console.error(e); @@ -263,7 +259,7 @@ export class ApiAccountMastodon { public async rejectFollow() { try { - const data = await this.client.rejectFollowRequest( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.rejectFollowRequest( (this.request.params as any).id ); return convertRelationship(data.data); } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index e27bc956fa..212c79b251 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -1,4 +1,4 @@ -import { IdConvertType as IdType, convertId, convertFilter } from '../converters.js'; +import { convertFilter } from '../converters.js'; import type { MegalodonInterface } from 'megalodon'; import type { FastifyRequest } from 'fastify'; @@ -23,7 +23,7 @@ export class ApiFilterMastodon { public async getFilter() { try { - const data = await this.client.getFilter( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.getFilter( (this.request.params as any).id ); return convertFilter(data.data); } catch (e: any) { console.error(e); @@ -45,7 +45,7 @@ export class ApiFilterMastodon { public async updateFilter() { try { const body: any = this.request.body; - const data = await this.client.updateFilter(convertId((this.request.params as any).id, IdType.SharkeyId), body.pharse, body.context); + const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context); return convertFilter(data.data); } catch (e: any) { console.error(e); @@ -55,7 +55,7 @@ export class ApiFilterMastodon { public async rmFilter() { try { - const data = await this.client.deleteFilter( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.deleteFilter( (this.request.params as any).id ); return data.data; } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index dc801dd053..c4628b58c4 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,5 +1,4 @@ -import { IdConvertType as IdType, convertId, convertNotification } from '../converters.js'; -import { convertTimelinesArgsId } from './timeline.js'; +import { convertNotification } from '../converters.js'; import type { MegalodonInterface, Entity } from 'megalodon'; import type { FastifyRequest } from 'fastify'; @@ -19,7 +18,7 @@ export class ApiNotifyMastodon { public async getNotifications() { try { - const data = await this.client.getNotifications( convertTimelinesArgsId(toLimitToInt(this.request.query)) ); + const data = await this.client.getNotifications( toLimitToInt(this.request.query) ); const notifs = data.data; const processed = notifs.map((n: Entity.Notification) => { const convertedn = convertNotification(n); @@ -39,7 +38,7 @@ export class ApiNotifyMastodon { public async getNotification() { try { - const data = await this.client.getNotification( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.getNotification( (this.request.params as any).id ); const notif = convertNotification(data.data); if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite'; return notif; @@ -51,7 +50,7 @@ export class ApiNotifyMastodon { public async rmNotification() { try { - const data = await this.client.dismissNotification( convertId((this.request.params as any).id, IdType.SharkeyId) ); + const data = await this.client.dismissNotification( (this.request.params as any).id ); return data.data; } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index 5c68402ed8..500129c901 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -1,69 +1,14 @@ -import { Converter } from 'megalodon'; -import { convertAccount, convertStatus } from '../converters.js'; -import { convertTimelinesArgsId, limitToInt } from './timeline.js'; +import { MastoConverters } from '../converters.js'; +import { limitToInt } from './timeline.js'; import type { MegalodonInterface } from 'megalodon'; import type { FastifyRequest } from 'fastify'; -async function getHighlight( - BASE_URL: string, - domain: string, - accessTokens: string | undefined, -) { - const accessTokenArr = accessTokens?.split(' ') ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - try { - const apicall = await fetch(`${BASE_URL}/api/notes/featured`, - { - method: 'POST', - headers: { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ i: accessToken }), - }); - const api = await apicall.json(); - const data: MisskeyEntity.Note[] = api; - return data.map((note) => Converter.note(note, domain)); - } catch (e: any) { - console.log(e); - console.log(e.response.data); - return []; - } -} - -async function getFeaturedUser( BASE_URL: string, host: string, accessTokens: string | undefined, limit: number ) { - const accessTokenArr = accessTokens?.split(' ') ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - try { - const apicall = await fetch(`${BASE_URL}/api/users`, - { - method: 'POST', - headers: { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ i: accessToken, limit, origin: 'local', sort: '+follower', state: 'alive' }), - }); - const api = await apicall.json(); - const data: MisskeyEntity.UserDetail[] = api; - return data.map((u) => { - return { - source: 'past_interactions', - account: Converter.userDetail(u, host), - }; - }); - } catch (e: any) { - console.log(e); - console.log(e.response.data); - return []; - } -} export class ApiSearchMastodon { private request: FastifyRequest; private client: MegalodonInterface; private BASE_URL: string; - constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string) { + constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) { this.request = request; this.client = client; this.BASE_URL = BASE_URL; @@ -71,7 +16,7 @@ export class ApiSearchMastodon { public async SearchV1() { try { - const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any)); + const query: any = limitToInt(this.request.query as any); const type = query.type || ''; const data = await this.client.search(query.q, { type: type, ...query }); return data.data; @@ -83,14 +28,14 @@ export class ApiSearchMastodon { public async SearchV2() { try { - const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any)); + const query: any = limitToInt(this.request.query as any); const type = query.type; const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null; const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null; const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null; const data = { - accounts: acct?.data.accounts.map((account) => convertAccount(account)) ?? [], - statuses: stat?.data.statuses.map((status) => convertStatus(status)) ?? [], + accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []), + statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []), hashtags: tags?.data.hashtags ?? [], }; return data; @@ -102,30 +47,39 @@ export class ApiSearchMastodon { public async getStatusTrends() { try { - const data = await getHighlight( - this.BASE_URL, - this.request.hostname, - this.request.headers.authorization, - ); - return data.map((status) => convertStatus(status)); + const data = await fetch(`${this.BASE_URL}/api/notes/featured`, + { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }) + .then(res => res.json()) + .then(data => data.map((status: any) => this.mastoConverter.convertStatus(status))); + return data; } catch (e: any) { console.error(e); - return e.response.data; + return []; } } public async getSuggestions() { try { - const data = await getFeaturedUser( - this.BASE_URL, - this.request.hostname, - this.request.headers.authorization, - (this.request.query as any).limit || 20, - ); - return data.map((suggestion) => { suggestion.account = convertAccount(suggestion.account); return suggestion; }); + const data = await fetch(`${this.BASE_URL}/api/users`, + { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }), + }).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; }))); + return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; })); } catch (e: any) { console.error(e); - return e.response.data; + return []; } } } diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index 2690a1036f..fe77646af4 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -1,8 +1,8 @@ import querystring from 'querystring'; import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js'; -import { convertId, IdConvertType as IdType, convertAccount, convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js'; +import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js'; import { getClient } from '../MastodonApiServerService.js'; -import { convertTimelinesArgsId, limitToInt } from './timeline.js'; +import { limitToInt } from './timeline.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; import type { Config } from '@/config.js'; @@ -18,9 +18,9 @@ export class ApiStatusMastodon { private fastify: FastifyInstance; private mastoconverter: MastoConverters; - constructor(fastify: FastifyInstance, config: Config, usersrepo: UsersRepository, notesrepo: NotesRepository, noteeditrepo: NoteEditRepository, userentity: UserEntityService) { + constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) { this.fastify = fastify; - this.mastoconverter = new MastoConverters(config, usersrepo, notesrepo, noteeditrepo, userentity); + this.mastoconverter = mastoconverter; } public async getStatus() { @@ -29,7 +29,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.getStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -44,8 +44,8 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatusSource(convertId(_request.params.id, IdType.SharkeyId)); - reply.send(convertStatusSource(data.data)); + const data = await client.getStatusSource(_request.params.id); + reply.send(data.data); } catch (e: any) { console.error(e); reply.code(_request.is404 ? 404 : 401).send(e.response.data); @@ -60,10 +60,7 @@ export class ApiStatusMastodon { const client = getClient(BASE_URL, accessTokens); const query: any = _request.query; try { - const data = await client.getStatusContext( - convertId(_request.params.id, IdType.SharkeyId), - convertTimelinesArgsId(limitToInt(query)), - ); + const data = await client.getStatusContext(_request.params.id, limitToInt(query)); 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); @@ -77,7 +74,8 @@ export class ApiStatusMastodon { public async getHistory() { this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => { try { - reply.send([]); + const edits = await this.mastoconverter.getEdits(_request.params.id); + reply.send(edits); } catch (e: any) { console.error(e); reply.code(401).send(e.response.data); @@ -91,8 +89,8 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatusRebloggedBy(convertId(_request.params.id, IdType.SharkeyId)); - reply.send(data.data.map((account: Entity.Account) => convertAccount(account))); + const data = await client.getStatusRebloggedBy(_request.params.id); + reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account)))); } catch (e: any) { console.error(e); reply.code(401).send(e.response.data); @@ -106,8 +104,8 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getStatusFavouritedBy(convertId(_request.params.id, IdType.SharkeyId)); - reply.send(data.data.map((account: Entity.Account) => convertAccount(account))); + const data = await client.getStatusFavouritedBy(_request.params.id); + reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account)))); } catch (e: any) { console.error(e); reply.code(401).send(e.response.data); @@ -121,7 +119,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getMedia(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.getMedia(_request.params.id); reply.send(convertAttachment(data.data)); } catch (e: any) { console.error(e); @@ -136,7 +134,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getPoll(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.getPoll(_request.params.id); reply.send(convertPoll(data.data)); } catch (e: any) { console.error(e); @@ -152,7 +150,7 @@ export class ApiStatusMastodon { const client = getClient(BASE_URL, accessTokens); const body: any = _request.body; try { - const data = await client.votePoll(convertId(_request.params.id, IdType.SharkeyId), body.choices); + const data = await client.votePoll(_request.params.id, body.choices); reply.send(convertPoll(data.data)); } catch (e: any) { console.error(e); @@ -168,8 +166,6 @@ export class ApiStatusMastodon { const client = getClient(BASE_URL, accessTokens); let body: any = _request.body; try { - if (body.in_reply_to_id) body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.SharkeyId); - if (body.quote_id) body.quote_id = convertId(body.quote_id, IdType.SharkeyId); if ( (!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]']) @@ -201,9 +197,6 @@ export class ApiStatusMastodon { } if (!body.media_ids) body.media_ids = undefined; if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; - if (body.media_ids) { - body.media_ids = (body.media_ids as string[]).map((p) => convertId(p, IdType.SharkeyId)); - } const { sensitive } = body; body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive; @@ -241,10 +234,7 @@ export class ApiStatusMastodon { try { if (!body.media_ids) body.media_ids = undefined; if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; - if (body.media_ids) { - 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); + const data = await client.editStatus(_request.params.id, body); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -259,10 +249,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = (await client.createEmojiReaction( - convertId(_request.params.id, IdType.SharkeyId), - '❤', - )) as any; + const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any; reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -277,10 +264,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteEmojiReaction( - convertId(_request.params.id, IdType.SharkeyId), - '❤', - ); + const data = await client.deleteEmojiReaction(_request.params.id, '❤'); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -295,7 +279,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.reblogStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.reblogStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -310,7 +294,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unreblogStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.unreblogStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -325,7 +309,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.bookmarkStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.bookmarkStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -340,7 +324,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unbookmarkStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.unbookmarkStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -355,7 +339,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.pinStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.pinStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -370,7 +354,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.unpinStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.unpinStatus(_request.params.id); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -385,7 +369,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.createEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name); + const data = await client.createEmojiReaction(_request.params.id, _request.params.name); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -400,7 +384,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name); + const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name); reply.send(await this.mastoconverter.convertStatus(data.data)); } catch (e: any) { console.error(e); @@ -415,7 +399,7 @@ export class ApiStatusMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.deleteStatus(convertId(_request.params.id, IdType.SharkeyId)); + const data = await client.deleteStatus(_request.params.id); reply.send(data.data); } catch (e: any) { console.error(e); diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index e4f510ea2b..f81b63b9ac 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,5 +1,5 @@ import { ParsedUrlQuery } from 'querystring'; -import { convertId, IdConvertType as IdType, convertAccount, convertConversation, convertList, MastoConverters } from '../converters.js'; +import { convertConversation, convertList, MastoConverters } from '../converters.js'; import { getClient } from '../MastodonApiServerService.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; @@ -32,20 +32,11 @@ export function argsToBools(q: ParsedUrlQuery) { return q; } -export function convertTimelinesArgsId(q: ParsedUrlQuery) { - if (typeof q.min_id === 'string') q.min_id = convertId(q.min_id, IdType.SharkeyId); - if (typeof q.max_id === 'string') q.max_id = convertId(q.max_id, IdType.SharkeyId); - if (typeof q.since_id === 'string') q.since_id = convertId(q.since_id, IdType.SharkeyId); - return q; -} - export class ApiTimelineMastodon { private fastify: FastifyInstance; - private mastoconverter: MastoConverters; - constructor(fastify: FastifyInstance, config: Config, usersRepository: UsersRepository, notesRepository: NotesRepository, noteEditRepository: NoteEditRepository, userEntityService: UserEntityService) { + constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) { this.fastify = fastify; - this.mastoconverter = new MastoConverters(config, usersRepository, notesRepository, noteEditRepository, userEntityService); } public async getTL() { @@ -56,8 +47,8 @@ export class ApiTimelineMastodon { try { const query: any = _request.query; const data = query.local === 'true' - ? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query)))) - : await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query)))); + ? await client.getLocalTimeline(argsToBools(limitToInt(query))) + : await client.getPublicTimeline(argsToBools(limitToInt(query))); reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)))); } catch (e: any) { console.error(e); @@ -74,7 +65,7 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); try { const query: any = _request.query; - const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(query))); + const data = await client.getHomeTimeline(limitToInt(query)); reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)))); } catch (e: any) { console.error(e); @@ -92,7 +83,7 @@ export class ApiTimelineMastodon { try { const query: any = _request.query; const params: any = _request.params; - const data = await client.getTagTimeline(params.hashtag, convertTimelinesArgsId(limitToInt(query))); + const data = await client.getTagTimeline(params.hashtag, limitToInt(query)); reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)))); } catch (e: any) { console.error(e); @@ -110,7 +101,7 @@ export class ApiTimelineMastodon { try { const query: any = _request.query; const params: any = _request.params; - const data = await client.getListTimeline(convertId(params.id, IdType.SharkeyId), convertTimelinesArgsId(limitToInt(query))); + const data = await client.getListTimeline(params.id, limitToInt(query)); reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)))); } catch (e: any) { console.error(e); @@ -127,7 +118,7 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); try { const query: any = _request.query; - const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(query))); + const data = await client.getConversationTimeline(limitToInt(query)); reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation))); } catch (e: any) { console.error(e); @@ -144,7 +135,7 @@ export class ApiTimelineMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); const params: any = _request.params; - const data = await client.getList(convertId(params.id, IdType.SharkeyId)); + const data = await client.getList(params.id); reply.send(convertList(data.data)); } catch (e: any) { console.error(e); @@ -160,8 +151,7 @@ export class ApiTimelineMastodon { const BASE_URL = `${_request.protocol}://${_request.hostname}`; const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); - const account = await client.verifyAccountCredentials(); - const data = await client.getLists(account.data.id); + const data = await client.getLists(); reply.send(data.data.map((list: Entity.List) => convertList(list))); } catch (e: any) { console.error(e); @@ -178,11 +168,8 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); const params: any = _request.params; const query: any = _request.query; - const data = await client.getAccountsInList( - convertId(params.id, IdType.SharkeyId), - convertTimelinesArgsId(query), - ); - reply.send(data.data.map((account: Entity.Account) => convertAccount(account))); + const data = await client.getAccountsInList(params.id, query); + reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account))); } catch (e: any) { console.error(e); console.error(e.response.data); @@ -199,10 +186,7 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); const params: any = _request.params; const query: any = _request.query; - const data = await client.addAccountsToList( - convertId(params.id, IdType.SharkeyId), - (query.accounts_id as string[]).map((id) => convertId(id, IdType.SharkeyId)), - ); + const data = await client.addAccountsToList(params.id, query.accounts_id); reply.send(data.data); } catch (e: any) { console.error(e); @@ -220,10 +204,7 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); const params: any = _request.params; const query: any = _request.query; - const data = await client.deleteAccountsFromList( - convertId(params.id, IdType.SharkeyId), - (query.accounts_id as string[]).map((id) => convertId(id, IdType.SharkeyId)), - ); + const data = await client.deleteAccountsFromList(params.id, query.accounts_id); reply.send(data.data); } catch (e: any) { console.error(e); @@ -258,7 +239,7 @@ export class ApiTimelineMastodon { const client = getClient(BASE_URL, accessTokens); const body: any = _request.body; const params: any = _request.params; - const data = await client.updateList(convertId(params.id, IdType.SharkeyId), body.title); + const data = await client.updateList(params.id, body.title); reply.send(convertList(data.data)); } catch (e: any) { console.error(e); @@ -275,8 +256,8 @@ export class ApiTimelineMastodon { const accessTokens = _request.headers.authorization; const client = getClient(BASE_URL, accessTokens); const params: any = _request.params; - const data = await client.deleteList(convertId(params.id, IdType.SharkeyId)); - reply.send(data.data); + const data = await client.deleteList(params.id); + reply.send({}); } catch (e: any) { console.error(e); console.error(e.response.data); |