Chat federation: storing chat information
This commit is contained in:
parent
850ea3e61f
commit
5028d37c18
@ -1,19 +1,24 @@
|
|||||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||||
import type { RemoteVideoHandlerParams } from './types'
|
import type { RemoteVideoHandlerParams } from './types'
|
||||||
|
import { storeVideoLiveChatInfos } from './storage'
|
||||||
|
import { sanitizePeertubeLiveChatInfos } from './sanitize'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function reads incoming ActivityPub data, to detect LiveChat informations.
|
||||||
|
* @param options server options
|
||||||
|
* @param param1 handler parameters
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
async function readIncomingAPVideo (
|
async function readIncomingAPVideo (
|
||||||
options: RegisterServerOptions,
|
options: RegisterServerOptions,
|
||||||
{ video, videoAPObject }: RemoteVideoHandlerParams
|
{ video, videoAPObject }: RemoteVideoHandlerParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!('peertubeLiveChat' in videoAPObject)) {
|
let peertubeLiveChat = ('peertubeLiveChat' in videoAPObject) ? videoAPObject.peertubeLiveChat : false
|
||||||
return
|
|
||||||
}
|
// We must sanitize peertubeLiveChat, as it comes for the outer world.
|
||||||
const logger = options.peertubeHelpers.logger
|
peertubeLiveChat = sanitizePeertubeLiveChatInfos(peertubeLiveChat)
|
||||||
// TODO: save the information.
|
|
||||||
logger.debug(
|
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
|
||||||
`Remote video uuid=${video.uuid} has a peertubeLiveChat attribute: ` +
|
|
||||||
JSON.stringify(videoAPObject.peertubeLiveChat)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-types'
|
import type { RegisterServerOptions, VideoObject } from '@peertube/peertube-types'
|
||||||
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink } from './types'
|
import type { LiveChatVideoObject, VideoBuildResultContext, LiveChatJSONLDLink, LiveChatJSONLDAttribute } from './types'
|
||||||
|
import { storeVideoLiveChatInfos } from './storage'
|
||||||
import { videoHasWebchat } from '../../../shared/lib/video'
|
import { videoHasWebchat } from '../../../shared/lib/video'
|
||||||
import { getBoshUri, getWSUri } from '../uri/webchat'
|
import { getBoshUri, getWSUri } from '../uri/webchat'
|
||||||
import { canonicalizePluginUri } from '../uri/canonicalize'
|
import { canonicalizePluginUri } from '../uri/canonicalize'
|
||||||
import { getProsodyDomain } from '../prosody/config/domain'
|
import { getProsodyDomain } from '../prosody/config/domain'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function adds LiveChat information on video ActivityPub data if relevant.
|
||||||
|
* @param options server options
|
||||||
|
* @param jsonld JSON-LD video data to fill
|
||||||
|
* @param context handler context
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
async function videoBuildJSONLD (
|
async function videoBuildJSONLD (
|
||||||
options: RegisterServerOptions,
|
options: RegisterServerOptions,
|
||||||
jsonld: VideoObject,
|
jsonld: VideoObject,
|
||||||
@ -36,8 +44,12 @@ async function videoBuildJSONLD (
|
|||||||
}, video)
|
}, video)
|
||||||
|
|
||||||
if (!hasChat) {
|
if (!hasChat) {
|
||||||
// logger.debug(`Video uuid=${video.uuid} has not livechat, adding peertubeLiveChat=false.`)
|
logger.debug(`Video uuid=${video.uuid} has not livechat, adding peertubeLiveChat=false.`)
|
||||||
(jsonld as LiveChatVideoObject).peertubeLiveChat = false
|
// Note: we store also outgoing data. Could help for migration/cleanup scripts, for example.
|
||||||
|
await storeVideoLiveChatInfos(options, video, false)
|
||||||
|
Object.assign(jsonld, {
|
||||||
|
peertubeLiveChat: false
|
||||||
|
})
|
||||||
return jsonld
|
return jsonld
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +83,16 @@ async function videoBuildJSONLD (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(jsonld as LiveChatVideoObject).peertubeLiveChat = {
|
const peertubeLiveChat: LiveChatJSONLDAttribute = {
|
||||||
type: 'xmpp',
|
type: 'xmpp',
|
||||||
jid: roomJID,
|
jid: roomJID,
|
||||||
links
|
links
|
||||||
}
|
}
|
||||||
|
Object.assign(jsonld, {
|
||||||
|
peertubeLiveChat
|
||||||
|
})
|
||||||
|
// Note: we store also outgoing data. Could help for migration/cleanup scripts, for example.
|
||||||
|
await storeVideoLiveChatInfos(options, video, peertubeLiveChat)
|
||||||
return jsonld
|
return jsonld
|
||||||
}
|
}
|
||||||
|
|
||||||
|
81
server/lib/federation/sanitize.ts
Normal file
81
server/lib/federation/sanitize.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { LiveChatJSONLDInfos, LiveChatJSONLDAttribute } from './types'
|
||||||
|
import { URL } from 'url'
|
||||||
|
|
||||||
|
function sanitizePeertubeLiveChatInfos (chatInfos: any): LiveChatJSONLDAttribute {
|
||||||
|
if (chatInfos === false) { return false }
|
||||||
|
if (typeof chatInfos !== 'object') { return false }
|
||||||
|
|
||||||
|
if (chatInfos.type !== 'xmpp') { return false }
|
||||||
|
if ((typeof chatInfos.jid) !== 'string') { return false }
|
||||||
|
if (!Array.isArray(chatInfos.links)) { return false }
|
||||||
|
|
||||||
|
const r: LiveChatJSONLDInfos = {
|
||||||
|
type: chatInfos.type,
|
||||||
|
jid: chatInfos.jid,
|
||||||
|
links: []
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of chatInfos.links) {
|
||||||
|
if ((typeof link) !== 'object') { continue }
|
||||||
|
if (['xmpp-bosh-anonymous', 'xmpp-websocket-anonymous'].includes(link.type)) {
|
||||||
|
if ((typeof link.jid) !== 'string') { continue }
|
||||||
|
if ((typeof link.url) !== 'string') { continue }
|
||||||
|
|
||||||
|
if (
|
||||||
|
!_validUrl(link.url, {
|
||||||
|
noSearchParams: true,
|
||||||
|
protocol: link.type === 'xmpp-websocket-anonymous' ? 'ws.' : 'http.'
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.links.push({
|
||||||
|
type: link.type,
|
||||||
|
jid: link.jid,
|
||||||
|
url: link.url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
interface URLConstraints {
|
||||||
|
protocol: 'http.' | 'ws.'
|
||||||
|
noSearchParams: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function _validUrl (s: string, constraints: URLConstraints): boolean {
|
||||||
|
if ((typeof s) !== 'string') { return false }
|
||||||
|
if (s === '') { return false }
|
||||||
|
let url: URL
|
||||||
|
try {
|
||||||
|
url = new URL(s)
|
||||||
|
} catch (_err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.protocol) {
|
||||||
|
if (constraints.protocol === 'http.') {
|
||||||
|
if (url.protocol !== 'https' && url.protocol !== 'http') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if (constraints.protocol === 'ws.') {
|
||||||
|
if (url.protocol !== 'wss' && url.protocol !== 'ws') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (constraints.noSearchParams) {
|
||||||
|
if (url.search !== '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
sanitizePeertubeLiveChatInfos
|
||||||
|
}
|
128
server/lib/federation/storage.ts
Normal file
128
server/lib/federation/storage.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import type { RegisterServerOptions, MVideoFullLight, MVideoAP } from '@peertube/peertube-types'
|
||||||
|
import type { LiveChatJSONLDAttribute } from './types'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
/*
|
||||||
|
Important Note: we could store these data in database. For example by using storageManager.storeData.
|
||||||
|
But I'm afraid there might be write concurrency issues, or performance issues.
|
||||||
|
Indeed, Peertube storageManager stores everything in one JSON attribute on the plugin tuple.
|
||||||
|
|
||||||
|
Moreover, remote instance can create many data, that will not be cleaned.
|
||||||
|
|
||||||
|
So, instead of using storageManager, or using a custom DB table, we will store data in files.
|
||||||
|
If a file exists, it means the video has a chat.
|
||||||
|
The file itself contains the JSON LiveChatInfos object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function stores remote LiveChat infos that are contained in ActivityPub objects.
|
||||||
|
* We store these data for remotes videos.
|
||||||
|
*
|
||||||
|
* But we also store the data for local videos, so we can know what data we send to other Peertube instances.
|
||||||
|
* This is not really used for now, but can help if we want, one day, to know which videos we must refresh on remote
|
||||||
|
* instances.
|
||||||
|
* @param options server options
|
||||||
|
* @param video video object
|
||||||
|
* @param liveChatInfos video ActivityPub data to read
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
async function storeVideoLiveChatInfos (
|
||||||
|
options: RegisterServerOptions,
|
||||||
|
video: MVideoFullLight | MVideoAP,
|
||||||
|
liveChatInfos: LiveChatJSONLDAttribute
|
||||||
|
): Promise<void> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
|
||||||
|
const remote = video.remote
|
||||||
|
const filePath = await _getFilePath(options, remote, video.uuid, video.url)
|
||||||
|
if (!filePath) {
|
||||||
|
logger.error('Cant compute the file path for storing liveChat infos for video ' + video.uuid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Video ${video.uuid} data should be stored in ${filePath}`)
|
||||||
|
|
||||||
|
if (!liveChatInfos) {
|
||||||
|
logger.debug(`${remote ? 'Remote' : 'Local'} video ${video.uuid} has no chat infos, removing if necessary`)
|
||||||
|
await _del(options, filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`${remote ? 'Remote' : 'Local'} video ${video.uuid} has caht infos to store`)
|
||||||
|
await _store(options, filePath, liveChatInfos)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _getFilePath (
|
||||||
|
options: RegisterServerOptions,
|
||||||
|
remote: boolean,
|
||||||
|
uuid: string,
|
||||||
|
videoUrl: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
try {
|
||||||
|
// some sanitization, just in case...
|
||||||
|
if (!/^(\w|-)+$/.test(uuid)) {
|
||||||
|
logger.error(`Video uuid seems not correct: ${uuid}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let subFolders: string[]
|
||||||
|
if (remote) {
|
||||||
|
const u = new URL(videoUrl)
|
||||||
|
const host = u.hostname
|
||||||
|
|
||||||
|
if (host.includes('..')) {
|
||||||
|
// to prevent exploits that could go outside the plugin data dir.
|
||||||
|
logger.error(`Video host seems not correct, contains ..: ${host}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
subFolders = ['remote', host]
|
||||||
|
} else {
|
||||||
|
subFolders = ['local']
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(
|
||||||
|
options.peertubeHelpers.plugin.getDataDirectoryPath(),
|
||||||
|
'videoInfos',
|
||||||
|
...subFolders,
|
||||||
|
uuid + '.json'
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _del (options: RegisterServerOptions, filePath: string): Promise<void> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) { return }
|
||||||
|
logger.info('Deleting file ' + filePath)
|
||||||
|
fs.rmSync(filePath)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _store (options: RegisterServerOptions, filePath: string, content: any): Promise<void> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
const dir = path.dirname(filePath)
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.promises.writeFile(filePath, JSON.stringify(content), {
|
||||||
|
encoding: 'utf-8'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
storeVideoLiveChatInfos
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user