diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index b275d1b142..91690aec1b 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -388,4 +388,212 @@ export class MfmService { return `

${doc.body.innerHTML}

`; } + + @bindThis + public async toMastoHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) { + if (nodes == null) { + return null; + } + + const { window } = new Window(); + + const doc = window.document; + + async function appendChildren(children: mfm.MfmNode[], targetElement: any): Promise { + if (children) { + for (const child of await Promise.all(children.map(async (x) => await (handlers as any)[x.type](x)))) targetElement.appendChild(child); + } + } + + const handlers: { + [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any; + } = { + async bold(node) { + const el = doc.createElement('span'); + el.textContent = '**'; + await appendChildren(node.children, el); + el.textContent += '**'; + return el; + }, + + async small(node) { + const el = doc.createElement('small'); + await appendChildren(node.children, el); + return el; + }, + + async strike(node) { + const el = doc.createElement('span'); + el.textContent = '~~'; + await appendChildren(node.children, el); + el.textContent += '~~'; + return el; + }, + + async italic(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, + + async fn(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, + + blockCode(node) { + const pre = doc.createElement('pre'); + const inner = doc.createElement('code'); + + const nodes = node.props.code + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); + + for (const x of intersperse('br', nodes)) { + inner.appendChild(x === 'br' ? doc.createElement('br') : x); + } + + pre.appendChild(inner); + return pre; + }, + + async center(node) { + const el = doc.createElement('div'); + await appendChildren(node.children, el); + return el; + }, + + emojiCode(node) { + return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); + }, + + unicodeEmoji(node) { + return doc.createTextNode(node.props.emoji); + }, + + hashtag: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); + a.textContent = `#${node.props.hashtag}`; + a.setAttribute('rel', 'tag'); + a.setAttribute('class', 'hashtag'); + return a; + }, + + inlineCode(node) { + const el = doc.createElement('code'); + el.textContent = node.props.code; + return el; + }, + + mathInline(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, + + mathBlock(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, + + async link(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + await appendChildren(node.children, a); + return a; + }, + + async mention(node) { + const { username, host, acct } = node.props; + const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); + + const el = doc.createElement('span'); + if (!resolved) { + el.textContent = acct; + } else { + el.setAttribute('class', 'h-card'); + el.setAttribute('translate', 'no'); + const a = doc.createElement('a'); + a.setAttribute('href', resolved.url ? resolved.url : resolved.uri); + a.className = 'u-url mention'; + const span = doc.createElement('span'); + span.textContent = resolved.username || username; + a.textContent = '@'; + a.appendChild(span); + el.appendChild(a); + } + + return el; + }, + + async quote(node) { + const el = doc.createElement('blockquote'); + await appendChildren(node.children, el); + return el; + }, + + text(node) { + const el = doc.createElement('span'); + const nodes = node.props.text + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); + + for (const x of intersperse('br', nodes)) { + el.appendChild(x === 'br' ? doc.createElement('br') : x); + } + + return el; + }, + + url(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + a.textContent = node.props.url.replace(/^https?:\/\//, ''); + return a; + }, + + search: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `https"google.com/${node.props.query}`); + a.textContent = node.props.content; + return a; + }, + + async plain(node) { + const el = doc.createElement('span'); + await appendChildren(node.children, el); + return el; + }, + }; + + await appendChildren(nodes, doc.body); + + if (quoteUri !== null) { + const a = doc.createElement('a'); + a.setAttribute('href', quoteUri); + a.textContent = quoteUri.replace(/^https?:\/\//, ''); + + const quote = doc.createElement('span'); + quote.setAttribute('class', 'quote-inline'); + quote.appendChild(doc.createElement('br')); + quote.appendChild(doc.createElement('br')); + quote.innerHTML += 'RE: '; + quote.appendChild(a); + + doc.body.appendChild(quote); + } + + return inline ? doc.body.innerHTML : `

${doc.body.innerHTML}

`; + } } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 6103b0e0f6..ccdee87b13 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -183,7 +183,7 @@ export class SearchService { } } const res = await this.meilisearchNoteIndex!.search(q, { - sort: [`createdAt:${opts.order}`], + sort: [`createdAt:${opts.order ? opts.order : 'desc'}`], matchingStrategy: 'all', attributesToRetrieve: ['id', 'createdAt'], filter: compileQuery(filter), diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index b2f29bcecf..8438b64805 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -142,3 +142,7 @@ export function toArray(x: T | T[] | undefined): T[] { export function toSingle(x: T | T[] | undefined): T | undefined { return Array.isArray(x) ? x[0] : x; } + +export function toSingleLast(x: T | T[] | undefined): T | undefined { + return Array.isArray(x) ? x.at(-1) : x; +} diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index fc6f019602..fc5eece01f 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -23,6 +23,7 @@ import { SigninService } from './api/SigninService.js'; import { SignupApiService } from './api/SignupApiService.js'; import { StreamingApiServerService } from './api/StreamingApiServerService.js'; import { ClientServerService } from './web/ClientServerService.js'; +import { MastoConverters } from './api/mastodon/converters.js'; import { FeedService } from './web/FeedService.js'; import { UrlPreviewService } from './web/UrlPreviewService.js'; import { MainChannelService } from './api/stream/channels/main.js'; @@ -87,6 +88,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; OpenApiServerService, MastodonApiServerService, OAuth2ProviderService, + MastoConverters, ], exports: [ ServerService, 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(/\r?\n/g, "
"); + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`') + .replace(/\r?\n/g, '
'); +@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 { - 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; - 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(`&`, "\'") 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 { + 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[] = []; + + 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 { + 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; + + 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); diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index ebd958834e..3bee6b8ae8 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -64,15 +64,15 @@ "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", "uuid": "^9.0.1", - "ws": "8.14.2" - }, - "devDependencies": { + "ws": "8.14.2", "@types/core-js": "^2.5.6", "@types/form-data": "^2.5.0", "@types/jest": "^29.5.5", "@types/object-assign-deep": "^0.4.1", "@types/parse-link-header": "^2.0.1", - "@types/uuid": "^9.0.4", + "@types/uuid": "^9.0.4" + }, + "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", "eslint": "^8.49.0", diff --git a/packages/megalodon/src/entities/account.ts b/packages/megalodon/src/entities/account.ts index 89c0f17c4b..e2219dd041 100644 --- a/packages/megalodon/src/entities/account.ts +++ b/packages/megalodon/src/entities/account.ts @@ -5,15 +5,16 @@ namespace Entity { export type Account = { id: string + fqn?: string username: string acct: string display_name: string locked: boolean discoverable?: boolean - group: boolean | null - noindex: boolean | null - suspended: boolean | null - limited: boolean | null + group?: boolean | null + noindex?: boolean | null + suspended?: boolean | null + limited?: boolean | null created_at: string followers_count: number following_count: number diff --git a/packages/megalodon/src/entities/field.ts b/packages/megalodon/src/entities/field.ts index 03e4604b02..71ce3c0cfa 100644 --- a/packages/megalodon/src/entities/field.ts +++ b/packages/megalodon/src/entities/field.ts @@ -2,6 +2,6 @@ namespace Entity { export type Field = { name: string value: string - verified_at: string | null + verified_at?: string | null } } diff --git a/packages/megalodon/src/entities/list.ts b/packages/megalodon/src/entities/list.ts index 58c264abab..281f02b110 100644 --- a/packages/megalodon/src/entities/list.ts +++ b/packages/megalodon/src/entities/list.ts @@ -2,7 +2,8 @@ namespace Entity { export type List = { id: string title: string - replies_policy: RepliesPolicy | null + replies_policy?: RepliesPolicy | null + exclusive?: RepliesPolicy | null } export type RepliesPolicy = 'followed' | 'list' | 'none' diff --git a/packages/megalodon/src/entities/status.ts b/packages/megalodon/src/entities/status.ts index 8842981eb3..da36a04717 100644 --- a/packages/megalodon/src/entities/status.ts +++ b/packages/megalodon/src/entities/status.ts @@ -17,7 +17,7 @@ namespace Entity { in_reply_to_account_id: string | null reblog: Status | null content: string - plain_content: string | null + plain_content?: string | null created_at: string emojis: Emoji[] replies_count: number diff --git a/packages/megalodon/src/megalodon.ts b/packages/megalodon/src/megalodon.ts index 19cd5c5551..e2245f7c21 100644 --- a/packages/megalodon/src/megalodon.ts +++ b/packages/megalodon/src/megalodon.ts @@ -1041,7 +1041,7 @@ export interface MegalodonInterface { * * @return Array of lists. */ - getLists(id: string): Promise>> + getLists(id?: string): Promise>> /** * Show a single list. * diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts index de2b6e2e78..7d68d4eddf 100644 --- a/packages/megalodon/src/misskey.ts +++ b/packages/megalodon/src/misskey.ts @@ -238,6 +238,21 @@ export default class Misskey implements MegalodonInterface { description: options.note }) } + if (options.avatar) { + params = Object.assign(params, { + avatarId: options.avatar + }) + } + if (options.header) { + params = Object.assign(params, { + bannerId: options.header + }) + } + if (options.fields_attributes) { + params = Object.assign(params, { + fields: options.fields_attributes + }) + } if (options.locked !== undefined) { params = Object.assign(params, { isLocked: options.locked.toString() === 'true' ? true : false @@ -1148,6 +1163,7 @@ export default class Misskey implements MegalodonInterface { media_ids?: Array | null poll?: { options?: Array; expires_in?: number; multiple?: boolean; hide_totals?: boolean } visibility?: "public" | "unlisted" | "private" | "direct" + in_reply_to_id?: string } ): Promise> { let params = { @@ -1160,6 +1176,11 @@ export default class Misskey implements MegalodonInterface { fileIds: _options.media_ids }) } + if (_options.in_reply_to_id) { + params = Object.assign(params, { + replyId: _options.in_reply_to_id + }) + } if (_options.poll) { let pollParam = { choices: _options.poll.options, @@ -1871,9 +1892,15 @@ export default class Misskey implements MegalodonInterface { /** * POST /api/users/lists/list */ - public async getLists(id: string): Promise>> { + public async getLists(id?: string): Promise>> { + if (id) { + return this.client + .post>('/api/users/lists/list', { userId: id }) + .then(res => ({ ...res, data: res.data.map(l => MisskeyAPI.Converter.list(l)) })) + } + return this.client - .post>('/api/users/lists/list', { userId: id }) + .post>('/api/users/lists/list', {}) .then(res => ({ ...res, data: res.data.map(l => MisskeyAPI.Converter.list(l)) })) } @@ -2017,7 +2044,7 @@ export default class Misskey implements MegalodonInterface { } if (options.exclude_type) { params = Object.assign(params, { - excludeType: options.exclude_type.map(e => MisskeyAPI.Converter.encodeNotificationType(e)) + excludeTypes: options.exclude_type.map(e => MisskeyAPI.Converter.encodeNotificationType(e)) }) } } diff --git a/packages/megalodon/src/misskey/api_client.ts b/packages/megalodon/src/misskey/api_client.ts index c30886f903..520928c9fe 100644 --- a/packages/megalodon/src/misskey/api_client.ts +++ b/packages/megalodon/src/misskey/api_client.ts @@ -78,8 +78,10 @@ namespace MisskeyAPI { acct = `${u.username}@${u.host}`; acctUrl = `https://${u.host}/@${u.username}`; } + const fqn = `${u.username}@${u.host ?? host}`; return { id: u.id, + fqn: fqn, username: u.username, acct: acct, display_name: u.name ? u.name : '', @@ -389,7 +391,7 @@ namespace MisskeyAPI { export const list = (l: Entity.List): MegalodonEntity.List => ({ id: l.id, title: l.name, - replies_policy: null + exclusive: null }) export const encodeNotificationType = ( @@ -465,8 +467,8 @@ namespace MisskeyAPI { export const stats = (s: Entity.Stats): MegalodonEntity.Stats => { return { - user_count: s.usersCount, - status_count: s.notesCount, + user_count: s.originalUsersCount, + status_count: s.originalNotesCount, domain_count: s.instances } } diff --git a/packages/megalodon/src/misskey/entities/field.ts b/packages/megalodon/src/misskey/entities/field.ts index 8bbb2d7c42..1e61178e56 100644 --- a/packages/megalodon/src/misskey/entities/field.ts +++ b/packages/megalodon/src/misskey/entities/field.ts @@ -3,5 +3,6 @@ namespace MisskeyEntity { name: string; value: string; verified?: string; + verified_at?: string; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac9b31ca2e..cd052f6678 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -994,7 +994,7 @@ importers: version: 7.5.0 storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.0)(@storybook/components@7.4.6)(@storybook/core-events@7.5.0)(@storybook/manager-api@7.5.0)(@storybook/preview-api@7.5.0)(@storybook/theming@7.5.0)(@storybook/types@7.5.0)(react-dom@18.2.0)(react@18.2.0) + version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.0)(@storybook/components@7.5.0)(@storybook/core-events@7.5.0)(@storybook/manager-api@7.5.0)(@storybook/preview-api@7.5.0)(@storybook/theming@7.5.0)(@storybook/types@7.5.0)(react-dom@18.2.0)(react@18.2.0) summaly: specifier: github:misskey-dev/summaly version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7 @@ -1016,9 +1016,27 @@ importers: packages/megalodon: dependencies: + '@types/core-js': + specifier: ^2.5.6 + version: 2.5.6 + '@types/form-data': + specifier: ^2.5.0 + version: 2.5.0 + '@types/jest': + specifier: ^29.5.5 + version: 29.5.5 '@types/oauth': specifier: ^0.9.2 version: 0.9.2 + '@types/object-assign-deep': + specifier: ^0.4.1 + version: 0.4.1 + '@types/parse-link-header': + specifier: ^2.0.1 + version: 2.0.1 + '@types/uuid': + specifier: ^9.0.4 + version: 9.0.4 '@types/ws': specifier: ^8.5.5 version: 8.5.5 @@ -1056,24 +1074,6 @@ importers: specifier: 8.14.2 version: 8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3) devDependencies: - '@types/core-js': - specifier: ^2.5.6 - version: 2.5.6 - '@types/form-data': - specifier: ^2.5.0 - version: 2.5.0 - '@types/jest': - specifier: ^29.5.5 - version: 29.5.5 - '@types/object-assign-deep': - specifier: ^0.4.1 - version: 0.4.1 - '@types/parse-link-header': - specifier: ^2.0.1 - version: 2.0.1 - '@types/uuid': - specifier: ^9.0.4 - version: 9.0.4 '@typescript-eslint/eslint-plugin': specifier: ^6.7.2 version: 6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.49.0)(typescript@5.1.6) @@ -1794,7 +1794,6 @@ packages: dependencies: '@babel/highlight': 7.22.13 chalk: 2.4.2 - dev: true /@babel/compat-data@7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} @@ -2049,7 +2048,6 @@ packages: '@babel/helper-validator-identifier': 7.22.15 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true /@babel/parser@7.21.8: resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==} @@ -4162,7 +4160,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.6.3 - dev: true /@jest/expect@29.7.0: resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} @@ -4247,7 +4244,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@sinclair/typebox': 0.27.8 - dev: true /@jest/source-map@29.6.3: resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} @@ -4322,7 +4318,6 @@ packages: '@types/node': 20.8.6 '@types/yargs': 17.0.19 chalk: 4.1.2 - dev: true /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.2.2)(vite@4.4.11): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} @@ -5510,7 +5505,6 @@ packages: /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true /@sindresorhus/is@4.6.0: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -6466,17 +6460,6 @@ packages: - supports-color dev: true - /@storybook/channels@7.4.6: - resolution: {integrity: sha512-yPv/sfo2c18fM3fvG0i1xse63vG8l33Al/OU0k/dtovltPu001/HVa1QgBgsb/QrEfZtvGjGhmtdVeYb39fv3A==} - dependencies: - '@storybook/client-logger': 7.4.6 - '@storybook/core-events': 7.4.6 - '@storybook/global': 5.0.0 - qs: 6.11.1 - telejson: 7.2.0 - tiny-invariant: 1.3.1 - dev: true - /@storybook/channels@7.5.0: resolution: {integrity: sha512-/7QJS1UA7TX3uhZqCpjv4Ib8nfMnDOJrBWvjiXiUONaRcSk/he5X+W1Zz/c7dgt+wkYuAh+evjc7glIaBhVNVQ==} dependencies: @@ -6540,12 +6523,6 @@ packages: - utf-8-validate dev: true - /@storybook/client-logger@7.4.6: - resolution: {integrity: sha512-XDw31ZziU//86PKuMRnmc+L/G0VopaGKENQOGEpvAXCU9IZASwGKlKAtcyosjrpi+ZiUXlMgUXCpXM7x3b1Ehw==} - dependencies: - '@storybook/global': 5.0.0 - dev: true - /@storybook/client-logger@7.5.0: resolution: {integrity: sha512-JV7J9vc69f9Il4uW62NIeweUU7O38VwFWxtCkhd0bcBA/9RG0go4M2avzxYYEAe9kIOX9IBBk8WGzMacwW4gKQ==} dependencies: @@ -6573,29 +6550,6 @@ packages: - supports-color dev: true - /@storybook/components@7.4.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-nIRBhewAgrJJVafyCzuaLx1l+YOfvvD5dOZ0JxZsxJsefOdw1jFpUqUZ5fIpQ2moyvrR0mAUFw378rBfMdHz5Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@storybook/client-logger': 7.4.6 - '@storybook/csf': 0.1.0 - '@storybook/global': 5.0.0 - '@storybook/theming': 7.4.6(react-dom@18.2.0)(react@18.2.0) - '@storybook/types': 7.4.6 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0) - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - dev: true - /@storybook/components@7.5.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-6lmZ6PbS27xN32vTJ/NvgaiKkFIQRzZuBeBIg2u+FoAEgCiCwRXjZKe/O8NZC2Xr0uf97+7U2P0kD4Hwr9SNhw==} peerDependencies: @@ -6657,12 +6611,6 @@ packages: - supports-color dev: true - /@storybook/core-events@7.4.6: - resolution: {integrity: sha512-r5vrE+32lwrJh1NGFr1a0mWjvxo7q8FXYShylcwRWpacmL5NTtLkrXOoJSeGvJ4yKNYkvxQFtOPId4lzDxa32w==} - dependencies: - ts-dedent: 2.2.0 - dev: true - /@storybook/core-events@7.5.0: resolution: {integrity: sha512-FsD+clTzayqprbVllnL8LLch+uCslJFDgsv7Zh99/zoi7OHtHyauoCZkdLBSiDzgc84qS41dY19HqX1/y7cnOw==} dependencies: @@ -6995,20 +6943,6 @@ packages: ts-dedent: 2.2.0 dev: true - /@storybook/theming@7.4.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-HW77iJ9ptCMqhoBOYFjRQw7VBap+38fkJGHP5KylEJCyYCgIAm2dEcQmtWpMVYFssSGcb6djfbtAMhYU4TL4Iw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0) - '@storybook/client-logger': 7.4.6 - '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: true - /@storybook/theming@7.5.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-uTo97oh+pvmlfsZocFq5qae0zGo0VGk7oiBqNSSw6CiTqE1rIuSxoPrMAY+oCTWCUZV7DjONIGvpnGl2QALsAw==} peerDependencies: @@ -7023,15 +6957,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/types@7.4.6: - resolution: {integrity: sha512-6QLXtMVsFZFpzPkdGWsu/iuc8na9dnS67AMOBKm5qCLPwtUJOYkwhMdFRSSeJthLRpzV7JLAL8Kwvl7MFP3QSw==} - dependencies: - '@storybook/channels': 7.4.6 - '@types/babel__core': 7.20.0 - '@types/express': 4.17.17 - file-system-cache: 2.3.0 - dev: true - /@storybook/types@7.5.0: resolution: {integrity: sha512-fiOUnHKFi/UZSfvc53F0WEQCiquqcSqslL3f5EffwQRiXfeXlGavJb0kU03BO+CvOXcliRn6qKSF2dL0Rgb7Xw==} dependencies: @@ -7087,7 +7012,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.3.4 - vue-component-type-helpers: 1.8.19 + vue-component-type-helpers: 1.8.22 transitivePeerDependencies: - encoding - supports-color @@ -7694,7 +7619,7 @@ packages: /@types/core-js@2.5.6: resolution: {integrity: sha512-zLzoC7avO4EYUUYCSzDaahSP1QJEpZQcPxqs91mPeFdh2NS4hQBcnRoEc9RuXfJ8cdN/KXUWukMmZGcKaWeOvw==} - dev: true + dev: false /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} @@ -7786,7 +7711,7 @@ packages: deprecated: This is a stub types definition. form-data provides its own type definitions, so you do not need this installed. dependencies: form-data: 4.0.0 - dev: true + dev: false /@types/glob@7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -7818,19 +7743,16 @@ packages: /@types/istanbul-lib-coverage@2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true /@types/istanbul-lib-report@3.0.0: resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} dependencies: '@types/istanbul-lib-coverage': 2.0.4 - dev: true /@types/istanbul-reports@3.0.1: resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} dependencies: '@types/istanbul-lib-report': 3.0.0 - dev: true /@types/jest@28.1.3: resolution: {integrity: sha512-Tsbjk8Y2hkBaY/gJsataeb4q9Mubw9EOz7+4RjPkzD5KjTvHHs7cpws22InaoXxAVAhF5HfFbzJjo6oKWqSZLw==} @@ -7844,7 +7766,6 @@ packages: dependencies: expect: 29.7.0 pretty-format: 29.7.0 - dev: true /@types/js-levenshtein@1.1.1: resolution: {integrity: sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==} @@ -7978,7 +7899,7 @@ packages: /@types/object-assign-deep@0.4.1: resolution: {integrity: sha512-uWJatOM1JKDdF6Fwa16124b76BtxvTz5Lv+ORGuI7dwqU4iqExXpeHrHOi1c8BU4FgSJ6PdH0skR9Zmz8+MUqQ==} - dev: true + dev: false /@types/offscreencanvas@2019.3.0: resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} @@ -7992,7 +7913,7 @@ packages: /@types/parse-link-header@2.0.1: resolution: {integrity: sha512-BrKNSrRTqn3UkMXvdVtr/znJch0PMBpEvEP8oBkxDx7eEGntuFLI+WpA5HGsNHK4SlqyhaMa+Ks0ViwyixQB5w==} - dev: true + dev: false /@types/pg@8.10.5: resolution: {integrity: sha512-GS3ebGcSJQqKSnq4/WnSH1XQvx0vTDLEmqLENk7onKvTnry9BWPsZiZeUMJlEPw+5bCQDzfxZFhxlUztpNCKgQ==} @@ -8135,7 +8056,6 @@ packages: /@types/stack-utils@2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} - dev: true /@types/throttle-debounce@5.0.0: resolution: {integrity: sha512-Pb7k35iCGFcGPECoNE4DYp3Oyf2xcTd3FbFQxXUI9hEYKUl6YX+KLf7HrBmgVcD05nl50LIH6i+80js4iYmWbw==} @@ -8159,7 +8079,6 @@ packages: /@types/uuid@9.0.4: resolution: {integrity: sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA==} - dev: true /@types/uuid@9.0.5: resolution: {integrity: sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ==} @@ -8202,7 +8121,6 @@ packages: /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - dev: true /@types/yargs@16.0.5: resolution: {integrity: sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==} @@ -8214,7 +8132,6 @@ packages: resolution: {integrity: sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==} dependencies: '@types/yargs-parser': 21.0.0 - dev: true /@types/yauzl@2.10.0: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} @@ -8960,7 +8877,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -8971,7 +8887,6 @@ packages: /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - dev: true /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} @@ -9902,7 +9817,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} @@ -10054,7 +9968,6 @@ packages: /ci-info@3.7.1: resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==} engines: {node: '>=8'} - dev: true /cjs-module-lexer@1.2.2: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} @@ -10172,7 +10085,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -10182,7 +10094,6 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -10967,7 +10878,6 @@ packages: /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true /diff@5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} @@ -11358,12 +11268,10 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} - dev: true /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -11775,7 +11683,6 @@ packages: jest-matcher-utils: 29.7.0 jest-message-util: 29.7.0 jest-util: 29.7.0 - dev: true /exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} @@ -13758,7 +13665,6 @@ packages: diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true /jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} @@ -13807,7 +13713,6 @@ packages: /jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true /jest-haste-map@29.7.0: resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} @@ -13854,7 +13759,6 @@ packages: jest-diff: 29.7.0 jest-get-type: 29.6.3 pretty-format: 29.7.0 - dev: true /jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} @@ -13869,7 +13773,6 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 - dev: true /jest-mock@27.5.1: resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} @@ -14027,7 +13930,6 @@ packages: ci-info: 3.7.1 graceful-fs: 4.2.11 picomatch: 2.3.1 - dev: true /jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} @@ -14136,7 +14038,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} @@ -16601,7 +16502,6 @@ packages: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true /pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} @@ -17090,7 +16990,6 @@ packages: /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} @@ -18227,7 +18126,6 @@ packages: engines: {node: '>=10'} dependencies: escape-string-regexp: 2.0.0 - dev: true /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -19667,8 +19565,8 @@ packages: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} dev: true - /vue-component-type-helpers@1.8.19: - resolution: {integrity: sha512-1OANGSZK4pzHF4uc86usWi+o5Y0zgoDtqWkPg6Am6ot+jHSAmpOah59V/4N82So5xRgivgCxGgK09lBy1XNUfQ==} + /vue-component-type-helpers@1.8.22: + resolution: {integrity: sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==} dev: true /vue-demi@0.13.11(vue@3.3.4): @@ -20210,7 +20108,7 @@ packages: sharp: 0.31.3 dev: false - github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.0)(@storybook/components@7.4.6)(@storybook/core-events@7.5.0)(@storybook/manager-api@7.5.0)(@storybook/preview-api@7.5.0)(@storybook/theming@7.5.0)(@storybook/types@7.5.0)(react-dom@18.2.0)(react@18.2.0): + github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.5.0)(@storybook/components@7.5.0)(@storybook/core-events@7.5.0)(@storybook/manager-api@7.5.0)(@storybook/preview-api@7.5.0)(@storybook/theming@7.5.0)(@storybook/types@7.5.0)(react-dom@18.2.0)(react@18.2.0): resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640 name: storybook-addon-misskey-theme @@ -20232,7 +20130,7 @@ packages: optional: true dependencies: '@storybook/blocks': 7.5.0(react-dom@18.2.0)(react@18.2.0) - '@storybook/components': 7.4.6(react-dom@18.2.0)(react@18.2.0) + '@storybook/components': 7.5.0(react-dom@18.2.0)(react@18.2.0) '@storybook/core-events': 7.5.0 '@storybook/manager-api': 7.5.0(react-dom@18.2.0)(react@18.2.0) '@storybook/preview-api': 7.5.0