diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/package.json | 25 | ||||
| -rw-r--r-- | packages/backend/src/core/FetchInstanceMetadataService.ts | 4 | ||||
| -rw-r--r-- | packages/backend/src/core/MfmService.ts | 483 | ||||
| -rw-r--r-- | packages/backend/src/core/WebfingerService.ts | 16 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApRendererService.ts | 45 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApRequestService.ts | 73 | ||||
| -rw-r--r-- | packages/backend/src/misc/truncate.ts | 4 | ||||
| -rw-r--r-- | packages/backend/src/misc/verify-field-link.ts | 2 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/fetch-rss.ts | 213 | ||||
| -rw-r--r-- | packages/backend/test/e2e/oauth.ts | 2 | ||||
| -rw-r--r-- | packages/backend/test/unit/MfmService.ts | 2 | ||||
| -rw-r--r-- | packages/backend/test/utils.ts | 4 | ||||
| -rw-r--r-- | packages/frontend-embed/package.json | 28 | ||||
| -rw-r--r-- | packages/frontend-shared/package.json | 1 | ||||
| -rw-r--r-- | packages/frontend/package.json | 38 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 48 |
16 files changed, 450 insertions, 538 deletions
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<string>(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 (<pre><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<K>) => any } = { + const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => 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(')')); + const rpStartEl = new Element('rp', {}); + rpStartEl.childNodes.push(new Text('(')); + const rpEndEl = new Element('rp', {}); + rpEndEl.childNodes.push(new Text(')')); - rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); - rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); - rubyEl.appendChild(rpStartEl); - rubyEl.appendChild(rtEl); - rubyEl.appendChild(rpEndEl); + 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<FIXME | 'br'>('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<T extends mfm.MfmNode>(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<K>) => any; + [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => 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 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(')')); - rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); - rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); - rubyEl.appendChild(rpStartEl); - rubyEl.appendChild(rtEl); - rubyEl.appendChild(rpEndEl); + 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<FIXME | 'br'>('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<FIXME | 'br'>('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<T extends mfm.MfmNode>(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 a = new Element('a', { + href: quoteUri, + }); + a.childNodes.push(new Text(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); + 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.appendChild(quote); + body.childNodes.push(quote); } - let result = body.outerHTML; + let result = domserializer.render(body, { + encodeEntities: 'utf8' + }); if (inline) { result = result.replace(/^<p>/, '').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<string | null> { 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<any>).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<AnyNode> | 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<AnyNode> | 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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 = '<p><ruby><span><span>*some*</span><span> text</span></span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>'; + const output = '<p><ruby><span><span>*some*</span> text</span><rp>(</rp><rt>ignore me</rt><rp>)</rp></ruby></p>'; 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; - }; }; }; }; |