From a81a00e94dfdf85348ce8f2d843675c93ab9f2f2 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Mon, 24 Mar 2025 11:03:20 -0400 Subject: rename MastodonConverters.ts to matching naming scheme --- .../api/mastodon/MastodonApiServerService.ts | 4 +- .../src/server/api/mastodon/MastodonConverters.ts | 435 +++++++++++++++++++++ .../backend/src/server/api/mastodon/converters.ts | 435 --------------------- .../src/server/api/mastodon/endpoints/account.ts | 4 +- .../src/server/api/mastodon/endpoints/filter.ts | 2 +- .../src/server/api/mastodon/endpoints/instance.ts | 4 +- .../server/api/mastodon/endpoints/notifications.ts | 4 +- .../src/server/api/mastodon/endpoints/search.ts | 4 +- .../src/server/api/mastodon/endpoints/status.ts | 4 +- .../src/server/api/mastodon/endpoints/timeline.ts | 4 +- 10 files changed, 450 insertions(+), 450 deletions(-) create mode 100644 packages/backend/src/server/api/mastodon/MastodonConverters.ts delete mode 100644 packages/backend/src/server/api/mastodon/converters.ts (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index d7afc1254e..b289ad7135 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -21,7 +21,7 @@ import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js'; import { ApiError } from '@/server/api/error.js'; import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js'; -import { convertAnnouncement, convertAttachment, MastoConverters, convertRelationship } from './converters.js'; +import { convertAnnouncement, convertAttachment, MastodonConverters, convertRelationship } from './MastodonConverters.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @@ -31,7 +31,7 @@ export class MastodonApiServerService { @Inject(DI.config) private readonly config: Config, - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly logger: MastodonLogger, private readonly clientService: MastodonClientService, private readonly apiAccountMastodon: ApiAccountMastodon, diff --git a/packages/backend/src/server/api/mastodon/MastodonConverters.ts b/packages/backend/src/server/api/mastodon/MastodonConverters.ts new file mode 100644 index 0000000000..11ddcd23da --- /dev/null +++ b/packages/backend/src/server/api/mastodon/MastodonConverters.ts @@ -0,0 +1,435 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Entity } from 'megalodon'; +import mfm from '@transfem-org/sfm-js'; +import { DI } from '@/di-symbols.js'; +import { MfmService } from '@/core/MfmService.js'; +import type { Config } from '@/config.js'; +import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { IdService } from '@/core/IdService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js'; +import { GetterService } from '@/server/api/GetterService.js'; + +// Missing from Megalodon apparently +// https://docs.joinmastodon.org/entities/StatusEdit/ +export interface StatusEdit { + content: string; + spoiler_text: string; + sensitive: boolean; + created_at: string; + account: MastodonEntity.Account; + poll?: { + options: { + title: string; + }[] + }, + media_attachments: MastodonEntity.Attachment[], + emojis: MastodonEntity.Emoji[], +} + +export const escapeMFM = (text: string): string => text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/`/g, '`') + .replace(/\r?\n/g, '
'); + +@Injectable() +export class MastodonConverters { + constructor( + @Inject(DI.config) + private readonly config: Config, + + @Inject(DI.userProfilesRepository) + private readonly userProfilesRepository: UserProfilesRepository, + + @Inject(DI.noteEditRepository) + private readonly noteEditRepository: NoteEditRepository, + + private readonly mfmService: MfmService, + private readonly getterService: GetterService, + private readonly customEmojiService: CustomEmojiService, + private readonly idService: IdService, + private readonly driveFileEntityService: DriveFileEntityService, + private readonly mastodonDataService: MastodonDataService, + ) {} + + private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention { + let acct = u.username; + let acctUrl = `https://${u.host || this.config.host}/@${u.username}`; + let url: string | null = null; + if (u.host) { + const info = m.find(r => r.username === u.username && r.host === u.host); + acct = `${u.username}@${u.host}`; + acctUrl = `https://${u.host}/@${u.username}`; + if (info) url = info.url ?? info.uri; + } + return { + id: u.id, + username: u.username, + acct: acct, + url: url ?? acctUrl, + }; + } + + public 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: Packed<'DriveFile'>): MastodonEntity.Attachment { + const { width, height } = f.properties; + const size = (width && height) ? `${width}x${height}` : undefined; + const aspect = (width && height) ? (width / height) : undefined; + + 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: { + original: { + width, + height, + size, + aspect, + }, + width, + height, + size, + aspect, + }, + description: f.comment ?? null, + blurhash: f.blurhash ?? null, + }; + } + + public async getUser(id: string): Promise { + return this.getterService.getUser(id).then(p => { + return p; + }); + } + + private encodeField(f: Entity.Field): MastodonEntity.Field { + return { + name: f.name, + value: this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), + verified_at: null, + }; + } + + public async convertAccount(account: Entity.Account | MiUser): Promise { + 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}`; + const acctUri = `https://${this.config.host}/users/${user.id}`; + if (user.host) { + acct = `${user.username}@${user.host}`; + acctUrl = `https://${user.host}/@${user.username}`; + } + 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: profile?.followersVisibility === 'public' ? user.followersCount : 0, + following_count: profile?.followingVisibility === 'public' ? user.followingCount : 0, + statuses_count: user.notesCount, + note: profile?.description ?? '', + url: user.uri ?? acctUrl, + uri: user.uri ?? acctUri, + avatar: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', + avatar_static: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', + header: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', + header_static: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', + emojis: emoji, + moved: null, //FIXME + fields: profile?.fields.map(p => this.encodeField(p)) ?? [], + bot: user.isBot, + discoverable: user.isExplorable, + noindex: user.noindex, + group: null, + suspended: user.isSuspended, + limited: user.isSilenced, + }); + } + + public async getEdits(id: string, me: MiLocalUser | null): Promise { + const note = await this.mastodonDataService.getNote(id, me); + 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: StatusEdit[] = []; + + // TODO this looks wrong, according to mastodon docs + let lastDate = this.idService.parse(note.id).date; + for (const edit of edits) { + const files = await this.driveFileEntityService.packManyByIds(edit.fileIds); + const item = { + account: noteUser, + content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '', + created_at: lastDate.toISOString(), + emojis: [], //FIXME + sensitive: edit.cw != null && edit.cw.length > 0, + spoiler_text: edit.cw ?? '', + media_attachments: files.length > 0 ? files.map((f) => this.encodeFile(f)) : [], + }; + lastDate = edit.updatedAt; + history.push(item); + } + + return history; + } + + private async convertReblog(status: Entity.Status | null, me: MiLocalUser | null): Promise { + if (!status) return null; + return await this.convertStatus(status, me); + } + + public async convertStatus(status: Entity.Status, me: MiLocalUser | null): Promise { + const convertedAccount = this.convertAccount(status.account); + const note = await this.mastodonDataService.requireNote(status.id, me); + const noteUser = await this.getUser(status.account.id); + const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers); + + 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, + }); + }); + + const mentions = Promise.all(note.mentions.map(p => + this.getUser(p) + .then(u => this.encode(u, 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; + }); + + // This must mirror the usual isQuote / isPureRenote logic used elsewhere. + const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId); + + const renote: Promise | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : 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 text = note.text; + const content = text !== null + ? quoteUri + .then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri) ?? escapeMFM(text)) + : ''; + + const reblogged = await this.mastodonDataService.hasReblog(note.id, me); + + // 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 ? this.convertReblog(status.reblog, me) : null, + content: content, + content_type: 'text/x.misskeymarkdown', + text: note.text, + created_at: status.created_at, + edited_at: note.updatedAt?.toISOString() ?? null, + emojis: emoji, + replies_count: note.repliesCount, + reblogs_count: note.renoteCount, + favourites_count: status.favourites_count, + reblogged, + favourited: status.favourited, + muted: status.muted, + sensitive: status.sensitive || !!note.cw, + spoiler_text: note.cw ?? '', + visibility: status.visibility, + media_attachments: status.media_attachments.map(a => convertAttachment(a)), + mentions: mentions, + tags: tags, + card: null, //FIXME + poll: status.poll ?? null, + application: null, //FIXME + language: null, //FIXME + pinned: false, //FIXME + bookmarked: false, //FIXME + quote_id: isQuote ? status.reblog?.id : undefined, + quote: isQuote ? this.convertReblog(status.reblog, me) : null, + reactions: status.emoji_reactions, + }); + } + + public async convertConversation(conversation: Entity.Conversation, me: MiLocalUser | null): Promise { + return { + id: conversation.id, + accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))), + last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null, + unread: conversation.unread, + }; + } + + public async convertNotification(notification: Entity.Notification, me: MiLocalUser | null): Promise { + return { + account: await this.convertAccount(notification.account), + created_at: notification.created_at, + id: notification.id, + status: notification.status ? await this.convertStatus(notification.status, me) : undefined, + type: notification.type, + }; + } + + // public convertEmoji(emoji: string): MastodonEntity.Emoji { + // const reaction: MastodonEntity.Reaction = { + // name: emoji, + // count: 1, + // }; + // + // if (emoji.startsWith(':')) { + // const [, name] = emoji.match(/^:([^@:]+(?:@[^@:]+)?):$/) ?? []; + // if (name) { + // const url = `${this.config.url}/emoji/${name}.webp`; + // reaction.url = url; + // reaction.static_url = url; + // } + // } + // + // return reaction; + // } +} + +function simpleConvert(data: T): T { + // copy the object to bypass weird pass by reference bugs + return Object.assign({}, data); +} + +export function convertAnnouncement(announcement: Entity.Announcement): MastodonEntity.Announcement { + return { + ...announcement, + updated_at: announcement.updated_at ?? announcement.published_at, + }; +} + +export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment { + const { width, height } = attachment.meta?.original ?? attachment.meta ?? {}; + const size = (width && height) ? `${width}x${height}` : undefined; + const aspect = (width && height) ? (width / height) : undefined; + return { + ...attachment, + meta: attachment.meta ? { + ...attachment.meta, + original: { + ...attachment.meta.original, + width, + height, + size, + aspect, + frame_rate: String(attachment.meta.fps), + duration: attachment.meta.duration, + bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined, + }, + width, + height, + size, + aspect, + } : null, + }; +} +export function convertFilter(filter: Entity.Filter): MastodonEntity.Filter { + return simpleConvert(filter); +} +export function convertList(list: Entity.List): MastodonEntity.List { + return { + id: list.id, + title: list.title, + replies_policy: list.replies_policy ?? 'followed', + }; +} +export function convertFeaturedTag(tag: Entity.FeaturedTag): MastodonEntity.FeaturedTag { + return simpleConvert(tag); +} + +export function convertPoll(poll: Entity.Poll): MastodonEntity.Poll { + return simpleConvert(poll); +} + +// Megalodon sometimes returns broken / stubbed relationship data +export function convertRelationship(relationship: Partial & { id: string }): MastodonEntity.Relationship { + return { + id: relationship.id, + following: relationship.following ?? false, + showing_reblogs: relationship.showing_reblogs ?? true, + notifying: relationship.notifying ?? true, + languages: [], + followed_by: relationship.followed_by ?? false, + blocking: relationship.blocking ?? false, + blocked_by: relationship.blocked_by ?? false, + muting: relationship.muting ?? false, + muting_notifications: relationship.muting_notifications ?? false, + requested: relationship.requested ?? false, + requested_by: relationship.requested_by ?? false, + domain_blocking: relationship.domain_blocking ?? false, + endorsed: relationship.endorsed ?? false, + note: relationship.note ?? '', + }; +} + diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts deleted file mode 100644 index 1adbd95642..0000000000 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ /dev/null @@ -1,435 +0,0 @@ -/* - * SPDX-FileCopyrightText: marie and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Entity } from 'megalodon'; -import mfm from '@transfem-org/sfm-js'; -import { DI } from '@/di-symbols.js'; -import { MfmService } from '@/core/MfmService.js'; -import type { Config } from '@/config.js'; -import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; -import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { IdService } from '@/core/IdService.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js'; -import { GetterService } from '@/server/api/GetterService.js'; - -// Missing from Megalodon apparently -// https://docs.joinmastodon.org/entities/StatusEdit/ -export interface StatusEdit { - content: string; - spoiler_text: string; - sensitive: boolean; - created_at: string; - account: MastodonEntity.Account; - poll?: { - options: { - title: string; - }[] - }, - media_attachments: MastodonEntity.Attachment[], - emojis: MastodonEntity.Emoji[], -} - -export const escapeMFM = (text: string): string => text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/`/g, '`') - .replace(/\r?\n/g, '
'); - -@Injectable() -export class MastoConverters { - constructor( - @Inject(DI.config) - private readonly config: Config, - - @Inject(DI.userProfilesRepository) - private readonly userProfilesRepository: UserProfilesRepository, - - @Inject(DI.noteEditRepository) - private readonly noteEditRepository: NoteEditRepository, - - private readonly mfmService: MfmService, - private readonly getterService: GetterService, - private readonly customEmojiService: CustomEmojiService, - private readonly idService: IdService, - private readonly driveFileEntityService: DriveFileEntityService, - private readonly mastodonDataService: MastodonDataService, - ) {} - - private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention { - let acct = u.username; - let acctUrl = `https://${u.host || this.config.host}/@${u.username}`; - let url: string | null = null; - if (u.host) { - const info = m.find(r => r.username === u.username && r.host === u.host); - acct = `${u.username}@${u.host}`; - acctUrl = `https://${u.host}/@${u.username}`; - if (info) url = info.url ?? info.uri; - } - return { - id: u.id, - username: u.username, - acct: acct, - url: url ?? acctUrl, - }; - } - - public 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: Packed<'DriveFile'>): MastodonEntity.Attachment { - const { width, height } = f.properties; - const size = (width && height) ? `${width}x${height}` : undefined; - const aspect = (width && height) ? (width / height) : undefined; - - 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: { - original: { - width, - height, - size, - aspect, - }, - width, - height, - size, - aspect, - }, - description: f.comment ?? null, - blurhash: f.blurhash ?? null, - }; - } - - public async getUser(id: string): Promise { - return this.getterService.getUser(id).then(p => { - return p; - }); - } - - private encodeField(f: Entity.Field): MastodonEntity.Field { - return { - name: f.name, - value: this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), - verified_at: null, - }; - } - - public async convertAccount(account: Entity.Account | MiUser): Promise { - 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}`; - const acctUri = `https://${this.config.host}/users/${user.id}`; - if (user.host) { - acct = `${user.username}@${user.host}`; - acctUrl = `https://${user.host}/@${user.username}`; - } - 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: profile?.followersVisibility === 'public' ? user.followersCount : 0, - following_count: profile?.followingVisibility === 'public' ? user.followingCount : 0, - statuses_count: user.notesCount, - note: profile?.description ?? '', - url: user.uri ?? acctUrl, - uri: user.uri ?? acctUri, - avatar: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', - avatar_static: user.avatarUrl ?? 'https://dev.joinsharkey.org/static-assets/avatar.png', - header: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', - header_static: user.bannerUrl ?? 'https://dev.joinsharkey.org/static-assets/transparent.png', - emojis: emoji, - moved: null, //FIXME - fields: profile?.fields.map(p => this.encodeField(p)) ?? [], - bot: user.isBot, - discoverable: user.isExplorable, - noindex: user.noindex, - group: null, - suspended: user.isSuspended, - limited: user.isSilenced, - }); - } - - public async getEdits(id: string, me: MiLocalUser | null): Promise { - const note = await this.mastodonDataService.getNote(id, me); - 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: StatusEdit[] = []; - - // TODO this looks wrong, according to mastodon docs - let lastDate = this.idService.parse(note.id).date; - for (const edit of edits) { - const files = await this.driveFileEntityService.packManyByIds(edit.fileIds); - const item = { - account: noteUser, - content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '', - created_at: lastDate.toISOString(), - emojis: [], //FIXME - sensitive: edit.cw != null && edit.cw.length > 0, - spoiler_text: edit.cw ?? '', - media_attachments: files.length > 0 ? files.map((f) => this.encodeFile(f)) : [], - }; - lastDate = edit.updatedAt; - history.push(item); - } - - return history; - } - - private async convertReblog(status: Entity.Status | null, me: MiLocalUser | null): Promise { - if (!status) return null; - return await this.convertStatus(status, me); - } - - public async convertStatus(status: Entity.Status, me: MiLocalUser | null): Promise { - const convertedAccount = this.convertAccount(status.account); - const note = await this.mastodonDataService.requireNote(status.id, me); - const noteUser = await this.getUser(status.account.id); - const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers); - - 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, - }); - }); - - const mentions = Promise.all(note.mentions.map(p => - this.getUser(p) - .then(u => this.encode(u, 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; - }); - - // This must mirror the usual isQuote / isPureRenote logic used elsewhere. - const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId); - - const renote: Promise | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : 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 text = note.text; - const content = text !== null - ? quoteUri - .then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri) ?? escapeMFM(text)) - : ''; - - const reblogged = await this.mastodonDataService.hasReblog(note.id, me); - - // 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 ? this.convertReblog(status.reblog, me) : null, - content: content, - content_type: 'text/x.misskeymarkdown', - text: note.text, - created_at: status.created_at, - edited_at: note.updatedAt?.toISOString() ?? null, - emojis: emoji, - replies_count: note.repliesCount, - reblogs_count: note.renoteCount, - favourites_count: status.favourites_count, - reblogged, - favourited: status.favourited, - muted: status.muted, - sensitive: status.sensitive || !!note.cw, - spoiler_text: note.cw ?? '', - visibility: status.visibility, - media_attachments: status.media_attachments.map(a => convertAttachment(a)), - mentions: mentions, - tags: tags, - card: null, //FIXME - poll: status.poll ?? null, - application: null, //FIXME - language: null, //FIXME - pinned: false, //FIXME - bookmarked: false, //FIXME - quote_id: isQuote ? status.reblog?.id : undefined, - quote: isQuote ? this.convertReblog(status.reblog, me) : null, - reactions: status.emoji_reactions, - }); - } - - public async convertConversation(conversation: Entity.Conversation, me: MiLocalUser | null): Promise { - return { - id: conversation.id, - accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))), - last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null, - unread: conversation.unread, - }; - } - - public async convertNotification(notification: Entity.Notification, me: MiLocalUser | null): Promise { - return { - account: await this.convertAccount(notification.account), - created_at: notification.created_at, - id: notification.id, - status: notification.status ? await this.convertStatus(notification.status, me) : undefined, - type: notification.type, - }; - } - - // public convertEmoji(emoji: string): MastodonEntity.Emoji { - // const reaction: MastodonEntity.Reaction = { - // name: emoji, - // count: 1, - // }; - // - // if (emoji.startsWith(':')) { - // const [, name] = emoji.match(/^:([^@:]+(?:@[^@:]+)?):$/) ?? []; - // if (name) { - // const url = `${this.config.url}/emoji/${name}.webp`; - // reaction.url = url; - // reaction.static_url = url; - // } - // } - // - // return reaction; - // } -} - -function simpleConvert(data: T): T { - // copy the object to bypass weird pass by reference bugs - return Object.assign({}, data); -} - -export function convertAnnouncement(announcement: Entity.Announcement): MastodonEntity.Announcement { - return { - ...announcement, - updated_at: announcement.updated_at ?? announcement.published_at, - }; -} - -export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment { - const { width, height } = attachment.meta?.original ?? attachment.meta ?? {}; - const size = (width && height) ? `${width}x${height}` : undefined; - const aspect = (width && height) ? (width / height) : undefined; - return { - ...attachment, - meta: attachment.meta ? { - ...attachment.meta, - original: { - ...attachment.meta.original, - width, - height, - size, - aspect, - frame_rate: String(attachment.meta.fps), - duration: attachment.meta.duration, - bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined, - }, - width, - height, - size, - aspect, - } : null, - }; -} -export function convertFilter(filter: Entity.Filter): MastodonEntity.Filter { - return simpleConvert(filter); -} -export function convertList(list: Entity.List): MastodonEntity.List { - return { - id: list.id, - title: list.title, - replies_policy: list.replies_policy ?? 'followed', - }; -} -export function convertFeaturedTag(tag: Entity.FeaturedTag): MastodonEntity.FeaturedTag { - return simpleConvert(tag); -} - -export function convertPoll(poll: Entity.Poll): MastodonEntity.Poll { - return simpleConvert(poll); -} - -// Megalodon sometimes returns broken / stubbed relationship data -export function convertRelationship(relationship: Partial & { id: string }): MastodonEntity.Relationship { - return { - id: relationship.id, - following: relationship.following ?? false, - showing_reblogs: relationship.showing_reblogs ?? true, - notifying: relationship.notifying ?? true, - languages: [], - followed_by: relationship.followed_by ?? false, - blocking: relationship.blocking ?? false, - blocked_by: relationship.blocked_by ?? false, - muting: relationship.muting ?? false, - muting_notifications: relationship.muting_notifications ?? false, - requested: relationship.requested ?? false, - requested_by: relationship.requested_by ?? false, - domain_blocking: relationship.domain_blocking ?? false, - endorsed: relationship.endorsed ?? false, - note: relationship.note ?? '', - }; -} - diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index f669b71efb..efb26ca53e 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -10,7 +10,7 @@ import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; import type { AccessTokensRepository, UserProfilesRepository } from '@/models/_.js'; import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js'; -import { MastoConverters, convertRelationship, convertFeaturedTag, convertList } from '../converters.js'; +import { MastodonConverters, convertRelationship, convertFeaturedTag, convertList } from '../MastodonConverters.js'; import type multer from 'fastify-multer'; import type { FastifyInstance } from 'fastify'; @@ -30,7 +30,7 @@ export class ApiAccountMastodon { private readonly accessTokensRepository: AccessTokensRepository, private readonly clientService: MastodonClientService, - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly driveService: DriveService, ) {} diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts index d02ddd1999..deac1e9aad 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import { toBoolean } from '@/server/api/mastodon/argsUtils.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; -import { convertFilter } from '../converters.js'; +import { convertFilter } from '../MastodonConverters.js'; import type { FastifyInstance } from 'fastify'; import type multer from 'fastify-multer'; diff --git a/packages/backend/src/server/api/mastodon/endpoints/instance.ts b/packages/backend/src/server/api/mastodon/endpoints/instance.ts index 1f08f0a3b0..d6ee92b466 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/instance.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/instance.ts @@ -9,7 +9,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type { MiMeta, UsersRepository } from '@/models/_.js'; -import { MastoConverters } from '@/server/api/mastodon/converters.js'; +import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { RoleService } from '@/core/RoleService.js'; import type { FastifyInstance } from 'fastify'; @@ -27,7 +27,7 @@ export class ApiInstanceMastodon { @Inject(DI.config) private readonly config: Config, - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly clientService: MastodonClientService, private readonly roleService: RoleService, ) {} diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 120b9ba7f9..c81b3ca236 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -5,7 +5,7 @@ import { Injectable } from '@nestjs/common'; import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/argsUtils.js'; -import { MastoConverters } from '@/server/api/mastodon/converters.js'; +import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js'; import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js'; import { MastodonClientService } from '../MastodonClientService.js'; import type { FastifyInstance } from 'fastify'; @@ -21,7 +21,7 @@ interface ApiNotifyMastodonRoute { @Injectable() export class ApiNotificationsMastodon { constructor( - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly clientService: MastodonClientService, ) {} diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts index 997a585077..7277a35220 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { attachMinMaxPagination, attachOffsetPagination } from '@/server/api/mastodon/pagination.js'; -import { MastoConverters } from '../converters.js'; +import { MastodonConverters } from '../MastodonConverters.js'; import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '../argsUtils.js'; import Account = Entity.Account; import Status = Entity.Status; @@ -23,7 +23,7 @@ interface ApiSearchMastodonRoute { @Injectable() export class ApiSearchMastodon { constructor( - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly clientService: MastodonClientService, ) {} diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index e64df3d74c..ea796e4f0b 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -8,7 +8,7 @@ import { Injectable } from '@nestjs/common'; import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js'; import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/argsUtils.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; -import { convertAttachment, convertPoll, MastoConverters } from '../converters.js'; +import { convertAttachment, convertPoll, MastodonConverters } from '../MastodonConverters.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; @@ -20,7 +20,7 @@ function normalizeQuery(data: Record) { @Injectable() export class ApiStatusMastodon { constructor( - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, private readonly clientService: MastodonClientService, ) {} diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index a333e77c3e..b6162d9eb2 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js'; -import { convertList, MastoConverters } from '../converters.js'; +import { convertList, MastodonConverters } from '../MastodonConverters.js'; import { parseTimelineArgs, TimelineArgs, toBoolean } from '../argsUtils.js'; import type { Entity } from 'megalodon'; import type { FastifyInstance } from 'fastify'; @@ -15,7 +15,7 @@ import type { FastifyInstance } from 'fastify'; export class ApiTimelineMastodon { constructor( private readonly clientService: MastodonClientService, - private readonly mastoConverters: MastoConverters, + private readonly mastoConverters: MastodonConverters, ) {} public register(fastify: FastifyInstance): void { -- cgit v1.2.3-freya