diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-11-09 17:51:58 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-11-27 19:40:51 +0100 |
commit | e4f97babf701481b55cc10fb3448feab5f97c867 (patch) | |
tree | af37402a594dc5ff09f71ecb0687e8cfe4cdb471 /server/controllers/activitypub | |
parent | 343ad675f2a26c15b86150a9a3552e619d5d44f4 (diff) | |
download | PeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.tar.gz PeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.tar.zst PeerTube-e4f97babf701481b55cc10fb3448feab5f97c867.zip |
Begin activitypub
Diffstat (limited to 'server/controllers/activitypub')
-rw-r--r-- | server/controllers/activitypub/client.ts | 65 | ||||
-rw-r--r-- | server/controllers/activitypub/inbox.ts | 72 | ||||
-rw-r--r-- | server/controllers/activitypub/index.ts | 15 | ||||
-rw-r--r-- | server/controllers/activitypub/pods.ts | 69 | ||||
-rw-r--r-- | server/controllers/activitypub/videos.ts | 589 |
5 files changed, 810 insertions, 0 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts new file mode 100644 index 000000000..28d08b3f4 --- /dev/null +++ b/server/controllers/activitypub/client.ts | |||
@@ -0,0 +1,65 @@ | |||
1 | // Intercept ActivityPub client requests | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { database as db } from '../../initializers' | ||
5 | import { executeIfActivityPub, localAccountValidator } from '../../middlewares' | ||
6 | import { pageToStartAndCount } from '../../helpers' | ||
7 | import { AccountInstance } from '../../models' | ||
8 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | ||
9 | import { ACTIVITY_PUB } from '../../initializers/constants' | ||
10 | import { asyncMiddleware } from '../../middlewares/async' | ||
11 | |||
12 | const activityPubClientRouter = express.Router() | ||
13 | |||
14 | activityPubClientRouter.get('/account/:name', | ||
15 | executeIfActivityPub(localAccountValidator), | ||
16 | executeIfActivityPub(asyncMiddleware(accountController)) | ||
17 | ) | ||
18 | |||
19 | activityPubClientRouter.get('/account/:name/followers', | ||
20 | executeIfActivityPub(localAccountValidator), | ||
21 | executeIfActivityPub(asyncMiddleware(accountFollowersController)) | ||
22 | ) | ||
23 | |||
24 | activityPubClientRouter.get('/account/:name/following', | ||
25 | executeIfActivityPub(localAccountValidator), | ||
26 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) | ||
27 | ) | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | activityPubClientRouter | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | async function accountController (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
38 | const account: AccountInstance = res.locals.account | ||
39 | |||
40 | return res.json(account.toActivityPubObject()).end() | ||
41 | } | ||
42 | |||
43 | async function accountFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
44 | const account: AccountInstance = res.locals.account | ||
45 | |||
46 | const page = req.params.page || 1 | ||
47 | const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) | ||
48 | |||
49 | const result = await db.Account.listFollowerUrlsForApi(account.name, start, count) | ||
50 | const activityPubResult = activityPubCollectionPagination(req.url, page, result) | ||
51 | |||
52 | return res.json(activityPubResult) | ||
53 | } | ||
54 | |||
55 | async function accountFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
56 | const account: AccountInstance = res.locals.account | ||
57 | |||
58 | const page = req.params.page || 1 | ||
59 | const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) | ||
60 | |||
61 | const result = await db.Account.listFollowingUrlsForApi(account.name, start, count) | ||
62 | const activityPubResult = activityPubCollectionPagination(req.url, page, result) | ||
63 | |||
64 | return res.json(activityPubResult) | ||
65 | } | ||
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts new file mode 100644 index 000000000..79d989c2c --- /dev/null +++ b/server/controllers/activitypub/inbox.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { | ||
4 | processCreateActivity, | ||
5 | processUpdateActivity, | ||
6 | processFlagActivity | ||
7 | } from '../../lib' | ||
8 | import { | ||
9 | Activity, | ||
10 | ActivityType, | ||
11 | RootActivity, | ||
12 | ActivityPubCollection, | ||
13 | ActivityPubOrderedCollection | ||
14 | } from '../../../shared' | ||
15 | import { | ||
16 | signatureValidator, | ||
17 | checkSignature, | ||
18 | asyncMiddleware | ||
19 | } from '../../middlewares' | ||
20 | import { logger } from '../../helpers' | ||
21 | |||
22 | const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { | ||
23 | Create: processCreateActivity, | ||
24 | Update: processUpdateActivity, | ||
25 | Flag: processFlagActivity | ||
26 | } | ||
27 | |||
28 | const inboxRouter = express.Router() | ||
29 | |||
30 | inboxRouter.post('/', | ||
31 | signatureValidator, | ||
32 | asyncMiddleware(checkSignature), | ||
33 | // inboxValidator, | ||
34 | asyncMiddleware(inboxController) | ||
35 | ) | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
39 | export { | ||
40 | inboxRouter | ||
41 | } | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | async function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
46 | const rootActivity: RootActivity = req.body | ||
47 | let activities: Activity[] = [] | ||
48 | |||
49 | if ([ 'Collection', 'CollectionPage' ].indexOf(rootActivity.type) !== -1) { | ||
50 | activities = (rootActivity as ActivityPubCollection).items | ||
51 | } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].indexOf(rootActivity.type) !== -1) { | ||
52 | activities = (rootActivity as ActivityPubOrderedCollection).orderedItems | ||
53 | } else { | ||
54 | activities = [ rootActivity as Activity ] | ||
55 | } | ||
56 | |||
57 | await processActivities(activities) | ||
58 | |||
59 | res.status(204).end() | ||
60 | } | ||
61 | |||
62 | async function processActivities (activities: Activity[]) { | ||
63 | for (const activity of activities) { | ||
64 | const activityProcessor = processActivity[activity.type] | ||
65 | if (activityProcessor === undefined) { | ||
66 | logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id }) | ||
67 | continue | ||
68 | } | ||
69 | |||
70 | await activityProcessor(activity) | ||
71 | } | ||
72 | } | ||
diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts new file mode 100644 index 000000000..7a4602b37 --- /dev/null +++ b/server/controllers/activitypub/index.ts | |||
@@ -0,0 +1,15 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { badRequest } from '../../helpers' | ||
4 | import { inboxRouter } from './inbox' | ||
5 | |||
6 | const remoteRouter = express.Router() | ||
7 | |||
8 | remoteRouter.use('/inbox', inboxRouter) | ||
9 | remoteRouter.use('/*', badRequest) | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | remoteRouter | ||
15 | } | ||
diff --git a/server/controllers/activitypub/pods.ts b/server/controllers/activitypub/pods.ts new file mode 100644 index 000000000..326eb61ac --- /dev/null +++ b/server/controllers/activitypub/pods.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | import { database as db } from '../../../initializers/database' | ||
4 | import { | ||
5 | checkSignature, | ||
6 | signatureValidator, | ||
7 | setBodyHostPort, | ||
8 | remotePodsAddValidator, | ||
9 | asyncMiddleware | ||
10 | } from '../../../middlewares' | ||
11 | import { sendOwnedDataToPod } from '../../../lib' | ||
12 | import { getMyPublicCert, getFormattedObjects } from '../../../helpers' | ||
13 | import { CONFIG } from '../../../initializers' | ||
14 | import { PodInstance } from '../../../models' | ||
15 | import { PodSignature, Pod as FormattedPod } from '../../../../shared' | ||
16 | |||
17 | const remotePodsRouter = express.Router() | ||
18 | |||
19 | remotePodsRouter.post('/remove', | ||
20 | signatureValidator, | ||
21 | checkSignature, | ||
22 | asyncMiddleware(removePods) | ||
23 | ) | ||
24 | |||
25 | remotePodsRouter.post('/list', | ||
26 | asyncMiddleware(remotePodsList) | ||
27 | ) | ||
28 | |||
29 | remotePodsRouter.post('/add', | ||
30 | setBodyHostPort, // We need to modify the host before running the validator! | ||
31 | remotePodsAddValidator, | ||
32 | asyncMiddleware(addPods) | ||
33 | ) | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | remotePodsRouter | ||
39 | } | ||
40 | |||
41 | // --------------------------------------------------------------------------- | ||
42 | |||
43 | async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
44 | const information = req.body | ||
45 | |||
46 | const pod = db.Pod.build(information) | ||
47 | const podCreated = await pod.save() | ||
48 | |||
49 | await sendOwnedDataToPod(podCreated.id) | ||
50 | |||
51 | const cert = await getMyPublicCert() | ||
52 | return res.json({ cert, email: CONFIG.ADMIN.EMAIL }) | ||
53 | } | ||
54 | |||
55 | async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
56 | const pods = await db.Pod.list() | ||
57 | |||
58 | return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length)) | ||
59 | } | ||
60 | |||
61 | async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
62 | const signature: PodSignature = req.body.signature | ||
63 | const host = signature.host | ||
64 | |||
65 | const pod = await db.Pod.loadByHost(host) | ||
66 | await pod.destroy() | ||
67 | |||
68 | return res.type('json').status(204).end() | ||
69 | } | ||
diff --git a/server/controllers/activitypub/videos.ts b/server/controllers/activitypub/videos.ts new file mode 100644 index 000000000..cba47f0a1 --- /dev/null +++ b/server/controllers/activitypub/videos.ts | |||
@@ -0,0 +1,589 @@ | |||
1 | import * as express from 'express' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | import * as Sequelize from 'sequelize' | ||
4 | |||
5 | import { database as db } from '../../../initializers/database' | ||
6 | import { | ||
7 | REQUEST_ENDPOINT_ACTIONS, | ||
8 | REQUEST_ENDPOINTS, | ||
9 | REQUEST_VIDEO_EVENT_TYPES, | ||
10 | REQUEST_VIDEO_QADU_TYPES | ||
11 | } from '../../../initializers' | ||
12 | import { | ||
13 | checkSignature, | ||
14 | signatureValidator, | ||
15 | remoteVideosValidator, | ||
16 | remoteQaduVideosValidator, | ||
17 | remoteEventsVideosValidator | ||
18 | } from '../../../middlewares' | ||
19 | import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers' | ||
20 | import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib' | ||
21 | import { PodInstance, VideoFileInstance } from '../../../models' | ||
22 | import { | ||
23 | RemoteVideoRequest, | ||
24 | RemoteVideoCreateData, | ||
25 | RemoteVideoUpdateData, | ||
26 | RemoteVideoRemoveData, | ||
27 | RemoteVideoReportAbuseData, | ||
28 | RemoteQaduVideoRequest, | ||
29 | RemoteQaduVideoData, | ||
30 | RemoteVideoEventRequest, | ||
31 | RemoteVideoEventData, | ||
32 | RemoteVideoChannelCreateData, | ||
33 | RemoteVideoChannelUpdateData, | ||
34 | RemoteVideoChannelRemoveData, | ||
35 | RemoteVideoAuthorRemoveData, | ||
36 | RemoteVideoAuthorCreateData | ||
37 | } from '../../../../shared' | ||
38 | import { VideoInstance } from '../../../models/video/video-interface' | ||
39 | |||
40 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | ||
41 | |||
42 | // Functions to call when processing a remote request | ||
43 | // FIXME: use RemoteVideoRequestType as id type | ||
44 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} | ||
45 | functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper | ||
46 | functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper | ||
47 | functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper | ||
48 | functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper | ||
49 | functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper | ||
50 | functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper | ||
51 | functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper | ||
52 | functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper | ||
53 | functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper | ||
54 | |||
55 | const remoteVideosRouter = express.Router() | ||
56 | |||
57 | remoteVideosRouter.post('/', | ||
58 | signatureValidator, | ||
59 | checkSignature, | ||
60 | remoteVideosValidator, | ||
61 | remoteVideos | ||
62 | ) | ||
63 | |||
64 | remoteVideosRouter.post('/qadu', | ||
65 | signatureValidator, | ||
66 | checkSignature, | ||
67 | remoteQaduVideosValidator, | ||
68 | remoteVideosQadu | ||
69 | ) | ||
70 | |||
71 | remoteVideosRouter.post('/events', | ||
72 | signatureValidator, | ||
73 | checkSignature, | ||
74 | remoteEventsVideosValidator, | ||
75 | remoteVideosEvents | ||
76 | ) | ||
77 | |||
78 | // --------------------------------------------------------------------------- | ||
79 | |||
80 | export { | ||
81 | remoteVideosRouter | ||
82 | } | ||
83 | |||
84 | // --------------------------------------------------------------------------- | ||
85 | |||
86 | function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
87 | const requests: RemoteVideoRequest[] = req.body.data | ||
88 | const fromPod = res.locals.secure.pod | ||
89 | |||
90 | // We need to process in the same order to keep consistency | ||
91 | Bluebird.each(requests, request => { | ||
92 | const data = request.data | ||
93 | |||
94 | // Get the function we need to call in order to process the request | ||
95 | const fun = functionsHash[request.type] | ||
96 | if (fun === undefined) { | ||
97 | logger.error('Unknown remote request type %s.', request.type) | ||
98 | return | ||
99 | } | ||
100 | |||
101 | return fun.call(this, data, fromPod) | ||
102 | }) | ||
103 | .catch(err => logger.error('Error managing remote videos.', err)) | ||
104 | |||
105 | // Don't block the other pod | ||
106 | return res.type('json').status(204).end() | ||
107 | } | ||
108 | |||
109 | function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
110 | const requests: RemoteQaduVideoRequest[] = req.body.data | ||
111 | const fromPod = res.locals.secure.pod | ||
112 | |||
113 | Bluebird.each(requests, request => { | ||
114 | const videoData = request.data | ||
115 | |||
116 | return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) | ||
117 | }) | ||
118 | .catch(err => logger.error('Error managing remote videos.', err)) | ||
119 | |||
120 | return res.type('json').status(204).end() | ||
121 | } | ||
122 | |||
123 | function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
124 | const requests: RemoteVideoEventRequest[] = req.body.data | ||
125 | const fromPod = res.locals.secure.pod | ||
126 | |||
127 | Bluebird.each(requests, request => { | ||
128 | const eventData = request.data | ||
129 | |||
130 | return processVideosEventsRetryWrapper(eventData, fromPod) | ||
131 | }) | ||
132 | .catch(err => logger.error('Error managing remote videos.', err)) | ||
133 | |||
134 | return res.type('json').status(204).end() | ||
135 | } | ||
136 | |||
137 | async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) { | ||
138 | const options = { | ||
139 | arguments: [ eventData, fromPod ], | ||
140 | errorMessage: 'Cannot process videos events with many retries.' | ||
141 | } | ||
142 | |||
143 | await retryTransactionWrapper(processVideosEvents, options) | ||
144 | } | ||
145 | |||
146 | async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { | ||
147 | await db.sequelize.transaction(async t => { | ||
148 | const sequelizeOptions = { transaction: t } | ||
149 | const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t) | ||
150 | |||
151 | let columnToUpdate | ||
152 | let qaduType | ||
153 | |||
154 | switch (eventData.eventType) { | ||
155 | case REQUEST_VIDEO_EVENT_TYPES.VIEWS: | ||
156 | columnToUpdate = 'views' | ||
157 | qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS | ||
158 | break | ||
159 | |||
160 | case REQUEST_VIDEO_EVENT_TYPES.LIKES: | ||
161 | columnToUpdate = 'likes' | ||
162 | qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES | ||
163 | break | ||
164 | |||
165 | case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: | ||
166 | columnToUpdate = 'dislikes' | ||
167 | qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES | ||
168 | break | ||
169 | |||
170 | default: | ||
171 | throw new Error('Unknown video event type.') | ||
172 | } | ||
173 | |||
174 | const query = {} | ||
175 | query[columnToUpdate] = eventData.count | ||
176 | |||
177 | await videoInstance.increment(query, sequelizeOptions) | ||
178 | |||
179 | const qadusParams = [ | ||
180 | { | ||
181 | videoId: videoInstance.id, | ||
182 | type: qaduType | ||
183 | } | ||
184 | ] | ||
185 | await quickAndDirtyUpdatesVideoToFriends(qadusParams, t) | ||
186 | }) | ||
187 | |||
188 | logger.info('Remote video event processed for video with uuid %s.', eventData.uuid) | ||
189 | } | ||
190 | |||
191 | async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) { | ||
192 | const options = { | ||
193 | arguments: [ videoData, fromPod ], | ||
194 | errorMessage: 'Cannot update quick and dirty the remote video with many retries.' | ||
195 | } | ||
196 | |||
197 | await retryTransactionWrapper(quickAndDirtyUpdateVideo, options) | ||
198 | } | ||
199 | |||
200 | async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { | ||
201 | let videoUUID = '' | ||
202 | |||
203 | await db.sequelize.transaction(async t => { | ||
204 | const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) | ||
205 | const sequelizeOptions = { transaction: t } | ||
206 | |||
207 | videoUUID = videoInstance.uuid | ||
208 | |||
209 | if (videoData.views) { | ||
210 | videoInstance.set('views', videoData.views) | ||
211 | } | ||
212 | |||
213 | if (videoData.likes) { | ||
214 | videoInstance.set('likes', videoData.likes) | ||
215 | } | ||
216 | |||
217 | if (videoData.dislikes) { | ||
218 | videoInstance.set('dislikes', videoData.dislikes) | ||
219 | } | ||
220 | |||
221 | await videoInstance.save(sequelizeOptions) | ||
222 | }) | ||
223 | |||
224 | logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) | ||
225 | } | ||
226 | |||
227 | // Handle retries on fail | ||
228 | async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { | ||
229 | const options = { | ||
230 | arguments: [ videoToCreateData, fromPod ], | ||
231 | errorMessage: 'Cannot insert the remote video with many retries.' | ||
232 | } | ||
233 | |||
234 | await retryTransactionWrapper(addRemoteVideo, options) | ||
235 | } | ||
236 | |||
237 | async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { | ||
238 | logger.debug('Adding remote video "%s".', videoToCreateData.uuid) | ||
239 | |||
240 | await db.sequelize.transaction(async t => { | ||
241 | const sequelizeOptions = { | ||
242 | transaction: t | ||
243 | } | ||
244 | |||
245 | const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) | ||
246 | if (videoFromDatabase) throw new Error('UUID already exists.') | ||
247 | |||
248 | const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) | ||
249 | if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') | ||
250 | |||
251 | const tags = videoToCreateData.tags | ||
252 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | ||
253 | |||
254 | const videoData = { | ||
255 | name: videoToCreateData.name, | ||
256 | uuid: videoToCreateData.uuid, | ||
257 | category: videoToCreateData.category, | ||
258 | licence: videoToCreateData.licence, | ||
259 | language: videoToCreateData.language, | ||
260 | nsfw: videoToCreateData.nsfw, | ||
261 | description: videoToCreateData.truncatedDescription, | ||
262 | channelId: videoChannel.id, | ||
263 | duration: videoToCreateData.duration, | ||
264 | createdAt: videoToCreateData.createdAt, | ||
265 | // FIXME: updatedAt does not seems to be considered by Sequelize | ||
266 | updatedAt: videoToCreateData.updatedAt, | ||
267 | views: videoToCreateData.views, | ||
268 | likes: videoToCreateData.likes, | ||
269 | dislikes: videoToCreateData.dislikes, | ||
270 | remote: true, | ||
271 | privacy: videoToCreateData.privacy | ||
272 | } | ||
273 | |||
274 | const video = db.Video.build(videoData) | ||
275 | await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData) | ||
276 | const videoCreated = await video.save(sequelizeOptions) | ||
277 | |||
278 | const tasks = [] | ||
279 | for (const fileData of videoToCreateData.files) { | ||
280 | const videoFileInstance = db.VideoFile.build({ | ||
281 | extname: fileData.extname, | ||
282 | infoHash: fileData.infoHash, | ||
283 | resolution: fileData.resolution, | ||
284 | size: fileData.size, | ||
285 | videoId: videoCreated.id | ||
286 | }) | ||
287 | |||
288 | tasks.push(videoFileInstance.save(sequelizeOptions)) | ||
289 | } | ||
290 | |||
291 | await Promise.all(tasks) | ||
292 | |||
293 | await videoCreated.setTags(tagInstances, sequelizeOptions) | ||
294 | }) | ||
295 | |||
296 | logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) | ||
297 | } | ||
298 | |||
299 | // Handle retries on fail | ||
300 | async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { | ||
301 | const options = { | ||
302 | arguments: [ videoAttributesToUpdate, fromPod ], | ||
303 | errorMessage: 'Cannot update the remote video with many retries' | ||
304 | } | ||
305 | |||
306 | await retryTransactionWrapper(updateRemoteVideo, options) | ||
307 | } | ||
308 | |||
309 | async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { | ||
310 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) | ||
311 | let videoInstance: VideoInstance | ||
312 | let videoFieldsSave: object | ||
313 | |||
314 | try { | ||
315 | await db.sequelize.transaction(async t => { | ||
316 | const sequelizeOptions = { | ||
317 | transaction: t | ||
318 | } | ||
319 | |||
320 | const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t) | ||
321 | videoFieldsSave = videoInstance.toJSON() | ||
322 | const tags = videoAttributesToUpdate.tags | ||
323 | |||
324 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | ||
325 | |||
326 | videoInstance.set('name', videoAttributesToUpdate.name) | ||
327 | videoInstance.set('category', videoAttributesToUpdate.category) | ||
328 | videoInstance.set('licence', videoAttributesToUpdate.licence) | ||
329 | videoInstance.set('language', videoAttributesToUpdate.language) | ||
330 | videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) | ||
331 | videoInstance.set('description', videoAttributesToUpdate.truncatedDescription) | ||
332 | videoInstance.set('duration', videoAttributesToUpdate.duration) | ||
333 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) | ||
334 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | ||
335 | videoInstance.set('views', videoAttributesToUpdate.views) | ||
336 | videoInstance.set('likes', videoAttributesToUpdate.likes) | ||
337 | videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) | ||
338 | videoInstance.set('privacy', videoAttributesToUpdate.privacy) | ||
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 | |||
379 | async 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 | |||
388 | async 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 | |||
400 | async 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 | |||
409 | async 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 | |||
430 | async 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 | |||
439 | async 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 | |||
450 | async 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 | |||
459 | async 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 | |||
491 | async 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 | |||
500 | async 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 | |||
518 | async 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 | |||
527 | async 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 | |||
538 | async 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 | |||
547 | async 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 | |||
566 | async 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 | |||
579 | async 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 | } | ||