Add masto verification link support
This commit is contained in:
parent
5b4c7fc20d
commit
9bacc48643
|
@ -61,7 +61,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="instanceDescription">Description</label>
|
<label i18n for="instanceDescription">Description</label>
|
||||||
<div class="label-small-info">
|
<div class="label-small-info">
|
||||||
<my-custom-markup-help></my-custom-markup-help>
|
<my-custom-markup-help supportRelMe="true"></my-custom-markup-help>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
|
|
|
@ -26,10 +26,14 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="description">Description</label>
|
<label i18n for="description">Description</label>
|
||||||
<textarea
|
|
||||||
id="description" formControlName="description" class="form-control"
|
<my-help helpType="markdownText" supportRelMe="true"></my-help>
|
||||||
[ngClass]="{ 'input-error': formErrors['description'] }"
|
|
||||||
></textarea>
|
<my-markdown-textarea
|
||||||
|
inputId="description" formControlName="description" class="form-control"
|
||||||
|
markdownType="enhanced" [formError]="formErrors['description']" withEmoji="true" withHtml="true"
|
||||||
|
></my-markdown-textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
||||||
{{ formErrors.description }}
|
{{ formErrors.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { Notifier, User, UserService } from '@app/core'
|
||||||
import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||||
|
import { MarkdownTextareaComponent } from '@app/shared/shared-forms/markdown-textarea.component'
|
||||||
|
import { HelpComponent } from '@app/shared/shared-main/buttons/help.component'
|
||||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -12,7 +14,7 @@ import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||||
templateUrl: './my-account-profile.component.html',
|
templateUrl: './my-account-profile.component.html',
|
||||||
styleUrls: [ './my-account-profile.component.scss' ],
|
styleUrls: [ './my-account-profile.component.scss' ],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgClass, AlertComponent ]
|
imports: [ NgIf, FormsModule, ReactiveFormsModule, NgClass, AlertComponent, HelpComponent, MarkdownTextareaComponent ]
|
||||||
})
|
})
|
||||||
export class MyAccountProfileComponent extends FormReactive implements OnInit {
|
export class MyAccountProfileComponent extends FormReactive implements OnInit {
|
||||||
@Input() user: User = null
|
@Input() user: User = null
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
<ng-container i18n>
|
<ng-container i18n>
|
||||||
<a class="text-decoration-underline" href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noreferrer noopener">Markdown compatible</a> that also supports <a class="text-decoration-underline" href="https://docs.joinpeertube.org/api/custom-client-markup" target="_blank" rel="noreferrer noopener">custom PeerTube HTML tags</a>
|
<a class="text-decoration-underline" href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noreferrer noopener">Markdown compatible</a> that also supports <a class="text-decoration-underline" href="https://docs.joinpeertube.org/api/custom-client-markup" target="_blank" rel="noreferrer noopener">custom PeerTube HTML tags</a>.
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@if (supportRelMe) {
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<ng-container i18n>
|
||||||
|
<a href="https://docs.joinmastodon.org/user/profile/#verification" target="_blank" rel="noopener noreferrer">Mastodon verification link</a> is also supported.
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component } from '@angular/core'
|
import { booleanAttribute, Component, Input } from '@angular/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-custom-markup-help',
|
selector: 'my-custom-markup-help',
|
||||||
|
@ -6,4 +6,5 @@ import { Component } from '@angular/core'
|
||||||
standalone: true
|
standalone: true
|
||||||
})
|
})
|
||||||
export class CustomMarkupHelpComponent {
|
export class CustomMarkupHelpComponent {
|
||||||
|
@Input({ transform: booleanAttribute }) supportRelMe = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,8 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
||||||
@Input({ required: true }) inputId: string
|
@Input({ required: true }) inputId: string
|
||||||
|
|
||||||
@Input() dir: string
|
@Input() dir: string
|
||||||
|
|
||||||
|
@Input({ transform: booleanAttribute }) withHtml = false
|
||||||
@Input({ transform: booleanAttribute }) withEmoji = false
|
@Input({ transform: booleanAttribute }) withEmoji = false
|
||||||
|
|
||||||
@ViewChild('textarea') textareaElement: ElementRef
|
@ViewChild('textarea') textareaElement: ElementRef
|
||||||
|
@ -163,9 +165,9 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
|
||||||
|
|
||||||
html = result
|
html = result
|
||||||
} else if (this.markdownType === 'text') {
|
} else if (this.markdownType === 'text') {
|
||||||
html = await this.markdownService.textMarkdownToHTML({ markdown: text, withEmoji: this.withEmoji })
|
html = await this.markdownService.textMarkdownToHTML({ markdown: text, withEmoji: this.withEmoji, withHtml: this.withHtml })
|
||||||
} else if (this.markdownType === 'enhanced') {
|
} else if (this.markdownType === 'enhanced') {
|
||||||
html = await this.markdownService.enhancedMarkdownToHTML({ markdown: text, withEmoji: this.withEmoji })
|
html = await this.markdownService.enhancedMarkdownToHTML({ markdown: text, withEmoji: this.withEmoji, withHtml: this.withHtml })
|
||||||
} else if (this.markdownType === 'to-unsafe-html') {
|
} else if (this.markdownType === 'to-unsafe-html') {
|
||||||
html = await this.markdownService.markdownToUnsafeHTML({ markdown: text })
|
html = await this.markdownService.markdownToUnsafeHTML({ markdown: text })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import { AfterContentInit, Component, ContentChildren, Input, OnChanges, OnInit, QueryList, TemplateRef } from '@angular/core'
|
import { NgIf, NgTemplateOutlet } from '@angular/common'
|
||||||
|
import {
|
||||||
|
AfterContentInit,
|
||||||
|
booleanAttribute,
|
||||||
|
Component,
|
||||||
|
ContentChildren,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
QueryList,
|
||||||
|
TemplateRef
|
||||||
|
} from '@angular/core'
|
||||||
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
|
import { GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
|
||||||
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ENHANCED_RULES, TEXT_RULES } from '@peertube/peertube-core-utils'
|
import { ENHANCED_RULES, TEXT_RULES } from '@peertube/peertube-core-utils'
|
||||||
import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
|
import { GlobalIconComponent } from '../../shared-icons/global-icon.component'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { NgIf, NgTemplateOutlet } from '@angular/common'
|
|
||||||
import { PeerTubeTemplateDirective } from '../common/peertube-template.directive'
|
import { PeerTubeTemplateDirective } from '../common/peertube-template.directive'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -20,6 +30,7 @@ export class HelpComponent implements OnInit, OnChanges, AfterContentInit {
|
||||||
@Input() iconName: GlobalIconName = 'help'
|
@Input() iconName: GlobalIconName = 'help'
|
||||||
@Input() title = $localize`Get help`
|
@Input() title = $localize`Get help`
|
||||||
@Input() autoClose = 'outside'
|
@Input() autoClose = 'outside'
|
||||||
|
@Input({ transform: booleanAttribute }) supportRelMe = false
|
||||||
|
|
||||||
@ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'preHtml' | 'customHtml' | 'postHtml'>>
|
@ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'preHtml' | 'customHtml' | 'postHtml'>>
|
||||||
|
|
||||||
|
@ -76,9 +87,17 @@ export class HelpComponent implements OnInit, OnChanges, AfterContentInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatMarkdownSupport (rules: string[]) {
|
private formatMarkdownSupport (rules: string[]) {
|
||||||
/* eslint-disable max-len */
|
let str =
|
||||||
return $localize`<a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noopener noreferrer">Markdown</a> compatible that supports:` +
|
// eslint-disable-next-line max-len
|
||||||
|
$localize`<a href="https://en.wikipedia.org/wiki/Markdown#Example" target="_blank" rel="noopener noreferrer">Markdown</a> compatible that supports:` +
|
||||||
this.createMarkdownList(rules)
|
this.createMarkdownList(rules)
|
||||||
|
|
||||||
|
if (this.supportRelMe) {
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
str += $localize`<a href="https://docs.joinmastodon.org/user/profile/#verification" target="_blank" rel="noopener noreferrer">Mastodon verification link</a> is also supported.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMarkdownList (rules: string[]) {
|
private createMarkdownList (rules: string[]) {
|
||||||
|
|
|
@ -30,10 +30,8 @@ export class SupportModalComponent {
|
||||||
|
|
||||||
const support = this.video?.support || this.videoChannel.support
|
const support = this.video?.support || this.videoChannel.support
|
||||||
|
|
||||||
this.markdownService.enhancedMarkdownToHTML({ markdown: support })
|
this.markdownService.enhancedMarkdownToHTML({ markdown: support, withEmoji: true, withHtml: true })
|
||||||
.then(r => {
|
.then(r => this.htmlSupport = r)
|
||||||
this.htmlSupport = r
|
|
||||||
})
|
|
||||||
|
|
||||||
this.displayName = this.video
|
this.displayName = this.video
|
||||||
? this.video.channel.displayName
|
? this.video.channel.displayName
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="display-name">Display name</label>
|
<label i18n for="display-name">Display name</label>
|
||||||
<input
|
<input
|
||||||
type="text" id="display-name" class="form-control d-block"
|
type="text" id="display-name" class="form-control"
|
||||||
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
|
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
|
||||||
>
|
>
|
||||||
<div *ngIf="formErrors['display-name']" class="form-error" role="alert">
|
<div *ngIf="formErrors['display-name']" class="form-error" role="alert">
|
||||||
|
@ -53,10 +53,15 @@
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="description">Description</label>
|
<label i18n for="description">Description</label>
|
||||||
<textarea
|
|
||||||
id="description" formControlName="description" class="form-control d-block"
|
|
||||||
[ngClass]="{ 'input-error': formErrors['description'] }"
|
<my-help helpType="markdownText" supportRelMe="true"></my-help>
|
||||||
></textarea>
|
|
||||||
|
<my-markdown-textarea
|
||||||
|
inputId="description" formControlName="description" class="form-control"
|
||||||
|
markdownType="enhanced" [formError]="formErrors['description']" withEmoji="true" withHtml="true"
|
||||||
|
></my-markdown-textarea>
|
||||||
|
|
||||||
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
<div *ngIf="formErrors.description" class="form-error" role="alert">
|
||||||
{{ formErrors.description }}
|
{{ formErrors.description }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,8 +75,8 @@
|
||||||
></my-help>
|
></my-help>
|
||||||
|
|
||||||
<my-markdown-textarea
|
<my-markdown-textarea
|
||||||
inputId="support" formControlName="support" class="d-block"
|
inputId="support" formControlName="support" class="form-control"
|
||||||
markdownType="enhanced" [formError]="formErrors['support']"
|
markdownType="enhanced" [formError]="formErrors['support']" withEmoji="true" withHtml="true"
|
||||||
></my-markdown-textarea>
|
></my-markdown-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,7 @@
|
||||||
"memoizee": "^0.4.14",
|
"memoizee": "^0.4.14",
|
||||||
"morgan": "^1.5.3",
|
"morgan": "^1.5.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"node-html-parser": "^6.1.13",
|
||||||
"node-media-server": "^2.1.4",
|
"node-media-server": "^2.1.4",
|
||||||
"nodemailer": "^6.0.0",
|
"nodemailer": "^6.0.0",
|
||||||
"opentelemetry-instrumentation-sequelize": "^0.41.0",
|
"opentelemetry-instrumentation-sequelize": "^0.41.0",
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* 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 { Account, HttpStatusCode, VideoPlaylistCreateResult } from '@peertube/peertube-models'
|
||||||
import { cleanupTests, makeGetRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
|
import { cleanupTests, makeGetRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
import { getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js'
|
import { getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js'
|
||||||
|
|
||||||
|
config.truncateThreshold = 0
|
||||||
|
|
||||||
describe('Test Open Graph and Twitter cards HTML tags', function () {
|
describe('Test Open Graph and Twitter cards HTML tags', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
let account: Account
|
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 = `<link href="${mastoLink}" rel="me">`
|
||||||
|
|
||||||
|
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 <a href="https://social.example.com/@username" rel="me">Follow me on Mastodon!</a>'
|
||||||
|
})
|
||||||
|
|
||||||
|
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: '<a rel="me" href="https://social.example.com/@username2">Follow me on Mastodon!</a>'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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: '<a>toto</a>coucou<a rel="me" href="https://social.example.com/@username3">Follow me on Mastodon!</a>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await check('/', 'https://social.example.com/@username3')
|
||||||
|
await check('/about', 'https://social.example.com/@username3', false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { omit } from '@peertube/peertube-core-utils'
|
import { omit, pick } from '@peertube/peertube-core-utils'
|
||||||
import {
|
import {
|
||||||
VideoPrivacy,
|
VideoPrivacy,
|
||||||
VideoPlaylistPrivacy,
|
VideoPlaylistPrivacy,
|
||||||
|
@ -55,7 +55,7 @@ export async function prepareClientTests () {
|
||||||
|
|
||||||
await servers[0].config.updateExistingConfig({
|
await servers[0].config.updateExistingConfig({
|
||||||
newConfig: {
|
newConfig: {
|
||||||
instance: { name: instanceConfig.name, shortDescription: instanceConfig.shortDescription }
|
instance: { ...pick(instanceConfig, [ 'name', 'shortDescription' ]) }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await servers[0].config.updateInstanceImage({ type: ActorImageType.AVATAR, fixture: instanceConfig.avatar })
|
await servers[0].config.updateInstanceImage({ type: ActorImageType.AVATAR, fixture: instanceConfig.avatar })
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Feed } from '@peertube/feed'
|
||||||
import { CustomTag, CustomXMLNS, Person } from '@peertube/feed/lib/typings/index.js'
|
import { CustomTag, CustomXMLNS, Person } from '@peertube/feed/lib/typings/index.js'
|
||||||
import { maxBy, pick } from '@peertube/peertube-core-utils'
|
import { maxBy, pick } from '@peertube/peertube-core-utils'
|
||||||
import { ActorImageType } from '@peertube/peertube-models'
|
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 { CONFIG } from '@server/initializers/config.js'
|
||||||
import { WEBSERVER } from '@server/initializers/constants.js'
|
import { WEBSERVER } from '@server/initializers/constants.js'
|
||||||
import { UserModel } from '@server/models/user/user.js'
|
import { UserModel } from '@server/models/user/user.js'
|
||||||
|
@ -35,7 +35,7 @@ export function initFeed (parameters: {
|
||||||
|
|
||||||
return new Feed({
|
return new Feed({
|
||||||
title: name,
|
title: name,
|
||||||
description: mdToOneLinePlainText(description),
|
description: mdToPlainText(description),
|
||||||
// updated: TODO: somehowGetLatestUpdate, // optional, default = today
|
// updated: TODO: somehowGetLatestUpdate, // optional, default = today
|
||||||
id: link || webserverUrl,
|
id: link || webserverUrl,
|
||||||
link: link || webserverUrl,
|
link: link || webserverUrl,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { VideoIncludeType } from '@peertube/peertube-models'
|
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 { CONFIG } from '@server/initializers/config.js'
|
||||||
import { WEBSERVER } from '@server/initializers/constants.js'
|
import { WEBSERVER } from '@server/initializers/constants.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
|
@ -47,7 +47,7 @@ export function getCommonVideoFeedAttributes (video: VideoModel) {
|
||||||
return {
|
return {
|
||||||
title: video.name,
|
title: video.name,
|
||||||
link: localLink,
|
link: localLink,
|
||||||
description: mdToOneLinePlainText(video.getTruncatedDescription()),
|
description: mdToPlainText(video.getTruncatedDescription()),
|
||||||
content: toSafeHtml(video.description),
|
content: toSafeHtml(video.description),
|
||||||
|
|
||||||
date: video.publishedAt,
|
date: video.publishedAt,
|
||||||
|
|
|
@ -29,7 +29,7 @@ const toSafeHtml = (text: string) => {
|
||||||
return sanitizeHtml(html, defaultSanitizeOptions)
|
return sanitizeHtml(html, defaultSanitizeOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdToOneLinePlainText = (text: string) => {
|
const mdToPlainText = (text: string) => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
|
|
||||||
markdownItForPlainText.render(text)
|
markdownItForPlainText.render(text)
|
||||||
|
@ -42,7 +42,7 @@ const mdToOneLinePlainText = (text: string) => {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
toSafeHtml,
|
toSafeHtml,
|
||||||
mdToOneLinePlainText
|
mdToPlainText
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -75,6 +75,7 @@ export class ActorHtml {
|
||||||
escapedTitle: escapeHTML(title),
|
escapedTitle: escapeHTML(title),
|
||||||
escapedSiteName: escapeHTML(siteName),
|
escapedSiteName: escapeHTML(siteName),
|
||||||
escapedTruncatedDescription,
|
escapedTruncatedDescription,
|
||||||
|
relMe: TagsHtml.findRelMe(entity.description),
|
||||||
image,
|
image,
|
||||||
ogType,
|
ogType,
|
||||||
twitterCard,
|
twitterCard,
|
||||||
|
|
|
@ -42,6 +42,10 @@ export class PageHtml {
|
||||||
escapedTitle: escapeHTML(CONFIG.INSTANCE.NAME),
|
escapedTitle: escapeHTML(CONFIG.INSTANCE.NAME),
|
||||||
escapedTruncatedDescription: escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION),
|
escapedTruncatedDescription: escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION),
|
||||||
|
|
||||||
|
relMe: url === WEBSERVER.URL
|
||||||
|
? TagsHtml.findRelMe(CONFIG.INSTANCE.DESCRIPTION)
|
||||||
|
: undefined,
|
||||||
|
|
||||||
image: avatar
|
image: avatar
|
||||||
? { url: ActorImageModel.getImageUrl(avatar), width: avatar.width, height: avatar.height }
|
? { url: ActorImageModel.getImageUrl(avatar), width: avatar.width, height: avatar.height }
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { escapeAttribute, escapeHTML } from '@peertube/peertube-core-utils'
|
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 { CONFIG } from '../../../initializers/config.js'
|
||||||
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, WEBSERVER } from '../../../initializers/constants.js'
|
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, WEBSERVER } from '../../../initializers/constants.js'
|
||||||
import { MVideo, MVideoPlaylist } from '../../../types/models/index.js'
|
import { MVideo, MVideoPlaylist } from '../../../types/models/index.js'
|
||||||
import { Hooks } from '../../plugins/hooks.js'
|
import { Hooks } from '../../plugins/hooks.js'
|
||||||
import truncate from 'lodash-es/truncate.js'
|
import { parse } from 'node-html-parser';
|
||||||
import { mdToOneLinePlainText } from '@server/helpers/markdown.js'
|
|
||||||
|
|
||||||
type Tags = {
|
type Tags = {
|
||||||
forbidIndexation: boolean
|
forbidIndexation: boolean
|
||||||
|
@ -29,6 +30,8 @@ type Tags = {
|
||||||
escapedTitle?: string
|
escapedTitle?: string
|
||||||
escapedTruncatedDescription?: string
|
escapedTruncatedDescription?: string
|
||||||
|
|
||||||
|
relMe?: string
|
||||||
|
|
||||||
image?: {
|
image?: {
|
||||||
url: string
|
url: string
|
||||||
width: number
|
width: number
|
||||||
|
@ -68,15 +71,25 @@ export class TagsHtml {
|
||||||
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag)
|
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) {
|
static async addTags (htmlStringPage: string, tagsValues: Tags, context: HookContext) {
|
||||||
const openGraphMetaTags = this.generateOpenGraphMetaTagsOptions(tagsValues)
|
const metaTags = {
|
||||||
const standardMetaTags = this.generateStandardMetaTagsOptions(tagsValues)
|
...this.generateOpenGraphMetaTagsOptions(tagsValues),
|
||||||
const twitterCardMetaTags = this.generateTwitterCardMetaTagsOptions(tagsValues)
|
...this.generateStandardMetaTagsOptions(tagsValues),
|
||||||
|
...this.generateTwitterCardMetaTagsOptions(tagsValues)
|
||||||
|
}
|
||||||
const schemaTags = await this.generateSchemaTagsOptions(tagsValues, context)
|
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 }[] = []
|
const oembedLinkTags: { type: string, href: string, escapedTitle: string }[] = []
|
||||||
|
|
||||||
|
@ -90,29 +103,12 @@ export class TagsHtml {
|
||||||
|
|
||||||
let tagsStr = ''
|
let tagsStr = ''
|
||||||
|
|
||||||
// Opengraph
|
for (const tagName of Object.keys(metaTags)) {
|
||||||
Object.keys(openGraphMetaTags).forEach(tagName => {
|
const tagValue = metaTags[tagName]
|
||||||
const tagValue = openGraphMetaTags[tagName]
|
if (!tagValue) continue
|
||||||
if (!tagValue) return
|
|
||||||
|
|
||||||
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
|
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
|
||||||
})
|
}
|
||||||
|
|
||||||
// Standard
|
|
||||||
Object.keys(standardMetaTags).forEach(tagName => {
|
|
||||||
const tagValue = standardMetaTags[tagName]
|
|
||||||
if (!tagValue) return
|
|
||||||
|
|
||||||
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Twitter card
|
|
||||||
Object.keys(twitterCardMetaTags).forEach(tagName => {
|
|
||||||
const tagValue = twitterCardMetaTags[tagName]
|
|
||||||
if (!tagValue) return
|
|
||||||
|
|
||||||
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
|
|
||||||
})
|
|
||||||
|
|
||||||
// OEmbed
|
// OEmbed
|
||||||
for (const oembedLinkTag of oembedLinkTags) {
|
for (const oembedLinkTag of oembedLinkTags) {
|
||||||
|
@ -125,6 +121,10 @@ export class TagsHtml {
|
||||||
tagsStr += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>`
|
tagsStr += `<script type="application/ld+json">${JSON.stringify(schemaTags)}</script>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (relMe) {
|
||||||
|
tagsStr += `<link href="${escapeAttribute(relMe)}" rel="me">`
|
||||||
|
}
|
||||||
|
|
||||||
// SEO, use origin URL
|
// SEO, use origin URL
|
||||||
if (forbidIndexation !== true && url) {
|
if (forbidIndexation !== true && url) {
|
||||||
tagsStr += `<link rel="canonical" href="${url}" />`
|
tagsStr += `<link rel="canonical" href="${url}" />`
|
||||||
|
@ -261,6 +261,6 @@ export class TagsHtml {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static buildEscapedTruncatedDescription (description: string) {
|
static buildEscapedTruncatedDescription (description: string) {
|
||||||
return truncate(mdToOneLinePlainText(description), { length: 200 })
|
return truncate(mdToPlainText(description), { length: 200 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8126,6 +8126,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"
|
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
|
||||||
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
|
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:
|
node-int64@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||||
|
|
Loading…
Reference in New Issue