]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/friends.ts
Use async/await in controllers
[github/Chocobozzz/PeerTube.git] / server / lib / friends.ts
CommitLineData
4d4e5cd4 1import * as request from 'request'
69818c93 2import * as Sequelize from 'sequelize'
6fcd19ba 3import * as Promise from 'bluebird'
f981dae8 4import { join } from 'path'
9f10b292 5
e02643f3 6import { database as db } from '../initializers/database'
65fcc311
C
7import {
8 API_VERSION,
9 CONFIG,
10 REQUESTS_IN_PARALLEL,
11 REQUEST_ENDPOINTS,
12 REQUEST_ENDPOINT_ACTIONS,
f981dae8
C
13 REMOTE_SCHEME,
14 STATIC_PATHS
65fcc311
C
15} from '../initializers'
16import {
17 logger,
18 getMyPublicCert,
19 makeSecureRequest,
6fcd19ba 20 makeRetryRequest
65fcc311
C
21} from '../helpers'
22import {
23 RequestScheduler,
69818c93
C
24 RequestSchedulerOptions,
25
65fcc311 26 RequestVideoQaduScheduler,
69818c93
C
27 RequestVideoQaduSchedulerOptions,
28
29 RequestVideoEventScheduler,
30 RequestVideoEventSchedulerOptions
65fcc311 31} from './request'
ee9e7b61
C
32import {
33 PodInstance,
34 VideoInstance
35} from '../models'
36import {
37 RequestEndpoint,
38 RequestVideoEventType,
4771e000
C
39 RequestVideoQaduType,
40 RemoteVideoCreateData,
41 RemoteVideoUpdateData,
42 RemoteVideoRemoveData,
e6d4b0ff
C
43 RemoteVideoReportAbuseData,
44 ResultList,
72c7248b
C
45 RemoteVideoRequestType,
46 Pod as FormattedPod,
47 RemoteVideoChannelCreateData,
48 RemoteVideoChannelUpdateData,
49 RemoteVideoChannelRemoveData,
50 RemoteVideoAuthorCreateData,
51 RemoteVideoAuthorRemoveData
ee9e7b61 52} from '../../shared'
69818c93 53
0a6658fd
C
54type QaduParam = { videoId: number, type: RequestVideoQaduType }
55type EventParam = { videoId: number, type: RequestVideoEventType }
65fcc311
C
56
57const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS]
9e167724 58
fe783f6b 59const requestScheduler = new RequestScheduler()
99fdec46
C
60const requestVideoQaduScheduler = new RequestVideoQaduScheduler()
61const requestVideoEventScheduler = new RequestVideoEventScheduler()
da691c46 62
65fcc311 63function activateSchedulers () {
c1a7ab7f 64 requestScheduler.activate()
99fdec46
C
65 requestVideoQaduScheduler.activate()
66 requestVideoEventScheduler.activate()
c1a7ab7f
C
67}
68
4771e000 69function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) {
ed04d94f 70 const options = {
72c7248b 71 type: ENDPOINT_ACTIONS.ADD_VIDEO,
65fcc311 72 endpoint: REQUEST_ENDPOINTS.VIDEOS,
ed04d94f
C
73 data: videoData,
74 transaction
75 }
6fcd19ba 76 return createRequest(options)
9f10b292
C
77}
78
4771e000 79function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) {
ed04d94f 80 const options = {
72c7248b 81 type: ENDPOINT_ACTIONS.UPDATE_VIDEO,
65fcc311 82 endpoint: REQUEST_ENDPOINTS.VIDEOS,
ed04d94f
C
83 data: videoData,
84 transaction
85 }
6fcd19ba 86 return createRequest(options)
55fa55a9
C
87}
88
91f6f169 89function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) {
ed04d94f 90 const options = {
72c7248b 91 type: ENDPOINT_ACTIONS.REMOVE_VIDEO,
65fcc311 92 endpoint: REQUEST_ENDPOINTS.VIDEOS,
69818c93 93 data: videoParams,
91f6f169 94 transaction
ed04d94f 95 }
6fcd19ba 96 return createRequest(options)
55fa55a9
C
97}
98
72c7248b
C
99function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) {
100 const options = {
101 type: ENDPOINT_ACTIONS.ADD_AUTHOR,
102 endpoint: REQUEST_ENDPOINTS.VIDEOS,
103 data: authorData,
104 transaction
105 }
106 return createRequest(options)
107}
108
109function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction: Sequelize.Transaction) {
110 const options = {
111 type: ENDPOINT_ACTIONS.REMOVE_AUTHOR,
112 endpoint: REQUEST_ENDPOINTS.VIDEOS,
113 data: authorData,
114 transaction
115 }
116 return createRequest(options)
117}
118
119function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) {
120 const options = {
121 type: ENDPOINT_ACTIONS.ADD_CHANNEL,
122 endpoint: REQUEST_ENDPOINTS.VIDEOS,
123 data: videoChannelData,
124 transaction
125 }
126 return createRequest(options)
127}
128
129function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) {
130 const options = {
131 type: ENDPOINT_ACTIONS.UPDATE_CHANNEL,
132 endpoint: REQUEST_ENDPOINTS.VIDEOS,
133 data: videoChannelData,
134 transaction
135 }
136 return createRequest(options)
137}
138
139function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction: Sequelize.Transaction) {
140 const options = {
141 type: ENDPOINT_ACTIONS.REMOVE_CHANNEL,
142 endpoint: REQUEST_ENDPOINTS.VIDEOS,
143 data: videoChannelParams,
144 transaction
145 }
146 return createRequest(options)
147}
148
4771e000 149function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) {
bd14d16a 150 const options = {
da691c46 151 type: ENDPOINT_ACTIONS.REPORT_ABUSE,
65fcc311 152 endpoint: REQUEST_ENDPOINTS.VIDEOS,
bd14d16a 153 data: reportData,
72c7248b 154 toIds: [ video.VideoChannel.Author.podId ],
6fcd19ba 155 transaction
bd14d16a 156 }
6fcd19ba 157 return createRequest(options)
7b1f49de
C
158}
159
6fcd19ba 160function quickAndDirtyUpdateVideoToFriends (qaduParam: QaduParam, transaction?: Sequelize.Transaction) {
9e167724 161 const options = {
69818c93
C
162 videoId: qaduParam.videoId,
163 type: qaduParam.type,
9e167724
C
164 transaction
165 }
6fcd19ba 166 return createVideoQaduRequest(options)
9e167724
C
167}
168
6fcd19ba 169function quickAndDirtyUpdatesVideoToFriends (qadusParams: QaduParam[], transaction: Sequelize.Transaction) {
d38b8281
C
170 const tasks = []
171
075f16ca 172 qadusParams.forEach(qaduParams => {
6fcd19ba 173 tasks.push(quickAndDirtyUpdateVideoToFriends(qaduParams, transaction))
d38b8281
C
174 })
175
6fcd19ba 176 return Promise.all(tasks)
d38b8281
C
177}
178
6fcd19ba 179function addEventToRemoteVideo (eventParam: EventParam, transaction?: Sequelize.Transaction) {
e4c87ec2 180 const options = {
69818c93
C
181 videoId: eventParam.videoId,
182 type: eventParam.type,
e4c87ec2
C
183 transaction
184 }
6fcd19ba 185 return createVideoEventRequest(options)
e4c87ec2
C
186}
187
6fcd19ba 188function addEventsToRemoteVideo (eventsParams: EventParam[], transaction: Sequelize.Transaction) {
d38b8281
C
189 const tasks = []
190
075f16ca 191 eventsParams.forEach(eventParams => {
6fcd19ba 192 tasks.push(addEventToRemoteVideo(eventParams, transaction))
d38b8281
C
193 })
194
6fcd19ba 195 return Promise.all(tasks)
d38b8281
C
196}
197
6fcd19ba
C
198function hasFriends () {
199 return db.Pod.countAll().then(count => count !== 0)
9f10b292
C
200}
201
6fcd19ba 202function makeFriends (hosts: string[]) {
bc503c2a 203 const podsScore = {}
9f10b292
C
204
205 logger.info('Make friends!')
6fcd19ba
C
206 return getMyPublicCert()
207 .then(cert => {
709756b8 208 return Promise.each(hosts, host => computeForeignPodsList(host, podsScore)).then(() => cert)
6fcd19ba
C
209 })
210 .then(cert => {
bc503c2a 211 logger.debug('Pods scores computed.', { podsScore: podsScore })
49abbbbe 212 const podsList = computeWinningPods(hosts, podsScore)
bc503c2a 213 logger.debug('Pods that we keep.', { podsToKeep: podsList })
9f10b292 214
6fcd19ba 215 return makeRequestsToWinningPods(cert, podsList)
c173e565 216 })
9f10b292
C
217}
218
6fcd19ba 219function quitFriends () {
9f10b292 220 // Stop pool requests
c1a7ab7f 221 requestScheduler.deactivate()
9f10b292 222
6fcd19ba
C
223 return requestScheduler.flush()
224 .then(() => {
225 return requestVideoQaduScheduler.flush()
226 })
227 .then(() => {
228 return db.Pod.list()
229 })
230 .then(pods => {
528a9efa 231 const requestParams = {
69818c93 232 method: 'POST' as 'POST',
65fcc311 233 path: '/api/' + API_VERSION + '/remote/pods/remove',
65fcc311 234 toPod: null
c173e565
C
235 }
236
e7ea2817 237 // Announce we quit them
528a9efa
C
238 // We don't care if the request fails
239 // The other pod will exclude us automatically after a while
6fcd19ba 240 return Promise.map(pods, pod => {
528a9efa 241 requestParams.toPod = pod
709756b8 242
6fcd19ba
C
243 return makeSecureRequest(requestParams)
244 }, { concurrency: REQUESTS_IN_PARALLEL })
245 .then(() => pods)
246 .catch(err => {
ad0997ad 247 logger.error('Some errors while quitting friends.', err)
6fcd19ba 248 // Don't stop the process
d592e0a9 249 return pods
e7ea2817 250 })
6fcd19ba
C
251 })
252 .then(pods => {
253 const tasks = []
254 pods.forEach(pod => tasks.push(pod.destroy()))
e7ea2817 255
6fcd19ba
C
256 return Promise.all(pods)
257 })
258 .then(() => {
259 logger.info('Removed all remote videos.')
260 // Don't forget to re activate the scheduler, even if there was an error
261 return requestScheduler.activate()
262 })
263 .finally(() => requestScheduler.activate())
9f10b292 264}
c173e565 265
72c7248b
C
266function sendOwnedDataToPod (podId: number) {
267 // First send authors
268 return sendOwnedAuthorsToPod(podId)
269 .then(() => sendOwnedChannelsToPod(podId))
270 .then(() => sendOwnedVideosToPod(podId))
271}
272
273function sendOwnedChannelsToPod (podId: number) {
274 return db.VideoChannel.listOwned()
275 .then(videoChannels => {
276 const tasks = []
277 videoChannels.forEach(videoChannel => {
278 const remoteVideoChannel = videoChannel.toAddRemoteJSON()
279 const options = {
280 type: 'add-channel' as 'add-channel',
281 endpoint: REQUEST_ENDPOINTS.VIDEOS,
282 data: remoteVideoChannel,
283 toIds: [ podId ],
284 transaction: null
285 }
286
287 const p = createRequest(options)
288 tasks.push(p)
289 })
290
291 return Promise.all(tasks)
292 })
293}
294
295function sendOwnedAuthorsToPod (podId: number) {
296 return db.Author.listOwned()
297 .then(authors => {
298 const tasks = []
299 authors.forEach(author => {
300 const remoteAuthor = author.toAddRemoteJSON()
301 const options = {
302 type: 'add-author' as 'add-author',
303 endpoint: REQUEST_ENDPOINTS.VIDEOS,
304 data: remoteAuthor,
305 toIds: [ podId ],
306 transaction: null
307 }
308
309 const p = createRequest(options)
310 tasks.push(p)
311 })
312
313 return Promise.all(tasks)
314 })
315}
316
69818c93 317function sendOwnedVideosToPod (podId: number) {
72c7248b 318 return db.Video.listOwnedAndPopulateAuthorAndTags()
6fcd19ba
C
319 .then(videosList => {
320 const tasks = []
321 videosList.forEach(video => {
322 const promise = video.toAddRemoteJSON()
323 .then(remoteVideo => {
324 const options = {
72c7248b 325 type: 'add-video' as 'add-video',
6fcd19ba
C
326 endpoint: REQUEST_ENDPOINTS.VIDEOS,
327 data: remoteVideo,
328 toIds: [ podId ],
329 transaction: null
330 }
331 return createRequest(options)
332 })
333 .catch(err => {
ad0997ad 334 logger.error('Cannot convert video to remote.', err)
6fcd19ba
C
335 // Don't break the process
336 return undefined
337 })
338
339 tasks.push(promise)
528a9efa 340 })
6fcd19ba
C
341
342 return Promise.all(tasks)
528a9efa 343 })
9f10b292 344}
c173e565 345
72c7248b
C
346function fetchRemotePreview (video: VideoInstance) {
347 const host = video.VideoChannel.Author.Pod.host
f981dae8
C
348 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
349
350 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
351}
352
d5f5a670
GS
353function removeFriend (pod: PodInstance) {
354 const requestParams = {
355 method: 'POST' as 'POST',
356 path: '/api/' + API_VERSION + '/remote/pods/remove',
357 toPod: pod
358 }
359
360 return makeSecureRequest(requestParams)
bda65bdc 361 .catch(err => logger.warn('Cannot notify friends %s we are quitting him.', pod.host, err))
d5f5a670 362 .then(() => pod.destroy())
bda65bdc
C
363 .then(() => logger.info('Removed friend %s.', pod.host))
364 .catch(err => logger.error('Cannot destroy friend %s.', pod.host, err))
d5f5a670
GS
365}
366
99fdec46
C
367function getRequestScheduler () {
368 return requestScheduler
369}
370
371function getRequestVideoQaduScheduler () {
372 return requestVideoQaduScheduler
373}
374
375function getRequestVideoEventScheduler () {
376 return requestVideoEventScheduler
377}
378
9f10b292 379// ---------------------------------------------------------------------------
c173e565 380
65fcc311
C
381export {
382 activateSchedulers,
383 addVideoToFriends,
72c7248b 384 removeVideoAuthorToFriends,
65fcc311 385 updateVideoToFriends,
72c7248b 386 addVideoAuthorToFriends,
65fcc311
C
387 reportAbuseVideoToFriend,
388 quickAndDirtyUpdateVideoToFriends,
389 quickAndDirtyUpdatesVideoToFriends,
390 addEventToRemoteVideo,
391 addEventsToRemoteVideo,
392 hasFriends,
393 makeFriends,
394 quitFriends,
d5f5a670 395 removeFriend,
65fcc311 396 removeVideoToFriends,
72c7248b 397 sendOwnedDataToPod,
65fcc311
C
398 getRequestScheduler,
399 getRequestVideoQaduScheduler,
f981dae8 400 getRequestVideoEventScheduler,
72c7248b
C
401 fetchRemotePreview,
402 addVideoChannelToFriends,
403 updateVideoChannelToFriends,
404 removeVideoChannelToFriends
65fcc311 405}
c173e565 406
9f10b292 407// ---------------------------------------------------------------------------
c173e565 408
6fcd19ba
C
409function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) {
410 // TODO: type res
e6d4b0ff
C
411 return getForeignPodsList(host).then(res => {
412 const foreignPodsList: { host: string }[] = res.data
528a9efa
C
413
414 // Let's give 1 point to the pod we ask the friends list
49abbbbe 415 foreignPodsList.push({ host })
89d1d8ba 416
6fcd19ba 417 foreignPodsList.forEach(foreignPod => {
49abbbbe 418 const foreignPodHost = foreignPod.host
89d1d8ba 419
49abbbbe
C
420 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
421 else podsScore[foreignPodHost] = 1
89d1d8ba 422 })
cbe2f7c3 423
6fcd19ba 424 return undefined
89d1d8ba
C
425 })
426}
427
69818c93 428function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: number }) {
89d1d8ba
C
429 // Build the list of pods to add
430 // Only add a pod if it exists in more than a half base pods
bc503c2a 431 const podsList = []
49abbbbe 432 const baseScore = hosts.length / 2
1e4b0080 433
6fcd19ba 434 Object.keys(podsScore).forEach(podHost => {
2c49ca42 435 // If the pod is not me and with a good score we add it
49abbbbe
C
436 if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
437 podsList.push({ host: podHost })
2c49ca42 438 }
89d1d8ba 439 })
e7ea2817 440
bc503c2a 441 return podsList
89d1d8ba
C
442}
443
6fcd19ba 444function getForeignPodsList (host: string) {
0aef76c4 445 return new Promise< ResultList<FormattedPod> >((res, rej) => {
8a02bd04 446 const path = '/api/' + API_VERSION + '/remote/pods/list'
c173e565 447
8a02bd04 448 request.post(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
6fcd19ba 449 if (err) return rej(err)
8425cb89 450
6fcd19ba
C
451 try {
452 const json = JSON.parse(body)
453 return res(json)
454 } catch (err) {
455 return rej(err)
456 }
457 })
9f10b292
C
458 })
459}
89d1d8ba 460
6fcd19ba 461function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
89d1d8ba 462 // Stop pool requests
c1a7ab7f 463 requestScheduler.deactivate()
89d1d8ba 464 // Flush pool requests
c1a7ab7f 465 requestScheduler.forceSend()
89d1d8ba 466
6fcd19ba 467 return Promise.map(podsList, pod => {
528a9efa 468 const params = {
8a02bd04 469 url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/remote/pods/add',
69818c93 470 method: 'POST' as 'POST',
528a9efa 471 json: {
65fcc311
C
472 host: CONFIG.WEBSERVER.HOST,
473 email: CONFIG.ADMIN.EMAIL,
528a9efa
C
474 publicKey: cert
475 }
89d1d8ba
C
476 }
477
6fcd19ba
C
478 return makeRetryRequest(params)
479 .then(({ response, body }) => {
480 body = body as { cert: string, email: string }
481
482 if (response.statusCode === 200) {
483 const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email })
484 return podObj.save()
485 .then(podCreated => {
486
487 // Add our videos to the request scheduler
72c7248b 488 sendOwnedDataToPod(podCreated.id)
6fcd19ba
C
489 })
490 .catch(err => {
ad0997ad 491 logger.error('Cannot add friend %s pod.', pod.host, err)
6fcd19ba
C
492 })
493 } else {
494 logger.error('Status not 200 for %s pod.', pod.host)
495 }
496 })
497 .catch(err => {
498 logger.error('Error with adding %s pod.', pod.host, { error: err.stack })
528a9efa 499 // Don't break the process
6fcd19ba
C
500 })
501 }, { concurrency: REQUESTS_IN_PARALLEL })
502 .then(() => logger.debug('makeRequestsToWinningPods finished.'))
503 .finally(() => {
528a9efa
C
504 // Final callback, we've ended all the requests
505 // Now we made new friends, we can re activate the pool of requests
c1a7ab7f 506 requestScheduler.activate()
89d1d8ba
C
507 })
508}
00057e85 509
55fa55a9 510// Wrapper that populate "toIds" argument with all our friends if it is not specified
69818c93 511type CreateRequestOptions = {
72c7248b 512 type: RemoteVideoRequestType
ee9e7b61 513 endpoint: RequestEndpoint
69818c93
C
514 data: Object
515 toIds?: number[]
516 transaction: Sequelize.Transaction
517}
6fcd19ba
C
518function createRequest (options: CreateRequestOptions) {
519 if (options.toIds !== undefined) return requestScheduler.createRequest(options as RequestSchedulerOptions)
feb4bdfd 520
55fa55a9 521 // If the "toIds" pods is not specified, we send the request to all our friends
6fcd19ba 522 return db.Pod.listAllIds(options.transaction).then(podIds => {
ed04d94f 523 const newOptions = Object.assign(options, { toIds: podIds })
6fcd19ba 524 return requestScheduler.createRequest(newOptions)
00057e85
C
525 })
526}
2c49ca42 527
6fcd19ba
C
528function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) {
529 return requestVideoQaduScheduler.createRequest(options)
9e167724
C
530}
531
6fcd19ba
C
532function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) {
533 return requestVideoEventScheduler.createRequest(options)
e4c87ec2
C
534}
535
69818c93 536function isMe (host: string) {
65fcc311 537 return host === CONFIG.WEBSERVER.HOST
2c49ca42 538}