]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/custom-validators/activitypub/videos.ts
Add latency setting support
[github/Chocobozzz/PeerTube.git] / server / helpers / custom-validators / activitypub / videos.ts
1 import validator from 'validator'
2 import { logger } from '@server/helpers/logger'
3 import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
4 import { LiveVideoLatencyMode, VideoState } from '../../../../shared/models/videos'
5 import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
6 import { peertubeTruncate } from '../../core-utils'
7 import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
8 import { isLiveLatencyModeValid } from '../video-lives'
9 import {
10 isVideoDurationValid,
11 isVideoNameValid,
12 isVideoStateValid,
13 isVideoTagValid,
14 isVideoTruncatedDescriptionValid,
15 isVideoViewsValid
16 } from '../videos'
17 import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
18
19 function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
20 return isBaseActivityValid(activity, 'Update') &&
21 sanitizeAndCheckVideoTorrentObject(activity.object)
22 }
23
24 function isActivityPubVideoDurationValid (value: string) {
25 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
26 return exists(value) &&
27 typeof value === 'string' &&
28 value.startsWith('PT') &&
29 value.endsWith('S') &&
30 isVideoDurationValid(value.replace(/[^0-9]+/g, ''))
31 }
32
33 function sanitizeAndCheckVideoTorrentObject (video: any) {
34 if (!video || video.type !== 'Video') return false
35
36 if (!setValidRemoteTags(video)) {
37 logger.debug('Video has invalid tags', { video })
38 return false
39 }
40 if (!setValidRemoteVideoUrls(video)) {
41 logger.debug('Video has invalid urls', { video })
42 return false
43 }
44 if (!setRemoteVideoTruncatedContent(video)) {
45 logger.debug('Video has invalid content', { video })
46 return false
47 }
48 if (!setValidAttributedTo(video)) {
49 logger.debug('Video has invalid attributedTo', { video })
50 return false
51 }
52 if (!setValidRemoteCaptions(video)) {
53 logger.debug('Video has invalid captions', { video })
54 return false
55 }
56 if (!setValidRemoteIcon(video)) {
57 logger.debug('Video has invalid icons', { video })
58 return false
59 }
60
61 // Default attributes
62 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
63 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
64 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
65 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
66 if (!isBooleanValid(video.isLiveBroadcast)) video.isLiveBroadcast = false
67 if (!isBooleanValid(video.liveSaveReplay)) video.liveSaveReplay = false
68 if (!isBooleanValid(video.permanentLive)) video.permanentLive = false
69 if (!isLiveLatencyModeValid(video.latencyMode)) video.latencyMode = LiveVideoLatencyMode.DEFAULT
70
71 return isActivityPubUrlValid(video.id) &&
72 isVideoNameValid(video.name) &&
73 isActivityPubVideoDurationValid(video.duration) &&
74 isUUIDValid(video.uuid) &&
75 (!video.category || isRemoteNumberIdentifierValid(video.category)) &&
76 (!video.licence || isRemoteNumberIdentifierValid(video.licence)) &&
77 (!video.language || isRemoteStringIdentifierValid(video.language)) &&
78 isVideoViewsValid(video.views) &&
79 isBooleanValid(video.sensitive) &&
80 isDateValid(video.published) &&
81 isDateValid(video.updated) &&
82 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
83 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
84 video.attributedTo.length !== 0
85 }
86
87 function isRemoteVideoUrlValid (url: any) {
88 return url.type === 'Link' &&
89 // Video file link
90 (
91 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
92 isActivityPubUrlValid(url.href) &&
93 validator.isInt(url.height + '', { min: 0 }) &&
94 validator.isInt(url.size + '', { min: 0 }) &&
95 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
96 ) ||
97 // Torrent link
98 (
99 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
100 isActivityPubUrlValid(url.href) &&
101 validator.isInt(url.height + '', { min: 0 })
102 ) ||
103 // Magnet link
104 (
105 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
106 validator.isLength(url.href, { min: 5 }) &&
107 validator.isInt(url.height + '', { min: 0 })
108 ) ||
109 // HLS playlist link
110 (
111 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
112 isActivityPubUrlValid(url.href) &&
113 isArray(url.tag)
114 ) ||
115 isAPVideoTrackerUrlObject(url) ||
116 isAPVideoFileUrlMetadataObject(url)
117 }
118
119 function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
120 return url &&
121 url.type === 'Link' &&
122 url.mediaType === 'application/json' &&
123 isArray(url.rel) && url.rel.includes('metadata')
124 }
125
126 function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
127 return isArray(url.rel) &&
128 url.rel.includes('tracker') &&
129 isActivityPubUrlValid(url.href)
130 }
131
132 // ---------------------------------------------------------------------------
133
134 export {
135 sanitizeAndCheckVideoTorrentUpdateActivity,
136 isRemoteStringIdentifierValid,
137 sanitizeAndCheckVideoTorrentObject,
138 isRemoteVideoUrlValid,
139 isAPVideoFileUrlMetadataObject,
140 isAPVideoTrackerUrlObject
141 }
142
143 // ---------------------------------------------------------------------------
144
145 function setValidRemoteTags (video: any) {
146 if (Array.isArray(video.tag) === false) return false
147
148 video.tag = video.tag.filter(t => {
149 return t.type === 'Hashtag' &&
150 isVideoTagValid(t.name)
151 })
152
153 return true
154 }
155
156 function setValidRemoteCaptions (video: any) {
157 if (!video.subtitleLanguage) video.subtitleLanguage = []
158
159 if (Array.isArray(video.subtitleLanguage) === false) return false
160
161 video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
162 if (!isActivityPubUrlValid(caption.url)) caption.url = null
163
164 return isRemoteStringIdentifierValid(caption)
165 })
166
167 return true
168 }
169
170 function isRemoteNumberIdentifierValid (data: any) {
171 return validator.isInt(data.identifier, { min: 0 })
172 }
173
174 function isRemoteStringIdentifierValid (data: any) {
175 return typeof data.identifier === 'string'
176 }
177
178 function isRemoteVideoContentValid (mediaType: string, content: string) {
179 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
180 }
181
182 function setValidRemoteIcon (video: any) {
183 if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
184 if (!video.icon) video.icon = []
185
186 video.icon = video.icon.filter(icon => {
187 return icon.type === 'Image' &&
188 isActivityPubUrlValid(icon.url) &&
189 icon.mediaType === 'image/jpeg' &&
190 validator.isInt(icon.width + '', { min: 0 }) &&
191 validator.isInt(icon.height + '', { min: 0 })
192 })
193
194 return video.icon.length !== 0
195 }
196
197 function setValidRemoteVideoUrls (video: any) {
198 if (Array.isArray(video.url) === false) return false
199
200 video.url = video.url.filter(u => isRemoteVideoUrlValid(u))
201
202 return true
203 }
204
205 function setRemoteVideoTruncatedContent (video: any) {
206 if (video.content) {
207 video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max })
208 }
209
210 return true
211 }