aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/activitypub/inbox.ts30
-rw-r--r--server/controllers/activitypub/pods.ts138
-rw-r--r--server/controllers/activitypub/videos.ts928
-rw-r--r--server/controllers/api/videos/index.ts30
-rw-r--r--server/helpers/activitypub.ts45
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts34
-rw-r--r--server/helpers/custom-validators/activitypub/index.ts1
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts12
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts223
-rw-r--r--server/helpers/custom-validators/index.ts1
-rw-r--r--server/helpers/custom-validators/video-authors.ts45
-rw-r--r--server/helpers/custom-validators/videos.ts16
-rw-r--r--server/helpers/requests.ts11
-rw-r--r--server/initializers/constants.ts56
-rw-r--r--server/lib/activitypub/misc.ts77
-rw-r--r--server/lib/activitypub/process-add.ts72
-rw-r--r--server/lib/activitypub/process-create.ts104
-rw-r--r--server/lib/activitypub/process-update.ts127
-rw-r--r--server/middlewares/validators/activitypub/activity.ts21
-rw-r--r--server/middlewares/validators/activitypub/videos.ts61
-rw-r--r--server/models/video/video-channel-interface.ts6
-rw-r--r--server/models/video/video-channel.ts35
-rw-r--r--server/models/video/video-interface.ts11
-rw-r--r--server/models/video/video.ts24
-rw-r--r--shared/models/activitypub/activity.ts9
-rw-r--r--shared/models/activitypub/objects/video-channel-object.ts5
-rw-r--r--shared/models/activitypub/objects/video-torrent-object.ts1
27 files changed, 1038 insertions, 1085 deletions
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index 79d989c2c..eee217650 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -1,26 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2 2import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, ActivityType, RootActivity } from '../../../shared'
3import {
4 processCreateActivity,
5 processUpdateActivity,
6 processFlagActivity
7} from '../../lib'
8import {
9 Activity,
10 ActivityType,
11 RootActivity,
12 ActivityPubCollection,
13 ActivityPubOrderedCollection
14} from '../../../shared'
15import {
16 signatureValidator,
17 checkSignature,
18 asyncMiddleware
19} from '../../middlewares'
20import { logger } from '../../helpers' 3import { logger } from '../../helpers'
4import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
5import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib'
6import { processAddActivity } from '../../lib/activitypub/process-add'
7import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares'
8import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
21 9
22const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { 10const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
23 Create: processCreateActivity, 11 Create: processCreateActivity,
12 Add: processAddActivity,
24 Update: processUpdateActivity, 13 Update: processUpdateActivity,
25 Flag: processFlagActivity 14 Flag: processFlagActivity
26} 15}
@@ -30,7 +19,7 @@ const inboxRouter = express.Router()
30inboxRouter.post('/', 19inboxRouter.post('/',
31 signatureValidator, 20 signatureValidator,
32 asyncMiddleware(checkSignature), 21 asyncMiddleware(checkSignature),
33 // inboxValidator, 22 activityPubValidator,
34 asyncMiddleware(inboxController) 23 asyncMiddleware(inboxController)
35) 24)
36 25
@@ -54,6 +43,9 @@ async function inboxController (req: express.Request, res: express.Response, nex
54 activities = [ rootActivity as Activity ] 43 activities = [ rootActivity as Activity ]
55 } 44 }
56 45
46 // Only keep activities we are able to process
47 activities = activities.filter(a => isActivityValid(a))
48
57 await processActivities(activities) 49 await processActivities(activities)
58 50
59 res.status(204).end() 51 res.status(204).end()
diff --git a/server/controllers/activitypub/pods.ts b/server/controllers/activitypub/pods.ts
index 326eb61ac..6cce57c1c 100644
--- a/server/controllers/activitypub/pods.ts
+++ b/server/controllers/activitypub/pods.ts
@@ -1,69 +1,69 @@
1import * as express from 'express' 1// import * as express from 'express'
2 2//
3import { database as db } from '../../../initializers/database' 3// import { database as db } from '../../../initializers/database'
4import { 4// import {
5 checkSignature, 5// checkSignature,
6 signatureValidator, 6// signatureValidator,
7 setBodyHostPort, 7// setBodyHostPort,
8 remotePodsAddValidator, 8// remotePodsAddValidator,
9 asyncMiddleware 9// asyncMiddleware
10} from '../../../middlewares' 10// } from '../../../middlewares'
11import { sendOwnedDataToPod } from '../../../lib' 11// import { sendOwnedDataToPod } from '../../../lib'
12import { getMyPublicCert, getFormattedObjects } from '../../../helpers' 12// import { getMyPublicCert, getFormattedObjects } from '../../../helpers'
13import { CONFIG } from '../../../initializers' 13// import { CONFIG } from '../../../initializers'
14import { PodInstance } from '../../../models' 14// import { PodInstance } from '../../../models'
15import { PodSignature, Pod as FormattedPod } from '../../../../shared' 15// import { PodSignature, Pod as FormattedPod } from '../../../../shared'
16 16//
17const remotePodsRouter = express.Router() 17// const remotePodsRouter = express.Router()
18 18//
19remotePodsRouter.post('/remove', 19// remotePodsRouter.post('/remove',
20 signatureValidator, 20// signatureValidator,
21 checkSignature, 21// checkSignature,
22 asyncMiddleware(removePods) 22// asyncMiddleware(removePods)
23) 23// )
24 24//
25remotePodsRouter.post('/list', 25// remotePodsRouter.post('/list',
26 asyncMiddleware(remotePodsList) 26// asyncMiddleware(remotePodsList)
27) 27// )
28 28//
29remotePodsRouter.post('/add', 29// remotePodsRouter.post('/add',
30 setBodyHostPort, // We need to modify the host before running the validator! 30// setBodyHostPort, // We need to modify the host before running the validator!
31 remotePodsAddValidator, 31// remotePodsAddValidator,
32 asyncMiddleware(addPods) 32// asyncMiddleware(addPods)
33) 33// )
34 34//
35// --------------------------------------------------------------------------- 35// // ---------------------------------------------------------------------------
36 36//
37export { 37// export {
38 remotePodsRouter 38// remotePodsRouter
39} 39// }
40 40//
41// --------------------------------------------------------------------------- 41// // ---------------------------------------------------------------------------
42 42//
43async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { 43// async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) {
44 const information = req.body 44// const information = req.body
45 45//
46 const pod = db.Pod.build(information) 46// const pod = db.Pod.build(information)
47 const podCreated = await pod.save() 47// const podCreated = await pod.save()
48 48//
49 await sendOwnedDataToPod(podCreated.id) 49// await sendOwnedDataToPod(podCreated.id)
50 50//
51 const cert = await getMyPublicCert() 51// const cert = await getMyPublicCert()
52 return res.json({ cert, email: CONFIG.ADMIN.EMAIL }) 52// return res.json({ cert, email: CONFIG.ADMIN.EMAIL })
53} 53// }
54 54//
55async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) { 55// async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) {
56 const pods = await db.Pod.list() 56// const pods = await db.Pod.list()
57 57//
58 return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length)) 58// return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length))
59} 59// }
60 60//
61async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { 61// async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) {
62 const signature: PodSignature = req.body.signature 62// const signature: PodSignature = req.body.signature
63 const host = signature.host 63// const host = signature.host
64 64//
65 const pod = await db.Pod.loadByHost(host) 65// const pod = await db.Pod.loadByHost(host)
66 await pod.destroy() 66// await pod.destroy()
67 67//
68 return res.type('json').status(204).end() 68// return res.type('json').status(204).end()
69} 69// }
diff --git a/server/controllers/activitypub/videos.ts b/server/controllers/activitypub/videos.ts
index cba47f0a1..9a1868ff7 100644
--- a/server/controllers/activitypub/videos.ts
+++ b/server/controllers/activitypub/videos.ts
@@ -1,589 +1,339 @@
1import * as express from 'express' 1// import * as express from 'express'
2import * as Bluebird from 'bluebird' 2// import * as Bluebird from 'bluebird'
3import * as Sequelize from 'sequelize' 3// import * as Sequelize from 'sequelize'
4 4//
5import { database as db } from '../../../initializers/database' 5// import { database as db } from '../../../initializers/database'
6import { 6// import {
7 REQUEST_ENDPOINT_ACTIONS, 7// REQUEST_ENDPOINT_ACTIONS,
8 REQUEST_ENDPOINTS, 8// REQUEST_ENDPOINTS,
9 REQUEST_VIDEO_EVENT_TYPES, 9// REQUEST_VIDEO_EVENT_TYPES,
10 REQUEST_VIDEO_QADU_TYPES 10// REQUEST_VIDEO_QADU_TYPES
11} from '../../../initializers' 11// } from '../../../initializers'
12import { 12// import {
13 checkSignature, 13// checkSignature,
14 signatureValidator, 14// signatureValidator,
15 remoteVideosValidator, 15// remoteVideosValidator,
16 remoteQaduVideosValidator, 16// remoteQaduVideosValidator,
17 remoteEventsVideosValidator 17// remoteEventsVideosValidator
18} from '../../../middlewares' 18// } from '../../../middlewares'
19import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers' 19// import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers'
20import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib' 20// import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib'
21import { PodInstance, VideoFileInstance } from '../../../models' 21// import { PodInstance, VideoFileInstance } from '../../../models'
22import { 22// import {
23 RemoteVideoRequest, 23// RemoteVideoRequest,
24 RemoteVideoCreateData, 24// RemoteVideoCreateData,
25 RemoteVideoUpdateData, 25// RemoteVideoUpdateData,
26 RemoteVideoRemoveData, 26// RemoteVideoRemoveData,
27 RemoteVideoReportAbuseData, 27// RemoteVideoReportAbuseData,
28 RemoteQaduVideoRequest, 28// RemoteQaduVideoRequest,
29 RemoteQaduVideoData, 29// RemoteQaduVideoData,
30 RemoteVideoEventRequest, 30// RemoteVideoEventRequest,
31 RemoteVideoEventData, 31// RemoteVideoEventData,
32 RemoteVideoChannelCreateData, 32// RemoteVideoChannelCreateData,
33 RemoteVideoChannelUpdateData, 33// RemoteVideoChannelUpdateData,
34 RemoteVideoChannelRemoveData, 34// RemoteVideoChannelRemoveData,
35 RemoteVideoAuthorRemoveData, 35// RemoteVideoAuthorRemoveData,
36 RemoteVideoAuthorCreateData 36// RemoteVideoAuthorCreateData
37} from '../../../../shared' 37// } from '../../../../shared'
38import { VideoInstance } from '../../../models/video/video-interface' 38// import { VideoInstance } from '../../../models/video/video-interface'
39 39//
40const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 40// const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
41 41//
42// Functions to call when processing a remote request 42// // Functions to call when processing a remote request
43// FIXME: use RemoteVideoRequestType as id type 43// // FIXME: use RemoteVideoRequestType as id type
44const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} 44// const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {}
45functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper 45// functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper
46functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper 46// functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper
47functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper 47// functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper
48functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper 48// functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper
49functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper 49// functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper
50functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper 50// functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper
51functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper 51// functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper
52functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper 52// functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper
53functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper 53// functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper
54 54//
55const remoteVideosRouter = express.Router() 55// const remoteVideosRouter = express.Router()
56 56//
57remoteVideosRouter.post('/', 57// remoteVideosRouter.post('/',
58 signatureValidator, 58// signatureValidator,
59 checkSignature, 59// checkSignature,
60 remoteVideosValidator, 60// remoteVideosValidator,
61 remoteVideos 61// remoteVideos
62) 62// )
63 63//
64remoteVideosRouter.post('/qadu', 64// remoteVideosRouter.post('/qadu',
65 signatureValidator, 65// signatureValidator,
66 checkSignature, 66// checkSignature,
67 remoteQaduVideosValidator, 67// remoteQaduVideosValidator,
68 remoteVideosQadu 68// remoteVideosQadu
69) 69// )
70 70//
71remoteVideosRouter.post('/events', 71// remoteVideosRouter.post('/events',
72 signatureValidator, 72// signatureValidator,
73 checkSignature, 73// checkSignature,
74 remoteEventsVideosValidator, 74// remoteEventsVideosValidator,
75 remoteVideosEvents 75// remoteVideosEvents
76) 76// )
77 77//
78// --------------------------------------------------------------------------- 78// // ---------------------------------------------------------------------------
79 79//
80export { 80// export {
81 remoteVideosRouter 81// remoteVideosRouter
82} 82// }
83 83//
84// --------------------------------------------------------------------------- 84// // ---------------------------------------------------------------------------
85 85//
86function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 86// function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const requests: RemoteVideoRequest[] = req.body.data 87// const requests: RemoteVideoRequest[] = req.body.data
88 const fromPod = res.locals.secure.pod 88// const fromPod = res.locals.secure.pod
89 89//
90 // We need to process in the same order to keep consistency 90// // We need to process in the same order to keep consistency
91 Bluebird.each(requests, request => { 91// Bluebird.each(requests, request => {
92 const data = request.data 92// const data = request.data
93 93//
94 // Get the function we need to call in order to process the request 94// // Get the function we need to call in order to process the request
95 const fun = functionsHash[request.type] 95// const fun = functionsHash[request.type]
96 if (fun === undefined) { 96// if (fun === undefined) {
97 logger.error('Unknown remote request type %s.', request.type) 97// logger.error('Unknown remote request type %s.', request.type)
98 return 98// return
99 } 99// }
100 100//
101 return fun.call(this, data, fromPod) 101// return fun.call(this, data, fromPod)
102 }) 102// })
103 .catch(err => logger.error('Error managing remote videos.', err)) 103// .catch(err => logger.error('Error managing remote videos.', err))
104 104//
105 // Don't block the other pod 105// // Don't block the other pod
106 return res.type('json').status(204).end() 106// return res.type('json').status(204).end()
107} 107// }
108 108//
109function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { 109// function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) {
110 const requests: RemoteQaduVideoRequest[] = req.body.data 110// const requests: RemoteQaduVideoRequest[] = req.body.data
111 const fromPod = res.locals.secure.pod 111// const fromPod = res.locals.secure.pod
112 112//
113 Bluebird.each(requests, request => { 113// Bluebird.each(requests, request => {
114 const videoData = request.data 114// const videoData = request.data
115 115//
116 return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) 116// return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod)
117 }) 117// })
118 .catch(err => logger.error('Error managing remote videos.', err)) 118// .catch(err => logger.error('Error managing remote videos.', err))
119 119//
120 return res.type('json').status(204).end() 120// return res.type('json').status(204).end()
121} 121// }
122 122//
123function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { 123// function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) {
124 const requests: RemoteVideoEventRequest[] = req.body.data 124// const requests: RemoteVideoEventRequest[] = req.body.data
125 const fromPod = res.locals.secure.pod 125// const fromPod = res.locals.secure.pod
126 126//
127 Bluebird.each(requests, request => { 127// Bluebird.each(requests, request => {
128 const eventData = request.data 128// const eventData = request.data
129 129//
130 return processVideosEventsRetryWrapper(eventData, fromPod) 130// return processVideosEventsRetryWrapper(eventData, fromPod)
131 }) 131// })
132 .catch(err => logger.error('Error managing remote videos.', err)) 132// .catch(err => logger.error('Error managing remote videos.', err))
133 133//
134 return res.type('json').status(204).end() 134// return res.type('json').status(204).end()
135} 135// }
136 136//
137async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) { 137// async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) {
138 const options = { 138// const options = {
139 arguments: [ eventData, fromPod ], 139// arguments: [ eventData, fromPod ],
140 errorMessage: 'Cannot process videos events with many retries.' 140// errorMessage: 'Cannot process videos events with many retries.'
141 } 141// }
142 142//
143 await retryTransactionWrapper(processVideosEvents, options) 143// await retryTransactionWrapper(processVideosEvents, options)
144} 144// }
145 145//
146async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { 146// async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) {
147 await db.sequelize.transaction(async t => { 147// await db.sequelize.transaction(async t => {
148 const sequelizeOptions = { transaction: t } 148// const sequelizeOptions = { transaction: t }
149 const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t) 149// const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t)
150 150//
151 let columnToUpdate 151// let columnToUpdate
152 let qaduType 152// let qaduType
153 153//
154 switch (eventData.eventType) { 154// switch (eventData.eventType) {
155 case REQUEST_VIDEO_EVENT_TYPES.VIEWS: 155// case REQUEST_VIDEO_EVENT_TYPES.VIEWS:
156 columnToUpdate = 'views' 156// columnToUpdate = 'views'
157 qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS 157// qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS
158 break 158// break
159 159//
160 case REQUEST_VIDEO_EVENT_TYPES.LIKES: 160// case REQUEST_VIDEO_EVENT_TYPES.LIKES:
161 columnToUpdate = 'likes' 161// columnToUpdate = 'likes'
162 qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES 162// qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES
163 break 163// break
164 164//
165 case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: 165// case REQUEST_VIDEO_EVENT_TYPES.DISLIKES:
166 columnToUpdate = 'dislikes' 166// columnToUpdate = 'dislikes'
167 qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES 167// qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES
168 break 168// break
169 169//
170 default: 170// default:
171 throw new Error('Unknown video event type.') 171// throw new Error('Unknown video event type.')
172 } 172// }
173 173//
174 const query = {} 174// const query = {}
175 query[columnToUpdate] = eventData.count 175// query[columnToUpdate] = eventData.count
176 176//
177 await videoInstance.increment(query, sequelizeOptions) 177// await videoInstance.increment(query, sequelizeOptions)
178 178//
179 const qadusParams = [ 179// const qadusParams = [
180 { 180// {
181 videoId: videoInstance.id, 181// videoId: videoInstance.id,
182 type: qaduType 182// type: qaduType
183 } 183// }
184 ] 184// ]
185 await quickAndDirtyUpdatesVideoToFriends(qadusParams, t) 185// await quickAndDirtyUpdatesVideoToFriends(qadusParams, t)
186 }) 186// })
187 187//
188 logger.info('Remote video event processed for video with uuid %s.', eventData.uuid) 188// logger.info('Remote video event processed for video with uuid %s.', eventData.uuid)
189} 189// }
190 190//
191async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) { 191// async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
192 const options = { 192// const options = {
193 arguments: [ videoData, fromPod ], 193// arguments: [ videoData, fromPod ],
194 errorMessage: 'Cannot update quick and dirty the remote video with many retries.' 194// errorMessage: 'Cannot update quick and dirty the remote video with many retries.'
195 } 195// }
196 196//
197 await retryTransactionWrapper(quickAndDirtyUpdateVideo, options) 197// await retryTransactionWrapper(quickAndDirtyUpdateVideo, options)
198} 198// }
199 199//
200async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { 200// async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) {
201 let videoUUID = '' 201// let videoUUID = ''
202 202//
203 await db.sequelize.transaction(async t => { 203// await db.sequelize.transaction(async t => {
204 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) 204// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t)
205 const sequelizeOptions = { transaction: t } 205// const sequelizeOptions = { transaction: t }
206 206//
207 videoUUID = videoInstance.uuid 207// videoUUID = videoInstance.uuid
208 208//
209 if (videoData.views) { 209// if (videoData.views) {
210 videoInstance.set('views', videoData.views) 210// videoInstance.set('views', videoData.views)
211 } 211// }
212 212//
213 if (videoData.likes) { 213// if (videoData.likes) {
214 videoInstance.set('likes', videoData.likes) 214// videoInstance.set('likes', videoData.likes)
215 } 215// }
216 216//
217 if (videoData.dislikes) { 217// if (videoData.dislikes) {
218 videoInstance.set('dislikes', videoData.dislikes) 218// videoInstance.set('dislikes', videoData.dislikes)
219 } 219// }
220 220//
221 await videoInstance.save(sequelizeOptions) 221// await videoInstance.save(sequelizeOptions)
222 }) 222// })
223 223//
224 logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) 224// logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
225} 225// }
226 226//
227// Handle retries on fail 227// async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
228async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { 228// const options = {
229 const options = { 229// arguments: [ videoToRemoveData, fromPod ],
230 arguments: [ videoToCreateData, fromPod ], 230// errorMessage: 'Cannot remove the remote video channel with many retries.'
231 errorMessage: 'Cannot insert the remote video with many retries.' 231// }
232 } 232//
233 233// await retryTransactionWrapper(removeRemoteVideo, options)
234 await retryTransactionWrapper(addRemoteVideo, options) 234// }
235} 235//
236 236// async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
237async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { 237// logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
238 logger.debug('Adding remote video "%s".', videoToCreateData.uuid) 238//
239 239// await db.sequelize.transaction(async t => {
240 await db.sequelize.transaction(async t => { 240// // We need the instance because we have to remove some other stuffs (thumbnail etc)
241 const sequelizeOptions = { 241// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
242 transaction: t 242// await videoInstance.destroy({ transaction: t })
243 } 243// })
244 244//
245 const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) 245// logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
246 if (videoFromDatabase) throw new Error('UUID already exists.') 246// }
247 247//
248 const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) 248// async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
249 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') 249// const options = {
250 250// arguments: [ authorAttributesToRemove, fromPod ],
251 const tags = videoToCreateData.tags 251// errorMessage: 'Cannot remove the remote video author with many retries.'
252 const tagInstances = await db.Tag.findOrCreateTags(tags, t) 252// }
253 253//
254 const videoData = { 254// await retryTransactionWrapper(removeRemoteVideoAuthor, options)
255 name: videoToCreateData.name, 255// }
256 uuid: videoToCreateData.uuid, 256//
257 category: videoToCreateData.category, 257// async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
258 licence: videoToCreateData.licence, 258// logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
259 language: videoToCreateData.language, 259//
260 nsfw: videoToCreateData.nsfw, 260// await db.sequelize.transaction(async t => {
261 description: videoToCreateData.truncatedDescription, 261// const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
262 channelId: videoChannel.id, 262// await videoAuthor.destroy({ transaction: t })
263 duration: videoToCreateData.duration, 263// })
264 createdAt: videoToCreateData.createdAt, 264//
265 // FIXME: updatedAt does not seems to be considered by Sequelize 265// logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)
266 updatedAt: videoToCreateData.updatedAt, 266// }
267 views: videoToCreateData.views, 267//
268 likes: videoToCreateData.likes, 268// async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
269 dislikes: videoToCreateData.dislikes, 269// const options = {
270 remote: true, 270// arguments: [ videoChannelAttributesToRemove, fromPod ],
271 privacy: videoToCreateData.privacy 271// errorMessage: 'Cannot remove the remote video channel with many retries.'
272 } 272// }
273 273//
274 const video = db.Video.build(videoData) 274// await retryTransactionWrapper(removeRemoteVideoChannel, options)
275 await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData) 275// }
276 const videoCreated = await video.save(sequelizeOptions) 276//
277 277// async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
278 const tasks = [] 278// logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
279 for (const fileData of videoToCreateData.files) { 279//
280 const videoFileInstance = db.VideoFile.build({ 280// await db.sequelize.transaction(async t => {
281 extname: fileData.extname, 281// const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
282 infoHash: fileData.infoHash, 282// await videoChannel.destroy({ transaction: t })
283 resolution: fileData.resolution, 283// })
284 size: fileData.size, 284//
285 videoId: videoCreated.id 285// logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
286 }) 286// }
287 287//
288 tasks.push(videoFileInstance.save(sequelizeOptions)) 288// async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
289 } 289// const options = {
290 290// arguments: [ reportData, fromPod ],
291 await Promise.all(tasks) 291// errorMessage: 'Cannot create remote abuse video with many retries.'
292 292// }
293 await videoCreated.setTags(tagInstances, sequelizeOptions) 293//
294 }) 294// await retryTransactionWrapper(reportAbuseRemoteVideo, options)
295 295// }
296 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) 296//
297} 297// async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
298 298// logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
299// Handle retries on fail 299//
300async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { 300// await db.sequelize.transaction(async t => {
301 const options = { 301// const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t)
302 arguments: [ videoAttributesToUpdate, fromPod ], 302// const videoAbuseData = {
303 errorMessage: 'Cannot update the remote video with many retries' 303// reporterUsername: reportData.reporterUsername,
304 } 304// reason: reportData.reportReason,
305 305// reporterPodId: fromPod.id,
306 await retryTransactionWrapper(updateRemoteVideo, options) 306// videoId: videoInstance.id
307} 307// }
308 308//
309async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { 309// await db.VideoAbuse.create(videoAbuseData)
310 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 310//
311 let videoInstance: VideoInstance 311// })
312 let videoFieldsSave: object 312//
313 313// logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)
314 try { 314// }
315 await db.sequelize.transaction(async t => { 315//
316 const sequelizeOptions = { 316// async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) {
317 transaction: t 317// try {
318 } 318// const video = await db.Video.loadLocalVideoByUUID(id, t)
319 319//
320 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t) 320// if (!video) throw new Error('Video ' + id + ' not found')
321 videoFieldsSave = videoInstance.toJSON() 321//
322 const tags = videoAttributesToUpdate.tags 322// return video
323 323// } catch (err) {
324 const tagInstances = await db.Tag.findOrCreateTags(tags, t) 324// logger.error('Cannot load owned video from id.', { error: err.stack, id })
325 325// throw err
326 videoInstance.set('name', videoAttributesToUpdate.name) 326// }
327 videoInstance.set('category', videoAttributesToUpdate.category) 327// }
328 videoInstance.set('licence', videoAttributesToUpdate.licence) 328//
329 videoInstance.set('language', videoAttributesToUpdate.language) 329// async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
330 videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) 330// try {
331 videoInstance.set('description', videoAttributesToUpdate.truncatedDescription) 331// const video = await db.Video.loadByHostAndUUID(podHost, uuid, t)
332 videoInstance.set('duration', videoAttributesToUpdate.duration) 332// if (!video) throw new Error('Video not found')
333 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) 333//
334 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) 334// return video
335 videoInstance.set('views', videoAttributesToUpdate.views) 335// } catch (err) {
336 videoInstance.set('likes', videoAttributesToUpdate.likes) 336// logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
337 videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) 337// throw err
338 videoInstance.set('privacy', videoAttributesToUpdate.privacy) 338// }
339 339// }
340 await videoInstance.save(sequelizeOptions)
341
342 // Remove old video files
343 const videoFileDestroyTasks: Bluebird<void>[] = []
344 for (const videoFile of videoInstance.VideoFiles) {
345 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
346 }
347 await Promise.all(videoFileDestroyTasks)
348
349 const videoFileCreateTasks: Bluebird<VideoFileInstance>[] = []
350 for (const fileData of videoAttributesToUpdate.files) {
351 const videoFileInstance = db.VideoFile.build({
352 extname: fileData.extname,
353 infoHash: fileData.infoHash,
354 resolution: fileData.resolution,
355 size: fileData.size,
356 videoId: videoInstance.id
357 })
358
359 videoFileCreateTasks.push(videoFileInstance.save(sequelizeOptions))
360 }
361
362 await Promise.all(videoFileCreateTasks)
363
364 await videoInstance.setTags(tagInstances, sequelizeOptions)
365 })
366
367 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
368 } catch (err) {
369 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
370 resetSequelizeInstance(videoInstance, videoFieldsSave)
371 }
372
373 // This is just a debug because we will retry the insert
374 logger.debug('Cannot update the remote video.', err)
375 throw err
376 }
377}
378
379async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
380 const options = {
381 arguments: [ videoToRemoveData, fromPod ],
382 errorMessage: 'Cannot remove the remote video channel with many retries.'
383 }
384
385 await retryTransactionWrapper(removeRemoteVideo, options)
386}
387
388async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
389 logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
390
391 await db.sequelize.transaction(async t => {
392 // We need the instance because we have to remove some other stuffs (thumbnail etc)
393 const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
394 await videoInstance.destroy({ transaction: t })
395 })
396
397 logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
398}
399
400async function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
401 const options = {
402 arguments: [ authorToCreateData, fromPod ],
403 errorMessage: 'Cannot insert the remote video author with many retries.'
404 }
405
406 await retryTransactionWrapper(addRemoteVideoAuthor, options)
407}
408
409async function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) {
410 logger.debug('Adding remote video author "%s".', authorToCreateData.uuid)
411
412 await db.sequelize.transaction(async t => {
413 const authorInDatabase = await db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t)
414 if (authorInDatabase) throw new Error('Author with UUID ' + authorToCreateData.uuid + ' already exists.')
415
416 const videoAuthorData = {
417 name: authorToCreateData.name,
418 uuid: authorToCreateData.uuid,
419 userId: null, // Not on our pod
420 podId: fromPod.id
421 }
422
423 const author = db.Author.build(videoAuthorData)
424 await author.save({ transaction: t })
425 })
426
427 logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid)
428}
429
430async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
431 const options = {
432 arguments: [ authorAttributesToRemove, fromPod ],
433 errorMessage: 'Cannot remove the remote video author with many retries.'
434 }
435
436 await retryTransactionWrapper(removeRemoteVideoAuthor, options)
437}
438
439async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) {
440 logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid)
441
442 await db.sequelize.transaction(async t => {
443 const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t)
444 await videoAuthor.destroy({ transaction: t })
445 })
446
447 logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)
448}
449
450async function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
451 const options = {
452 arguments: [ videoChannelToCreateData, fromPod ],
453 errorMessage: 'Cannot insert the remote video channel with many retries.'
454 }
455
456 await retryTransactionWrapper(addRemoteVideoChannel, options)
457}
458
459async function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) {
460 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
461
462 await db.sequelize.transaction(async t => {
463 const videoChannelInDatabase = await db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid)
464 if (videoChannelInDatabase) {
465 throw new Error('Video channel with UUID ' + videoChannelToCreateData.uuid + ' already exists.')
466 }
467
468 const authorUUID = videoChannelToCreateData.ownerUUID
469 const podId = fromPod.id
470
471 const author = await db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t)
472 if (!author) throw new Error('Unknown author UUID' + authorUUID + '.')
473
474 const videoChannelData = {
475 name: videoChannelToCreateData.name,
476 description: videoChannelToCreateData.description,
477 uuid: videoChannelToCreateData.uuid,
478 createdAt: videoChannelToCreateData.createdAt,
479 updatedAt: videoChannelToCreateData.updatedAt,
480 remote: true,
481 authorId: author.id
482 }
483
484 const videoChannel = db.VideoChannel.build(videoChannelData)
485 await videoChannel.save({ transaction: t })
486 })
487
488 logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
489}
490
491async function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
492 const options = {
493 arguments: [ videoChannelAttributesToUpdate, fromPod ],
494 errorMessage: 'Cannot update the remote video channel with many retries.'
495 }
496
497 await retryTransactionWrapper(updateRemoteVideoChannel, options)
498}
499
500async function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) {
501 logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid)
502
503 await db.sequelize.transaction(async t => {
504 const sequelizeOptions = { transaction: t }
505
506 const videoChannelInstance = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t)
507 videoChannelInstance.set('name', videoChannelAttributesToUpdate.name)
508 videoChannelInstance.set('description', videoChannelAttributesToUpdate.description)
509 videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt)
510 videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt)
511
512 await videoChannelInstance.save(sequelizeOptions)
513 })
514
515 logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid)
516}
517
518async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
519 const options = {
520 arguments: [ videoChannelAttributesToRemove, fromPod ],
521 errorMessage: 'Cannot remove the remote video channel with many retries.'
522 }
523
524 await retryTransactionWrapper(removeRemoteVideoChannel, options)
525}
526
527async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
528 logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
529
530 await db.sequelize.transaction(async t => {
531 const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
532 await videoChannel.destroy({ transaction: t })
533 })
534
535 logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
536}
537
538async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
539 const options = {
540 arguments: [ reportData, fromPod ],
541 errorMessage: 'Cannot create remote abuse video with many retries.'
542 }
543
544 await retryTransactionWrapper(reportAbuseRemoteVideo, options)
545}
546
547async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
548 logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID)
549
550 await db.sequelize.transaction(async t => {
551 const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t)
552 const videoAbuseData = {
553 reporterUsername: reportData.reporterUsername,
554 reason: reportData.reportReason,
555 reporterPodId: fromPod.id,
556 videoId: videoInstance.id
557 }
558
559 await db.VideoAbuse.create(videoAbuseData)
560
561 })
562
563 logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)
564}
565
566async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) {
567 try {
568 const video = await db.Video.loadLocalVideoByUUID(id, t)
569
570 if (!video) throw new Error('Video ' + id + ' not found')
571
572 return video
573 } catch (err) {
574 logger.error('Cannot load owned video from id.', { error: err.stack, id })
575 throw err
576 }
577}
578
579async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) {
580 try {
581 const video = await db.Video.loadByHostAndUUID(podHost, uuid, t)
582 if (!video) throw new Error('Video not found')
583
584 return video
585 } catch (err) {
586 logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid })
587 throw err
588 }
589}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 4dd09917b..964db151d 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -10,7 +10,8 @@ import {
10 VIDEO_CATEGORIES, 10 VIDEO_CATEGORIES,
11 VIDEO_LICENCES, 11 VIDEO_LICENCES,
12 VIDEO_LANGUAGES, 12 VIDEO_LANGUAGES,
13 VIDEO_PRIVACIES 13 VIDEO_PRIVACIES,
14 VIDEO_MIMETYPE_EXT
14} from '../../../initializers' 15} from '../../../initializers'
15import { 16import {
16 addEventToRemoteVideo, 17 addEventToRemoteVideo,
@@ -50,6 +51,7 @@ import { abuseVideoRouter } from './abuse'
50import { blacklistRouter } from './blacklist' 51import { blacklistRouter } from './blacklist'
51import { rateVideoRouter } from './rate' 52import { rateVideoRouter } from './rate'
52import { videoChannelRouter } from './channel' 53import { videoChannelRouter } from './channel'
54import { getActivityPubUrl } from '../../../helpers/activitypub'
53 55
54const videosRouter = express.Router() 56const videosRouter = express.Router()
55 57
@@ -59,19 +61,18 @@ const storage = multer.diskStorage({
59 cb(null, CONFIG.STORAGE.VIDEOS_DIR) 61 cb(null, CONFIG.STORAGE.VIDEOS_DIR)
60 }, 62 },
61 63
62 filename: (req, file, cb) => { 64 filename: async (req, file, cb) => {
63 let extension = '' 65 const extension = VIDEO_MIMETYPE_EXT[file.mimetype]
64 if (file.mimetype === 'video/webm') extension = 'webm' 66 let randomString = ''
65 else if (file.mimetype === 'video/mp4') extension = 'mp4' 67
66 else if (file.mimetype === 'video/ogg') extension = 'ogv' 68 try {
67 generateRandomString(16) 69 randomString = await generateRandomString(16)
68 .then(randomString => { 70 } catch (err) {
69 cb(null, randomString + '.' + extension) 71 logger.error('Cannot generate random string for file name.', err)
70 }) 72 randomString = 'fake-random-string'
71 .catch(err => { 73 }
72 logger.error('Cannot generate random string for file name.', err) 74
73 throw err 75 cb(null, randomString + '.' + extension)
74 })
75 } 76 }
76}) 77})
77 78
@@ -190,6 +191,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
190 channelId: res.locals.videoChannel.id 191 channelId: res.locals.videoChannel.id
191 } 192 }
192 const video = db.Video.build(videoData) 193 const video = db.Video.build(videoData)
194 video.url = getActivityPubUrl('video', video.uuid)
193 195
194 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) 196 const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename)
195 const videoFileHeight = await getVideoFileHeight(videoFilePath) 197 const videoFileHeight = await getVideoFileHeight(videoFilePath)
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index ecb509b66..75de2278c 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -2,10 +2,48 @@ import * as url from 'url'
2 2
3import { database as db } from '../initializers' 3import { database as db } from '../initializers'
4import { logger } from './logger' 4import { logger } from './logger'
5import { doRequest } from './requests' 5import { doRequest, doRequestAndSaveToFile } from './requests'
6import { isRemoteAccountValid } from './custom-validators' 6import { isRemoteAccountValid } from './custom-validators'
7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' 7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
8import { ResultList } from '../../shared/models/result-list.model' 8import { ResultList } from '../../shared/models/result-list.model'
9import { CONFIG } from '../initializers/constants'
10import { VideoInstance } from '../models/video/video-interface'
11import { ActivityIconObject } from '../../shared/index'
12import { join } from 'path'
13
14function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
15 const thumbnailName = video.getThumbnailName()
16 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
17
18 const options = {
19 method: 'GET',
20 uri: icon.url
21 }
22 return doRequestAndSaveToFile(options, thumbnailPath)
23}
24
25function getActivityPubUrl (type: 'video' | 'videoChannel', uuid: string) {
26 if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + uuid
27 else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + uuid
28
29 return ''
30}
31
32async function getOrCreateAccount (accountUrl: string) {
33 let account = await db.Account.loadByUrl(accountUrl)
34
35 // We don't have this account in our database, fetch it on remote
36 if (!account) {
37 const { account } = await fetchRemoteAccountAndCreatePod(accountUrl)
38
39 if (!account) throw new Error('Cannot fetch remote account.')
40
41 // Save our new account in database
42 await account.save()
43 }
44
45 return account
46}
9 47
10async function fetchRemoteAccountAndCreatePod (accountUrl: string) { 48async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
11 const options = { 49 const options = {
@@ -100,7 +138,10 @@ function activityPubCollectionPagination (url: string, page: number, result: Res
100export { 138export {
101 fetchRemoteAccountAndCreatePod, 139 fetchRemoteAccountAndCreatePod,
102 activityPubContextify, 140 activityPubContextify,
103 activityPubCollectionPagination 141 activityPubCollectionPagination,
142 getActivityPubUrl,
143 generateThumbnailFromUrl,
144 getOrCreateAccount
104} 145}
105 146
106// --------------------------------------------------------------------------- 147// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
new file mode 100644
index 000000000..dd671c4cf
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -0,0 +1,34 @@
1import * as validator from 'validator'
2import {
3 isVideoChannelCreateActivityValid,
4 isVideoTorrentAddActivityValid,
5 isVideoTorrentUpdateActivityValid,
6 isVideoChannelUpdateActivityValid
7} from './videos'
8
9function isRootActivityValid (activity: any) {
10 return Array.isArray(activity['@context']) &&
11 (
12 (activity.type === 'Collection' || activity.type === 'OrderedCollection') &&
13 validator.isInt(activity.totalItems, { min: 0 }) &&
14 Array.isArray(activity.items)
15 ) ||
16 (
17 validator.isURL(activity.id) &&
18 validator.isURL(activity.actor)
19 )
20}
21
22function isActivityValid (activity: any) {
23 return isVideoTorrentAddActivityValid(activity) ||
24 isVideoChannelCreateActivityValid(activity) ||
25 isVideoTorrentUpdateActivityValid(activity) ||
26 isVideoChannelUpdateActivityValid(activity)
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 isRootActivityValid,
33 isActivityValid
34}
diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts
index 800f0ddf3..0eba06a7b 100644
--- a/server/helpers/custom-validators/activitypub/index.ts
+++ b/server/helpers/custom-validators/activitypub/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './activity'
2export * from './signature' 3export * from './signature'
3export * from './misc' 4export * from './misc'
4export * from './videos' 5export * from './videos'
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 806d33483..f049f5a8c 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -12,6 +12,16 @@ function isActivityPubUrlValid (url: string) {
12 return exists(url) && validator.isURL(url, isURLOptions) 12 return exists(url) && validator.isURL(url, isURLOptions)
13} 13}
14 14
15function isBaseActivityValid (activity: any, type: string) {
16 return Array.isArray(activity['@context']) &&
17 activity.type === type &&
18 validator.isURL(activity.id) &&
19 validator.isURL(activity.actor) &&
20 Array.isArray(activity.to) &&
21 activity.to.every(t => validator.isURL(t))
22}
23
15export { 24export {
16 isActivityPubUrlValid 25 isActivityPubUrlValid,
26 isBaseActivityValid
17} 27}
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index e0ffba679..9233a1359 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,184 +1,117 @@
1import 'express-validator' 1import * as validator from 'validator'
2import { has, values } from 'lodash'
3 2
4import { 3import {
5 REQUEST_ENDPOINTS, 4 ACTIVITY_PUB
6 REQUEST_ENDPOINT_ACTIONS,
7 REQUEST_VIDEO_EVENT_TYPES
8} from '../../../initializers' 5} from '../../../initializers'
9import { isArray, isDateValid, isUUIDValid } from '../misc' 6import { isDateValid, isUUIDValid } from '../misc'
10import { 7import {
11 isVideoThumbnailDataValid,
12 isVideoAbuseReasonValid,
13 isVideoAbuseReporterUsernameValid,
14 isVideoViewsValid, 8 isVideoViewsValid,
15 isVideoLikesValid,
16 isVideoDislikesValid,
17 isVideoEventCountValid,
18 isRemoteVideoCategoryValid,
19 isRemoteVideoLicenceValid,
20 isRemoteVideoLanguageValid,
21 isVideoNSFWValid, 9 isVideoNSFWValid,
22 isVideoTruncatedDescriptionValid, 10 isVideoTruncatedDescriptionValid,
23 isVideoDurationValid, 11 isVideoDurationValid,
24 isVideoFileInfoHashValid,
25 isVideoNameValid, 12 isVideoNameValid,
26 isVideoTagsValid, 13 isVideoTagValid
27 isVideoFileExtnameValid,
28 isVideoFileResolutionValid
29} from '../videos' 14} from '../videos'
30import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' 15import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
31import { isVideoAuthorNameValid } from '../video-authors' 16import { isBaseActivityValid } from './misc'
32 17
33const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] 18function isVideoTorrentAddActivityValid (activity: any) {
34 19 return isBaseActivityValid(activity, 'Add') &&
35const checkers: { [ id: string ]: (obj: any) => boolean } = {} 20 isVideoTorrentObjectValid(activity.object)
36checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo 21}
37checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo 22
38checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo 23function isVideoTorrentUpdateActivityValid (activity: any) {
39checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo 24 return isBaseActivityValid(activity, 'Update') &&
40checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel 25 isVideoTorrentObjectValid(activity.object)
41checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel
42checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel
43checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor
44checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor
45
46function removeBadRequestVideos (requests: any[]) {
47 for (let i = requests.length - 1; i >= 0 ; i--) {
48 const request = requests[i]
49 const video = request.data
50
51 if (
52 !video ||
53 checkers[request.type] === undefined ||
54 checkers[request.type](video) === false
55 ) {
56 requests.splice(i, 1)
57 }
58 }
59} 26}
60 27
61function removeBadRequestVideosQadu (requests: any[]) { 28function isVideoTorrentObjectValid (video: any) {
62 for (let i = requests.length - 1; i >= 0 ; i--) { 29 return video.type === 'Video' &&
63 const request = requests[i] 30 isVideoNameValid(video.name) &&
64 const video = request.data 31 isVideoDurationValid(video.duration) &&
65 32 isUUIDValid(video.uuid) &&
66 if ( 33 setValidRemoteTags(video) &&
67 !video || 34 isRemoteIdentifierValid(video.category) &&
68 ( 35 isRemoteIdentifierValid(video.licence) &&
69 isUUIDValid(video.uuid) && 36 isRemoteIdentifierValid(video.language) &&
70 (has(video, 'views') === false || isVideoViewsValid(video.views)) && 37 isVideoViewsValid(video.video) &&
71 (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && 38 isVideoNSFWValid(video.nsfw) &&
72 (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) 39 isDateValid(video.published) &&
73 ) === false 40 isDateValid(video.updated) &&
74 ) { 41 isRemoteVideoContentValid(video.mediaType, video.content) &&
75 requests.splice(i, 1) 42 isRemoteVideoIconValid(video.icon) &&
76 } 43 setValidRemoteVideoUrls(video.url)
77 }
78} 44}
79 45
80function removeBadRequestVideosEvents (requests: any[]) { 46function isVideoChannelCreateActivityValid (activity: any) {
81 for (let i = requests.length - 1; i >= 0 ; i--) { 47 return isBaseActivityValid(activity, 'Create') &&
82 const request = requests[i] 48 isVideoChannelObjectValid(activity.object)
83 const eventData = request.data 49}
84 50
85 if ( 51function isVideoChannelUpdateActivityValid (activity: any) {
86 !eventData || 52 return isBaseActivityValid(activity, 'Update') &&
87 ( 53 isVideoChannelObjectValid(activity.object)
88 isUUIDValid(eventData.uuid) && 54}
89 values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && 55
90 isVideoEventCountValid(eventData.count) 56function isVideoChannelObjectValid (videoChannel: any) {
91 ) === false 57 return videoChannel.type === 'VideoChannel' &&
92 ) { 58 isVideoChannelNameValid(videoChannel.name) &&
93 requests.splice(i, 1) 59 isVideoChannelDescriptionValid(videoChannel.description) &&
94 } 60 isUUIDValid(videoChannel.uuid)
95 }
96} 61}
97 62
98// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
99 64
100export { 65export {
101 removeBadRequestVideos, 66 isVideoTorrentAddActivityValid,
102 removeBadRequestVideosQadu, 67 isVideoChannelCreateActivityValid,
103 removeBadRequestVideosEvents 68 isVideoTorrentUpdateActivityValid,
69 isVideoChannelUpdateActivityValid
104} 70}
105 71
106// --------------------------------------------------------------------------- 72// ---------------------------------------------------------------------------
107 73
108function isCommonVideoAttributesValid (video: any) { 74function setValidRemoteTags (video: any) {
109 return isDateValid(video.createdAt) && 75 if (Array.isArray(video.tag) === false) return false
110 isDateValid(video.updatedAt) &&
111 isRemoteVideoCategoryValid(video.category) &&
112 isRemoteVideoLicenceValid(video.licence) &&
113 isRemoteVideoLanguageValid(video.language) &&
114 isVideoNSFWValid(video.nsfw) &&
115 isVideoTruncatedDescriptionValid(video.truncatedDescription) &&
116 isVideoDurationValid(video.duration) &&
117 isVideoNameValid(video.name) &&
118 isVideoTagsValid(video.tags) &&
119 isUUIDValid(video.uuid) &&
120 isVideoViewsValid(video.views) &&
121 isVideoLikesValid(video.likes) &&
122 isVideoDislikesValid(video.dislikes) &&
123 isArray(video.files) &&
124 video.files.every(videoFile => {
125 if (!videoFile) return false
126
127 return (
128 isVideoFileInfoHashValid(videoFile.infoHash) &&
129 isVideoFileExtnameValid(videoFile.extname) &&
130 isVideoFileResolutionValid(videoFile.resolution)
131 )
132 })
133}
134 76
135function checkAddVideo (video: any) { 77 const newTag = video.tag.filter(t => {
136 return isCommonVideoAttributesValid(video) && 78 return t.type === 'Hashtag' &&
137 isUUIDValid(video.channelUUID) && 79 isVideoTagValid(t.name)
138 isVideoThumbnailDataValid(video.thumbnailData) 80 })
139}
140 81
141function checkUpdateVideo (video: any) { 82 video.tag = newTag
142 return isCommonVideoAttributesValid(video) 83 return true
143} 84}
144 85
145function checkRemoveVideo (video: any) { 86function isRemoteIdentifierValid (data: any) {
146 return isUUIDValid(video.uuid) 87 return validator.isInt(data.identifier, { min: 0 })
147} 88}
148 89
149function checkReportVideo (abuse: any) { 90function isRemoteVideoContentValid (mediaType: string, content: string) {
150 return isUUIDValid(abuse.videoUUID) && 91 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
151 isVideoAbuseReasonValid(abuse.reportReason) &&
152 isVideoAbuseReporterUsernameValid(abuse.reporterUsername)
153} 92}
154 93
155function checkAddVideoChannel (videoChannel: any) { 94function isRemoteVideoIconValid (icon: any) {
156 return isUUIDValid(videoChannel.uuid) && 95 return icon.type === 'Image' &&
157 isVideoChannelNameValid(videoChannel.name) && 96 validator.isURL(icon.url) &&
158 isVideoChannelDescriptionValid(videoChannel.description) && 97 icon.mediaType === 'image/jpeg' &&
159 isDateValid(videoChannel.createdAt) && 98 validator.isInt(icon.width, { min: 0 }) &&
160 isDateValid(videoChannel.updatedAt) && 99 validator.isInt(icon.height, { min: 0 })
161 isUUIDValid(videoChannel.ownerUUID)
162} 100}
163 101
164function checkUpdateVideoChannel (videoChannel: any) { 102function setValidRemoteVideoUrls (video: any) {
165 return isUUIDValid(videoChannel.uuid) && 103 if (Array.isArray(video.url) === false) return false
166 isVideoChannelNameValid(videoChannel.name) &&
167 isVideoChannelDescriptionValid(videoChannel.description) &&
168 isDateValid(videoChannel.createdAt) &&
169 isDateValid(videoChannel.updatedAt) &&
170 isUUIDValid(videoChannel.ownerUUID)
171}
172 104
173function checkRemoveVideoChannel (videoChannel: any) { 105 const newUrl = video.url.filter(u => isRemoteVideoUrlValid(u))
174 return isUUIDValid(videoChannel.uuid) 106 video.url = newUrl
175}
176 107
177function checkAddAuthor (author: any) { 108 return true
178 return isUUIDValid(author.uuid) &&
179 isVideoAuthorNameValid(author.name)
180} 109}
181 110
182function checkRemoveAuthor (author: any) { 111function isRemoteVideoUrlValid (url: any) {
183 return isUUIDValid(author.uuid) 112 return url.type === 'Link' &&
113 ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 &&
114 validator.isURL(url.url) &&
115 validator.isInt(url.width, { min: 0 }) &&
116 validator.isInt(url.size, { min: 0 })
184} 117}
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index 869b08870..58a40249b 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -3,6 +3,5 @@ export * from './misc'
3export * from './pods' 3export * from './pods'
4export * from './pods' 4export * from './pods'
5export * from './users' 5export * from './users'
6export * from './video-authors'
7export * from './video-channels' 6export * from './video-channels'
8export * from './videos' 7export * from './videos'
diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts
deleted file mode 100644
index 48ca9b200..000000000
--- a/server/helpers/custom-validators/video-authors.ts
+++ /dev/null
@@ -1,45 +0,0 @@
1import * as Promise from 'bluebird'
2import * as validator from 'validator'
3import * as express from 'express'
4import 'express-validator'
5
6import { database as db } from '../../initializers'
7import { AuthorInstance } from '../../models'
8import { logger } from '../logger'
9
10import { isUserUsernameValid } from './users'
11
12function isVideoAuthorNameValid (value: string) {
13 return isUserUsernameValid(value)
14}
15
16function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) {
17 let promise: Promise<AuthorInstance>
18 if (validator.isInt(id)) {
19 promise = db.Author.load(+id)
20 } else { // UUID
21 promise = db.Author.loadByUUID(id)
22 }
23
24 promise.then(author => {
25 if (!author) {
26 return res.status(404)
27 .json({ error: 'Video author not found' })
28 .end()
29 }
30
31 res.locals.author = author
32 callback()
33 })
34 .catch(err => {
35 logger.error('Error in video author request validator.', err)
36 return res.sendStatus(500)
37 })
38}
39
40// ---------------------------------------------------------------------------
41
42export {
43 checkVideoAuthorExists,
44 isVideoAuthorNameValid
45}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index f3fdcaf2d..83407f17b 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -73,19 +73,26 @@ function isVideoDescriptionValid (value: string) {
73} 73}
74 74
75function isVideoDurationValid (value: string) { 75function isVideoDurationValid (value: string) {
76 return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) 76 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
77 return exists(value) &&
78 typeof value === 'string' &&
79 value.startsWith('PT') &&
80 value.endsWith('S') &&
81 validator.isInt(value.replace(/[^0-9]+/, ''), VIDEOS_CONSTRAINTS_FIELDS.DURATION)
77} 82}
78 83
79function isVideoNameValid (value: string) { 84function isVideoNameValid (value: string) {
80 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) 85 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME)
81} 86}
82 87
88function isVideoTagValid (tag: string) {
89 return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
90}
91
83function isVideoTagsValid (tags: string[]) { 92function isVideoTagsValid (tags: string[]) {
84 return isArray(tags) && 93 return isArray(tags) &&
85 validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) && 94 validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) &&
86 tags.every(tag => { 95 tags.every(tag => isVideoTagValid(tag))
87 return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG)
88 })
89} 96}
90 97
91function isVideoThumbnailValid (value: string) { 98function isVideoThumbnailValid (value: string) {
@@ -209,6 +216,7 @@ export {
209 isRemoteVideoPrivacyValid, 216 isRemoteVideoPrivacyValid,
210 isVideoFileResolutionValid, 217 isVideoFileResolutionValid,
211 checkVideoExists, 218 checkVideoExists,
219 isVideoTagValid,
212 isRemoteVideoCategoryValid, 220 isRemoteVideoCategoryValid,
213 isRemoteVideoLicenceValid, 221 isRemoteVideoLicenceValid,
214 isRemoteVideoLanguageValid 222 isRemoteVideoLanguageValid
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 8c4c983f7..31cedd768 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -10,6 +10,7 @@ import {
10import { PodInstance } from '../models' 10import { PodInstance } from '../models'
11import { PodSignature } from '../../shared' 11import { PodSignature } from '../../shared'
12import { signObject } from './peertube-crypto' 12import { signObject } from './peertube-crypto'
13import { createWriteStream } from 'fs'
13 14
14function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { 15function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
15 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { 16 return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => {
@@ -17,6 +18,15 @@ function doRequest (requestOptions: request.CoreOptions & request.UriOptions) {
17 }) 18 })
18} 19}
19 20
21function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) {
22 return new Promise<request.RequestResponse>((res, rej) => {
23 request(requestOptions)
24 .on('response', response => res(response as request.RequestResponse))
25 .on('error', err => rej(err))
26 .pipe(createWriteStream(destPath))
27 })
28}
29
20type MakeRetryRequestParams = { 30type MakeRetryRequestParams = {
21 url: string, 31 url: string,
22 method: 'GET' | 'POST', 32 method: 'GET' | 'POST',
@@ -88,6 +98,7 @@ function makeSecureRequest (params: MakeSecureRequestParams) {
88 98
89export { 99export {
90 doRequest, 100 doRequest,
101 doRequestAndSaveToFile,
91 makeRetryRequest, 102 makeRetryRequest,
92 makeSecureRequest 103 makeSecureRequest
93} 104}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index cb838cf16..e1f877e80 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -203,6 +203,12 @@ const VIDEO_PRIVACIES = {
203 [VideoPrivacy.PRIVATE]: 'Private' 203 [VideoPrivacy.PRIVATE]: 'Private'
204} 204}
205 205
206const VIDEO_MIMETYPE_EXT = {
207 'video/webm': 'webm',
208 'video/ogg': 'ogv',
209 'video/mp4': 'mp4'
210}
211
206// --------------------------------------------------------------------------- 212// ---------------------------------------------------------------------------
207 213
208// Score a pod has when we create it as a friend 214// Score a pod has when we create it as a friend
@@ -212,7 +218,14 @@ const FRIEND_SCORE = {
212} 218}
213 219
214const ACTIVITY_PUB = { 220const ACTIVITY_PUB = {
215 COLLECTION_ITEMS_PER_PAGE: 10 221 COLLECTION_ITEMS_PER_PAGE: 10,
222 VIDEO_URL_MIME_TYPES: [
223 'video/mp4',
224 'video/webm',
225 'video/ogg',
226 'application/x-bittorrent',
227 'application/x-bittorrent;x-scheme-handler/magnet'
228 ]
216} 229}
217 230
218// --------------------------------------------------------------------------- 231// ---------------------------------------------------------------------------
@@ -245,42 +258,6 @@ const REQUESTS_VIDEO_EVENT_LIMIT_PER_POD = 50
245// Number of requests to retry for replay requests module 258// Number of requests to retry for replay requests module
246const RETRY_REQUESTS = 5 259const RETRY_REQUESTS = 5
247 260
248const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = {
249 VIDEOS: 'videos'
250}
251
252const REQUEST_ENDPOINT_ACTIONS: {
253 [ id: string ]: {
254 [ id: string ]: RemoteVideoRequestType
255 }
256} = {}
257REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = {
258 ADD_VIDEO: 'add-video',
259 UPDATE_VIDEO: 'update-video',
260 REMOVE_VIDEO: 'remove-video',
261 ADD_CHANNEL: 'add-channel',
262 UPDATE_CHANNEL: 'update-channel',
263 REMOVE_CHANNEL: 'remove-channel',
264 ADD_AUTHOR: 'add-author',
265 REMOVE_AUTHOR: 'remove-author',
266 REPORT_ABUSE: 'report-abuse'
267}
268
269const REQUEST_VIDEO_QADU_ENDPOINT = 'videos/qadu'
270const REQUEST_VIDEO_EVENT_ENDPOINT = 'videos/events'
271
272const REQUEST_VIDEO_QADU_TYPES: { [ id: string ]: RequestVideoQaduType } = {
273 LIKES: 'likes',
274 DISLIKES: 'dislikes',
275 VIEWS: 'views'
276}
277
278const REQUEST_VIDEO_EVENT_TYPES: { [ id: string ]: RequestVideoEventType } = {
279 LIKES: 'likes',
280 DISLIKES: 'dislikes',
281 VIEWS: 'views'
282}
283
284const REMOTE_SCHEME = { 261const REMOTE_SCHEME = {
285 HTTP: 'https', 262 HTTP: 'https',
286 WS: 'wss' 263 WS: 'wss'
@@ -306,8 +283,6 @@ let JOBS_FETCHING_INTERVAL = 60000
306 283
307// --------------------------------------------------------------------------- 284// ---------------------------------------------------------------------------
308 285
309// const SIGNATURE_ALGORITHM = 'RSA-SHA256'
310// const SIGNATURE_ENCODING = 'hex'
311const PRIVATE_RSA_KEY_SIZE = 2048 286const PRIVATE_RSA_KEY_SIZE = 2048
312 287
313// Password encryption 288// Password encryption
@@ -412,5 +387,6 @@ export {
412 VIDEO_LANGUAGES, 387 VIDEO_LANGUAGES,
413 VIDEO_PRIVACIES, 388 VIDEO_PRIVACIES,
414 VIDEO_LICENCES, 389 VIDEO_LICENCES,
415 VIDEO_RATE_TYPES 390 VIDEO_RATE_TYPES,
391 VIDEO_MIMETYPE_EXT
416} 392}
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts
new file mode 100644
index 000000000..05e77ebc3
--- /dev/null
+++ b/server/lib/activitypub/misc.ts
@@ -0,0 +1,77 @@
1import * as magnetUtil from 'magnet-uri'
2import * as Sequelize from 'sequelize'
3import { VideoTorrentObject } from '../../../shared'
4import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
5import { database as db } from '../../initializers'
6import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants'
7import { VideoChannelInstance } from '../../models/video/video-channel-interface'
8import { VideoFileAttributes } from '../../models/video/video-file-interface'
9import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
10
11async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelInstance, videoObject: VideoTorrentObject, t: Sequelize.Transaction) {
12 const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoObject.uuid, videoObject.id, t)
13 if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.')
14
15 const duration = videoObject.duration.replace(/[^\d]+/, '')
16 const videoData: VideoAttributes = {
17 name: videoObject.name,
18 uuid: videoObject.uuid,
19 url: videoObject.id,
20 category: parseInt(videoObject.category.identifier, 10),
21 licence: parseInt(videoObject.licence.identifier, 10),
22 language: parseInt(videoObject.language.identifier, 10),
23 nsfw: videoObject.nsfw,
24 description: videoObject.content,
25 channelId: videoChannel.id,
26 duration: parseInt(duration, 10),
27 createdAt: videoObject.published,
28 // FIXME: updatedAt does not seems to be considered by Sequelize
29 updatedAt: videoObject.updated,
30 views: videoObject.views,
31 likes: 0,
32 dislikes: 0,
33 // likes: videoToCreateData.likes,
34 // dislikes: videoToCreateData.dislikes,
35 remote: true,
36 privacy: 1
37 // privacy: videoToCreateData.privacy
38 }
39
40 return videoData
41}
42
43function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
44 const fileUrls = videoObject.url
45 .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1)
46
47 const attributes: VideoFileAttributes[] = []
48 for (const url of fileUrls) {
49 // Fetch associated magnet uri
50 const magnet = videoObject.url
51 .find(u => {
52 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width
53 })
54 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url)
55
56 const parsed = magnetUtil.decode(magnet.url)
57 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
58
59 const attribute = {
60 extname: VIDEO_MIMETYPE_EXT[url.mimeType],
61 infoHash: parsed.infoHash,
62 resolution: url.width,
63 size: url.size,
64 videoId: videoCreated.id
65 }
66 attributes.push(attribute)
67 }
68
69 return attributes
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 videoFileActivityUrlToDBAttributes,
76 videoActivityObjectToDBAttributes
77}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
new file mode 100644
index 000000000..40541aca3
--- /dev/null
+++ b/server/lib/activitypub/process-add.ts
@@ -0,0 +1,72 @@
1import { VideoTorrentObject } from '../../../shared'
2import { ActivityAdd } from '../../../shared/models/activitypub/activity'
3import { generateThumbnailFromUrl, logger, retryTransactionWrapper, getOrCreateAccount } from '../../helpers'
4import { database as db } from '../../initializers'
5import { AccountInstance } from '../../models/account/account-interface'
6import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
7import Bluebird = require('bluebird')
8
9async function processAddActivity (activity: ActivityAdd) {
10 const activityObject = activity.object
11 const activityType = activityObject.type
12 const account = await getOrCreateAccount(activity.actor)
13
14 if (activityType === 'Video') {
15 return processAddVideo(account, activity.id, activityObject as VideoTorrentObject)
16 }
17
18 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
19 return Promise.resolve(undefined)
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 processAddActivity
26}
27
28// ---------------------------------------------------------------------------
29
30function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
31 const options = {
32 arguments: [ account, videoChannelUrl ,video ],
33 errorMessage: 'Cannot insert the remote video with many retries.'
34 }
35
36 return retryTransactionWrapper(addRemoteVideo, options)
37}
38
39async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) {
40 logger.debug('Adding remote video %s.', videoToCreateData.url)
41
42 await db.sequelize.transaction(async t => {
43 const sequelizeOptions = {
44 transaction: t
45 }
46
47 const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t)
48 if (!videoChannel) throw new Error('Video channel not found.')
49
50 if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
51
52 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t)
53 const video = db.Video.build(videoData)
54
55 // Don't block on request
56 generateThumbnailFromUrl(video, videoToCreateData.icon)
57 .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
58
59 const videoCreated = await video.save(sequelizeOptions)
60
61 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
62
63 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
64 await Promise.all(tasks)
65
66 const tags = videoToCreateData.tag.map(t => t.name)
67 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
68 await videoCreated.setTags(tagInstances, sequelizeOptions)
69 })
70
71 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
72}
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
index 114ff1848..471674ead 100644
--- a/server/lib/activitypub/process-create.ts
+++ b/server/lib/activitypub/process-create.ts
@@ -1,23 +1,23 @@
1import { 1import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared'
2 ActivityCreate, 2import { ActivityAdd } from '../../../shared/models/activitypub/activity'
3 VideoTorrentObject, 3import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers'
4 VideoChannelObject
5} from '../../../shared'
6import { database as db } from '../../initializers' 4import { database as db } from '../../initializers'
7import { logger, retryTransactionWrapper } from '../../helpers' 5import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
6import Bluebird = require('bluebird')
7import { AccountInstance } from '../../models/account/account-interface'
8import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
8 9
9function processCreateActivity (activity: ActivityCreate) { 10async function processCreateActivity (activity: ActivityCreate) {
10 const activityObject = activity.object 11 const activityObject = activity.object
11 const activityType = activityObject.type 12 const activityType = activityObject.type
13 const account = await getOrCreateAccount(activity.actor)
12 14
13 if (activityType === 'Video') { 15 if (activityType === 'VideoChannel') {
14 return processCreateVideo(activityObject as VideoTorrentObject) 16 return processCreateVideoChannel(account, activityObject as VideoChannelObject)
15 } else if (activityType === 'VideoChannel') {
16 return processCreateVideoChannel(activityObject as VideoChannelObject)
17 } 17 }
18 18
19 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 19 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
20 return Promise.resolve() 20 return Promise.resolve(undefined)
21} 21}
22 22
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
@@ -28,77 +28,37 @@ export {
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31function processCreateVideo (video: VideoTorrentObject) { 31function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
32 const options = { 32 const options = {
33 arguments: [ video ], 33 arguments: [ account, videoChannelToCreateData ],
34 errorMessage: 'Cannot insert the remote video with many retries.' 34 errorMessage: 'Cannot insert the remote video channel with many retries.'
35 } 35 }
36 36
37 return retryTransactionWrapper(addRemoteVideo, options) 37 return retryTransactionWrapper(addRemoteVideoChannel, options)
38} 38}
39 39
40async function addRemoteVideo (videoToCreateData: VideoTorrentObject) { 40async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
41 logger.debug('Adding remote video %s.', videoToCreateData.url) 41 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
42 42
43 await db.sequelize.transaction(async t => { 43 await db.sequelize.transaction(async t => {
44 const sequelizeOptions = { 44 let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
45 transaction: t 45 if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
46 } 46
47 47 const videoChannelData = {
48 const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) 48 name: videoChannelToCreateData.name,
49 if (videoFromDatabase) throw new Error('UUID already exists.') 49 description: videoChannelToCreateData.content,
50 50 uuid: videoChannelToCreateData.uuid,
51 const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) 51 createdAt: videoChannelToCreateData.published,
52 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') 52 updatedAt: videoChannelToCreateData.updated,
53
54 const tags = videoToCreateData.tags
55 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
56
57 const videoData = {
58 name: videoToCreateData.name,
59 uuid: videoToCreateData.uuid,
60 category: videoToCreateData.category,
61 licence: videoToCreateData.licence,
62 language: videoToCreateData.language,
63 nsfw: videoToCreateData.nsfw,
64 description: videoToCreateData.truncatedDescription,
65 channelId: videoChannel.id,
66 duration: videoToCreateData.duration,
67 createdAt: videoToCreateData.createdAt,
68 // FIXME: updatedAt does not seems to be considered by Sequelize
69 updatedAt: videoToCreateData.updatedAt,
70 views: videoToCreateData.views,
71 likes: videoToCreateData.likes,
72 dislikes: videoToCreateData.dislikes,
73 remote: true, 53 remote: true,
74 privacy: videoToCreateData.privacy 54 accountId: account.id
75 }
76
77 const video = db.Video.build(videoData)
78 await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
79 const videoCreated = await video.save(sequelizeOptions)
80
81 const tasks = []
82 for (const fileData of videoToCreateData.files) {
83 const videoFileInstance = db.VideoFile.build({
84 extname: fileData.extname,
85 infoHash: fileData.infoHash,
86 resolution: fileData.resolution,
87 size: fileData.size,
88 videoId: videoCreated.id
89 })
90
91 tasks.push(videoFileInstance.save(sequelizeOptions))
92 } 55 }
93 56
94 await Promise.all(tasks) 57 videoChannel = db.VideoChannel.build(videoChannelData)
58 videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
95 59
96 await videoCreated.setTags(tagInstances, sequelizeOptions) 60 await videoChannel.save({ transaction: t })
97 }) 61 })
98 62
99 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) 63 logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
100}
101
102function processCreateVideoChannel (videoChannel: VideoChannelObject) {
103
104} 64}
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts
index 187c7be7c..cd8a4b8e2 100644
--- a/server/lib/activitypub/process-update.ts
+++ b/server/lib/activitypub/process-update.ts
@@ -1,15 +1,25 @@
1import { 1import { VideoChannelObject, VideoTorrentObject } from '../../../shared'
2 ActivityCreate, 2import { ActivityUpdate } from '../../../shared/models/activitypub/activity'
3 VideoTorrentObject, 3import { getOrCreateAccount } from '../../helpers/activitypub'
4 VideoChannelObject 4import { retryTransactionWrapper } from '../../helpers/database-utils'
5} from '../../../shared' 5import { logger } from '../../helpers/logger'
6import { resetSequelizeInstance } from '../../helpers/utils'
7import { database as db } from '../../initializers'
8import { AccountInstance } from '../../models/account/account-interface'
9import { VideoInstance } from '../../models/video/video-interface'
10import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
11import Bluebird = require('bluebird')
12
13async function processUpdateActivity (activity: ActivityUpdate) {
14 const account = await getOrCreateAccount(activity.actor)
6 15
7function processUpdateActivity (activity: ActivityCreate) {
8 if (activity.object.type === 'Video') { 16 if (activity.object.type === 'Video') {
9 return processUpdateVideo(activity.object) 17 return processUpdateVideo(account, activity.object)
10 } else if (activity.object.type === 'VideoChannel') { 18 } else if (activity.object.type === 'VideoChannel') {
11 return processUpdateVideoChannel(activity.object) 19 return processUpdateVideoChannel(account, activity.object)
12 } 20 }
21
22 return undefined
13} 23}
14 24
15// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
@@ -20,10 +30,107 @@ export {
20 30
21// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
22 32
23function processUpdateVideo (video: VideoTorrentObject) { 33function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
34 const options = {
35 arguments: [ account, video ],
36 errorMessage: 'Cannot update the remote video with many retries'
37 }
24 38
39 return retryTransactionWrapper(updateRemoteVideo, options)
25} 40}
26 41
27function processUpdateVideoChannel (videoChannel: VideoChannelObject) { 42async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) {
43 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
44 let videoInstance: VideoInstance
45 let videoFieldsSave: object
46
47 try {
48 await db.sequelize.transaction(async t => {
49 const sequelizeOptions = {
50 transaction: t
51 }
52
53 const videoInstance = await db.Video.loadByUrl(videoAttributesToUpdate.id, t)
54 if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
55
56 if (videoInstance.VideoChannel.Account.id !== account.id) {
57 throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
58 }
59
60 const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate, t)
61 videoInstance.set('name', videoData.name)
62 videoInstance.set('category', videoData.category)
63 videoInstance.set('licence', videoData.licence)
64 videoInstance.set('language', videoData.language)
65 videoInstance.set('nsfw', videoData.nsfw)
66 videoInstance.set('description', videoData.description)
67 videoInstance.set('duration', videoData.duration)
68 videoInstance.set('createdAt', videoData.createdAt)
69 videoInstance.set('updatedAt', videoData.updatedAt)
70 videoInstance.set('views', videoData.views)
71 // videoInstance.set('likes', videoData.likes)
72 // videoInstance.set('dislikes', videoData.dislikes)
73 // videoInstance.set('privacy', videoData.privacy)
74
75 await videoInstance.save(sequelizeOptions)
76
77 // Remove old video files
78 const videoFileDestroyTasks: Bluebird<void>[] = []
79 for (const videoFile of videoInstance.VideoFiles) {
80 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
81 }
82 await Promise.all(videoFileDestroyTasks)
83
84 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
85 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
86 await Promise.all(tasks)
87
88 const tags = videoAttributesToUpdate.tag.map(t => t.name)
89 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
90 await videoInstance.setTags(tagInstances, sequelizeOptions)
91 })
92
93 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
94 } catch (err) {
95 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
96 resetSequelizeInstance(videoInstance, videoFieldsSave)
97 }
98
99 // This is just a debug because we will retry the insert
100 logger.debug('Cannot update the remote video.', err)
101 throw err
102 }
103}
104
105async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
106 const options = {
107 arguments: [ account, videoChannel ],
108 errorMessage: 'Cannot update the remote video channel with many retries.'
109 }
110
111 await retryTransactionWrapper(updateRemoteVideoChannel, options)
112}
113
114async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
115 logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
116
117 await db.sequelize.transaction(async t => {
118 const sequelizeOptions = { transaction: t }
119
120 const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
121 if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
122
123 if (videoChannelInstance.Account.id !== account.id) {
124 throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
125 }
126
127 videoChannelInstance.set('name', videoChannel.name)
128 videoChannelInstance.set('description', videoChannel.content)
129 videoChannelInstance.set('createdAt', videoChannel.published)
130 videoChannelInstance.set('updatedAt', videoChannel.updated)
131
132 await videoChannelInstance.save(sequelizeOptions)
133 })
28 134
135 logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
29} 136}
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
new file mode 100644
index 000000000..78a6d1444
--- /dev/null
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -0,0 +1,21 @@
1import { body } from 'express-validator/check'
2import * as express from 'express'
3
4import { logger, isRootActivityValid } from '../../../helpers'
5import { checkErrors } from '../utils'
6
7const activityPubValidator = [
8 body('data').custom(isRootActivityValid),
9
10 (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 logger.debug('Checking activity pub parameters', { parameters: req.body })
12
13 checkErrors(req, res, next)
14 }
15]
16
17// ---------------------------------------------------------------------------
18
19export {
20 activityPubValidator
21}
diff --git a/server/middlewares/validators/activitypub/videos.ts b/server/middlewares/validators/activitypub/videos.ts
deleted file mode 100644
index 497320cc1..000000000
--- a/server/middlewares/validators/activitypub/videos.ts
+++ /dev/null
@@ -1,61 +0,0 @@
1import { body } from 'express-validator/check'
2import * as express from 'express'
3
4import {
5 logger,
6 isArray,
7 removeBadRequestVideos,
8 removeBadRequestVideosQadu,
9 removeBadRequestVideosEvents
10} from '../../../helpers'
11import { checkErrors } from '../utils'
12
13const remoteVideosValidator = [
14 body('data').custom(isArray),
15
16 (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking remoteVideos parameters', { parameters: req.body })
18
19 checkErrors(req, res, () => {
20 removeBadRequestVideos(req.body.data)
21
22 return next()
23 })
24 }
25]
26
27const remoteQaduVideosValidator = [
28 body('data').custom(isArray),
29
30 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 logger.debug('Checking remoteQaduVideos parameters', { parameters: req.body })
32
33 checkErrors(req, res, () => {
34 removeBadRequestVideosQadu(req.body.data)
35
36 return next()
37 })
38 }
39]
40
41const remoteEventsVideosValidator = [
42 body('data').custom(isArray),
43
44 (req: express.Request, res: express.Response, next: express.NextFunction) => {
45 logger.debug('Checking remoteEventsVideos parameters', { parameters: req.body })
46
47 checkErrors(req, res, () => {
48 removeBadRequestVideosEvents(req.body.data)
49
50 return next()
51 })
52 }
53]
54
55// ---------------------------------------------------------------------------
56
57export {
58 remoteVideosValidator,
59 remoteQaduVideosValidator,
60 remoteEventsVideosValidator
61}
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts
index 477f97cd4..55e772063 100644
--- a/server/models/video/video-channel-interface.ts
+++ b/server/models/video/video-channel-interface.ts
@@ -24,6 +24,8 @@ export namespace VideoChannelMethods {
24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 24 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> 25 export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
26 export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance> 26 export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
27 export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
28 export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
27} 29}
28 30
29export interface VideoChannelClass { 31export interface VideoChannelClass {
@@ -37,6 +39,8 @@ export interface VideoChannelClass {
37 loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount 39 loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
38 loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount 40 loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
39 loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos 41 loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
42 loadByUrl: VideoChannelMethods.LoadByUrl
43 loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
40} 44}
41 45
42export interface VideoChannelAttributes { 46export interface VideoChannelAttributes {
@@ -45,7 +49,7 @@ export interface VideoChannelAttributes {
45 name: string 49 name: string
46 description: string 50 description: string
47 remote: boolean 51 remote: boolean
48 url: string 52 url?: string
49 53
50 Account?: AccountInstance 54 Account?: AccountInstance
51 Videos?: VideoInstance[] 55 Videos?: VideoInstance[]
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index c17828f3e..93a611fa0 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -25,6 +25,8 @@ let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
25let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount 25let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
26let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 26let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
27let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos 27let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
28let loadByUrl: VideoChannelMethods.LoadByUrl
29let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
28 30
29export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 31export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
30 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', 32 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
@@ -94,12 +96,14 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
94 loadByUUID, 96 loadByUUID,
95 loadByHostAndUUID, 97 loadByHostAndUUID,
96 loadAndPopulateAccountAndVideos, 98 loadAndPopulateAccountAndVideos,
97 countByAccount 99 countByAccount,
100 loadByUrl,
101 loadByUUIDOrUrl
98 ] 102 ]
99 const instanceMethods = [ 103 const instanceMethods = [
100 isOwned, 104 isOwned,
101 toFormattedJSON, 105 toFormattedJSON,
102 toActivityPubObject, 106 toActivityPubObject
103 ] 107 ]
104 addMethodsToModel(VideoChannel, classMethods, instanceMethods) 108 addMethodsToModel(VideoChannel, classMethods, instanceMethods)
105 109
@@ -254,6 +258,33 @@ loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
254 return VideoChannel.findOne(query) 258 return VideoChannel.findOne(query)
255} 259}
256 260
261loadByUrl = function (url: string, t?: Sequelize.Transaction) {
262 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
263 where: {
264 url
265 }
266 }
267
268 if (t !== undefined) query.transaction = t
269
270 return VideoChannel.findOne(query)
271}
272
273loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) {
274 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
275 where: {
276 [Sequelize.Op.or]: [
277 { uuid },
278 { url }
279 ]
280 },
281 }
282
283 if (t !== undefined) query.transaction = t
284
285 return VideoChannel.findOne(query)
286}
287
257loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { 288loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
258 const query: Sequelize.FindOptions<VideoChannelAttributes> = { 289 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
259 where: { 290 where: {
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index e62e25a82..a0ac43e1e 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -69,6 +69,7 @@ export namespace VideoMethods {
69 export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance> 69 export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance>
70 export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance> 70 export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance>
71 export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance> 71 export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance>
72 export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
72 73
73 export type RemoveThumbnail = (this: VideoInstance) => Promise<void> 74 export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
74 export type RemovePreview = (this: VideoInstance) => Promise<void> 75 export type RemovePreview = (this: VideoInstance) => Promise<void>
@@ -89,6 +90,7 @@ export interface VideoClass {
89 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID 90 loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
90 loadByUUID: VideoMethods.LoadByUUID 91 loadByUUID: VideoMethods.LoadByUUID
91 loadByUrl: VideoMethods.LoadByUrl 92 loadByUrl: VideoMethods.LoadByUrl
93 loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
92 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 94 loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
93 loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags 95 loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
94 searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags 96 searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
@@ -109,7 +111,10 @@ export interface VideoAttributes {
109 likes?: number 111 likes?: number
110 dislikes?: number 112 dislikes?: number
111 remote: boolean 113 remote: boolean
112 url: string 114 url?: string
115
116 createdAt?: Date
117 updatedAt?: Date
113 118
114 parentId?: number 119 parentId?: number
115 channelId?: number 120 channelId?: number
@@ -120,9 +125,6 @@ export interface VideoAttributes {
120} 125}
121 126
122export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { 127export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
123 createdAt: Date
124 updatedAt: Date
125
126 createPreview: VideoMethods.CreatePreview 128 createPreview: VideoMethods.CreatePreview
127 createThumbnail: VideoMethods.CreateThumbnail 129 createThumbnail: VideoMethods.CreateThumbnail
128 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash 130 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
@@ -158,4 +160,3 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
158} 160}
159 161
160export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} 162export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
161
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 94af1ece5..b5d333347 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -25,7 +25,8 @@ import {
25 statPromise, 25 statPromise,
26 generateImageFromVideoFile, 26 generateImageFromVideoFile,
27 transcode, 27 transcode,
28 getVideoFileHeight 28 getVideoFileHeight,
29 getActivityPubUrl
29} from '../../helpers' 30} from '../../helpers'
30import { 31import {
31 CONFIG, 32 CONFIG,
@@ -88,7 +89,7 @@ let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccount
88let listOwnedByAccount: VideoMethods.ListOwnedByAccount 89let listOwnedByAccount: VideoMethods.ListOwnedByAccount
89let load: VideoMethods.Load 90let load: VideoMethods.Load
90let loadByUUID: VideoMethods.LoadByUUID 91let loadByUUID: VideoMethods.LoadByUUID
91let loadByUrl: VideoMethods.LoadByUrl 92let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
92let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID 93let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
93let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount 94let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
94let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags 95let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
@@ -277,6 +278,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
277 loadAndPopulateAccount, 278 loadAndPopulateAccount,
278 loadAndPopulateAccountAndPodAndTags, 279 loadAndPopulateAccountAndPodAndTags,
279 loadByHostAndUUID, 280 loadByHostAndUUID,
281 loadByUUIDOrURL,
280 loadByUUID, 282 loadByUUID,
281 loadLocalVideoByUUID, 283 loadLocalVideoByUUID,
282 loadByUUIDAndPopulateAccountAndPodAndTags, 284 loadByUUIDAndPopulateAccountAndPodAndTags,
@@ -595,6 +597,7 @@ toActivityPubObject = function (this: VideoInstance) {
595 597
596 const videoObject: VideoTorrentObject = { 598 const videoObject: VideoTorrentObject = {
597 type: 'Video', 599 type: 'Video',
600 id: getActivityPubUrl('video', this.uuid),
598 name: this.name, 601 name: this.name,
599 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration 602 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
600 duration: 'PT' + this.duration + 'S', 603 duration: 'PT' + this.duration + 'S',
@@ -731,6 +734,7 @@ getCategoryLabel = function (this: VideoInstance) {
731 734
732getLicenceLabel = function (this: VideoInstance) { 735getLicenceLabel = function (this: VideoInstance) {
733 let licenceLabel = VIDEO_LICENCES[this.licence] 736 let licenceLabel = VIDEO_LICENCES[this.licence]
737
734 // Maybe our pod is not up to date and there are new licences since our version 738 // Maybe our pod is not up to date and there are new licences since our version
735 if (!licenceLabel) licenceLabel = 'Unknown' 739 if (!licenceLabel) licenceLabel = 'Unknown'
736 740
@@ -946,6 +950,22 @@ loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
946 return Video.findOne(query) 950 return Video.findOne(query)
947} 951}
948 952
953loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
954 const query: Sequelize.FindOptions<VideoAttributes> = {
955 where: {
956 [Sequelize.Op.or]: [
957 { uuid },
958 { url }
959 ]
960 },
961 include: [ Video['sequelize'].models.VideoFile ]
962 }
963
964 if (t !== undefined) query.transaction = t
965
966 return Video.findOne(query)
967}
968
949loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) { 969loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
950 const query: Sequelize.FindOptions<VideoAttributes> = { 970 const query: Sequelize.FindOptions<VideoAttributes> = {
951 where: { 971 where: {
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index 0274416b2..dc562c00a 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -7,7 +7,7 @@ import { ActivityPubSignature } from './activitypub-signature'
7export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag 7export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag
8 8
9// Flag -> report abuse 9// Flag -> report abuse
10export type ActivityType = 'Create' | 'Update' | 'Flag' 10export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag'
11 11
12export interface BaseActivity { 12export interface BaseActivity {
13 '@context'?: any[] 13 '@context'?: any[]
@@ -20,7 +20,12 @@ export interface BaseActivity {
20 20
21export interface ActivityCreate extends BaseActivity { 21export interface ActivityCreate extends BaseActivity {
22 type: 'Create' 22 type: 'Create'
23 object: VideoTorrentObject | VideoChannelObject 23 object: VideoChannelObject
24}
25
26export interface ActivityAdd extends BaseActivity {
27 type: 'Add'
28 object: VideoTorrentObject
24} 29}
25 30
26export interface ActivityUpdate extends BaseActivity { 31export interface ActivityUpdate extends BaseActivity {
diff --git a/shared/models/activitypub/objects/video-channel-object.ts b/shared/models/activitypub/objects/video-channel-object.ts
index d64b4aed8..72efe42b3 100644
--- a/shared/models/activitypub/objects/video-channel-object.ts
+++ b/shared/models/activitypub/objects/video-channel-object.ts
@@ -2,7 +2,10 @@ import { ActivityIdentifierObject } from './common-objects'
2 2
3export interface VideoChannelObject { 3export interface VideoChannelObject {
4 type: 'VideoChannel' 4 type: 'VideoChannel'
5 id: string
5 name: string 6 name: string
6 content: string 7 content: string
7 uuid: ActivityIdentifierObject 8 uuid: string
9 published: Date
10 updated: Date
8} 11}
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
index 00cc0a649..5685a43e0 100644
--- a/shared/models/activitypub/objects/video-torrent-object.ts
+++ b/shared/models/activitypub/objects/video-torrent-object.ts
@@ -7,6 +7,7 @@ import {
7 7
8export interface VideoTorrentObject { 8export interface VideoTorrentObject {
9 type: 'Video' 9 type: 'Video'
10 id: string
10 name: string 11 name: string
11 duration: string 12 duration: string
12 uuid: string 13 uuid: string