View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1113 Closes #1074, #1104, and #1105 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
8926ba06a6
16 changed files with 960 additions and 181 deletions
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
|
@ -12954,6 +12954,10 @@ export interface Locale extends ILocale {
|
||||||
* Unable to process quote. This post may be missing context.
|
* Unable to process quote. This post may be missing context.
|
||||||
*/
|
*/
|
||||||
"quoteUnavailable": string;
|
"quoteUnavailable": string;
|
||||||
|
/**
|
||||||
|
* One or more media attachments are unavailable and cannot be shown.
|
||||||
|
*/
|
||||||
|
"attachmentFailed": string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Authorized Fetch
|
* Authorized Fetch
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
"@simplewebauthn/server": "12.0.0",
|
"@simplewebauthn/server": "12.0.0",
|
||||||
"@sinonjs/fake-timers": "11.3.1",
|
"@sinonjs/fake-timers": "11.3.1",
|
||||||
"@smithy/node-http-handler": "2.5.0",
|
"@smithy/node-http-handler": "2.5.0",
|
||||||
"mfm-js": "npm:@transfem-org/sfm-js@0.24.6",
|
"mfm-js": "npm:@transfem-org/sfm-js@0.24.8",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,38 @@ export class MfmService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace iframe with link so we can generate previews.
|
||||||
|
// We shouldn't normally see this, but federated blogging platforms (WordPress, MicroBlog.Pub) can send it.
|
||||||
|
case 'iframe': {
|
||||||
|
const txt: string | undefined = node.attribs.title || node.attribs.alt;
|
||||||
|
const href: string | undefined = node.attribs.src;
|
||||||
|
if (href) {
|
||||||
|
if (href.match(/[\s>]/)) {
|
||||||
|
if (txt) {
|
||||||
|
// href is invalid + has a label => render a pseudo-link
|
||||||
|
text += `${text} (${href})`;
|
||||||
|
} else {
|
||||||
|
// href is invalid + no label => render plain text
|
||||||
|
text += href;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (txt) {
|
||||||
|
// href is valid + has a label => render a link
|
||||||
|
const label = txt
|
||||||
|
.replaceAll('[', '(')
|
||||||
|
.replaceAll(']', ')')
|
||||||
|
.replaceAll(/\r?\n/, ' ')
|
||||||
|
.replaceAll('`', '\'');
|
||||||
|
text += `[${label}](<${href}>)`;
|
||||||
|
} else {
|
||||||
|
// href is valid + no label => render a plain URL
|
||||||
|
text += `<${href}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: // includes inline elements
|
default: // includes inline elements
|
||||||
{
|
{
|
||||||
appendChildren(node.childNodes);
|
appendChildren(node.childNodes);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { load as cheerio } from 'cheerio/slim';
|
||||||
|
import type { IApDocument } from '@/core/activitypub/type.js';
|
||||||
|
import type { CheerioAPI } from 'cheerio/slim';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds HTML elements representing inline media and returns them as simulated AP documents.
|
||||||
|
* Returns an empty array if the input cannot be parsed, or no media was found.
|
||||||
|
* @param html Input HTML to analyze.
|
||||||
|
*/
|
||||||
|
export function extractMediaFromHtml(html: string): IApDocument[] {
|
||||||
|
const $ = parseHtml(html);
|
||||||
|
if (!$) return [];
|
||||||
|
|
||||||
|
const attachments = new Map<string, IApDocument>();
|
||||||
|
|
||||||
|
// <img> tags, including <picture> and <object> fallback elements
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img
|
||||||
|
$('img[src]')
|
||||||
|
.toArray()
|
||||||
|
.forEach(img => attachments.set(img.attribs.src, {
|
||||||
|
type: 'Image',
|
||||||
|
url: img.attribs.src,
|
||||||
|
name: img.attribs.alt || img.attribs.title || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// <object> tags
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/object
|
||||||
|
$('object[data]')
|
||||||
|
.toArray()
|
||||||
|
.forEach(object => attachments.set(object.attribs.data, {
|
||||||
|
type: 'Document',
|
||||||
|
url: object.attribs.data,
|
||||||
|
name: object.attribs.alt || object.attribs.title || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// <embed> tags
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/embed
|
||||||
|
$('embed[src]')
|
||||||
|
.toArray()
|
||||||
|
.forEach(embed => attachments.set(embed.attribs.src, {
|
||||||
|
type: 'Document',
|
||||||
|
url: embed.attribs.src,
|
||||||
|
name: embed.attribs.alt || embed.attribs.title || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// <audio> tags
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/audio
|
||||||
|
$('audio[src]')
|
||||||
|
.toArray()
|
||||||
|
.forEach(audio => attachments.set(audio.attribs.src, {
|
||||||
|
type: 'Audio',
|
||||||
|
url: audio.attribs.src,
|
||||||
|
name: audio.attribs.alt || audio.attribs.title || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// <video> tags
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/video
|
||||||
|
$('video[src]')
|
||||||
|
.toArray()
|
||||||
|
.forEach(audio => attachments.set(audio.attribs.src, {
|
||||||
|
type: 'Video',
|
||||||
|
url: audio.attribs.src,
|
||||||
|
name: audio.attribs.alt || audio.attribs.title || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// TODO support <svg>? We would need to extract it directly from the HTML and save to a temp file.
|
||||||
|
|
||||||
|
return Array.from(attachments.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHtml(html: string): CheerioAPI | null {
|
||||||
|
try {
|
||||||
|
return cheerio(html);
|
||||||
|
} catch {
|
||||||
|
// Don't worry about invalid HTML
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse, inspect, extract } from 'mfm-js';
|
||||||
|
import type { IApDocument } from '@/core/activitypub/type.js';
|
||||||
|
import type { MfmNode, MfmText } from 'mfm-js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds MFM notes representing inline media and returns them as simulated AP documents.
|
||||||
|
* Returns an empty array if the input cannot be parsed, or no media was found.
|
||||||
|
* @param mfm Input MFM to analyze.
|
||||||
|
*/
|
||||||
|
export function extractMediaFromMfm(mfm: string): IApDocument[] {
|
||||||
|
const nodes = parseMfm(mfm);
|
||||||
|
if (nodes == null) return [];
|
||||||
|
|
||||||
|
const attachments = new Map<string, IApDocument>();
|
||||||
|
|
||||||
|
inspect(nodes, node => {
|
||||||
|
if (node.type === 'link' && node.props.image) {
|
||||||
|
const alt: string[] = [];
|
||||||
|
|
||||||
|
inspect(node.children, node => {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'text':
|
||||||
|
alt.push(node.props.text);
|
||||||
|
break;
|
||||||
|
case 'unicodeEmoji':
|
||||||
|
alt.push(node.props.emoji);
|
||||||
|
break;
|
||||||
|
case 'emojiCode':
|
||||||
|
alt.push(':');
|
||||||
|
alt.push(node.props.name);
|
||||||
|
alt.push(':');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attachments.set(node.props.url, {
|
||||||
|
type: 'Image',
|
||||||
|
url: node.props.url,
|
||||||
|
name: alt.length > 0
|
||||||
|
? alt.join('')
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(attachments.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMfm(mfm: string): MfmNode[] | null {
|
||||||
|
try {
|
||||||
|
return parse(mfm);
|
||||||
|
} catch {
|
||||||
|
// Don't worry about invalid MFM
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IPost } from '@/core/activitypub/type.js';
|
||||||
|
import { toArray } from '@/misc/prelude/array.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets content of a specified media type from a provided object.
|
||||||
|
*
|
||||||
|
* Optionally supports a "permissive" mode which enables the following changes:
|
||||||
|
* 1. MIME types are checked in a case-insensitive manner.
|
||||||
|
* 2. MIME types are matched based on inclusion, not strict equality.
|
||||||
|
* 3. A candidate content is considered to match if it has no specified MIME type.
|
||||||
|
*
|
||||||
|
* Note: this method is written defensively to protect against malform remote objects.
|
||||||
|
* When extending or modifying it, please be sure to work with "unknown" type and validate everything.
|
||||||
|
*
|
||||||
|
* Note: the logic in this method is carefully ordered to match the selection priority of existing code in ApNoteService.
|
||||||
|
* Please do not re-arrange it without testing!
|
||||||
|
* New checks can be added to the end of the method to safely extend the existing logic.
|
||||||
|
*
|
||||||
|
* @param object AP object to extract content from.
|
||||||
|
* @param mimeType MIME type to look for.
|
||||||
|
* @param permissive Enables permissive mode, as described above. Defaults to false (disabled).
|
||||||
|
*/
|
||||||
|
export function getContentByType(object: IPost | Record<string, unknown>, mimeType: string, permissive = false): string | null {
|
||||||
|
// Case 1: Extended "source" property
|
||||||
|
if (object.source && typeof(object.source) === 'object') {
|
||||||
|
// "source" is permitted to be an array, though no implementations are known to do this yet.
|
||||||
|
const sources = toArray(object.source) as Record<string, unknown>[];
|
||||||
|
for (const source of sources) {
|
||||||
|
if (typeof (source.content) === 'string' && checkMediaType(source.mediaType)) {
|
||||||
|
return source.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Special case for MFM
|
||||||
|
if (typeof(object._misskey_content) === 'string' && mimeType === 'text/x.misskeymarkdown') {
|
||||||
|
return object._misskey_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: AP native "content" property
|
||||||
|
if (typeof(object.content) === 'string' && checkMediaType(object.mediaType)) {
|
||||||
|
return object.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Checks if the provided media type matches the input parameters.
|
||||||
|
function checkMediaType(mediaType: unknown): boolean {
|
||||||
|
if (typeof(mediaType) === 'string') {
|
||||||
|
// Strict match
|
||||||
|
if (mediaType === mimeType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissive match
|
||||||
|
if (permissive && mediaType.toLowerCase().includes(mimeType.toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissive fallback match
|
||||||
|
if (permissive && mediaType == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ export class ApImageService {
|
||||||
uri: image.url,
|
uri: image.url,
|
||||||
sensitive: !!(image.sensitive),
|
sensitive: !!(image.sensitive),
|
||||||
isLink: !shouldBeCached,
|
isLink: !shouldBeCached,
|
||||||
comment: truncate(image.name ?? undefined, this.config.maxRemoteAltTextLength),
|
comment: truncate(image.summary || image.name || undefined, this.config.maxRemoteAltTextLength),
|
||||||
});
|
});
|
||||||
if (!file.isLink || file.url === image.url) return file;
|
if (!file.isLink || file.url === image.url) return file;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { UnrecoverableError } from 'bullmq';
|
import { UnrecoverableError } from 'bullmq';
|
||||||
|
import promiseLimit from 'promise-limit';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
|
@ -27,6 +28,9 @@ import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||||
|
import { extractMediaFromHtml } from '@/core/activitypub/misc/extract-media-from-html.js';
|
||||||
|
import { extractMediaFromMfm } from '@/core/activitypub/misc/extract-media-from-mfm.js';
|
||||||
|
import { getContentByType } from '@/core/activitypub/misc/get-content-by-type.js';
|
||||||
import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument, isLink } from '../type.js';
|
import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument, isLink } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
|
|
@ -206,12 +210,10 @@ export class ApNoteService {
|
||||||
const cw = note.summary === '' ? null : note.summary;
|
const cw = note.summary === '' ? null : note.summary;
|
||||||
|
|
||||||
// テキストのパース
|
// テキストのパース
|
||||||
let text: string | null = null;
|
let text =
|
||||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
getContentByType(note, 'text/x.misskeymarkdown') ??
|
||||||
text = note.source.content;
|
getContentByType(note, 'text/markdown');
|
||||||
} else if (typeof note._misskey_content !== 'undefined') {
|
if (text == null && typeof note.content === 'string') {
|
||||||
text = note._misskey_content;
|
|
||||||
} else if (typeof note.content === 'string') {
|
|
||||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,21 +250,14 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processErrors: string[] = [];
|
||||||
|
|
||||||
// 添付ファイル
|
// 添付ファイル
|
||||||
const files: MiDriveFile[] = [];
|
// Note: implementation moved to getAttachment function to avoid duplication.
|
||||||
|
// Please copy any upstream changes to that method! (It's in the bottom of this class)
|
||||||
for (const attach of toArray(note.attachment)) {
|
const { files, hasFileError } = await this.getAttachments(note, actor);
|
||||||
attach.sensitive ??= note.sensitive;
|
if (hasFileError) {
|
||||||
const file = await this.apImageService.resolveImage(actor, attach);
|
processErrors.push('attachmentFailed');
|
||||||
if (file) files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some software (Peertube) attaches a thumbnail under "icon" instead of "attachment"
|
|
||||||
const icon = getBestIcon(note);
|
|
||||||
if (icon) {
|
|
||||||
icon.sensitive ??= note.sensitive;
|
|
||||||
const file = await this.apImageService.resolveImage(actor, icon);
|
|
||||||
if (file) files.push(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// リプライ
|
// リプライ
|
||||||
|
|
@ -284,7 +279,9 @@ export class ApNoteService {
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
const quote = await this.getQuote(note, entryUri, resolver);
|
const quote = await this.getQuote(note, entryUri, resolver);
|
||||||
const processErrors = quote === null ? ['quoteUnavailable'] : null;
|
if (quote === null) {
|
||||||
|
processErrors.push('quoteUnavailable');
|
||||||
|
}
|
||||||
|
|
||||||
if (reply && reply.userHost == null && reply.localOnly) {
|
if (reply && reply.userHost == null && reply.localOnly) {
|
||||||
throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot reply to local-only note');
|
throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot reply to local-only note');
|
||||||
|
|
@ -328,7 +325,7 @@ export class ApNoteService {
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote ?? null,
|
renote: quote ?? null,
|
||||||
processErrors,
|
processErrors: processErrors.length > 0 ? processErrors : null,
|
||||||
name: note.name,
|
name: note.name,
|
||||||
cw,
|
cw,
|
||||||
text,
|
text,
|
||||||
|
|
@ -412,12 +409,10 @@ export class ApNoteService {
|
||||||
const cw = note.summary === '' ? null : note.summary;
|
const cw = note.summary === '' ? null : note.summary;
|
||||||
|
|
||||||
// テキストのパース
|
// テキストのパース
|
||||||
let text: string | null = null;
|
let text =
|
||||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
getContentByType(note, 'text/x.misskeymarkdown') ??
|
||||||
text = note.source.content;
|
getContentByType(note, 'text/markdown');
|
||||||
} else if (typeof note._misskey_content !== 'undefined') {
|
if (text == null && typeof note.content === 'string') {
|
||||||
text = note._misskey_content;
|
|
||||||
} else if (typeof note.content === 'string') {
|
|
||||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,21 +441,12 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processErrors: string[] = [];
|
||||||
|
|
||||||
// 添付ファイル
|
// 添付ファイル
|
||||||
const files: MiDriveFile[] = [];
|
const { files, hasFileError } = await this.getAttachments(note, actor);
|
||||||
|
if (hasFileError) {
|
||||||
for (const attach of toArray(note.attachment)) {
|
processErrors.push('attachmentFailed');
|
||||||
attach.sensitive ??= note.sensitive;
|
|
||||||
const file = await this.apImageService.resolveImage(actor, attach);
|
|
||||||
if (file) files.push(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some software (Peertube) attaches a thumbnail under "icon" instead of "attachment"
|
|
||||||
const icon = getBestIcon(note);
|
|
||||||
if (icon) {
|
|
||||||
icon.sensitive ??= note.sensitive;
|
|
||||||
const file = await this.apImageService.resolveImage(actor, icon);
|
|
||||||
if (file) files.push(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// リプライ
|
// リプライ
|
||||||
|
|
@ -482,7 +468,9 @@ export class ApNoteService {
|
||||||
|
|
||||||
// 引用
|
// 引用
|
||||||
const quote = await this.getQuote(note, entryUri, resolver);
|
const quote = await this.getQuote(note, entryUri, resolver);
|
||||||
const processErrors = quote === null ? ['quoteUnavailable'] : null;
|
if (quote === null) {
|
||||||
|
processErrors.push('quoteUnavailable');
|
||||||
|
}
|
||||||
|
|
||||||
if (quote && quote.userHost == null && quote.localOnly) {
|
if (quote && quote.userHost == null && quote.localOnly) {
|
||||||
throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot quote a local-only note');
|
throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot quote a local-only note');
|
||||||
|
|
@ -523,7 +511,7 @@ export class ApNoteService {
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote ?? null,
|
renote: quote ?? null,
|
||||||
processErrors,
|
processErrors: processErrors.length > 0 ? processErrors : null,
|
||||||
name: note.name,
|
name: note.name,
|
||||||
cw,
|
cw,
|
||||||
text,
|
text,
|
||||||
|
|
@ -722,10 +710,95 @@ export class ApNoteService {
|
||||||
// Permanent error - return null
|
// Permanent error - return null
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and saves all media attachments from the provided note.
|
||||||
|
* Returns an array of all the created files.
|
||||||
|
*/
|
||||||
|
private async getAttachments(note: IPost, actor: MiRemoteUser): Promise<{ files: MiDriveFile[], hasFileError: boolean }> {
|
||||||
|
const attachments = new Map<string, IApDocument & { url: string }>();
|
||||||
|
|
||||||
|
// Extract inline media from HTML content.
|
||||||
|
// Don't use source.content, _misskey_content, or anything else because those aren't HTML.
|
||||||
|
const htmlContent = getContentByType(note, 'text/html', true);
|
||||||
|
if (htmlContent) {
|
||||||
|
for (const attach of extractMediaFromHtml(htmlContent)) {
|
||||||
|
if (hasUrl(attach)) {
|
||||||
|
attachments.set(attach.url, attach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract inline media from MFM / markdown content.
|
||||||
|
const mfmContent =
|
||||||
|
getContentByType(note, 'text/x.misskeymarkdown') ??
|
||||||
|
getContentByType(note, 'text/markdown');
|
||||||
|
if (mfmContent) {
|
||||||
|
for (const attach of extractMediaFromMfm(mfmContent)) {
|
||||||
|
if (hasUrl(attach)) {
|
||||||
|
attachments.set(attach.url, attach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some software (Peertube) attaches a thumbnail under "icon" instead of "attachment"
|
||||||
|
const icon = getBestIcon(note);
|
||||||
|
if (icon) {
|
||||||
|
if (hasUrl(icon)) {
|
||||||
|
attachments.set(icon.url, icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate AP attachments last, to overwrite any "fallback" elements that may have been inlined in HTML.
|
||||||
|
// AP attachments should be considered canonical.
|
||||||
|
for (const attach of toArray(note.attachment)) {
|
||||||
|
if (hasUrl(attach)) {
|
||||||
|
attachments.set(attach.url, attach);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve all files w/ concurrency 2.
|
||||||
|
// This prevents one big file from blocking the others.
|
||||||
|
const limiter = promiseLimit<MiDriveFile | null>(2);
|
||||||
|
const results = await Promise
|
||||||
|
.all(Array
|
||||||
|
.from(attachments.values())
|
||||||
|
.map(attach => limiter(async () => {
|
||||||
|
attach.sensitive ??= note.sensitive;
|
||||||
|
return await this.resolveImage(actor, attach);
|
||||||
|
})));
|
||||||
|
|
||||||
|
// Process results
|
||||||
|
let hasFileError = false;
|
||||||
|
const files: MiDriveFile[] = [];
|
||||||
|
for (const result of results) {
|
||||||
|
if (result != null) {
|
||||||
|
files.push(result);
|
||||||
|
} else {
|
||||||
|
hasFileError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { files, hasFileError };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveImage(actor: MiRemoteUser, attachment: IApDocument & { url: string }): Promise<MiDriveFile | null> {
|
||||||
|
try {
|
||||||
|
return await this.apImageService.resolveImage(actor, attachment);
|
||||||
|
} catch (err) {
|
||||||
|
if (isRetryableError(err)) {
|
||||||
|
this.logger.warn(`Temporary failure to resolve attachment at ${attachment.url}: ${renderInlineError(err)}`);
|
||||||
|
throw err;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Permanent failure to resolve attachment at ${attachment.url}: ${renderInlineError(err)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBestIcon(note: IObject): IObject | null {
|
function getBestIcon(note: IObject): IApDocument | null {
|
||||||
const icons: IObject[] = toArray(note.icon);
|
const icons: IApDocument[] = toArray(note.icon);
|
||||||
if (icons.length < 2) {
|
if (icons.length < 2) {
|
||||||
return icons[0] ?? null;
|
return icons[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -741,3 +814,8 @@ function getBestIcon(note: IObject): IObject | null {
|
||||||
return best;
|
return best;
|
||||||
}, null as IApDocument | null) ?? null;
|
}, null as IApDocument | null) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Need this to make TypeScript happy...
|
||||||
|
function hasUrl<T extends IObject>(object: T): object is T & { url: string } {
|
||||||
|
return typeof(object.url) === 'string';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export interface IObject {
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
attributedTo?: ApObject;
|
attributedTo?: ApObject;
|
||||||
attachment?: any[];
|
attachment?: IApDocument[];
|
||||||
inReplyTo?: any;
|
inReplyTo?: any;
|
||||||
replies?: ICollection | IOrderedCollection | string;
|
replies?: ICollection | IOrderedCollection | string;
|
||||||
content?: string | null;
|
content?: string | null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extractMediaFromHtml } from '@/core/activitypub/misc/extract-media-from-html.js';
|
||||||
|
|
||||||
|
describe(extractMediaFromHtml, () => {
|
||||||
|
it('should return empty for invalid input', () => {
|
||||||
|
const result = extractMediaFromHtml('<broken html');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty for empty input', () => {
|
||||||
|
const result = extractMediaFromHtml('');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty for input without attachments', () => {
|
||||||
|
const result = extractMediaFromHtml('<div>No media here!</div>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract img tags', () => {
|
||||||
|
const result = extractMediaFromHtml('<img src="https://example.com/img.png" alt=""/>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/img.png',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore img tags without src', () => {
|
||||||
|
const result = extractMediaFromHtml('<img alt=""/>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract picture tags with img', () => {
|
||||||
|
const result = extractMediaFromHtml('<picture><img src="https://example.com/picture.png" alt=""/></picture>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/picture.png',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore picture tags without img', () => {
|
||||||
|
const result = extractMediaFromHtml('<picture><source src="https://example.com/picture.png"/></picture>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore picture tags without src', () => {
|
||||||
|
const result = extractMediaFromHtml('<picture><source/><img alt=""/></picture>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract object tags', () => {
|
||||||
|
const result = extractMediaFromHtml('<object data="https://example.com/object.dat"></object>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/object.dat',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore object tags without data', () => {
|
||||||
|
const result = extractMediaFromHtml('<object></object>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract object tags with img fallback', () => {
|
||||||
|
const result = extractMediaFromHtml('<object><img src="https://example.com/object.png" alt=""/></object>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/object.png',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore object tags with empty img fallback', () => {
|
||||||
|
const result = extractMediaFromHtml('<object><img alt=""/></object>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract embed tags', () => {
|
||||||
|
const result = extractMediaFromHtml('<embed src="https://example.com/embed.dat"/>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/embed.dat',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore embed tags without src', () => {
|
||||||
|
const result = extractMediaFromHtml('<embed/>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract audio tags', () => {
|
||||||
|
const result = extractMediaFromHtml('<audio src="https://example.com/audio.mp3"></audio>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Audio',
|
||||||
|
url: 'https://example.com/audio.mp3',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore audio tags without src', () => {
|
||||||
|
const result = extractMediaFromHtml('<audio></audio>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract video tags', () => {
|
||||||
|
const result = extractMediaFromHtml('<video src="https://example.com/video.mp4"></video>');
|
||||||
|
expect(result).toEqual([{
|
||||||
|
type: 'Video',
|
||||||
|
url: 'https://example.com/video.mp4',
|
||||||
|
name: null,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore video tags without src', () => {
|
||||||
|
const result = extractMediaFromHtml('<video></video>');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract alt text from alt property', () => {
|
||||||
|
const result = extractMediaFromHtml(`
|
||||||
|
<img src="https://example.com/img.png" alt="img tag" title="wrong"/>
|
||||||
|
<picture><img src="https://example.com/picture.png" alt="picture tag" title="wrong"/></picture>
|
||||||
|
<object data="https://example.com/object-1.dat" alt="object tag" title="wrong"></object>
|
||||||
|
<object><img src="https://example.com/object-2.png" alt="object tag" title="wrong"/></object>
|
||||||
|
<embed src="https://example.com/embed.dat" alt="embed tag" title="wrong"/>
|
||||||
|
<audio src="https://example.com/audio.mp3" alt="audio tag" title="wrong"/>
|
||||||
|
<video src="https://example.com/video.mp4" alt="video tag" title="wrong"/>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/img.png',
|
||||||
|
name: 'img tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/picture.png',
|
||||||
|
name: 'picture tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/object-2.png',
|
||||||
|
name: 'object tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/object-1.dat',
|
||||||
|
name: 'object tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/embed.dat',
|
||||||
|
name: 'embed tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Audio',
|
||||||
|
url: 'https://example.com/audio.mp3',
|
||||||
|
name: 'audio tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Video',
|
||||||
|
url: 'https://example.com/video.mp4',
|
||||||
|
name: 'video tag',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract alt text from title property', () => {
|
||||||
|
const result = extractMediaFromHtml(`
|
||||||
|
<img src="https://example.com/img.png" title="img tag"/>
|
||||||
|
<picture><img src="https://example.com/picture.png" title="picture tag"/></picture>
|
||||||
|
<object data="https://example.com/object-1.dat" title="object tag"></object>
|
||||||
|
<object><img src="https://example.com/object-2.png" title="object tag"/></object>
|
||||||
|
<embed src="https://example.com/embed.dat" title="embed tag"/>
|
||||||
|
<audio src="https://example.com/audio.mp3" title="audio tag"/>
|
||||||
|
<video src="https://example.com/video.mp4" title="video tag"/>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/img.png',
|
||||||
|
name: 'img tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/picture.png',
|
||||||
|
name: 'picture tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/object-2.png',
|
||||||
|
name: 'object tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/object-1.dat',
|
||||||
|
name: 'object tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/embed.dat',
|
||||||
|
name: 'embed tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Audio',
|
||||||
|
url: 'https://example.com/audio.mp3',
|
||||||
|
name: 'audio tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Video',
|
||||||
|
url: 'https://example.com/video.mp4',
|
||||||
|
name: 'video tag',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore missing alt text', () => {
|
||||||
|
const result = extractMediaFromHtml(`
|
||||||
|
<img src="https://example.com/img.png"/>
|
||||||
|
<picture><img src="https://example.com/picture.png"/></picture>
|
||||||
|
<object data="https://example.com/object-1.dat"></object>
|
||||||
|
<object><img src="https://example.com/object-2.png"/></object>
|
||||||
|
<embed src="https://example.com/embed.dat"/>
|
||||||
|
<audio src="https://example.com/audio.mp3"/>
|
||||||
|
<video src="https://example.com/video.mp4"/>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/img.png',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/picture.png',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/object-2.png',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/object-1.dat',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/embed.dat',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Audio',
|
||||||
|
url: 'https://example.com/audio.mp3',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Video',
|
||||||
|
url: 'https://example.com/video.mp4',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should de-duplicate attachments', () => {
|
||||||
|
const result = extractMediaFromHtml(`
|
||||||
|
<img src="https://example.com/1.png" alt="img 1"/>
|
||||||
|
<img src="https://example.com/2.png" alt="img 2"/>
|
||||||
|
<embed src="https://example.com/1.png" alt="embed 1"/>
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Document',
|
||||||
|
url: 'https://example.com/1.png',
|
||||||
|
name: 'embed 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/2.png',
|
||||||
|
name: 'img 2',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extractMediaFromMfm } from '@/core/activitypub/misc/extract-media-from-mfm.js';
|
||||||
|
|
||||||
|
describe(extractMediaFromMfm, () => {
|
||||||
|
it('should return empty for empty input', () => {
|
||||||
|
const result = extractMediaFromMfm('');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty for invalid input', () => {
|
||||||
|
const result = extractMediaFromMfm('*broken markdown\0');
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract all image links', () => {
|
||||||
|
const result = extractMediaFromMfm(`
|
||||||
|

|
||||||
|

|
||||||
|
****
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/images/1.png',
|
||||||
|
name: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/images/2.png',
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/images/3.png',
|
||||||
|
name: '3',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore regular links', () => {
|
||||||
|
const result = extractMediaFromMfm(`
|
||||||
|
[1](https://example.com/images/1.png)
|
||||||
|
[](https://example.com/images/2.png)
|
||||||
|
**[3](https://example.com/images/3.png)**
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore silent links', () => {
|
||||||
|
const result = extractMediaFromMfm(`
|
||||||
|
?[1](https://example.com/images/1.png)
|
||||||
|
?[](https://example.com/images/2.png)
|
||||||
|
**?[3](https://example.com/images/3.png)**
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract complex text', () => {
|
||||||
|
const result = extractMediaFromMfm('');
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/image.png',
|
||||||
|
name: 'this is an image with complex text! :owo: 💙',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should de-duplicate images', () => {
|
||||||
|
const result = extractMediaFromMfm(`
|
||||||
|

|
||||||
|

|
||||||
|
****
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'Image',
|
||||||
|
url: 'https://example.com/images/1.png',
|
||||||
|
name: '3',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContentByType } from '@/core/activitypub/misc/get-content-by-type.js';
|
||||||
|
|
||||||
|
describe(getContentByType, () => {
|
||||||
|
describe('when permissive', () => {
|
||||||
|
it('should return source.content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
},
|
||||||
|
_misskey_content: 'misskey content',
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeYMarkdown, text/markdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown', true);
|
||||||
|
|
||||||
|
expect(content).toBe('source content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return _misskey_content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: 'misskey content',
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeYMarkdown, text/markdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown', true);
|
||||||
|
|
||||||
|
expect(content).toBe('misskey content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: null,
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeYMarkdown, text/markdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown', true);
|
||||||
|
|
||||||
|
expect(content).toBe('native content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when nothing matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: null,
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown', true);
|
||||||
|
|
||||||
|
expect(content).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for invalid inputs', () => {
|
||||||
|
const objects = [
|
||||||
|
{},
|
||||||
|
{ source: 'nope' },
|
||||||
|
{ content: null },
|
||||||
|
{ _misskey_content: 123 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = objects.map(c => getContentByType(c, 'text/misskeymarkdown', true));
|
||||||
|
|
||||||
|
const expected = objects.map(() => null);
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when not permissive', () => {
|
||||||
|
it('should return source.content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
},
|
||||||
|
_misskey_content: 'misskey content',
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown');
|
||||||
|
|
||||||
|
expect(content).toBe('source content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return _misskey_content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: 'misskey content',
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown');
|
||||||
|
|
||||||
|
expect(content).toBe('misskey content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content when it matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: null,
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/x.misskeymarkdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown');
|
||||||
|
|
||||||
|
expect(content).toBe('native content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when nothing matches', () => {
|
||||||
|
const obj = {
|
||||||
|
source: {
|
||||||
|
content: 'source content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
},
|
||||||
|
_misskey_content: null,
|
||||||
|
content: 'native content',
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = getContentByType(obj, 'text/x.misskeymarkdown');
|
||||||
|
|
||||||
|
expect(content).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for invalid inputs', () => {
|
||||||
|
const objects = [
|
||||||
|
{},
|
||||||
|
{ source: 'nope' },
|
||||||
|
{ content: null },
|
||||||
|
{ _misskey_content: 123 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = objects.map(c => getContentByType(c, 'text/misskeymarkdown'));
|
||||||
|
|
||||||
|
const expected = objects.map(() => null);
|
||||||
|
expect(results).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@phosphor-icons/web": "2.1.2",
|
"@phosphor-icons/web": "2.1.2",
|
||||||
"mfm-js": "npm:@transfem-org/sfm-js@0.24.6",
|
"mfm-js": "npm:@transfem-org/sfm-js@0.24.8",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@
|
||||||
"@vue/compiler-core": "3.5.14",
|
"@vue/compiler-core": "3.5.14",
|
||||||
"@vue/compiler-sfc": "3.5.14",
|
"@vue/compiler-sfc": "3.5.14",
|
||||||
"@vue/runtime-core": "3.5.14",
|
"@vue/runtime-core": "3.5.14",
|
||||||
"mfm-js": "npm:@transfem-org/sfm-js@0.24.6",
|
"mfm-js": "npm:@transfem-org/sfm-js@0.24.8",
|
||||||
"acorn": "8.14.1",
|
"acorn": "8.14.1",
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
|
|
||||||
150
pnpm-lock.yaml
generated
150
pnpm-lock.yaml
generated
|
|
@ -282,8 +282,8 @@ importers:
|
||||||
specifier: 0.50.0
|
specifier: 0.50.0
|
||||||
version: 0.50.0
|
version: 0.50.0
|
||||||
mfm-js:
|
mfm-js:
|
||||||
specifier: npm:@transfem-org/sfm-js@0.24.6
|
specifier: npm:@transfem-org/sfm-js@0.24.8
|
||||||
version: '@transfem-org/sfm-js@0.24.6'
|
version: '@transfem-org/sfm-js@0.24.8'
|
||||||
mime-types:
|
mime-types:
|
||||||
specifier: 2.1.35
|
specifier: 2.1.35
|
||||||
version: 2.1.35
|
version: 2.1.35
|
||||||
|
|
@ -935,7 +935,7 @@ importers:
|
||||||
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))
|
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 3.1.2
|
specifier: 3.1.2
|
||||||
version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
||||||
'@vue/compiler-core':
|
'@vue/compiler-core':
|
||||||
specifier: 3.5.14
|
specifier: 3.5.14
|
||||||
version: 3.5.14
|
version: 3.5.14
|
||||||
|
|
@ -976,8 +976,8 @@ importers:
|
||||||
specifier: 0.30.17
|
specifier: 0.30.17
|
||||||
version: 0.30.17
|
version: 0.30.17
|
||||||
mfm-js:
|
mfm-js:
|
||||||
specifier: npm:@transfem-org/sfm-js@0.24.6
|
specifier: npm:@transfem-org/sfm-js@0.24.8
|
||||||
version: '@transfem-org/sfm-js@0.24.6'
|
version: '@transfem-org/sfm-js@0.24.8'
|
||||||
micromatch:
|
micromatch:
|
||||||
specifier: 4.0.8
|
specifier: 4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
|
|
@ -1037,7 +1037,7 @@ importers:
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 3.1.2
|
specifier: 3.1.2
|
||||||
version: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
version: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
||||||
vitest-fetch-mock:
|
vitest-fetch-mock:
|
||||||
specifier: 0.4.5
|
specifier: 0.4.5
|
||||||
version: 0.4.5(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
version: 0.4.5(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
||||||
|
|
@ -1069,8 +1069,8 @@ importers:
|
||||||
specifier: 2.2.3
|
specifier: 2.2.3
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
mfm-js:
|
mfm-js:
|
||||||
specifier: npm:@transfem-org/sfm-js@0.24.6
|
specifier: npm:@transfem-org/sfm-js@0.24.8
|
||||||
version: '@transfem-org/sfm-js@0.24.6'
|
version: '@transfem-org/sfm-js@0.24.8'
|
||||||
misskey-js:
|
misskey-js:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../misskey-js
|
version: link:../misskey-js
|
||||||
|
|
@ -1137,7 +1137,7 @@ importers:
|
||||||
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))
|
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.14(typescript@5.8.3))
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 3.1.2
|
specifier: 3.1.2
|
||||||
version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
||||||
'@vue/compiler-sfc':
|
'@vue/compiler-sfc':
|
||||||
specifier: 3.5.14
|
specifier: 3.5.14
|
||||||
version: 3.5.14
|
version: 3.5.14
|
||||||
|
|
@ -1283,7 +1283,7 @@ importers:
|
||||||
version: 29.7.0
|
version: 29.7.0
|
||||||
ts-jest:
|
ts-jest:
|
||||||
specifier: 29.3.4
|
specifier: 29.3.4
|
||||||
version: 29.3.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.25.3)(jest@29.7.0(@types/node@22.15.2))(typescript@5.8.3)
|
version: 29.3.4(@babel/core@7.23.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.25.3)(jest@29.7.0(@types/node@22.15.2))(typescript@5.8.3)
|
||||||
|
|
||||||
packages/misskey-bubble-game:
|
packages/misskey-bubble-game:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -4012,8 +4012,8 @@ packages:
|
||||||
resolution: {integrity: sha1-LSVFMGgZU9oQlHSVb5XEzOG+yeQ=, tarball: https://activitypub.software/api/v4/projects/229/packages/npm/@transfem-org/cli-highlight/-/@transfem-org/cli-highlight-2.1.12.tgz}
|
resolution: {integrity: sha1-LSVFMGgZU9oQlHSVb5XEzOG+yeQ=, tarball: https://activitypub.software/api/v4/projects/229/packages/npm/@transfem-org/cli-highlight/-/@transfem-org/cli-highlight-2.1.12.tgz}
|
||||||
engines: {node: ^22.0.0}
|
engines: {node: ^22.0.0}
|
||||||
|
|
||||||
'@transfem-org/sfm-js@0.24.6':
|
'@transfem-org/sfm-js@0.24.8':
|
||||||
resolution: {integrity: sha1-7t+TkCd3PZk+RbbrGbZ/iMs2y7o=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.6.tgz}
|
resolution: {integrity: sha1-G97++XwNPZZaxIExiJbm2kJZSg0=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.8.tgz}
|
||||||
|
|
||||||
'@transfem-org/summaly@5.2.2':
|
'@transfem-org/summaly@5.2.2':
|
||||||
resolution: {integrity: sha1-MO7cCppxE0luitQqz9A6RiWHpco=, tarball: https://activitypub.software/api/v4/projects/217/packages/npm/@transfem-org/summaly/-/@transfem-org/summaly-5.2.2.tgz}
|
resolution: {integrity: sha1-MO7cCppxE0luitQqz9A6RiWHpco=, tarball: https://activitypub.software/api/v4/projects/217/packages/npm/@transfem-org/summaly/-/@transfem-org/summaly-5.2.2.tgz}
|
||||||
|
|
@ -11427,56 +11427,26 @@ snapshots:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
|
|
@ -11487,78 +11457,36 @@ snapshots:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
||||||
'@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)':
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)':
|
'@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
|
|
@ -14159,7 +14087,7 @@ snapshots:
|
||||||
highlight.js: 11.11.1
|
highlight.js: 11.11.1
|
||||||
htmlparser2: 9.1.0
|
htmlparser2: 9.1.0
|
||||||
|
|
||||||
'@transfem-org/sfm-js@0.24.6':
|
'@transfem-org/sfm-js@0.24.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@twemoji/parser': 15.0.0
|
'@twemoji/parser': 15.0.0
|
||||||
|
|
||||||
|
|
@ -14595,7 +14523,7 @@ snapshots:
|
||||||
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
||||||
vue: 3.5.14(typescript@5.8.3)
|
vue: 3.5.14(typescript@5.8.3)
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
|
'@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
|
|
@ -14609,7 +14537,7 @@ snapshots:
|
||||||
std-env: 3.9.0
|
std-env: 3.9.0
|
||||||
test-exclude: 7.0.1
|
test-exclude: 7.0.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
@ -15265,20 +15193,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-jest@29.7.0(@babel/core@7.24.7):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@jest/transform': 29.7.0
|
|
||||||
'@types/babel__core': 7.20.0
|
|
||||||
babel-plugin-istanbul: 6.1.1
|
|
||||||
babel-preset-jest: 29.6.3(@babel/core@7.24.7)
|
|
||||||
chalk: 4.1.2
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
slash: 3.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
babel-plugin-istanbul@6.1.1:
|
babel-plugin-istanbul@6.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-plugin-utils': 7.22.5
|
'@babel/helper-plugin-utils': 7.22.5
|
||||||
|
|
@ -15312,36 +15226,12 @@ snapshots:
|
||||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5)
|
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5)
|
||||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5)
|
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5)
|
||||||
|
|
||||||
babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
'@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7)
|
|
||||||
'@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7)
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
babel-preset-jest@29.6.3(@babel/core@7.23.5):
|
babel-preset-jest@29.6.3(@babel/core@7.23.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.23.5
|
'@babel/core': 7.23.5
|
||||||
babel-plugin-jest-hoist: 29.6.3
|
babel-plugin-jest-hoist: 29.6.3
|
||||||
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5)
|
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5)
|
||||||
|
|
||||||
babel-preset-jest@29.6.3(@babel/core@7.24.7):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.24.7
|
|
||||||
babel-plugin-jest-hoist: 29.6.3
|
|
||||||
babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7)
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
babel-walk@3.0.0-canary-5:
|
babel-walk@3.0.0-canary-5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.27.1
|
'@babel/types': 7.27.1
|
||||||
|
|
@ -21316,7 +21206,7 @@ snapshots:
|
||||||
|
|
||||||
ts-dedent@2.2.0: {}
|
ts-dedent@2.2.0: {}
|
||||||
|
|
||||||
ts-jest@29.3.4(@babel/core@7.24.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.25.3)(jest@29.7.0(@types/node@22.15.2))(typescript@5.8.3):
|
ts-jest@29.3.4(@babel/core@7.23.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.25.3)(jest@29.7.0(@types/node@22.15.2))(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
bs-logger: 0.2.6
|
bs-logger: 0.2.6
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
|
|
@ -21331,10 +21221,10 @@ snapshots:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.24.7
|
'@babel/core': 7.23.5
|
||||||
'@jest/transform': 29.7.0
|
'@jest/transform': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-jest: 29.7.0(@babel/core@7.24.7)
|
babel-jest: 29.7.0(@babel/core@7.23.5)
|
||||||
esbuild: 0.25.3
|
esbuild: 0.25.3
|
||||||
|
|
||||||
ts-map@1.0.3: {}
|
ts-map@1.0.3: {}
|
||||||
|
|
@ -21700,9 +21590,9 @@ snapshots:
|
||||||
|
|
||||||
vitest-fetch-mock@0.4.5(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)):
|
vitest-fetch-mock@0.4.5(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
|
||||||
|
|
||||||
vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
|
vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0)(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 3.1.2
|
'@vitest/expect': 3.1.2
|
||||||
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,7 @@ translationFailed: "Failed to translate note. Please try again later or contact
|
||||||
|
|
||||||
_processErrors:
|
_processErrors:
|
||||||
quoteUnavailable: "Unable to process quote. This post may be missing context."
|
quoteUnavailable: "Unable to process quote. This post may be missing context."
|
||||||
|
attachmentFailed: "One or more media attachments are unavailable and cannot be shown."
|
||||||
|
|
||||||
authorizedFetchSection: "Authorized Fetch"
|
authorizedFetchSection: "Authorized Fetch"
|
||||||
authorizedFetchLabel: "Allow unsigned ActivityPub requests:"
|
authorizedFetchLabel: "Allow unsigned ActivityPub requests:"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue