]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/friends.js
424a30801f441f90960b220bbb6b1e656d355536
[github/Chocobozzz/PeerTube.git] / server / lib / friends.js
1 'use strict'
2
3 const each = require('async/each')
4 const eachLimit = require('async/eachLimit')
5 const eachSeries = require('async/eachSeries')
6 const request = require('request')
7 const waterfall = require('async/waterfall')
8
9 const constants = require('../initializers/constants')
10 const db = require('../initializers/database')
11 const logger = require('../helpers/logger')
12 const peertubeCrypto = require('../helpers/peertube-crypto')
13 const requests = require('../helpers/requests')
14 const RequestScheduler = require('./request-scheduler')
15 const RequestVideoQaduScheduler = require('./request-video-qadu-scheduler')
16
17 const ENDPOINT_ACTIONS = constants.REQUEST_ENDPOINT_ACTIONS[constants.REQUEST_ENDPOINTS.VIDEOS]
18
19 const requestScheduler = new RequestScheduler()
20 const requestSchedulerVideoQadu = new RequestVideoQaduScheduler()
21
22 const friends = {
23 activate,
24 addVideoToFriends,
25 updateVideoToFriends,
26 reportAbuseVideoToFriend,
27 quickAndDirtyUpdateVideoToFriends,
28 hasFriends,
29 makeFriends,
30 quitFriends,
31 removeVideoToFriends,
32 sendOwnedVideosToPod
33 }
34
35 function activate () {
36 requestScheduler.activate()
37 requestSchedulerVideoQadu.activate()
38 }
39
40 function addVideoToFriends (videoData, transaction, callback) {
41 const options = {
42 type: ENDPOINT_ACTIONS.ADD,
43 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
44 data: videoData,
45 transaction
46 }
47 createRequest(options, callback)
48 }
49
50 function updateVideoToFriends (videoData, transaction, callback) {
51 const options = {
52 type: ENDPOINT_ACTIONS.UPDATE,
53 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
54 data: videoData,
55 transaction
56 }
57 createRequest(options, callback)
58 }
59
60 function removeVideoToFriends (videoParams) {
61 const options = {
62 type: ENDPOINT_ACTIONS.REMOVE,
63 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
64 data: videoParams
65 }
66 createRequest(options)
67 }
68
69 function reportAbuseVideoToFriend (reportData, video) {
70 const options = {
71 type: ENDPOINT_ACTIONS.REPORT_ABUSE,
72 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
73 data: reportData,
74 toIds: [ video.Author.podId ]
75 }
76 createRequest(options)
77 }
78
79 function quickAndDirtyUpdateVideoToFriends (videoId, type, transaction, callback) {
80 const options = {
81 videoId,
82 type,
83 transaction
84 }
85 return createVideoQaduRequest(options, callback)
86 }
87
88 function hasFriends (callback) {
89 db.Pod.countAll(function (err, count) {
90 if (err) return callback(err)
91
92 const hasFriends = (count !== 0)
93 callback(null, hasFriends)
94 })
95 }
96
97 function makeFriends (hosts, callback) {
98 const podsScore = {}
99
100 logger.info('Make friends!')
101 peertubeCrypto.getMyPublicCert(function (err, cert) {
102 if (err) {
103 logger.error('Cannot read public cert.')
104 return callback(err)
105 }
106
107 eachSeries(hosts, function (host, callbackEach) {
108 computeForeignPodsList(host, podsScore, callbackEach)
109 }, function (err) {
110 if (err) return callback(err)
111
112 logger.debug('Pods scores computed.', { podsScore: podsScore })
113 const podsList = computeWinningPods(hosts, podsScore)
114 logger.debug('Pods that we keep.', { podsToKeep: podsList })
115
116 makeRequestsToWinningPods(cert, podsList, callback)
117 })
118 })
119 }
120
121 function quitFriends (callback) {
122 // Stop pool requests
123 requestScheduler.deactivate()
124
125 waterfall([
126 function flushRequests (callbackAsync) {
127 requestScheduler.flush(err => callbackAsync(err))
128 },
129
130 function flushVideoQaduRequests (callbackAsync) {
131 requestSchedulerVideoQadu.flush(err => callbackAsync(err))
132 },
133
134 function getPodsList (callbackAsync) {
135 return db.Pod.list(callbackAsync)
136 },
137
138 function announceIQuitMyFriends (pods, callbackAsync) {
139 const requestParams = {
140 method: 'POST',
141 path: '/api/' + constants.API_VERSION + '/pods/remove',
142 sign: true
143 }
144
145 // Announce we quit them
146 // We don't care if the request fails
147 // The other pod will exclude us automatically after a while
148 eachLimit(pods, constants.REQUESTS_IN_PARALLEL, function (pod, callbackEach) {
149 requestParams.toPod = pod
150 requests.makeSecureRequest(requestParams, callbackEach)
151 }, function (err) {
152 if (err) {
153 logger.error('Some errors while quitting friends.', { err: err })
154 // Don't stop the process
155 }
156
157 return callbackAsync(null, pods)
158 })
159 },
160
161 function removePodsFromDB (pods, callbackAsync) {
162 each(pods, function (pod, callbackEach) {
163 pod.destroy().asCallback(callbackEach)
164 }, callbackAsync)
165 }
166 ], function (err) {
167 // Don't forget to re activate the scheduler, even if there was an error
168 requestScheduler.activate()
169
170 if (err) return callback(err)
171
172 logger.info('Removed all remote videos.')
173 return callback(null)
174 })
175 }
176
177 function sendOwnedVideosToPod (podId) {
178 db.Video.listOwnedAndPopulateAuthorAndTags(function (err, videosList) {
179 if (err) {
180 logger.error('Cannot get the list of videos we own.')
181 return
182 }
183
184 videosList.forEach(function (video) {
185 video.toAddRemoteJSON(function (err, remoteVideo) {
186 if (err) {
187 logger.error('Cannot convert video to remote.', { error: err })
188 // Don't break the process
189 return
190 }
191
192 const options = {
193 type: 'add',
194 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
195 data: remoteVideo,
196 toIds: [ podId ]
197 }
198 createRequest(options)
199 })
200 })
201 })
202 }
203
204 // ---------------------------------------------------------------------------
205
206 module.exports = friends
207
208 // ---------------------------------------------------------------------------
209
210 function computeForeignPodsList (host, podsScore, callback) {
211 getForeignPodsList(host, function (err, res) {
212 if (err) return callback(err)
213
214 const foreignPodsList = res.data
215
216 // Let's give 1 point to the pod we ask the friends list
217 foreignPodsList.push({ host })
218
219 foreignPodsList.forEach(function (foreignPod) {
220 const foreignPodHost = foreignPod.host
221
222 if (podsScore[foreignPodHost]) podsScore[foreignPodHost]++
223 else podsScore[foreignPodHost] = 1
224 })
225
226 return callback()
227 })
228 }
229
230 function computeWinningPods (hosts, podsScore) {
231 // Build the list of pods to add
232 // Only add a pod if it exists in more than a half base pods
233 const podsList = []
234 const baseScore = hosts.length / 2
235
236 Object.keys(podsScore).forEach(function (podHost) {
237 // If the pod is not me and with a good score we add it
238 if (isMe(podHost) === false && podsScore[podHost] > baseScore) {
239 podsList.push({ host: podHost })
240 }
241 })
242
243 return podsList
244 }
245
246 function getForeignPodsList (host, callback) {
247 const path = '/api/' + constants.API_VERSION + '/pods'
248
249 request.get(constants.REMOTE_SCHEME.HTTP + '://' + host + path, function (err, response, body) {
250 if (err) return callback(err)
251
252 try {
253 const json = JSON.parse(body)
254 return callback(null, json)
255 } catch (err) {
256 return callback(err)
257 }
258 })
259 }
260
261 function makeRequestsToWinningPods (cert, podsList, callback) {
262 // Stop pool requests
263 requestScheduler.deactivate()
264 // Flush pool requests
265 requestScheduler.forceSend()
266
267 eachLimit(podsList, constants.REQUESTS_IN_PARALLEL, function (pod, callbackEach) {
268 const params = {
269 url: constants.REMOTE_SCHEME.HTTP + '://' + pod.host + '/api/' + constants.API_VERSION + '/pods/',
270 method: 'POST',
271 json: {
272 host: constants.CONFIG.WEBSERVER.HOST,
273 email: constants.CONFIG.ADMIN.EMAIL,
274 publicKey: cert
275 }
276 }
277
278 requests.makeRetryRequest(params, function (err, res, body) {
279 if (err) {
280 logger.error('Error with adding %s pod.', pod.host, { error: err })
281 // Don't break the process
282 return callbackEach()
283 }
284
285 if (res.statusCode === 200) {
286 const podObj = db.Pod.build({ host: pod.host, publicKey: body.cert, email: body.email })
287 podObj.save().asCallback(function (err, podCreated) {
288 if (err) {
289 logger.error('Cannot add friend %s pod.', pod.host, { error: err })
290 return callbackEach()
291 }
292
293 // Add our videos to the request scheduler
294 sendOwnedVideosToPod(podCreated.id)
295
296 return callbackEach()
297 })
298 } else {
299 logger.error('Status not 200 for %s pod.', pod.host)
300 return callbackEach()
301 }
302 })
303 }, function endRequests () {
304 // Final callback, we've ended all the requests
305 // Now we made new friends, we can re activate the pool of requests
306 requestScheduler.activate()
307
308 logger.debug('makeRequestsToWinningPods finished.')
309 return callback()
310 })
311 }
312
313 // Wrapper that populate "toIds" argument with all our friends if it is not specified
314 // { type, endpoint, data, toIds, transaction }
315 function createRequest (options, callback) {
316 if (!callback) callback = function () {}
317 if (options.toIds) return requestScheduler.createRequest(options, callback)
318
319 // If the "toIds" pods is not specified, we send the request to all our friends
320 db.Pod.listAllIds(options.transaction, function (err, podIds) {
321 if (err) {
322 logger.error('Cannot get pod ids', { error: err })
323 return
324 }
325
326 const newOptions = Object.assign(options, { toIds: podIds })
327 return requestScheduler.createRequest(newOptions, callback)
328 })
329 }
330
331 function createVideoQaduRequest (options, callback) {
332 if (!callback) callback = function () {}
333
334 requestSchedulerVideoQadu.createRequest(options, callback)
335 }
336
337 function isMe (host) {
338 return host === constants.CONFIG.WEBSERVER.HOST
339 }