Merge branch 'main' into documentation

This commit is contained in:
John Livingston 2023-07-06 14:11:28 +02:00
commit c01b3c1733
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 290 additions and 50 deletions

View File

@ -5,6 +5,7 @@
## New Features
* Implementing the [FEP-1970](https://codeberg.org/fediverse/fep/src/branch/main/fep/1970/fep-1970.md) draft for ActivityPub chat declaration.
* Podcast RSS feed support (thanks to [Alecks Gates](https://github.com/agates)).
## 7.1.0

143
package-lock.json generated
View File

@ -17,7 +17,8 @@
"validate-color": "^2.2.1"
},
"devDependencies": {
"@peertube/peertube-types": "^5.1.0",
"@peertube/feed": "^5.1.0",
"@peertube/peertube-types": "^5.2.0",
"@tsconfig/node12": "^1.0.9",
"@types/async": "^3.2.9",
"@types/express": "^4.17.13",
@ -2557,16 +2558,29 @@
"node": ">=14"
}
},
"node_modules/@peertube/peertube-types": {
"node_modules/@peertube/feed": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@peertube/peertube-types/-/peertube-types-5.1.0.tgz",
"integrity": "sha512-n0FMlKzHae/HuBXXeUd5nWUmBN+BMiyRkwftRquFqyQObwTwltqUooL+DBcjetFsRmPTObvF4kPccQ7LTLqkXQ==",
"resolved": "https://registry.npmjs.org/@peertube/feed/-/feed-5.1.0.tgz",
"integrity": "sha512-ggwIbjxh4oc1aAGYV7ZxtIpiEIGq3Rkg6FxvOSrk/EPZ76rExoIJCjKeSyd4zb/sGkyKldy+bGs1OUUVidWWTQ==",
"dev": true,
"dependencies": {
"xml-js": "^1.6.11"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@peertube/peertube-types": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@peertube/peertube-types/-/peertube-types-5.2.0.tgz",
"integrity": "sha512-t5o/W4cF+E8FJXvFKBuGuCGU01Ad7jodyO7go//UYzgTde4CpxVJIE4G/8fKB30Kl0GCtqm2gncj31gilxZJ0g==",
"dev": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.190.0",
"@node-oauth/oauth2-server": "^4.2.0",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/sdk-metrics": "^1.8.0",
"@peertube/feed": "^5.1.0",
"@types/bluebird": "^3.5.33",
"@types/express": "4.17.9",
"@types/fluent-ffmpeg": "^2.1.16",
@ -2581,25 +2595,25 @@
"bullmq": "^3.6.6",
"execa": "^5.1.1",
"express": "^4.18.1",
"express-validator": "^6.4.0",
"express-validator": "^7.0.1",
"fluent-ffmpeg": "^2.1.0",
"fs-extra": "^11.1.0",
"got": "^11.8.2",
"hpagent": "^1.0.0",
"ioredis": "^5.2.3",
"lru-cache": "^7.13.0",
"lru-cache": "^9.1.1",
"memoizee": "^0.4.14",
"multer": "^1.4.5-lts.1",
"parse-torrent": "^9.1.0",
"parse-torrent": "^9",
"reflect-metadata": "^0.1.12",
"sequelize": "6.29.0",
"sequelize": "6.31.1",
"sequelize-typescript": "^2.0.0-beta.1",
"short-uuid": "^4.2.0",
"socket.io": "^4.5.4",
"winston": "3.8.2"
},
"engines": {
"node": ">=12.x",
"node": ">=16.x",
"yarn": ">=1.x"
}
},
@ -2622,12 +2636,12 @@
"dev": true
},
"node_modules/@peertube/peertube-types/node_modules/lru-cache": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz",
"integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==",
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz",
"integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==",
"dev": true,
"engines": {
"node": ">=12"
"node": "14 || >=16.14"
}
},
"node_modules/@sindresorhus/is": {
@ -5736,13 +5750,13 @@
}
},
"node_modules/express-validator": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz",
"integrity": "sha512-ZWHJfnRgePp3FKRSKMtnZVnD1s8ZchWD+jSl7UMseGIqhweCo1Z9916/xXBbJAa6PrA3pUZfkOvIsHZG4ZtIMw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz",
"integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.21",
"validator": "^13.7.0"
"validator": "^13.9.0"
},
"engines": {
"node": ">= 8.0.0"
@ -8804,6 +8818,12 @@
"node": ">=8.9.0"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -8844,9 +8864,9 @@
"dev": true
},
"node_modules/sequelize": {
"version": "6.29.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.29.0.tgz",
"integrity": "sha512-m8Wi90rs3NZP9coXE52c7PL4Q078nwYZXqt1IxPvgki7nOFn0p/F0eKsYDBXCPw9G8/BCEa6zZNk0DQUAT4ypA==",
"version": "6.31.1",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.1.tgz",
"integrity": "sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==",
"dev": true,
"funding": [
{
@ -10276,9 +10296,9 @@
}
},
"node_modules/validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz",
"integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==",
"dev": true,
"engines": {
"node": ">= 0.10"
@ -10441,6 +10461,18 @@
}
}
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dev": true,
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -12611,16 +12643,26 @@
"integrity": "sha512-hO+bdeGOlJwqowUBoZF5LyP3ORUFOP1G0GRv8N45W/cztXbT2ZEXaAzfokRS9Xc9FWmYrDj32mF6SzH6wuoIyA==",
"dev": true
},
"@peertube/peertube-types": {
"@peertube/feed": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@peertube/peertube-types/-/peertube-types-5.1.0.tgz",
"integrity": "sha512-n0FMlKzHae/HuBXXeUd5nWUmBN+BMiyRkwftRquFqyQObwTwltqUooL+DBcjetFsRmPTObvF4kPccQ7LTLqkXQ==",
"resolved": "https://registry.npmjs.org/@peertube/feed/-/feed-5.1.0.tgz",
"integrity": "sha512-ggwIbjxh4oc1aAGYV7ZxtIpiEIGq3Rkg6FxvOSrk/EPZ76rExoIJCjKeSyd4zb/sGkyKldy+bGs1OUUVidWWTQ==",
"dev": true,
"requires": {
"xml-js": "^1.6.11"
}
},
"@peertube/peertube-types": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@peertube/peertube-types/-/peertube-types-5.2.0.tgz",
"integrity": "sha512-t5o/W4cF+E8FJXvFKBuGuCGU01Ad7jodyO7go//UYzgTde4CpxVJIE4G/8fKB30Kl0GCtqm2gncj31gilxZJ0g==",
"dev": true,
"requires": {
"@aws-sdk/client-s3": "^3.190.0",
"@node-oauth/oauth2-server": "^4.2.0",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/sdk-metrics": "^1.8.0",
"@peertube/feed": "^5.1.0",
"@types/bluebird": "^3.5.33",
"@types/express": "4.17.9",
"@types/fluent-ffmpeg": "^2.1.16",
@ -12635,18 +12677,18 @@
"bullmq": "^3.6.6",
"execa": "^5.1.1",
"express": "^4.18.1",
"express-validator": "^6.4.0",
"express-validator": "^7.0.1",
"fluent-ffmpeg": "^2.1.0",
"fs-extra": "^11.1.0",
"got": "^11.8.2",
"hpagent": "^1.0.0",
"ioredis": "^5.2.3",
"lru-cache": "^7.13.0",
"lru-cache": "^9.1.1",
"memoizee": "^0.4.14",
"multer": "^1.4.5-lts.1",
"parse-torrent": "^9.1.0",
"parse-torrent": "^9",
"reflect-metadata": "^0.1.12",
"sequelize": "6.29.0",
"sequelize": "6.31.1",
"sequelize-typescript": "^2.0.0-beta.1",
"short-uuid": "^4.2.0",
"socket.io": "^4.5.4",
@ -12672,9 +12714,9 @@
"dev": true
},
"lru-cache": {
"version": "7.14.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz",
"integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==",
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz",
"integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==",
"dev": true
}
}
@ -15053,13 +15095,13 @@
}
},
"express-validator": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.14.0.tgz",
"integrity": "sha512-ZWHJfnRgePp3FKRSKMtnZVnD1s8ZchWD+jSl7UMseGIqhweCo1Z9916/xXBbJAa6PrA3pUZfkOvIsHZG4ZtIMw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz",
"integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==",
"dev": true,
"requires": {
"lodash": "^4.17.21",
"validator": "^13.7.0"
"validator": "^13.9.0"
}
},
"ext": {
@ -17285,6 +17327,12 @@
"chokidar": ">=3.0.0 <4.0.0"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -17321,9 +17369,9 @@
}
},
"sequelize": {
"version": "6.29.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.29.0.tgz",
"integrity": "sha512-m8Wi90rs3NZP9coXE52c7PL4Q078nwYZXqt1IxPvgki7nOFn0p/F0eKsYDBXCPw9G8/BCEa6zZNk0DQUAT4ypA==",
"version": "6.31.1",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.1.tgz",
"integrity": "sha512-cahWtRrYLjqoZP/aurGBoaxn29qQCF4bxkAUPEQ/ozjJjt6mtL4Q113S3N39mQRmX5fgxRbli+bzZARP/N51eg==",
"dev": true,
"requires": {
"@types/debug": "^4.1.7",
@ -18403,9 +18451,9 @@
}
},
"validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz",
"integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==",
"dev": true
},
"vary": {
@ -18531,6 +18579,15 @@
"dev": true,
"requires": {}
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dev": true,
"requires": {
"sax": "^1.2.4"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -40,7 +40,8 @@
"validate-color": "^2.2.1"
},
"devDependencies": {
"@peertube/peertube-types": "^5.1.0",
"@peertube/feed": "^5.1.0",
"@peertube/peertube-types": "^5.2.0",
"@tsconfig/node12": "^1.0.9",
"@types/async": "^3.2.9",
"@types/express": "^4.17.13",

87
server/lib/rss/init.ts Normal file
View File

@ -0,0 +1,87 @@
import type { RegisterServerOptions, Video } from '@peertube/peertube-types'
import type { CustomTag } from '@peertube/feed/lib/typings'
import { videoHasWebchat } from '../../../shared/lib/video'
import { fillVideoCustomFields } from '../custom-fields'
import { getProsodyDomain } from '../prosody/config/domain'
import { getPublicChatUri } from '../uri/webchat'
async function initRSS (options: RegisterServerOptions): Promise<void> {
const logger = options.peertubeHelpers.logger
const registerHook = options.registerHook
logger.info('Registring RSS hooks...')
registerHook({
target: 'filter:feed.podcast.video.create-custom-tags.result',
handler: async (
result: CustomTag[], { video, liveItem }: { video: Video, liveItem: boolean }
): Promise<CustomTag[]> => {
if (!liveItem) {
// Note: the Podcast RSS feed specification does not handle chats for non-live.
// So we just return here.
return result
}
// FIXME: calling getSettings for each RSS entry is not optimal.
// Settings should be cached somewhere on the plugin level.
// (i already have some plans to do something for this)
const settings = await options.settingsManager.getSettings([
'chat-per-live-video',
'chat-all-lives',
'chat-all-non-lives',
'chat-videos-list',
'prosody-room-type',
'federation-dont-publish-remotely',
'prosody-room-allow-s2s',
'prosody-s2s-port'
])
if (settings['federation-dont-publish-remotely']) {
// Chat must not be published to the outer world.
return result
}
await fillVideoCustomFields(options, video)
const hasChat = await videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],
'chat-videos-list': settings['chat-videos-list'] as string
}, video)
if (!hasChat) {
logger.debug(`Video uuid=${video.uuid} has not livechat, no need to add podcast:chat tag.`)
return result
}
const prosodyDomain = await getProsodyDomain(options)
const podcastChat: any = {
name: 'podcast:chat',
attributes: {
server: prosodyDomain,
protocol: 'xmpp',
// space: will be added only if external XMPP connections are available
embedUrl: getPublicChatUri(options, video)
}
}
// In order to connect to the chat using standard xmpp, it requires these settings:
// - prosody-room-allow-s2s
// - prosody-s2s-port
if (settings['prosody-room-allow-s2s'] && settings['prosody-s2s-port']) {
let roomJID: string
if (settings['prosody-room-type'] === 'channel') {
roomJID = `channel.${video.channel.id}@room.${prosodyDomain}`
} else {
roomJID = `${video.uuid}@room.${prosodyDomain}`
}
podcastChat.attributes.space = roomJID
}
return result.concat([podcastChat])
}
})
}
export {
initRSS
}

