From 1a964cb6c037efe8e71c6e91e5c2ce032d22e107 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 21 May 2025 19:17:51 -0400 Subject: pcleanup dependencies: * Consolidate multiple different HTML/XML/RSS libraries to use the Cheerio stack * Remove unused deps * Move dev dependencies to correct section * Pin versions where missing --- packages/backend/package.json | 25 +- .../src/core/FetchInstanceMetadataService.ts | 4 +- packages/backend/src/core/MfmService.ts | 493 +++++++++++---------- packages/backend/src/core/WebfingerService.ts | 16 +- .../src/core/activitypub/ApRendererService.ts | 45 +- .../src/core/activitypub/ApRequestService.ts | 73 ++- packages/backend/src/misc/truncate.ts | 4 +- packages/backend/src/misc/verify-field-link.ts | 2 +- .../backend/src/server/api/endpoints/fetch-rss.ts | 213 ++++----- packages/backend/test/e2e/oauth.ts | 2 +- packages/backend/test/unit/MfmService.ts | 2 +- packages/backend/test/utils.ts | 4 +- packages/frontend-embed/package.json | 28 +- packages/frontend-shared/package.json | 1 - packages/frontend/package.json | 38 +- packages/misskey-js/src/autogen/types.ts | 48 +- 16 files changed, 455 insertions(+), 543 deletions(-) (limited to 'packages') diff --git a/packages/backend/package.json b/packages/backend/package.json index bad6990ba5..69d38f3bfb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -99,10 +99,8 @@ "archiver": "7.0.1", "argon2": "^0.40.1", "axios": "1.7.4", - "async-mutex": "0.5.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "body-parser": "1.20.3", "bullmq": "5.51.1", "cacheable-lookup": "7.0.0", "canvas": "^3.1.0", @@ -110,13 +108,14 @@ "chalk": "5.4.1", "chalk-template": "1.1.0", "cheerio": "1.0.0", - "chokidar": "3.6.0", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fast-xml-parser": "4.4.1", + "dom-serializer": "2.0.0", + "domhandler": "5.0.3", + "domutils": "3.2.2", "fastify": "5.3.2", "fastify-raw-body": "5.0.0", "feed": "4.2.2", @@ -125,10 +124,9 @@ "form-data": "4.0.2", "glob": "11.0.0", "got": "14.4.7", - "happy-dom": "16.8.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", - "http-link-header": "1.1.3", + "htmlparser2": "10.0.0", "ioredis": "5.6.1", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", @@ -136,26 +134,20 @@ "js-yaml": "4.1.0", "json5": "2.2.3", "jsonld": "8.3.3", - "jsrsasign": "11.1.0", "juice": "11.0.1", "megalodon": "workspace:*", "meilisearch": "0.50.0", - "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "moment": "^2.30.1", + "moment": "2.30.1", "ms": "3.0.0-canary.1", "nanoid": "5.1.5", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.10.1", - "oauth": "0.10.2", - "oauth2orize": "1.12.0", - "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", "otpauth": "9.4.0", - "parse5": "7.3.0", "pg": "8.15.6", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", @@ -165,20 +157,16 @@ "pug": "3.0.3", "qrcode": "1.5.4", "random-seed": "0.3.0", - "ratelimiter": "3.4.1", "re2": "1.21.4", "redis-info": "3.1.0", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", - "rss-parser": "3.13.0", - "rxjs": "7.8.2", "sanitize-html": "2.16.0", "secure-json-parse": "3.0.2", "sharp": "0.34.1", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", - "stringz": "2.1.0", "systeminformation": "5.25.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", @@ -202,12 +190,10 @@ "@types/accepts": "1.3.7", "@types/archiver": "6.0.3", "@types/bcryptjs": "2.4.6", - "@types/body-parser": "1.19.5", "@types/color-convert": "2.0.4", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.27", "@types/htmlescape": "1.1.3", - "@types/http-link-header": "1.0.7", "@types/jest": "29.5.14", "@types/js-yaml": "4.0.9", "@types/jsonld": "1.5.15", @@ -225,7 +211,6 @@ "@types/pug": "2.0.10", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", - "@types/ratelimiter": "3.4.6", "@types/redis-info": "3.0.3", "@types/rename": "1.0.7", "@types/sanitize-html": "2.15.0", diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 9bfd7381f1..6fcfdfb596 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -7,7 +7,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import tinycolor from 'tinycolor2'; import * as Redis from 'ioredis'; -import { load as cheerio } from 'cheerio'; +import { load as cheerio } from 'cheerio/slim'; import type { MiInstance } from '@/models/Instance.js'; import type Logger from '@/logger.js'; import { DI } from '@/di-symbols.js'; @@ -16,7 +16,7 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; -import type { CheerioAPI } from 'cheerio'; +import type { CheerioAPI } from 'cheerio/slim'; type NodeInfo = { openRegistrations?: unknown; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 1ee3bd2275..d85ac7c807 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -5,25 +5,22 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import * as parse5 from 'parse5'; -import { type Document, type HTMLParagraphElement, Window } from 'happy-dom'; +import { isText, isTag, Text } from 'domhandler'; +import * as htmlparser2 from 'htmlparser2'; +import { Node, Document, ChildNode, Element, ParentNode } from 'domhandler'; +import * as domserializer from 'dom-serializer'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import type { DefaultTreeAdapterMap } from 'parse5'; import type * as mfm from '@transfem-org/sfm-js'; -const treeAdapter = parse5.defaultTreeAdapter; -type Node = DefaultTreeAdapterMap['node']; -type ChildNode = DefaultTreeAdapterMap['childNode']; - const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; -export type Appender = (document: Document, body: HTMLParagraphElement) => void; +export type Appender = (document: Document, body: Element) => void; @Injectable() export class MfmService { @@ -40,7 +37,7 @@ export class MfmService { const normalizedHashtagNames = hashtagNames == null ? undefined : new Set(hashtagNames.map(x => normalizeForSearch(x))); - const dom = parse5.parseFragment(html); + const dom = htmlparser2.parseDocument(html); let text = ''; @@ -51,37 +48,31 @@ export class MfmService { return text.trim(); function getText(node: Node): string { - if (treeAdapter.isTextNode(node)) return node.value; - if (!treeAdapter.isElementNode(node)) return ''; - if (node.nodeName === 'br') return '\n'; - - if (node.childNodes) { - return node.childNodes.map(n => getText(n)).join(''); - } + if (isText(node)) return node.data; + if (!isTag(node)) return ''; + if (node.tagName === 'br') return '\n'; - return ''; + return node.childNodes.map(n => getText(n)).join(''); } function appendChildren(childNodes: ChildNode[]): void { - if (childNodes) { - for (const n of childNodes) { - analyze(n); - } + for (const n of childNodes) { + analyze(n); } } function analyze(node: Node) { - if (treeAdapter.isTextNode(node)) { - text += node.value; + if (isText(node)) { + text += node.data; return; } // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) { + if (!isTag(node)) { return; } - switch (node.nodeName) { + switch (node.tagName) { case 'br': { text += '\n'; break; @@ -89,19 +80,19 @@ export class MfmService { case 'a': { const txt = getText(node); - const rel = node.attrs.find(x => x.name === 'rel'); - const href = node.attrs.find(x => x.name === 'href'); + const rel = node.attribs.rel; + const href = node.attribs.href; // ハッシュタグ if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { text += txt; // メンション - } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { + } else if (txt.startsWith('@') && !(rel && rel.startsWith('me '))) { const part = txt.split('@'); if (part.length === 2 && href) { //#region ホスト名部分が省略されているので復元する - const acct = `${txt}@${(new URL(href.value)).hostname}`; + const acct = `${txt}@${(new URL(href)).hostname}`; text += acct; //#endregion } else if (part.length === 3) { @@ -116,17 +107,17 @@ export class MfmService { if (!href) { return txt; } - if (!txt || txt === href.value) { // #6383: Missing text node - if (href.value.match(urlRegexFull)) { - return href.value; + if (!txt || txt === href) { // #6383: Missing text node + if (href.match(urlRegexFull)) { + return href; } else { - return `<${href.value}>`; + return `<${href}>`; } } - if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { - return `[${txt}](<${href.value}>)`; // #6846 + if (href.match(urlRegex) && !href.match(urlRegexFull)) { + return `[${txt}](<${href}>)`; // #6846 } else { - return `[${txt}](${href.value})`; + return `[${txt}](${href})`; } }; @@ -185,14 +176,17 @@ export class MfmService { case 'ruby--': { let ruby: [string, string][] = []; for (const child of node.childNodes) { - if (child.nodeName === 'rp') { + if (isText(child) && !/\s|\[|\]/.test(child.data)) { + ruby.push([child.data, '']); + continue; + } + if (!isTag(child)) { continue; } - if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) { - ruby.push([child.value, '']); + if (child.tagName === 'rp') { continue; } - if (child.nodeName === 'rt' && ruby.length > 0) { + if (child.tagName === 'rt' && ruby.length > 0) { const rt = getText(child); if (/\s|\[|\]/.test(rt)) { // If any space is included in rt, it is treated as a normal text @@ -217,7 +211,7 @@ export class MfmService { // block code (
)
 				case 'pre': {
-					if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+					if (node.childNodes.length === 1 && isTag(node.childNodes[0]) && node.childNodes[0].tagName === 'code') {
 						text += '\n```\n';
 						text += getText(node.childNodes[0]);
 						text += '\n```\n';
@@ -302,17 +296,17 @@ export class MfmService {
 						let nonRtNodes = [];
 						// scan children, ignore `rp`, split on `rt`
 						for (const child of node.childNodes) {
-							if (treeAdapter.isTextNode(child)) {
+							if (isText(child)) {
 								nonRtNodes.push(child);
 								continue;
 							}
-							if (!treeAdapter.isElementNode(child)) {
+							if (!isTag(child)) {
 								continue;
 							}
-							if (child.nodeName === 'rp') {
+							if (child.tagName === 'rp') {
 								continue;
 							}
-							if (child.nodeName === 'rt') {
+							if (child.tagName === 'rt') {
 								// the only case in which we don't need a `$[group ]`
 								// is when both sides of the ruby are simple words
 								const needsGroup = nonRtNodes.length > 1 ||
@@ -350,45 +344,44 @@ export class MfmService {
 			return null;
 		}
 
-		const { happyDOM, window } = new Window();
-
-		const doc = window.document;
+		const doc = new Document([]);
 
-		const body = doc.createElement('p');
+		const body = new Element('p', {});
+		doc.childNodes.push(body);
 
-		function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
-			if (children) {
-				for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
+		function appendChildren(children: mfm.MfmNode[], targetElement: ParentNode): void {
+			for (const child of children.map(x => handle(x))) {
+				targetElement.childNodes.push(child);
 			}
 		}
 
 		function fnDefault(node: mfm.MfmFn) {
-			const el = doc.createElement('i');
+			const el = new Element('i', {});
 			appendChildren(node.children, el);
 			return el;
 		}
 
-		const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any } = {
+		const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => ChildNode } = {
 			bold: (node) => {
-				const el = doc.createElement('b');
+				const el = new Element('b', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			small: (node) => {
-				const el = doc.createElement('small');
+				const el = new Element('small', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			strike: (node) => {
-				const el = doc.createElement('del');
+				const el = new Element('del', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			italic: (node) => {
-				const el = doc.createElement('i');
+				const el = new Element('i', {});
 				appendChildren(node.children, el);
 				return el;
 			},
@@ -399,11 +392,12 @@ export class MfmService {
 						const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
 						try {
 							const date = new Date(parseInt(text, 10) * 1000);
-							const el = doc.createElement('time');
-							el.setAttribute('datetime', date.toISOString());
-							el.textContent = date.toISOString();
+							const el = new Element('time', {
+								datetime: date.toISOString(),
+							});
+							el.childNodes.push(new Text(date.toISOString()));
 							return el;
-						} catch (err) {
+						} catch {
 							return fnDefault(node);
 						}
 					}
@@ -412,20 +406,20 @@ export class MfmService {
 						if (node.children.length === 1) {
 							const child = node.children[0];
 							const text = child.type === 'text' ? child.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
+							const rubyEl = new Element('ruby', {});
+							const rtEl = new Element('rt', {});
 
 							// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
-
-							rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
-							rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
+							const rpStartEl = new Element('rp', {});
+							rpStartEl.childNodes.push(new Text('('));
+							const rpEndEl = new Element('rp', {});
+							rpEndEl.childNodes.push(new Text(')'));
+
+							rubyEl.childNodes.push(new Text(text.split(' ')[0]));
+							rtEl.childNodes.push(new Text(text.split(' ')[1]));
+							rubyEl.childNodes.push(rpStartEl);
+							rubyEl.childNodes.push(rtEl);
+							rubyEl.childNodes.push(rpEndEl);
 							return rubyEl;
 						} else {
 							const rt = node.children.at(-1);
@@ -435,20 +429,20 @@ export class MfmService {
 							}
 
 							const text = rt.type === 'text' ? rt.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
+							const rubyEl = new Element('ruby', {});
+							const rtEl = new Element('rt', {});
 
 							// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
+							const rpStartEl = new Element('rp', {});
+							rpStartEl.childNodes.push(new Text('('));
+							const rpEndEl = new Element('rp', {});
+							rpEndEl.childNodes.push(new Text(')'));
 
 							appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
-							rtEl.appendChild(doc.createTextNode(text.trim()));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
+							rtEl.childNodes.push(new Text(text.trim()));
+							rubyEl.childNodes.push(rpStartEl);
+							rubyEl.childNodes.push(rtEl);
+							rubyEl.childNodes.push(rpEndEl);
 							return rubyEl;
 						}
 					}
@@ -456,7 +450,7 @@ export class MfmService {
 					// hack for ruby, should never be needed because we should
 					// never send this out to other instances
 					case 'group': {
-						const el = doc.createElement('span');
+						const el = new Element('span', {});
 						appendChildren(node.children, el);
 						return el;
 					}
@@ -468,125 +462,135 @@ export class MfmService {
 			},
 
 			blockCode: (node) => {
-				const pre = doc.createElement('pre');
-				const inner = doc.createElement('code');
-				inner.textContent = node.props.code;
-				pre.appendChild(inner);
+				const pre = new Element('pre', {});
+				const inner = new Element('code', {});
+				inner.childNodes.push(new Text(node.props.code));
+				pre.childNodes.push(inner);
 				return pre;
 			},
 
 			center: (node) => {
-				const el = doc.createElement('div');
+				const el = new Element('div', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			emojiCode: (node) => {
-				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+				return new Text(`\u200B:${node.props.name}:\u200B`);
 			},
 
 			unicodeEmoji: (node) => {
-				return doc.createTextNode(node.props.emoji);
+				return new Text(node.props.emoji);
 			},
 
 			hashtag: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
-				a.textContent = `#${node.props.hashtag}`;
-				a.setAttribute('rel', 'tag');
+				const a = new Element('a', {
+					href: `${this.config.url}/tags/${node.props.hashtag}`,
+					rel: 'tag',
+				});
+				a.childNodes.push(new Text(`#${node.props.hashtag}`));
 				return a;
 			},
 
 			inlineCode: (node) => {
-				const el = doc.createElement('code');
-				el.textContent = node.props.code;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.code));
 				return el;
 			},
 
 			mathInline: (node) => {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.formula));
 				return el;
 			},
 
 			mathBlock: (node) => {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.formula));
 				return el;
 			},
 
 			link: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', node.props.url);
+				const a = new Element('a', {
+					href: node.props.url,
+				});
 				appendChildren(node.children, a);
 				return a;
 			},
 
 			mention: (node) => {
-				const a = doc.createElement('a');
 				const { username, host, acct } = node.props;
 				const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
-				a.setAttribute('href', remoteUserInfo
-					? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
-					: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
-				a.className = 'u-url mention';
-				a.textContent = acct;
+
+				const a = new Element('a', {
+					href: remoteUserInfo
+						? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
+						: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`,
+					class: 'u-url mention',
+				});
+				a.childNodes.push(new Text(acct));
 				return a;
 			},
 
 			quote: (node) => {
-				const el = doc.createElement('blockquote');
+				const el = new Element('blockquote', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			text: (node) => {
 				if (!node.props.text.match(/[\r\n]/)) {
-					return doc.createTextNode(node.props.text);
+					return new Text(node.props.text);
 				}
 
-				const el = doc.createElement('span');
-				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
+				const el = new Element('span', {});
+				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => new Text(x));
 
 				for (const x of intersperse('br', nodes)) {
-					el.appendChild(x === 'br' ? doc.createElement('br') : x);
+					el.childNodes.push(x === 'br' ? new Element('br', {}) : x);
 				}
 
 				return el;
 			},
 
 			url: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', node.props.url);
-				a.textContent = node.props.url;
+				const a = new Element('a', {
+					href: node.props.url,
+				});
+				a.childNodes.push(new Text(node.props.url));
 				return a;
 			},
 
 			search: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
-				a.textContent = node.props.content;
+				const a = new Element('a', {
+					href: `https://www.google.com/search?q=${node.props.query}`,
+				});
+				a.childNodes.push(new Text(node.props.content));
 				return a;
 			},
 
 			plain: (node) => {
-				const el = doc.createElement('span');
+				const el = new Element('span', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 		};
 
+		// Utility function to make TypeScript behave
+		function handle(node: T): ChildNode {
+			const handler = handlers[node.type] as (node: T) => ChildNode;
+			return handler(node);
+		}
+
 		appendChildren(nodes, body);
 
 		for (const additionalAppender of additionalAppenders) {
 			additionalAppender(doc, body);
 		}
 
-		const serialized = body.outerHTML;
-
-		happyDOM.close().catch(err => {});
-
-		return serialized;
+		return domserializer.render(body, {
+			encodeEntities: 'utf8'
+		});
 	}
 
 	// the toMastoApiHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
@@ -598,55 +602,55 @@ export class MfmService {
 			return null;
 		}
 
-		const { happyDOM, window } = new Window();
-
-		const doc = window.document;
+		const doc = new Document([]);
 
-		const body = doc.createElement('p');
+		const body = new Element('p', {});
+		doc.childNodes.push(body);
 
-		function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
-			if (children) {
-				for (const child of children.map((x) => (handlers as any)[x.type](x))) targetElement.appendChild(child);
+		function appendChildren(children: mfm.MfmNode[], targetElement: ParentNode): void {
+			for (const child of children) {
+				const result = handle(child);
+				targetElement.childNodes.push(result);
 			}
 		}
 
 		const handlers: {
-			[K in mfm.MfmNode['type']]: (node: mfm.NodeType) => any;
+			[K in mfm.MfmNode['type']]: (node: mfm.NodeType) => ChildNode;
 		} = {
 			bold(node) {
-				const el = doc.createElement('span');
-				el.textContent = '**';
+				const el = new Element('span', {});
+				el.childNodes.push(new Text('**'));
 				appendChildren(node.children, el);
-				el.textContent += '**';
+				el.childNodes.push(new Text('**'));
 				return el;
 			},
 
 			small(node) {
-				const el = doc.createElement('small');
+				const el = new Element('small', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			strike(node) {
-				const el = doc.createElement('span');
-				el.textContent = '~~';
+				const el = new Element('span', {});
+				el.childNodes.push(new Text('~~'));
 				appendChildren(node.children, el);
-				el.textContent += '~~';
+				el.childNodes.push(new Text('~~'));
 				return el;
 			},
 
 			italic(node) {
-				const el = doc.createElement('span');
-				el.textContent = '*';
+				const el = new Element('span', {});
+				el.childNodes.push(new Text('*'));
 				appendChildren(node.children, el);
-				el.textContent += '*';
+				el.childNodes.push(new Text('*'));
 				return el;
 			},
 
 			fn(node) {
 				switch (node.props.name) {
 					case 'group': { // hack for ruby
-						const el = doc.createElement('span');
+						const el = new Element('span', {});
 						appendChildren(node.children, el);
 						return el;
 					}
@@ -654,119 +658,121 @@ export class MfmService {
 						if (node.children.length === 1) {
 							const child = node.children[0];
 							const text = child.type === 'text' ? child.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
-
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
-
-							rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
-							rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
+							const rubyEl = new Element('ruby', {});
+							const rtEl = new Element('rt', {});
+
+							const rpStartEl = new Element('rp', {});
+							rpStartEl.childNodes.push(new Text('('));
+							const rpEndEl = new Element('rp', {});
+							rpEndEl.childNodes.push(new Text(')'));
+
+							rubyEl.childNodes.push(new Text(text.split(' ')[0]));
+							rtEl.childNodes.push(new Text(text.split(' ')[1]));
+							rubyEl.childNodes.push(rpStartEl);
+							rubyEl.childNodes.push(rtEl);
+							rubyEl.childNodes.push(rpEndEl);
 							return rubyEl;
 						} else {
 							const rt = node.children.at(-1);
 
 							if (!rt) {
-								const el = doc.createElement('span');
+								const el = new Element('span', {});
 								appendChildren(node.children, el);
 								return el;
 							}
 
 							const text = rt.type === 'text' ? rt.props.text : '';
-							const rubyEl = doc.createElement('ruby');
-							const rtEl = doc.createElement('rt');
+							const rubyEl = new Element('ruby', {});
+							const rtEl = new Element('rt', {});
 
-							const rpStartEl = doc.createElement('rp');
-							rpStartEl.appendChild(doc.createTextNode('('));
-							const rpEndEl = doc.createElement('rp');
-							rpEndEl.appendChild(doc.createTextNode(')'));
+							const rpStartEl = new Element('rp', {});
+							rpStartEl.childNodes.push(new Text('('));
+							const rpEndEl = new Element('rp', {});
+							rpEndEl.childNodes.push(new Text(')'));
 
 							appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
-							rtEl.appendChild(doc.createTextNode(text.trim()));
-							rubyEl.appendChild(rpStartEl);
-							rubyEl.appendChild(rtEl);
-							rubyEl.appendChild(rpEndEl);
+							rtEl.childNodes.push(new Text(text.trim()));
+							rubyEl.childNodes.push(rpStartEl);
+							rubyEl.childNodes.push(rtEl);
+							rubyEl.childNodes.push(rpEndEl);
 							return rubyEl;
 						}
 					}
 
 					default: {
-						const el = doc.createElement('span');
-						el.textContent = '*';
+						const el = new Element('span', {});
+						el.childNodes.push(new Text('*'));
 						appendChildren(node.children, el);
-						el.textContent += '*';
+						el.childNodes.push(new Text('*'));
 						return el;
 					}
 				}
 			},
 
 			blockCode(node) {
-				const pre = doc.createElement('pre');
-				const inner = doc.createElement('code');
+				const pre = new Element('pre', {});
+				const inner = new Element('code', {});
 
 				const nodes = node.props.code
 					.split(/\r\n|\r|\n/)
-					.map((x) => doc.createTextNode(x));
+					.map((x) => new Text(x));
 
 				for (const x of intersperse('br', nodes)) {
-					inner.appendChild(x === 'br' ? doc.createElement('br') : x);
+					inner.childNodes.push(x === 'br' ? new Element('br', {}) : x);
 				}
 
-				pre.appendChild(inner);
+				pre.childNodes.push(inner);
 				return pre;
 			},
 
 			center(node) {
-				const el = doc.createElement('div');
+				const el = new Element('div', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			emojiCode(node) {
-				return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+				return new Text(`\u200B:${node.props.name}:\u200B`);
 			},
 
 			unicodeEmoji(node) {
-				return doc.createTextNode(node.props.emoji);
+				return new Text(node.props.emoji);
 			},
 
 			hashtag: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`);
-				a.textContent = `#${node.props.hashtag}`;
-				a.setAttribute('rel', 'tag');
-				a.setAttribute('class', 'hashtag');
+				const a = new Element('a', {
+					href: `${this.config.url}/tags/${node.props.hashtag}`,
+					rel: 'tag',
+					class: 'hashtag',
+				});
+				a.childNodes.push(new Text(`#${node.props.hashtag}`));
 				return a;
 			},
 
 			inlineCode(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.code;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.code));
 				return el;
 			},
 
 			mathInline(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.formula));
 				return el;
 			},
 
 			mathBlock(node) {
-				const el = doc.createElement('code');
-				el.textContent = node.props.formula;
+				const el = new Element('code', {});
+				el.childNodes.push(new Text(node.props.formula));
 				return el;
 			},
 
 			link(node) {
-				const a = doc.createElement('a');
-				a.setAttribute('rel', 'nofollow noopener noreferrer');
-				a.setAttribute('target', '_blank');
-				a.setAttribute('href', node.props.url);
+				const a = new Element('a', {
+					rel: 'nofollow noopener noreferrer',
+					target: '_blank',
+					href: node.props.url,
+				});
 				appendChildren(node.children, a);
 				return a;
 			},
@@ -775,92 +781,107 @@ export class MfmService {
 				const { username, host, acct } = node.props;
 				const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
 
-				const el = doc.createElement('span');
+				const el = new Element('span', {});
 				if (!resolved) {
-					el.textContent = acct;
+					el.childNodes.push(new Text(acct));
 				} else {
-					el.setAttribute('class', 'h-card');
-					el.setAttribute('translate', 'no');
-					const a = doc.createElement('a');
-					a.setAttribute('href', resolved.url ? resolved.url : resolved.uri);
-					a.className = 'u-url mention';
-					const span = doc.createElement('span');
-					span.textContent = resolved.username || username;
-					a.textContent = '@';
-					a.appendChild(span);
-					el.appendChild(a);
+					el.attribs.class = 'h-card';
+					el.attribs.translate = 'no';
+					const a = new Element('a', {
+						href: resolved.url ? resolved.url : resolved.uri,
+						class: 'u-url mention',
+					});
+					const span = new Element('span', {});
+					span.childNodes.push(new Text(resolved.username || username));
+					a.childNodes.push(new Text('@'));
+					a.childNodes.push(span);
+					el.childNodes.push(a);
 				}
 
 				return el;
 			},
 
 			quote(node) {
-				const el = doc.createElement('blockquote');
+				const el = new Element('blockquote', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 
 			text(node) {
-				const el = doc.createElement('span');
+				if (!node.props.text.match(/[\r\n]/)) {
+					return new Text(node.props.text);
+				}
+
+				const el = new Element('span', {});
 				const nodes = node.props.text
 					.split(/\r\n|\r|\n/)
-					.map((x) => doc.createTextNode(x));
+					.map((x) => new Text(x));
 
 				for (const x of intersperse('br', nodes)) {
-					el.appendChild(x === 'br' ? doc.createElement('br') : x);
+					el.childNodes.push(x === 'br' ? new Element('br', {}) : x);
 				}
 
 				return el;
 			},
 
 			url(node) {
-				const a = doc.createElement('a');
-				a.setAttribute('rel', 'nofollow noopener noreferrer');
-				a.setAttribute('target', '_blank');
-				a.setAttribute('href', node.props.url);
-				a.textContent = node.props.url.replace(/^https?:\/\//, '');
+				const a = new Element('a', {
+					rel: 'nofollow noopener noreferrer',
+					target: '_blank',
+					href: node.props.url,
+				});
+				a.childNodes.push(new Text(node.props.url.replace(/^https?:\/\//, '')));
 				return a;
 			},
 
 			search: (node) => {
-				const a = doc.createElement('a');
-				a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`);
-				a.textContent = node.props.content;
+				const a = new Element('a', {
+					href: `https://www.google.com/search?q=${node.props.query}`,
+				});
+				a.childNodes.push(new Text(node.props.content));
 				return a;
 			},
 
 			plain(node) {
-				const el = doc.createElement('span');
+				const el = new Element('span', {});
 				appendChildren(node.children, el);
 				return el;
 			},
 		};
 
+		// Utility function to make TypeScript behave
+		function handle(node: T): ChildNode {
+			const handler = handlers[node.type] as (node: T) => ChildNode;
+			return handler(node);
+		}
+
 		appendChildren(nodes, body);
 
 		if (quoteUri !== null) {
-			const a = doc.createElement('a');
-			a.setAttribute('href', quoteUri);
-			a.textContent = quoteUri.replace(/^https?:\/\//, '');
-
-			const quote = doc.createElement('span');
-			quote.setAttribute('class', 'quote-inline');
-			quote.appendChild(doc.createElement('br'));
-			quote.appendChild(doc.createElement('br'));
-			quote.innerHTML += 'RE: ';
-			quote.appendChild(a);
-
-			body.appendChild(quote);
+			const a = new Element('a', {
+				href: quoteUri,
+			});
+			a.childNodes.push(new Text(quoteUri.replace(/^https?:\/\//, '')));
+
+			const quote = new Element('span', {
+				class: 'quote-inline',
+			});
+			quote.childNodes.push(new Element('br', {}));
+			quote.childNodes.push(new Element('br', {}));
+			quote.childNodes.push(new Text('RE: '));
+			quote.childNodes.push(a);
+
+			body.childNodes.push(quote);
 		}
 
-		let result = body.outerHTML;
+		let result = domserializer.render(body, {
+			encodeEntities: 'utf8'
+		});
 
 		if (inline) {
 			result = result.replace(/^

/, '').replace(/<\/p>$/, ''); } - happyDOM.close().catch(() => {}); - return result; } } diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 664963f3a3..bb9f0be4c6 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -5,7 +5,7 @@ import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; -import { XMLParser } from 'fast-xml-parser'; +import { load as cheerio } from 'cheerio/slim'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; @@ -101,14 +101,12 @@ export class WebfingerService { private async fetchWebFingerTemplateFromHostMeta(url: string): Promise { try { const res = await this.httpRequestService.getHtml(url, 'application/xrd+xml'); - const options = { - ignoreAttributes: false, - isArray: (_name: string, jpath: string) => jpath === 'XRD.Link', - }; - const parser = new XMLParser(options); - const hostMeta = parser.parse(res); - const template = (hostMeta['XRD']['Link'] as Array).filter(p => p['@_rel'] === 'lrdd')[0]['@_template']; - return template.indexOf('{uri}') < 0 ? null : template; + const hostMeta = cheerio(res, { + xml: true, + }); + + const template = hostMeta('XRD > Link[rel="lrdd"][template*="{uri}"]').attr('template'); + return template ?? null; } catch (err) { this.logger.error(`error while request host-meta for ${url}: ${renderInlineError(err)}`); return null; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 6068d707de..789611fd97 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import * as mfm from '@transfem-org/sfm-js'; import { UnrecoverableError } from 'bullmq'; +import { Element, Text } from 'domhandler'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; @@ -475,16 +476,18 @@ export class ApRendererService { // the claas name `quote-inline` is used in non-misskey clients for styling quote notes. // For compatibility, the span part should be kept as possible. apAppend.push((doc, body) => { - body.appendChild(doc.createElement('br')); - body.appendChild(doc.createElement('br')); - const span = doc.createElement('span'); - span.className = 'quote-inline'; - span.appendChild(doc.createTextNode('RE: ')); - const link = doc.createElement('a'); - link.setAttribute('href', quote); - link.textContent = quote; - span.appendChild(link); - body.appendChild(span); + body.childNodes.push(new Element('br', {})); + body.childNodes.push(new Element('br', {})); + const span = new Element('span', { + class: 'quote-inline', + }); + span.childNodes.push(new Text('RE: ')); + const link = new Element('a', { + href: quote, + }); + link.childNodes.push(new Text(quote)); + span.childNodes.push(link); + body.childNodes.push(span); }); } @@ -839,16 +842,18 @@ export class ApRendererService { // the claas name `quote-inline` is used in non-misskey clients for styling quote notes. // For compatibility, the span part should be kept as possible. apAppend.push((doc, body) => { - body.appendChild(doc.createElement('br')); - body.appendChild(doc.createElement('br')); - const span = doc.createElement('span'); - span.className = 'quote-inline'; - span.appendChild(doc.createTextNode('RE: ')); - const link = doc.createElement('a'); - link.setAttribute('href', quote); - link.textContent = quote; - span.appendChild(link); - body.appendChild(span); + body.childNodes.push(new Element('br', {})); + body.childNodes.push(new Element('br', {})); + const span = new Element('span', { + class: 'quote-inline', + }); + span.childNodes.push(new Text('RE: ')); + const link = new Element('a', { + href: quote, + }); + link.childNodes.push(new Text(quote)); + span.childNodes.push(link); + body.childNodes.push(span); }); } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 4c7cac2169..e4db9b237c 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -6,7 +6,7 @@ import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { Window } from 'happy-dom'; +import { load as cheerio } from 'cheerio/slim'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiUser } from '@/models/User.js'; @@ -18,6 +18,8 @@ import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; import type { IObject, IObjectWithId } from './type.js'; +import type { Cheerio, CheerioAPI } from 'cheerio/slim'; +import type { AnyNode } from 'domhandler'; type Request = { url: string; @@ -219,53 +221,33 @@ export class ApRequestService { (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true ) { - const html = await res.text(); - const { window, happyDOM } = new Window({ - settings: { - disableJavaScriptEvaluation: true, - disableJavaScriptFileLoading: true, - disableCSSFileLoading: true, - disableComputedStyleRendering: true, - handleDisabledFileLoadingAsSuccess: true, - navigation: { - disableMainFrameNavigation: true, - disableChildFrameNavigation: true, - disableChildPageNavigation: true, - disableFallbackToSetURL: true, - }, - timer: { - maxTimeout: 0, - maxIntervalTime: 0, - maxIntervalIterations: 0, - }, - }, - }); - const document = window.document; + let alternate: Cheerio | null; try { - document.documentElement.innerHTML = html; + const html = await res.text(); + const document = cheerio(html); // Search for any matching value in priority order: // 1. Type=AP > Type=none > Type=anything // 2. Alternate > Canonical // 3. Page order (fallback) - const alternate = - document.querySelector('head > link[href][rel="alternate"][type="application/activity+json"]') ?? - document.querySelector('head > link[href][rel="canonical"][type="application/activity+json"]') ?? - document.querySelector('head > link[href][rel="alternate"]:not([type])') ?? - document.querySelector('head > link[href][rel="canonical"]:not([type])') ?? - document.querySelector('head > link[href][rel="alternate"]') ?? - document.querySelector('head > link[href][rel="canonical"]'); - - if (alternate) { - const href = alternate.getAttribute('href'); - if (href && this.apUtilityService.haveSameAuthority(url, href)) { - return await this.signedGet(href, user, allowAnonymous, false); - } - } + alternate = selectFirst(document, [ + 'head > link[href][rel="alternate"][type="application/activity+json"]', + 'head > link[href][rel="canonical"][type="application/activity+json"]', + 'head > link[href][rel="alternate"]:not([type])', + 'head > link[href][rel="canonical"]:not([type])', + 'head > link[href][rel="alternate"]', + 'head > link[href][rel="canonical"]', + ]); } catch { // something went wrong parsing the HTML, ignore the whole thing - } finally { - happyDOM.close().catch(err => {}); + alternate = null; + } + + if (alternate) { + const href = alternate.attr('href'); + if (href && this.apUtilityService.haveSameAuthority(url, href)) { + return await this.signedGet(href, user, allowAnonymous, false); + } } } //#endregion @@ -285,3 +267,14 @@ export class ApRequestService { return activity as IObjectWithId; } } + +function selectFirst($: CheerioAPI, selectors: string[]): Cheerio | null { + for (const selector of selectors) { + const selection = $(selector); + if (selection.length > 0) { + return selection; + } + } + + return null; +} diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts index 1c8a274609..a313ab7854 100644 --- a/packages/backend/src/misc/truncate.ts +++ b/packages/backend/src/misc/truncate.ts @@ -3,14 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { substring } from 'stringz'; - export function truncate(input: string, size: number): string; export function truncate(input: string | undefined, size: number): string | undefined; export function truncate(input: string | undefined, size: number): string | undefined { if (!input) { return input; } else { - return substring(input, 0, size); + return input.slice(0, size); } } diff --git a/packages/backend/src/misc/verify-field-link.ts b/packages/backend/src/misc/verify-field-link.ts index 62542eaaa0..f9fc352806 100644 --- a/packages/backend/src/misc/verify-field-link.ts +++ b/packages/backend/src/misc/verify-field-link.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { load as cheerio } from 'cheerio'; +import { load as cheerio } from 'cheerio/slim'; import type { HttpRequestService } from '@/core/HttpRequestService.js'; type Field = { name: string, value: string }; diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 03f35f16a5..11244b30f6 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import Parser from 'rss-parser'; import { Injectable } from '@nestjs/common'; +import { parseFeed } from 'htmlparser2'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; - -const rssParser = new Parser(); +import { ApiError } from '../error.js'; +import type { FeedItem } from 'domutils'; export const meta = { tags: ['meta'], @@ -17,52 +17,32 @@ export const meta = { allowGet: true, cacheSec: 60 * 3, + errors: { + fetchFailed: { + id: '88f4356f-719d-4715-b4fc-703a10a812d2', + code: 'FETCH_FAILED', + message: 'Failed to fetch RSS feed', + }, + }, + res: { type: 'object', properties: { - image: { - type: 'object', + type: { + type: 'string', + optional: false, + }, + id: { + type: 'string', optional: true, - properties: { - link: { - type: 'string', - optional: true, - }, - url: { - type: 'string', - optional: false, - }, - title: { - type: 'string', - optional: true, - }, - }, }, - paginationLinks: { - type: 'object', + updated: { + type: 'string', + optional: true, + }, + author: { + type: 'string', optional: true, - properties: { - self: { - type: 'string', - optional: true, - }, - first: { - type: 'string', - optional: true, - }, - next: { - type: 'string', - optional: true, - }, - last: { - type: 'string', - optional: true, - }, - prev: { - type: 'string', - optional: true, - }, - }, }, link: { type: 'string', @@ -94,113 +74,42 @@ export const meta = { type: 'string', optional: true, }, - creator: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - content: { - type: 'string', - optional: true, - }, - isoDate: { + description: { type: 'string', optional: true, }, - categories: { + media: { type: 'array', - optional: true, + optional: false, items: { - type: 'string', - }, - }, - contentSnippet: { - type: 'string', - optional: true, - }, - enclosure: { - type: 'object', - optional: true, - properties: { - url: { - type: 'string', - optional: false, - }, - length: { - type: 'number', - optional: true, - }, - type: { - type: 'string', - optional: true, + type: 'object', + properties: { + medium: { + type: 'string', + optional: true, + }, + url: { + type: 'string', + optional: true, + }, + type: { + type: 'string', + optional: true, + }, + lang: { + type: 'string', + optional: true, + }, }, }, }, }, }, }, - feedUrl: { - type: 'string', - optional: true, - }, description: { type: 'string', optional: true, }, - itunes: { - type: 'object', - optional: true, - additionalProperties: true, - properties: { - image: { - type: 'string', - optional: true, - }, - owner: { - type: 'object', - optional: true, - properties: { - name: { - type: 'string', - optional: true, - }, - email: { - type: 'string', - optional: true, - }, - }, - }, - author: { - type: 'string', - optional: true, - }, - summary: { - type: 'string', - optional: true, - }, - explicit: { - type: 'string', - optional: true, - }, - categories: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - keywords: { - type: 'array', - optional: true, - items: { - type: 'string', - }, - }, - }, - }, }, }, @@ -224,7 +133,7 @@ export default class extends Endpoint { // eslint- constructor( private httpRequestService: HttpRequestService, ) { - super(meta, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps) => { const res = await this.httpRequestService.send(ps.url, { method: 'GET', headers: { @@ -234,8 +143,38 @@ export default class extends Endpoint { // eslint- }); const text = await res.text(); + const feed = parseFeed(text, { + xmlMode: true, + }); + + if (!feed) { + throw new ApiError(meta.errors.fetchFailed); + } - return rssParser.parseString(text); + return { + type: feed.type, + id: feed.id, + title: feed.title, + link: feed.link, + description: feed.description, + updated: feed.updated?.toISOString(), + author: feed.author, + items: feed.items + .filter((item): item is FeedItem & { link: string, title: string } => !!item.link && !!item.title) + .map(item => ({ + guid: item.id, + title: item.title, + link: item.link, + description: item.description, + pubDate: item.pubDate?.toISOString(), + media: item.media.map(media => ({ + medium: media.medium, + url: media.url, + type: media.type, + lang: media.lang, + })), + })), + }; }); } } diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 47851e9474..1dc8d87593 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -19,7 +19,7 @@ import { ResourceOwnerPassword, } from 'simple-oauth2'; import pkceChallenge from 'pkce-challenge'; -import { load as cheerio } from 'cheerio'; +import { load as cheerio } from 'cheerio/slim'; import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify'; import { api, port, sendEnvUpdateRequest, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index e54c006a4f..f96f3977d0 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -86,7 +86,7 @@ describe('MfmService', () => { test('ruby', async () => { const input = '$[ruby $[group *some* text] ignore me]'; - const output = '

*some* text(ignore me)

'; + const output = '

*some* text(ignore me)

'; assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output); }); }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 7f2768488f..5da5353e09 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -11,12 +11,12 @@ import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; import fetch, { File, RequestInit, type Headers } from 'node-fetch'; import { DataSource } from 'typeorm'; -import { load as cheerio } from 'cheerio'; +import { load as cheerio } from 'cheerio/slim'; import { type Response } from 'node-fetch'; import Fastify from 'fastify'; import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; -import type { CheerioAPI } from 'cheerio'; +import type { CheerioAPI } from 'cheerio/slim'; import type * as misskey from 'misskey-js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 1a851df49b..5191fe6852 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -12,34 +12,24 @@ "dependencies": { "@discordapp/twemoji": "15.1.0", "@phosphor-icons/web": "^2.0.3", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.1.4", "@transfem-org/sfm-js": "0.24.5", - "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.3", - "@vue/compiler-sfc": "3.5.14", - "astring": "1.9.0", "buraha": "0.0.1", - "estree-walker": "3.0.3", "frontend-shared": "workspace:*", "json5": "2.2.3", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.40.0", - "sass": "1.87.0", "shiki": "3.3.0", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.15", - "tsconfig-paths": "4.2.0", - "typescript": "5.8.3", "uuid": "11.1.0", - "vite": "6.3.3", "vue": "3.5.14" }, "devDependencies": { "@misskey-dev/summaly": "5.2.1", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", "@testing-library/vue": "8.1.0", + "@twemoji/parser": "15.1.1", "@types/estree": "1.0.7", "@types/micromatch": "4.0.9", "@types/node": "22.15.2", @@ -48,12 +38,16 @@ "@types/ws": "8.18.1", "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", + "@vitejs/plugin-vue": "5.2.3", "@vitest/coverage-v8": "3.1.2", + "@vue/compiler-sfc": "3.5.14", "@vue/runtime-core": "3.5.14", "acorn": "8.14.1", + "astring": "1.9.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "10.0.0", + "estree-walker": "3.0.3", "fast-glob": "3.3.3", "happy-dom": "17.4.4", "intersection-observer": "0.12.2", @@ -61,7 +55,13 @@ "msw": "2.7.5", "nodemon": "3.1.10", "prettier": "3.5.3", + "rollup": "4.40.0", + "sass": "1.87.0", "start-server-and-test": "2.0.11", + "tsc-alias": "1.8.15", + "tsconfig-paths": "4.2.0", + "typescript": "5.8.3", + "vite": "6.3.3", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "2.2.10", "vue-eslint-parser": "10.1.3", diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index f129121d19..b4a5dd89f5 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -35,7 +35,6 @@ ], "dependencies": { "misskey-js": "workspace:*", - "nodemon": "3.1.7", "vue": "3.5.13" } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 640ebe70d6..d04a04c78c 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -20,19 +20,11 @@ "@github/webauthn-json": "2.1.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@misskey-dev/browser-image-resizer": "2024.1.0", - "@phosphor-icons/web": "^2.0.3", - "@rollup/plugin-json": "6.1.0", - "@rollup/plugin-replace": "6.0.2", - "@rollup/pluginutils": "5.1.4", + "@phosphor-icons/web": "2.1.2", "@ruffle-rs/ruffle": "0.1.0-nightly.2024.10.15", "@sentry/vue": "9.14.0", "@syuilo/aiscript": "0.19.0", - "@transfem-org/sfm-js": "0.24.6", - "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.3", - "@vue/compiler-sfc": "3.5.14", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", - "astring": "1.9.0", "broadcast-channel": "7.1.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", @@ -45,38 +37,30 @@ "compare-versions": "6.1.1", "cropperjs": "2.0.0", "date-fns": "4.1.0", - "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "frontend-shared": "workspace:*", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", - "katex": "0.16.10", - "magic-string": "0.30.17", + "katex": "0.16.22", "matter-js": "0.20.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "moment": "^2.30.1", + "moment": "2.30.1", "photoswipe": "5.4.4", "promise-limit": "2.7.0", "punycode.js": "2.3.1", - "rollup": "4.40.0", "sanitize-html": "2.16.0", - "sass": "1.87.0", "shiki": "3.3.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.176.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.15", - "tsconfig-paths": "4.2.0", "typescript": "5.8.3", "uuid": "11.1.0", "v-code-diff": "1.13.1", - "vite": "6.3.3", "vue": "3.5.14", "vuedraggable": "next", "wanakana": "5.3.1" @@ -86,6 +70,9 @@ }, "devDependencies": { "@misskey-dev/summaly": "5.2.1", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-replace": "6.0.2", + "@rollup/pluginutils": "5.1.4", "@storybook/addon-actions": "8.6.12", "@storybook/addon-essentials": "8.6.12", "@storybook/addon-interactions": "8.6.12", @@ -105,6 +92,7 @@ "@storybook/vue3": "8.6.12", "@storybook/vue3-vite": "8.6.12", "@testing-library/vue": "8.1.0", + "@twemoji/parser": "15.1.1", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.7", "@types/katex": "^0.16.7", @@ -119,16 +107,22 @@ "@types/ws": "8.18.1", "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", + "@vitejs/plugin-vue": "5.2.3", "@vitest/coverage-v8": "3.1.2", "@vue/compiler-core": "3.5.14", + "@vue/compiler-sfc": "3.5.14", "@vue/runtime-core": "3.5.14", + "@transfem-org/sfm-js": "0.24.6", "acorn": "8.14.1", + "astring": "1.9.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "10.0.0", + "estree-walker": "3.0.3", "fast-glob": "3.3.3", "happy-dom": "17.4.4", "intersection-observer": "0.12.2", + "magic-string": "0.30.17", "micromatch": "4.0.8", "minimatch": "10.0.1", "msw": "2.7.5", @@ -137,10 +131,16 @@ "prettier": "3.5.3", "react": "19.1.0", "react-dom": "19.1.0", + "rollup": "4.40.0", + "sass": "1.87.0", "seedrandom": "3.0.5", "start-server-and-test": "2.0.11", "storybook": "8.6.12", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", + "three": "0.176.0", + "tsc-alias": "1.8.15", + "tsconfig-paths": "4.2.0", + "vite": "6.3.3", "vite-plugin-turbosnap": "1.0.3", "vitest": "3.1.2", "vitest-fetch-mock": "0.4.5", diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c09901c214..6cb52fcbea 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -19581,18 +19581,10 @@ export type operations = { 200: { content: { 'application/json': { - image?: { - link?: string; - url: string; - title?: string; - }; - paginationLinks?: { - self?: string; - first?: string; - next?: string; - last?: string; - prev?: string; - }; + type: string; + id?: string; + updated?: string; + author?: string; link?: string; title?: string; items: { @@ -19600,33 +19592,15 @@ export type operations = { guid?: string; title?: string; pubDate?: string; - creator?: string; - summary?: string; - content?: string; - isoDate?: string; - categories?: string[]; - contentSnippet?: string; - enclosure?: { - url: string; - length?: number; - type?: string; - }; + description?: string; + media: { + medium?: string; + url?: string; + type?: string; + lang?: string; + }[]; }[]; - feedUrl?: string; description?: string; - itunes?: { - image?: string; - owner?: { - name?: string; - email?: string; - }; - author?: string; - summary?: string; - explicit?: string; - categories?: string[]; - keywords?: string[]; - [key: string]: unknown; - }; }; }; }; -- cgit v1.2.3-freya