@@ -53,10 +53,15 @@
UPDATE CHANNEL
diff --git a/package.json b/package.json
index c1335bb9ba3..00937ef8d52 100644
--- a/package.json
+++ b/package.json
@@ -162,6 +162,7 @@
"memoizee": "^0.4.14",
"morgan": "^1.5.3",
"multer": "^1.4.5-lts.1",
+ "node-html-parser": "^6.1.13",
"node-media-server": "^2.1.4",
"nodemailer": "^6.0.0",
"opentelemetry-instrumentation-sequelize": "^0.41.0",
diff --git a/packages/tests/src/client/og-twitter-tags.ts b/packages/tests/src/client/og-twitter-tags.ts
index f0fc8943f09..7e263fc0fb0 100644
--- a/packages/tests/src/client/og-twitter-tags.ts
+++ b/packages/tests/src/client/og-twitter-tags.ts
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import { expect } from 'chai'
+import { config, expect } from 'chai'
import { Account, HttpStatusCode, VideoPlaylistCreateResult } from '@peertube/peertube-models'
import { cleanupTests, makeGetRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
import { getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js'
+config.truncateThreshold = 0
+
describe('Test Open Graph and Twitter cards HTML tags', function () {
let servers: PeerTubeServer[]
let account: Account
@@ -239,6 +241,51 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
})
})
+ describe('Mastodon link', function () {
+
+ async function check (path: string, mastoLink: string, exist = true) {
+ const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
+ const text = res.text
+
+ const expected = `
`
+
+ if (exist)expect(text).to.contain(expected)
+ else expect(text).to.not.contain(expected)
+ }
+
+ it('Should correctly include Mastodon link in account', async function () {
+ await servers[0].users.updateMe({
+ description: 'hi, please
Follow me on Mastodon!'
+ })
+
+ await check('/a/root', 'https://social.example.com/@username')
+ })
+
+ it('Should correctly include Mastodon link in channel', async function () {
+ await servers[0].channels.update({
+ channelName: 'root_channel',
+ attributes: {
+ description: '
Follow me on Mastodon!'
+ }
+ })
+
+ await check('/c/root_channel', 'https://social.example.com/@username2')
+ })
+
+ it('Should correctly include Mastodon link on homepage', async function () {
+ await servers[0].config.updateExistingConfig({
+ newConfig: {
+ instance: {
+ description: '
totocoucou
Follow me on Mastodon!'
+ }
+ }
+ })
+
+ await check('/', 'https://social.example.com/@username3')
+ await check('/about', 'https://social.example.com/@username3', false)
+ })
+ })
+
after(async function () {
await cleanupTests(servers)
})
diff --git a/packages/tests/src/shared/client.ts b/packages/tests/src/shared/client.ts
index f9572806e09..6ca9fee5500 100644
--- a/packages/tests/src/shared/client.ts
+++ b/packages/tests/src/shared/client.ts
@@ -1,4 +1,4 @@
-import { omit } from '@peertube/peertube-core-utils'
+import { omit, pick } from '@peertube/peertube-core-utils'
import {
VideoPrivacy,
VideoPlaylistPrivacy,
@@ -55,7 +55,7 @@ export async function prepareClientTests () {
await servers[0].config.updateExistingConfig({
newConfig: {
- instance: { name: instanceConfig.name, shortDescription: instanceConfig.shortDescription }
+ instance: { ...pick(instanceConfig, [ 'name', 'shortDescription' ]) }
}
})
await servers[0].config.updateInstanceImage({ type: ActorImageType.AVATAR, fixture: instanceConfig.avatar })
diff --git a/server/core/controllers/feeds/shared/common-feed-utils.ts b/server/core/controllers/feeds/shared/common-feed-utils.ts
index 71963646d20..8e94288f12c 100644
--- a/server/core/controllers/feeds/shared/common-feed-utils.ts
+++ b/server/core/controllers/feeds/shared/common-feed-utils.ts
@@ -2,7 +2,7 @@ import { Feed } from '@peertube/feed'
import { CustomTag, CustomXMLNS, Person } from '@peertube/feed/lib/typings/index.js'
import { maxBy, pick } from '@peertube/peertube-core-utils'
import { ActorImageType } from '@peertube/peertube-models'
-import { mdToOneLinePlainText } from '@server/helpers/markdown.js'
+import { mdToPlainText } from '@server/helpers/markdown.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { UserModel } from '@server/models/user/user.js'
@@ -35,7 +35,7 @@ export function initFeed (parameters: {
return new Feed({
title: name,
- description: mdToOneLinePlainText(description),
+ description: mdToPlainText(description),
// updated: TODO: somehowGetLatestUpdate, // optional, default = today
id: link || webserverUrl,
link: link || webserverUrl,
diff --git a/server/core/controllers/feeds/shared/video-feed-utils.ts b/server/core/controllers/feeds/shared/video-feed-utils.ts
index 260dac35fcc..01a047563bd 100644
--- a/server/core/controllers/feeds/shared/video-feed-utils.ts
+++ b/server/core/controllers/feeds/shared/video-feed-utils.ts
@@ -1,5 +1,5 @@
import { VideoIncludeType } from '@peertube/peertube-models'
-import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown.js'
+import { mdToPlainText, toSafeHtml } from '@server/helpers/markdown.js'
import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { getServerActor } from '@server/models/application/application.js'
@@ -47,7 +47,7 @@ export function getCommonVideoFeedAttributes (video: VideoModel) {
return {
title: video.name,
link: localLink,
- description: mdToOneLinePlainText(video.getTruncatedDescription()),
+ description: mdToPlainText(video.getTruncatedDescription()),
content: toSafeHtml(video.description),
date: video.publishedAt,
diff --git a/server/core/helpers/markdown.ts b/server/core/helpers/markdown.ts
index e88ff11d21d..ae67b4b4429 100644
--- a/server/core/helpers/markdown.ts
+++ b/server/core/helpers/markdown.ts
@@ -29,7 +29,7 @@ const toSafeHtml = (text: string) => {
return sanitizeHtml(html, defaultSanitizeOptions)
}
-const mdToOneLinePlainText = (text: string) => {
+const mdToPlainText = (text: string) => {
if (!text) return ''
markdownItForPlainText.render(text)
@@ -42,7 +42,7 @@ const mdToOneLinePlainText = (text: string) => {
export {
toSafeHtml,
- mdToOneLinePlainText
+ mdToPlainText
}
// ---------------------------------------------------------------------------
diff --git a/server/core/lib/html/shared/actor-html.ts b/server/core/lib/html/shared/actor-html.ts
index 76d2ebcb7b2..806260105f9 100644
--- a/server/core/lib/html/shared/actor-html.ts
+++ b/server/core/lib/html/shared/actor-html.ts
@@ -75,6 +75,7 @@ export class ActorHtml {
escapedTitle: escapeHTML(title),
escapedSiteName: escapeHTML(siteName),
escapedTruncatedDescription,
+ relMe: TagsHtml.findRelMe(entity.description),
image,
ogType,
twitterCard,
diff --git a/server/core/lib/html/shared/page-html.ts b/server/core/lib/html/shared/page-html.ts
index 6106d3317aa..88259975148 100644
--- a/server/core/lib/html/shared/page-html.ts
+++ b/server/core/lib/html/shared/page-html.ts
@@ -42,6 +42,10 @@ export class PageHtml {
escapedTitle: escapeHTML(CONFIG.INSTANCE.NAME),
escapedTruncatedDescription: escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION),
+ relMe: url === WEBSERVER.URL
+ ? TagsHtml.findRelMe(CONFIG.INSTANCE.DESCRIPTION)
+ : undefined,
+
image: avatar
? { url: ActorImageModel.getImageUrl(avatar), width: avatar.width, height: avatar.height }
: undefined,
diff --git a/server/core/lib/html/shared/tags-html.ts b/server/core/lib/html/shared/tags-html.ts
index c900a998694..9f1dfa78334 100644
--- a/server/core/lib/html/shared/tags-html.ts
+++ b/server/core/lib/html/shared/tags-html.ts
@@ -1,10 +1,11 @@
import { escapeAttribute, escapeHTML } from '@peertube/peertube-core-utils'
+import { mdToPlainText } from '@server/helpers/markdown.js'
+import truncate from 'lodash-es/truncate.js'
import { CONFIG } from '../../../initializers/config.js'
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, WEBSERVER } from '../../../initializers/constants.js'
import { MVideo, MVideoPlaylist } from '../../../types/models/index.js'
import { Hooks } from '../../plugins/hooks.js'
-import truncate from 'lodash-es/truncate.js'
-import { mdToOneLinePlainText } from '@server/helpers/markdown.js'
+import { parse } from 'node-html-parser';
type Tags = {
forbidIndexation: boolean
@@ -29,6 +30,8 @@ type Tags = {
escapedTitle?: string
escapedTruncatedDescription?: string
+ relMe?: string
+
image?: {
url: string
width: number
@@ -68,15 +71,25 @@ export class TagsHtml {
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag)
}
+ static findRelMe (content: string) {
+ if (!content) return undefined
+
+ const html = parse(content)
+
+ return html.querySelector('a[rel=me]')?.getAttribute('href') || undefined
+ }
+
// ---------------------------------------------------------------------------
static async addTags (htmlStringPage: string, tagsValues: Tags, context: HookContext) {
- const openGraphMetaTags = this.generateOpenGraphMetaTagsOptions(tagsValues)
- const standardMetaTags = this.generateStandardMetaTagsOptions(tagsValues)
- const twitterCardMetaTags = this.generateTwitterCardMetaTagsOptions(tagsValues)
+ const metaTags = {
+ ...this.generateOpenGraphMetaTagsOptions(tagsValues),
+ ...this.generateStandardMetaTagsOptions(tagsValues),
+ ...this.generateTwitterCardMetaTagsOptions(tagsValues)
+ }
const schemaTags = await this.generateSchemaTagsOptions(tagsValues, context)
- const { url, escapedTitle, oembedUrl, forbidIndexation } = tagsValues
+ const { url, escapedTitle, oembedUrl, forbidIndexation, relMe } = tagsValues
const oembedLinkTags: { type: string, href: string, escapedTitle: string }[] = []
@@ -90,29 +103,12 @@ export class TagsHtml {
let tagsStr = ''
- // Opengraph
- Object.keys(openGraphMetaTags).forEach(tagName => {
- const tagValue = openGraphMetaTags[tagName]
- if (!tagValue) return
+ for (const tagName of Object.keys(metaTags)) {
+ const tagValue = metaTags[tagName]
+ if (!tagValue) continue
tagsStr += `
`
- })
-
- // Standard
- Object.keys(standardMetaTags).forEach(tagName => {
- const tagValue = standardMetaTags[tagName]
- if (!tagValue) return
-
- tagsStr += `
`
- })
-
- // Twitter card
- Object.keys(twitterCardMetaTags).forEach(tagName => {
- const tagValue = twitterCardMetaTags[tagName]
- if (!tagValue) return
-
- tagsStr += `
`
- })
+ }
// OEmbed
for (const oembedLinkTag of oembedLinkTags) {
@@ -125,6 +121,10 @@ export class TagsHtml {
tagsStr += ``
}
+ if (relMe) {
+ tagsStr += `
`
+ }
+
// SEO, use origin URL
if (forbidIndexation !== true && url) {
tagsStr += `
`
@@ -261,6 +261,6 @@ export class TagsHtml {
// ---------------------------------------------------------------------------
static buildEscapedTruncatedDescription (description: string) {
- return truncate(mdToOneLinePlainText(description), { length: 200 })
+ return truncate(mdToPlainText(description), { length: 200 })
}
}
diff --git a/yarn.lock b/yarn.lock
index a44f8a7d1ff..2a7def58647 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8131,6 +8131,14 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.3.0, node-gyp-build@^4.8.2:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
+node-html-parser@^6.1.13:
+ version "6.1.13"
+ resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4"
+ integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==
+ dependencies:
+ css-select "^5.1.0"
+ he "1.2.0"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"