]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/friends.ts
Use async/await in lib and initializers
[github/Chocobozzz/PeerTube.git] / server / lib / friends.ts
CommitLineData
4d4e5cd4 1import * as request from 'request'
69818c93 2import * as Sequelize from 'sequelize'
f5028693 3import * as Bluebird 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
f5028693 191 for (const eventParams of eventsParams) {
6fcd19ba 192 tasks.push(addEventToRemoteVideo(eventParams, transaction))
f5028693 193 }
d38b8281 194
6fcd19ba 195 return Promise.all(tasks)
d38b8281
C
196}
197
f5028693
C
198async function hasFriends () {
199 const count = await db.Pod.countAll()
200
201 return count !== 0
9f10b292
C
202}
203
f5028693 204async function makeFriends (hosts: string[]) {
bc503c2a 205 const podsScore = {}
9f10b292
C
206
207 logger.info('Make friends!')
f5028693 208 const cert = await getMyPublicCert()
9f10b292 209
f5028693
C
210 for (const host of hosts) {
211 await computeForeignPodsList(host, podsScore)
212 }
213
214 logger.debug('Pods scores computed.', { podsScore: podsScore })
215
216 const podsList = computeWinningPods(hosts, podsScore)
217 logger.debug('Pods that we keep.', { podsToKeep: podsList })
218
219 return makeRequestsToWinningPods(cert, podsList)
9f10b292
C
220}
221
f5028693 222async function quitFriends () {
9f10b292 223 // Stop pool requests
c1a7ab7f 224 requestScheduler.deactivate()
9f10b292 225
f5028693
C
226 try {
227 await requestScheduler.flush()
228
229 await requestVideoQaduScheduler.flush()
230
231 const pods = await db.Pod.list()
232 const requestParams = {
233 method: 'POST' as 'POST',
234 path: '/api/' + API_VERSION + '/remote/pods/remove',
235 toPod: null
236 }
c173e565 237
f5028693
C
238 // Announce we quit them
239 // We don't care if the request fails
240 // The other pod will exclude us automatically after a while
241 try {
242 await Bluebird.map(pods, pod => {
528a9efa 243 requestParams.toPod = pod
709756b8 244
6fcd19ba
C
245 return makeSecureRequest(requestParams)
246 }, { concurrency: REQUESTS_IN_PARALLEL })
f5028693
C
247 } catch (err) { // Don't stop the process
248 logger.error('Some errors while quitting friends.', err)
249 }
e7ea2817 250
f5028693
C
251 const tasks = []
252 for (const pod of pods) {
253 tasks.push(pod.destroy())
254 }
255 await Promise.all(pods)
256
257 logger.info('Removed all remote videos.')
258
259 requestScheduler.activate()
260 } catch (err) {
261 // Don't forget to re activate the scheduler, even if there was an error
262 requestScheduler.activate()
263
264 throw err
265 }
9f10b292 266}
c173e565 267
f5028693 268async function sendOwnedDataToPod (podId: number) {
72c7248b 269 // First send authors
f5028693
C
270 await sendOwnedAuthorsToPod(podId)
271 await sendOwnedChannelsToPod(podId)
272 await sendOwnedVideosToPod(podId)
273}
274
275async function sendOwnedChannelsToPod (podId: number) {
276 const videoChannels = await db.VideoChannel.listOwned()
277
278 const tasks: Promise<any>[] = []
279 for (const videoChannel of videoChannels) {
280 const remoteVideoChannel = videoChannel.toAddRemoteJSON()
281 const options = {
282 type: 'add-channel' as 'add-channel',
283 endpoint: REQUEST_ENDPOINTS.VIDEOS,
284 data: remoteVideoChannel,
285 toIds: [ podId ],
286 transaction: null
287 }
288
289 const p = createRequest(options)
290 tasks.push(p)
291 }
292
293 await Promise.all(tasks)
72c7248b
C
294}
295
f5028693
C
296async function sendOwnedAuthorsToPod (podId: number) {
297 const authors = await db.Author.listOwned()
298 const tasks: Promise<any>[] = []
72c7248b 299
f5028693
C
300 for (const author of authors) {
301 const remoteAuthor = author.toAddRemoteJSON()
302 const options = {
303 type: 'add-author' as 'add-author',
304 endpoint: REQUEST_ENDPOINTS.VIDEOS,
305 data: remoteAuthor,
306 toIds: [ podId ],
307 transaction: null
308 }
72c7248b 309
f5028693
C
310 const p = createRequest(options)
311 tasks.push(p)
312 }
313
314 await Promise.all(tasks)
72c7248b
C
315}
316
f5028693
C
317async function sendOwnedVideosToPod (podId: number) {
318 const videosList = await db.Video.listOwnedAndPopulateAuthorAndTags()
319 const tasks: Bluebird<any>[] = []
320
321 for (const video of videosList) {
322 const promise = video.toAddRemoteJSON()
323 .then(remoteVideo => {
72c7248b 324 const options = {
f5028693 325 type: 'add-video' as 'add-video',
72c7248b 326 endpoint: REQUEST_ENDPOINTS.VIDEOS,
f5028693 327 data: remoteVideo,
72c7248b
C
328 toIds: [ podId ],
329 transaction: null
330 }
f5028693 331 return createRequest(options)
72c7248b 332 })
f5028693
C
333 .catch(err => {
334 logger.error('Cannot convert video to remote.', err)
335 // Don't break the process
336 return undefined
528a9efa 337 })
6fcd19ba 338
f5028693
C
339 tasks.push(promise)
340 }
341
342 await Promise.all(tasks)
9f10b292 343}
c173e565 344
72c7248b
C
345function fetchRemotePreview (video: VideoInstance) {
346 const host = video.VideoChannel.Author.Pod.host
f981dae8
C
347 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
348
349 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
350}
351
f5028693 352async function removeFriend (pod: PodInstance) {
d5f5a670
GS
353 const requestParams = {
354 method: 'POST' as 'POST',
355 path: '/api/' + API_VERSION + '/remote/pods/remove',
356 toPod: pod
357 }
358
f5028693
C
359 try {
360 await makeSecureRequest(requestParams)
361 } catch (err) {
362 logger.warn('Cannot notify friends %s we are quitting him.', pod.host, err)
363 }
364
365 try {
366 await pod.destroy()
367
368 logger.info('Removed friend %s.', pod.host)
369 } catch (err) {
370 logger.error('Cannot destroy friend %s.', pod.host, err)
371 }
d5f5a670
GS
372}
373
99fdec46
C
374function getRequestScheduler () {
375 return requestScheduler
376}
377
378function getRequestVideoQaduScheduler () {
379 return requestVideoQaduScheduler
380}
381
382function getRequestVideoEventScheduler () {
383 return requestVideoEventScheduler
384}
385
9f10b292 386// ---------------------------------------------------------------------------
c173e565 387
65fcc311
C
388export {
389 activateSchedulers,
390 addVideoToFriends,
72c7248b 391 removeVideoAuthorToFriends,
65fcc311 392 updateVideoToFriends,
72c7248b 393 addVideoAuthorToFriends,
65fcc311
C
394 reportAbuseVideoToFriend,
395 quickAndDirtyUpdateVideoToFriends,
396 quickAndDirtyUpdatesVideoToFriends,
397 addEventToRemoteVideo,
398 addEventsToRemoteVideo,
399 hasFriends,
400 makeFriends,
401 quitFriends,
d5f5a670 402 removeFriend,
65fcc311 403 removeVideoToFriends,
72c7248b 404 sendOwnedDataToPod,
65fcc311
C
405 getRequestScheduler,
406 getRequestVideoQaduScheduler,
f981dae8 407 getRequestVideoEventScheduler,
72c7248b
C
408 fetchRemotePreview,
409 addVideoChannelToFriends,
410 updateVideoChannelToFriends,
411 removeVideoChannelToFriends
65fcc311 412}
c173e565 413
9f10b292 414// ---------------------------------------------------------------------------
c173e565 415
f5028693
C
416async function computeForeignPodsList (host: string, podsScore: { [ host: string ]: number }) {
417 const result = await getForeignPodsList(host)
418 const foreignPodsList: { host: string }[] = result.data
528a9efa 419
f5028693
C
420 // Let's give 1 point to the pod we ask the friends list
421 foreignPodsList.push({ host })
89d1d8ba 422
f5028693
C
423 for (const foreignPod of foreignPodsList) {
424 const foreignPodHost = foreignPod.host
89d1d8ba 425
f5028693
C
426 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
427 else podsScore[foreignPodHost] = 1
428 }
cbe2f7c3 429
f5028693 430 return undefined
89d1d8ba
C
431}
432
69818c93 433function computeWinningPods (hosts: string[], podsScore: { [ host: string ]: number }) {
89d1d8ba
C
434 // Build the list of pods to add
435 // Only add a pod if it exists in more than a half base pods
bc503c2a 436 const podsList = []
49abbbbe 437 const baseScore = hosts.length / 2
1e4b0080 438
f5028693 439 for (const podHost of Object.keys(podsScore)) {
2c49ca42 440 // If the pod is not me and with a good score we add it
49abbbbe
C
441 if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
442 podsList.push({ host: podHost })
2c49ca42 443 }
f5028693 444 }
e7ea2817 445
bc503c2a 446 return podsList
89d1d8ba
C
447}
448
6fcd19ba 449function getForeignPodsList (host: string) {
0aef76c4 450 return new Promise< ResultList<FormattedPod> >((res, rej) => {
8a02bd04 451 const path = '/api/' + API_VERSION + '/remote/pods/list'
c173e565 452
8a02bd04 453 request.post(REMOTE_SCHEME.HTTP + '://' + host + path, (err, response, body) => {
6fcd19ba 454 if (err) return rej(err)
8425cb89 455
6fcd19ba 456 try {
f5028693 457 const json: ResultList<FormattedPod> = JSON.parse(body)
6fcd19ba
C
458 return res(json)
459 } catch (err) {
460 return rej(err)
461 }
462 })
9f10b292
C
463 })
464}
89d1d8ba 465
f5028693 466async function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) {
89d1d8ba 467 // Stop pool requests
c1a7ab7f 468 requestScheduler.deactivate()
89d1d8ba 469 // Flush pool requests
c1a7ab7f 470 requestScheduler.forceSend()
89d1d8ba 471
f5028693
C
472 try {
473 await Bluebird.map(podsList, async pod => {
474 const params = {
475 url: REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + API_VERSION + '/remote/pods/add',
476 method: 'POST' as 'POST',
477 json: {
478 host: CONFIG.WEBSERVER.HOST,
479 email: CONFIG.ADMIN.EMAIL,
480 publicKey: cert
481 }
528a9efa 482 }
89d1d8ba 483
f5028693
C
484 const { response, body } = await makeRetryRequest(params)
485 const typedBody = body as { cert: string, email: string }
486
487 if (response.statusCode === 200) {
488 const podObj = db.Pod.build({ host: pod.host, publicKey: typedBody.cert, email: typedBody.email })
489
490 let podCreated: PodInstance
491 try {
492 podCreated = await podObj.save()
493 } catch (err) {
494 logger.error('Cannot add friend %s pod.', pod.host, err)
6fcd19ba 495 }
f5028693
C
496
497 // Add our videos to the request scheduler
498 sendOwnedDataToPod(podCreated.id)
499 .catch(err => logger.warn('Cannot send owned data to pod %d.', podCreated.id, err))
500 } else {
501 logger.error('Status not 200 for %s pod.', pod.host)
502 }
503 }, { concurrency: REQUESTS_IN_PARALLEL })
504
505 logger.debug('makeRequestsToWinningPods finished.')
506
507 requestScheduler.activate()
508 } catch (err) {
528a9efa
C
509 // Final callback, we've ended all the requests
510 // Now we made new friends, we can re activate the pool of requests
c1a7ab7f 511 requestScheduler.activate()
f5028693 512 }
89d1d8ba 513}
00057e85 514
55fa55a9 515// Wrapper that populate "toIds" argument with all our friends if it is not specified
69818c93 516type CreateRequestOptions = {
72c7248b 517 type: RemoteVideoRequestType
ee9e7b61 518 endpoint: RequestEndpoint
69818c93
C
519 data: Object
520 toIds?: number[]
521 transaction: Sequelize.Transaction
522}
f5028693
C
523async function createRequest (options: CreateRequestOptions) {
524 if (options.toIds !== undefined) {
525 await requestScheduler.createRequest(options as RequestSchedulerOptions)
526 return undefined
527 }
feb4bdfd 528
55fa55a9 529 // If the "toIds" pods is not specified, we send the request to all our friends
f5028693
C
530 const podIds = await db.Pod.listAllIds(options.transaction)
531
532 const newOptions = Object.assign(options, { toIds: podIds })
533 await requestScheduler.createRequest(newOptions)
534
535 return undefined
00057e85 536}
2c49ca42 537
6fcd19ba
C
538function createVideoQaduRequest (options: RequestVideoQaduSchedulerOptions) {
539 return requestVideoQaduScheduler.createRequest(options)
9e167724
C
540}
541
6fcd19ba
C
542function createVideoEventRequest (options: RequestVideoEventSchedulerOptions) {
543 return requestVideoEventScheduler.createRequest(options)
e4c87ec2
C
544}
545
69818c93 546function isMe (host: string) {
65fcc311 547 return host === CONFIG.WEBSERVER.HOST
2c49ca42 548}