View File

@ -1,5 +1,4 @@
import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-types'
import type { RegisterServerOptions, VideoObject, Video } from '@peertube/peertube-types'
import { getBaseRouterRoute, getBaseWebSocketRoute } from '../helpers'
import { canonicalizePluginUri } from './canonicalize'
@ -19,7 +18,7 @@ export function getWSS2SUri (options: RegisterServerOptions): string | undefined
return base + 'xmpp-websocket-s2s'
}
export function getPublicChatUri (options: RegisterServerOptions, video: VideoObject): string {
export function getPublicChatUri (options: RegisterServerOptions, video: VideoObject | Video): string {
const url = getBaseRouterRoute(options) + 'webchat/room/' + encodeURIComponent(video.uuid)
return canonicalizePluginUri(options, url, {
removePluginVersion: true

View File

@ -4,6 +4,7 @@ import { initSettings } from './lib/settings'
import { initCustomFields } from './lib/custom-fields'
import { initRouters } from './lib/routers/index'
import { initFederation } from './lib/federation/init'
import { initRSS } from './lib/rss/init'
import { prepareProsody, ensureProsodyRunning, ensureProsodyNotRunning } from './lib/prosody/ctl'
import { unloadDebugMode } from './lib/debug'
import { loadLoc } from './lib/loc'
@ -30,6 +31,7 @@ async function register (options: RegisterServerOptions): Promise<any> {
await initCustomFields(options)
await initRouters(options)
await initFederation(options)
await initRSS(options)
try {
await prepareProsody(options)

View File

@ -141,7 +141,7 @@ If you want to display the chat in a web page or in an iframe, here is what you
* get the Video ActivityPub object,
* if there is no `attachment` key, stop.
* loop through the `attachment` values (if `attachment is not an array, just iterate on this single value)
* loop through the `attachment` values (if `attachment` is not an array, just iterate on this single value)
* search for an entry with `rel` === `discussion`, and with `href` using the `https` scheme (that begins with `https://`)
* if found, open this href
@ -149,7 +149,7 @@ If you want to open the chat room using the XMPP protocol:
* get the Video ActivityPub object,
* if there is no `attachment` key, stop.
* loop through the `attachment` values (if `attachment is not an array, just iterate on this single value)
* loop through the `attachment` values (if `attachment` is not an array, just iterate on this single value)
* search for an entry with `rel` === `discussion`, and with `href` using the `xmpp` scheme (that begins with `xmpp://`)
* if found, open this xmpp uri with your client, or connect to the XMPP room at that address
@ -159,7 +159,9 @@ In the ActivityPub object, there is also a `peertubeLiveChat` entry.
Don't use the content of this entry. This is specific to the livechat plugin, and can be changed or removed in a near future.
It is currently required for some endpoint discovery.
### Using RSS
### Using Podcast RSS feed
The livechat plugin adds some data in Podcast RSS feeds under the [`<podcast:liveItem>`](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#live-item), so that the chat can be discovered for live streams.
{{% notice warning %}}
This part is not implemented yet, but should be available for v7.2.0 release.
@ -168,3 +170,94 @@ This part is not implemented yet, but should be available for v7.2.0 release.
{{% notice info %}}
This requires Peertube >= 5.2
{{% /notice %}}
{{% notice info %}}
The `<podcast:chat>` element is currently only supported for live streams.
{{% /notice %}}
This follows the [`<podcast:chat>`](https://github.com/Podcastindex-org/podcast-namespace/discussions/502) proposal.
{{% notice warning %}}
At the time of the writing, this proposal is in draft status, and the livechat plugin is a Proof-of-concept.
Until the proposal is adopted, the specification can change, and the livechat plugin will be adapted accordingly.
{{% /notice %}}
Basically, the chat will be declared as tag under on the `<podcast:liveItem>` element.
By default, here is an example of what you will get:
```xml
<podcast:liveItem status="live" start="2023-07-06T18:00:00.000Z">
<title>The video title</title>
<guid isPermaLink="false">e32b4890-983b-4ce5-8b46-f2d6bc1d8819_2023-07-06T18:00:00.000Z</guid>
<link>https://yourinstance.tld/videos/watch/8df24108-6e70-4fc8-b1cc-f2db7fcdd535</link>
<podcast:socialInteract
uri="https://yourinstance.tld/videos/watch/8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
protocol="activitypub"
accountUrl="https://yourinstance.tld/a/youraccount"
/>
<enclosure url="https://yourinstance.tld/path/to/video/master.m3u8" type="application/x-mpegURL" />
<podcast:alternateEnclosure type="application/x-mpegURL" lang="en" title="HLS" default="true">
<podcast:source uri="https://yourinstance.tld/path/to/video/master.m3u8" />
</podcast:alternateEnclosure>
<itunes:image href="https://yourinstance.tld/lazy-static/previews/8df24108-6e70-4fc8-b1cc-f2db7fcdd535.jpg" />
<podcast:chat
server="yourinstance.tld"
protocol="xmpp"
embedUrl="https://yourinstance.tld/plugins/livechat/router/webchat/room/8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
/>
</podcast:liveItem>
```
In case the instance has activated the
[external XMPP clients connection](/peertube-plugin-livechat/documentation/admin/advanced/xmpp_clients/) feature:
```xml
<podcast:liveItem status="live" start="2023-07-06T18:00:00.000Z">
<title>The video title</title>
<guid isPermaLink="false">e32b4890-983b-4ce5-8b46-f2d6bc1d8819_2023-07-06T18:00:00.000Z</guid>
<link>https://yourinstance.tld/videos/watch/8df24108-6e70-4fc8-b1cc-f2db7fcdd535</link>
<podcast:socialInteract
uri="https://yourinstance.tld/videos/watch/8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
protocol="activitypub"
accountUrl="https://yourinstance.tld/a/youraccount"
/>
<enclosure url="https://yourinstance.tld/path/to/video/master.m3u8" type="application/x-mpegURL" />
<podcast:alternateEnclosure type="application/x-mpegURL" lang="en" title="HLS" default="true">
<podcast:source uri="https://yourinstance.tld/path/to/video/master.m3u8" />
</podcast:alternateEnclosure>
<itunes:image href="https://yourinstance.tld/lazy-static/previews/8df24108-6e70-4fc8-b1cc-f2db7fcdd535.jpg" />
<podcast:chat
server="yourinstance.tld"
protocol="xmpp"
space="8df24108-6e70-4fc8-b1cc-f2db7fcdd535@room.yourinstance.tld"
embedUrl="https://yourinstance.tld/plugins/livechat/router/webchat/room/8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
/>
</podcast:liveItem>
```
#### Algorithm
If you want to display the chat in a web page or in an iframe, here is what you should do:
* get the Podcast RSS feed for the channel,
* if there is no `<podcast:liveItem>` element under the `<channel>`, stop.
* find the `<podcast:liveItem>` you are looking for
* `<podcast:socialInteract>` can be used to cross-reference the items with ActivityPub
* if there is no `<podcast:chat>` element under the `<podcast:liveItem>`, stop.
* loop through the `<podcast:chat>` values (if `<podcast:chat>` is not an array, just iterate on this single value)
* there should only be one, but you should expect to handle several just in case
* search for the first entry `protocol` === `xmpp` and an `embedUrl` attribute
* if found, open this embedUrl
If you want to open the chat room using the XMPP protocol:
* get the Podcast RSS feed for the channel,
* if there is no `<podcast:liveItem>` element under the `<channel>`, stop.
* find the `<podcast:liveItem>` you are looking for
* `<podcast:socialInteract>` can be used to cross-reference the items with ActivityPub
* loop through the `<podcast:chat>` values (if `<podcast:chat>` is not an array, just iterate on this single value)
* there should only be one, but you should expect to handle several just in case
* search for the first entry `protocol` === `xmpp` and a `space` attribute
* space should be an XMPP JID for a MUC
* if found, open this XMPP JID with your client after converting it to a join URI, or connect to the XMPP room at that address