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