diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-11-10 14:34:45 +0100 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-11-27 19:40:51 +0100 |
commit | 0d0e8dd0904b380b70e19ebcb4763d65601c4632 (patch) | |
tree | acb625d7c88fbe863fa14bf6783fafe4a8e35137 | |
parent | e4f97babf701481b55cc10fb3448feab5f97c867 (diff) | |
download | PeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.tar.gz PeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.tar.zst PeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.zip |
Continue activitypub
27 files changed, 1038 insertions, 1085 deletions
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index 79d989c2c..eee217650 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts | |||
@@ -1,26 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | 2 | import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, ActivityType, RootActivity } from '../../../shared' | |
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' | 3 | import { logger } from '../../helpers' |
4 | import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' | ||
5 | import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib' | ||
6 | import { processAddActivity } from '../../lib/activitypub/process-add' | ||
7 | import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares' | ||
8 | import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' | ||
21 | 9 | ||
22 | const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { | 10 | const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { |
23 | Create: processCreateActivity, | 11 | Create: processCreateActivity, |
12 | Add: processAddActivity, | ||
24 | Update: processUpdateActivity, | 13 | Update: processUpdateActivity, |
25 | Flag: processFlagActivity | 14 | Flag: processFlagActivity |
26 | } | 15 | } |
@@ -30,7 +19,7 @@ const inboxRouter = express.Router() | |||
30 | inboxRouter.post('/', | 19 | inboxRouter.post('/', |
31 | signatureValidator, | 20 | signatureValidator, |
32 | asyncMiddleware(checkSignature), | 21 | asyncMiddleware(checkSignature), |
33 | // inboxValidator, | 22 | activityPubValidator, |
34 | asyncMiddleware(inboxController) | 23 | asyncMiddleware(inboxController) |
35 | ) | 24 | ) |
36 | 25 | ||
@@ -54,6 +43,9 @@ async function inboxController (req: express.Request, res: express.Response, nex | |||
54 | activities = [ rootActivity as Activity ] | 43 | activities = [ rootActivity as Activity ] |
55 | } | 44 | } |
56 | 45 | ||
46 | // Only keep activities we are able to process | ||
47 | activities = activities.filter(a => isActivityValid(a)) | ||
48 | |||
57 | await processActivities(activities) | 49 | await processActivities(activities) |
58 | 50 | ||
59 | res.status(204).end() | 51 | res.status(204).end() |
diff --git a/server/controllers/activitypub/pods.ts b/server/controllers/activitypub/pods.ts index 326eb61ac..6cce57c1c 100644 --- a/server/controllers/activitypub/pods.ts +++ b/server/controllers/activitypub/pods.ts | |||
@@ -1,69 +1,69 @@ | |||
1 | import * as express from 'express' | 1 | // import * as express from 'express' |
2 | 2 | // | |
3 | import { database as db } from '../../../initializers/database' | 3 | // import { database as db } from '../../../initializers/database' |
4 | import { | 4 | // import { |
5 | checkSignature, | 5 | // checkSignature, |
6 | signatureValidator, | 6 | // signatureValidator, |
7 | setBodyHostPort, | 7 | // setBodyHostPort, |
8 | remotePodsAddValidator, | 8 | // remotePodsAddValidator, |
9 | asyncMiddleware | 9 | // asyncMiddleware |
10 | } from '../../../middlewares' | 10 | // } from '../../../middlewares' |
11 | import { sendOwnedDataToPod } from '../../../lib' | 11 | // import { sendOwnedDataToPod } from '../../../lib' |
12 | import { getMyPublicCert, getFormattedObjects } from '../../../helpers' | 12 | // import { getMyPublicCert, getFormattedObjects } from '../../../helpers' |
13 | import { CONFIG } from '../../../initializers' | 13 | // import { CONFIG } from '../../../initializers' |
14 | import { PodInstance } from '../../../models' | 14 | // import { PodInstance } from '../../../models' |
15 | import { PodSignature, Pod as FormattedPod } from '../../../../shared' | 15 | // import { PodSignature, Pod as FormattedPod } from '../../../../shared' |
16 | 16 | // | |
17 | const remotePodsRouter = express.Router() | 17 | // const remotePodsRouter = express.Router() |
18 | 18 | // | |
19 | remotePodsRouter.post('/remove', | 19 | // remotePodsRouter.post('/remove', |
20 | signatureValidator, | 20 | // signatureValidator, |
21 | checkSignature, | 21 | // checkSignature, |
22 | asyncMiddleware(removePods) | 22 | // asyncMiddleware(removePods) |
23 | ) | 23 | // ) |
24 | 24 | // | |
25 | remotePodsRouter.post('/list', | 25 | // remotePodsRouter.post('/list', |
26 | asyncMiddleware(remotePodsList) | 26 | // asyncMiddleware(remotePodsList) |
27 | ) | 27 | // ) |
28 | 28 | // | |
29 | remotePodsRouter.post('/add', | 29 | // remotePodsRouter.post('/add', |
30 | setBodyHostPort, // We need to modify the host before running the validator! | 30 | // setBodyHostPort, // We need to modify the host before running the validator! |
31 | remotePodsAddValidator, | 31 | // remotePodsAddValidator, |
32 | asyncMiddleware(addPods) | 32 | // asyncMiddleware(addPods) |
33 | ) | 33 | // ) |
34 | 34 | // | |
35 | // --------------------------------------------------------------------------- | 35 | // // --------------------------------------------------------------------------- |
36 | 36 | // | |
37 | export { | 37 | // export { |
38 | remotePodsRouter | 38 | // remotePodsRouter |
39 | } | 39 | // } |
40 | 40 | // | |
41 | // --------------------------------------------------------------------------- | 41 | // // --------------------------------------------------------------------------- |
42 | 42 | // | |
43 | async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { | 43 | // async function addPods (req: express.Request, res: express.Response, next: express.NextFunction) { |
44 | const information = req.body | 44 | // const information = req.body |
45 | 45 | // | |
46 | const pod = db.Pod.build(information) | 46 | // const pod = db.Pod.build(information) |
47 | const podCreated = await pod.save() | 47 | // const podCreated = await pod.save() |
48 | 48 | // | |
49 | await sendOwnedDataToPod(podCreated.id) | 49 | // await sendOwnedDataToPod(podCreated.id) |
50 | 50 | // | |
51 | const cert = await getMyPublicCert() | 51 | // const cert = await getMyPublicCert() |
52 | return res.json({ cert, email: CONFIG.ADMIN.EMAIL }) | 52 | // return res.json({ cert, email: CONFIG.ADMIN.EMAIL }) |
53 | } | 53 | // } |
54 | 54 | // | |
55 | async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) { | 55 | // async function remotePodsList (req: express.Request, res: express.Response, next: express.NextFunction) { |
56 | const pods = await db.Pod.list() | 56 | // const pods = await db.Pod.list() |
57 | 57 | // | |
58 | return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length)) | 58 | // return res.json(getFormattedObjects<FormattedPod, PodInstance>(pods, pods.length)) |
59 | } | 59 | // } |
60 | 60 | // | |
61 | async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { | 61 | // async function removePods (req: express.Request, res: express.Response, next: express.NextFunction) { |
62 | const signature: PodSignature = req.body.signature | 62 | // const signature: PodSignature = req.body.signature |
63 | const host = signature.host | 63 | // const host = signature.host |
64 | 64 | // | |
65 | const pod = await db.Pod.loadByHost(host) | 65 | // const pod = await db.Pod.loadByHost(host) |
66 | await pod.destroy() | 66 | // await pod.destroy() |
67 | 67 | // | |
68 | return res.type('json').status(204).end() | 68 | // return res.type('json').status(204).end() |
69 | } | 69 | // } |
diff --git a/server/controllers/activitypub/videos.ts b/server/controllers/activitypub/videos.ts index cba47f0a1..9a1868ff7 100644 --- a/server/controllers/activitypub/videos.ts +++ b/server/controllers/activitypub/videos.ts | |||
@@ -1,589 +1,339 @@ | |||
1 | import * as express from 'express' | 1 | // import * as express from 'express' |
2 | import * as Bluebird from 'bluebird' | 2 | // import * as Bluebird from 'bluebird' |
3 | import * as Sequelize from 'sequelize' | 3 | // import * as Sequelize from 'sequelize' |
4 | 4 | // | |
5 | import { database as db } from '../../../initializers/database' | 5 | // import { database as db } from '../../../initializers/database' |
6 | import { | 6 | // import { |
7 | REQUEST_ENDPOINT_ACTIONS, | 7 | // REQUEST_ENDPOINT_ACTIONS, |
8 | REQUEST_ENDPOINTS, | 8 | // REQUEST_ENDPOINTS, |
9 | REQUEST_VIDEO_EVENT_TYPES, | 9 | // REQUEST_VIDEO_EVENT_TYPES, |
10 | REQUEST_VIDEO_QADU_TYPES | 10 | // REQUEST_VIDEO_QADU_TYPES |
11 | } from '../../../initializers' | 11 | // } from '../../../initializers' |
12 | import { | 12 | // import { |
13 | checkSignature, | 13 | // checkSignature, |
14 | signatureValidator, | 14 | // signatureValidator, |
15 | remoteVideosValidator, | 15 | // remoteVideosValidator, |
16 | remoteQaduVideosValidator, | 16 | // remoteQaduVideosValidator, |
17 | remoteEventsVideosValidator | 17 | // remoteEventsVideosValidator |
18 | } from '../../../middlewares' | 18 | // } from '../../../middlewares' |
19 | import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers' | 19 | // import { logger, retryTransactionWrapper, resetSequelizeInstance } from '../../../helpers' |
20 | import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib' | 20 | // import { quickAndDirtyUpdatesVideoToFriends, fetchVideoChannelByHostAndUUID } from '../../../lib' |
21 | import { PodInstance, VideoFileInstance } from '../../../models' | 21 | // import { PodInstance, VideoFileInstance } from '../../../models' |
22 | import { | 22 | // import { |
23 | RemoteVideoRequest, | 23 | // RemoteVideoRequest, |
24 | RemoteVideoCreateData, | 24 | // RemoteVideoCreateData, |
25 | RemoteVideoUpdateData, | 25 | // RemoteVideoUpdateData, |
26 | RemoteVideoRemoveData, | 26 | // RemoteVideoRemoveData, |
27 | RemoteVideoReportAbuseData, | 27 | // RemoteVideoReportAbuseData, |
28 | RemoteQaduVideoRequest, | 28 | // RemoteQaduVideoRequest, |
29 | RemoteQaduVideoData, | 29 | // RemoteQaduVideoData, |
30 | RemoteVideoEventRequest, | 30 | // RemoteVideoEventRequest, |
31 | RemoteVideoEventData, | 31 | // RemoteVideoEventData, |
32 | RemoteVideoChannelCreateData, | 32 | // RemoteVideoChannelCreateData, |
33 | RemoteVideoChannelUpdateData, | 33 | // RemoteVideoChannelUpdateData, |
34 | RemoteVideoChannelRemoveData, | 34 | // RemoteVideoChannelRemoveData, |
35 | RemoteVideoAuthorRemoveData, | 35 | // RemoteVideoAuthorRemoveData, |
36 | RemoteVideoAuthorCreateData | 36 | // RemoteVideoAuthorCreateData |
37 | } from '../../../../shared' | 37 | // } from '../../../../shared' |
38 | import { VideoInstance } from '../../../models/video/video-interface' | 38 | // import { VideoInstance } from '../../../models/video/video-interface' |
39 | 39 | // | |
40 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | 40 | // const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] |
41 | 41 | // | |
42 | // Functions to call when processing a remote request | 42 | // // Functions to call when processing a remote request |
43 | // FIXME: use RemoteVideoRequestType as id type | 43 | // // FIXME: use RemoteVideoRequestType as id type |
44 | const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} | 44 | // const functionsHash: { [ id: string ]: (...args) => Promise<any> } = {} |
45 | functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper | 45 | // functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper |
46 | functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper | 46 | // functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper |
47 | functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper | 47 | // functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper |
48 | functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper | 48 | // functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper |
49 | functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper | 49 | // functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper |
50 | functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper | 50 | // functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper |
51 | functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper | 51 | // functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper |
52 | functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper | 52 | // functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper |
53 | functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper | 53 | // functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper |
54 | 54 | // | |
55 | const remoteVideosRouter = express.Router() | 55 | // const remoteVideosRouter = express.Router() |
56 | 56 | // | |
57 | remoteVideosRouter.post('/', | 57 | // remoteVideosRouter.post('/', |
58 | signatureValidator, | 58 | // signatureValidator, |
59 | checkSignature, | 59 | // checkSignature, |
60 | remoteVideosValidator, | 60 | // remoteVideosValidator, |
61 | remoteVideos | 61 | // remoteVideos |
62 | ) | 62 | // ) |
63 | 63 | // | |
64 | remoteVideosRouter.post('/qadu', | 64 | // remoteVideosRouter.post('/qadu', |
65 | signatureValidator, | 65 | // signatureValidator, |
66 | checkSignature, | 66 | // checkSignature, |
67 | remoteQaduVideosValidator, | 67 | // remoteQaduVideosValidator, |
68 | remoteVideosQadu | 68 | // remoteVideosQadu |
69 | ) | 69 | // ) |
70 | 70 | // | |
71 | remoteVideosRouter.post('/events', | 71 | // remoteVideosRouter.post('/events', |
72 | signatureValidator, | 72 | // signatureValidator, |
73 | checkSignature, | 73 | // checkSignature, |
74 | remoteEventsVideosValidator, | 74 | // remoteEventsVideosValidator, |
75 | remoteVideosEvents | 75 | // remoteVideosEvents |
76 | ) | 76 | // ) |
77 | 77 | // | |
78 | // --------------------------------------------------------------------------- | 78 | // // --------------------------------------------------------------------------- |
79 | 79 | // | |
80 | export { | 80 | // export { |
81 | remoteVideosRouter | 81 | // remoteVideosRouter |
82 | } | 82 | // } |
83 | 83 | // | |
84 | // --------------------------------------------------------------------------- | 84 | // // --------------------------------------------------------------------------- |
85 | 85 | // | |
86 | function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 86 | // function remoteVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
87 | const requests: RemoteVideoRequest[] = req.body.data | 87 | // const requests: RemoteVideoRequest[] = req.body.data |
88 | const fromPod = res.locals.secure.pod | 88 | // const fromPod = res.locals.secure.pod |
89 | 89 | // | |
90 | // We need to process in the same order to keep consistency | 90 | // // We need to process in the same order to keep consistency |
91 | Bluebird.each(requests, request => { | 91 | // Bluebird.each(requests, request => { |
92 | const data = request.data | 92 | // const data = request.data |
93 | 93 | // | |
94 | // Get the function we need to call in order to process the request | 94 | // // Get the function we need to call in order to process the request |
95 | const fun = functionsHash[request.type] | 95 | // const fun = functionsHash[request.type] |
96 | if (fun === undefined) { | 96 | // if (fun === undefined) { |
97 | logger.error('Unknown remote request type %s.', request.type) | 97 | // logger.error('Unknown remote request type %s.', request.type) |
98 | return | 98 | // return |
99 | } | 99 | // } |
100 | 100 | // | |
101 | return fun.call(this, data, fromPod) | 101 | // return fun.call(this, data, fromPod) |
102 | }) | 102 | // }) |
103 | .catch(err => logger.error('Error managing remote videos.', err)) | 103 | // .catch(err => logger.error('Error managing remote videos.', err)) |
104 | 104 | // | |
105 | // Don't block the other pod | 105 | // // Don't block the other pod |
106 | return res.type('json').status(204).end() | 106 | // return res.type('json').status(204).end() |
107 | } | 107 | // } |
108 | 108 | // | |
109 | function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { | 109 | // function remoteVideosQadu (req: express.Request, res: express.Response, next: express.NextFunction) { |
110 | const requests: RemoteQaduVideoRequest[] = req.body.data | 110 | // const requests: RemoteQaduVideoRequest[] = req.body.data |
111 | const fromPod = res.locals.secure.pod | 111 | // const fromPod = res.locals.secure.pod |
112 | 112 | // | |
113 | Bluebird.each(requests, request => { | 113 | // Bluebird.each(requests, request => { |
114 | const videoData = request.data | 114 | // const videoData = request.data |
115 | 115 | // | |
116 | return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) | 116 | // return quickAndDirtyUpdateVideoRetryWrapper(videoData, fromPod) |
117 | }) | 117 | // }) |
118 | .catch(err => logger.error('Error managing remote videos.', err)) | 118 | // .catch(err => logger.error('Error managing remote videos.', err)) |
119 | 119 | // | |
120 | return res.type('json').status(204).end() | 120 | // return res.type('json').status(204).end() |
121 | } | 121 | // } |
122 | 122 | // | |
123 | function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { | 123 | // function remoteVideosEvents (req: express.Request, res: express.Response, next: express.NextFunction) { |
124 | const requests: RemoteVideoEventRequest[] = req.body.data | 124 | // const requests: RemoteVideoEventRequest[] = req.body.data |
125 | const fromPod = res.locals.secure.pod | 125 | // const fromPod = res.locals.secure.pod |
126 | 126 | // | |
127 | Bluebird.each(requests, request => { | 127 | // Bluebird.each(requests, request => { |
128 | const eventData = request.data | 128 | // const eventData = request.data |
129 | 129 | // | |
130 | return processVideosEventsRetryWrapper(eventData, fromPod) | 130 | // return processVideosEventsRetryWrapper(eventData, fromPod) |
131 | }) | 131 | // }) |
132 | .catch(err => logger.error('Error managing remote videos.', err)) | 132 | // .catch(err => logger.error('Error managing remote videos.', err)) |
133 | 133 | // | |
134 | return res.type('json').status(204).end() | 134 | // return res.type('json').status(204).end() |
135 | } | 135 | // } |
136 | 136 | // | |
137 | async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) { | 137 | // async function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromPod: PodInstance) { |
138 | const options = { | 138 | // const options = { |
139 | arguments: [ eventData, fromPod ], | 139 | // arguments: [ eventData, fromPod ], |
140 | errorMessage: 'Cannot process videos events with many retries.' | 140 | // errorMessage: 'Cannot process videos events with many retries.' |
141 | } | 141 | // } |
142 | 142 | // | |
143 | await retryTransactionWrapper(processVideosEvents, options) | 143 | // await retryTransactionWrapper(processVideosEvents, options) |
144 | } | 144 | // } |
145 | 145 | // | |
146 | async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { | 146 | // async function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { |
147 | await db.sequelize.transaction(async t => { | 147 | // await db.sequelize.transaction(async t => { |
148 | const sequelizeOptions = { transaction: t } | 148 | // const sequelizeOptions = { transaction: t } |
149 | const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t) | 149 | // const videoInstance = await fetchLocalVideoByUUID(eventData.uuid, t) |
150 | 150 | // | |
151 | let columnToUpdate | 151 | // let columnToUpdate |
152 | let qaduType | 152 | // let qaduType |
153 | 153 | // | |
154 | switch (eventData.eventType) { | 154 | // switch (eventData.eventType) { |
155 | case REQUEST_VIDEO_EVENT_TYPES.VIEWS: | 155 | // case REQUEST_VIDEO_EVENT_TYPES.VIEWS: |
156 | columnToUpdate = 'views' | 156 | // columnToUpdate = 'views' |
157 | qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS | 157 | // qaduType = REQUEST_VIDEO_QADU_TYPES.VIEWS |
158 | break | 158 | // break |
159 | 159 | // | |
160 | case REQUEST_VIDEO_EVENT_TYPES.LIKES: | 160 | // case REQUEST_VIDEO_EVENT_TYPES.LIKES: |
161 | columnToUpdate = 'likes' | 161 | // columnToUpdate = 'likes' |
162 | qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES | 162 | // qaduType = REQUEST_VIDEO_QADU_TYPES.LIKES |
163 | break | 163 | // break |
164 | 164 | // | |
165 | case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: | 165 | // case REQUEST_VIDEO_EVENT_TYPES.DISLIKES: |
166 | columnToUpdate = 'dislikes' | 166 | // columnToUpdate = 'dislikes' |
167 | qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES | 167 | // qaduType = REQUEST_VIDEO_QADU_TYPES.DISLIKES |
168 | break | 168 | // break |
169 | 169 | // | |
170 | default: | 170 | // default: |
171 | throw new Error('Unknown video event type.') | 171 | // throw new Error('Unknown video event type.') |
172 | } | 172 | // } |
173 | 173 | // | |
174 | const query = {} | 174 | // const query = {} |
175 | query[columnToUpdate] = eventData.count | 175 | // query[columnToUpdate] = eventData.count |
176 | 176 | // | |
177 | await videoInstance.increment(query, sequelizeOptions) | 177 | // await videoInstance.increment(query, sequelizeOptions) |
178 | 178 | // | |
179 | const qadusParams = [ | 179 | // const qadusParams = [ |
180 | { | 180 | // { |
181 | videoId: videoInstance.id, | 181 | // videoId: videoInstance.id, |
182 | type: qaduType | 182 | // type: qaduType |
183 | } | 183 | // } |
184 | ] | 184 | // ] |
185 | await quickAndDirtyUpdatesVideoToFriends(qadusParams, t) | 185 | // await quickAndDirtyUpdatesVideoToFriends(qadusParams, t) |
186 | }) | 186 | // }) |
187 | 187 | // | |
188 | logger.info('Remote video event processed for video with uuid %s.', eventData.uuid) | 188 | // logger.info('Remote video event processed for video with uuid %s.', eventData.uuid) |
189 | } | 189 | // } |
190 | 190 | // | |
191 | async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) { | 191 | // async function quickAndDirtyUpdateVideoRetryWrapper (videoData: RemoteQaduVideoData, fromPod: PodInstance) { |
192 | const options = { | 192 | // const options = { |
193 | arguments: [ videoData, fromPod ], | 193 | // arguments: [ videoData, fromPod ], |
194 | errorMessage: 'Cannot update quick and dirty the remote video with many retries.' | 194 | // errorMessage: 'Cannot update quick and dirty the remote video with many retries.' |
195 | } | 195 | // } |
196 | 196 | // | |
197 | await retryTransactionWrapper(quickAndDirtyUpdateVideo, options) | 197 | // await retryTransactionWrapper(quickAndDirtyUpdateVideo, options) |
198 | } | 198 | // } |
199 | 199 | // | |
200 | async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { | 200 | // async function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodInstance) { |
201 | let videoUUID = '' | 201 | // let videoUUID = '' |
202 | 202 | // | |
203 | await db.sequelize.transaction(async t => { | 203 | // await db.sequelize.transaction(async t => { |
204 | const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) | 204 | // const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) |
205 | const sequelizeOptions = { transaction: t } | 205 | // const sequelizeOptions = { transaction: t } |
206 | 206 | // | |
207 | videoUUID = videoInstance.uuid | 207 | // videoUUID = videoInstance.uuid |
208 | 208 | // | |
209 | if (videoData.views) { | 209 | // if (videoData.views) { |
210 | videoInstance.set('views', videoData.views) | 210 | // videoInstance.set('views', videoData.views) |
211 | } | 211 | // } |
212 | 212 | // | |
213 | if (videoData.likes) { | 213 | // if (videoData.likes) { |
214 | videoInstance.set('likes', videoData.likes) | 214 | // videoInstance.set('likes', videoData.likes) |
215 | } | 215 | // } |
216 | 216 | // | |
217 | if (videoData.dislikes) { | 217 | // if (videoData.dislikes) { |
218 | videoInstance.set('dislikes', videoData.dislikes) | 218 | // videoInstance.set('dislikes', videoData.dislikes) |
219 | } | 219 | // } |
220 | 220 | // | |
221 | await videoInstance.save(sequelizeOptions) | 221 | // await videoInstance.save(sequelizeOptions) |
222 | }) | 222 | // }) |
223 | 223 | // | |
224 | logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) | 224 | // logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) |
225 | } | 225 | // } |
226 | 226 | // | |
227 | // Handle retries on fail | 227 | // async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { |
228 | async function addRemoteVideoRetryWrapper (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { | 228 | // const options = { |
229 | const options = { | 229 | // arguments: [ videoToRemoveData, fromPod ], |
230 | arguments: [ videoToCreateData, fromPod ], | 230 | // errorMessage: 'Cannot remove the remote video channel with many retries.' |
231 | errorMessage: 'Cannot insert the remote video with many retries.' | 231 | // } |
232 | } | 232 | // |
233 | 233 | // await retryTransactionWrapper(removeRemoteVideo, options) | |
234 | await retryTransactionWrapper(addRemoteVideo, options) | 234 | // } |
235 | } | 235 | // |
236 | 236 | // async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { | |
237 | async function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodInstance) { | 237 | // logger.debug('Removing remote video "%s".', videoToRemoveData.uuid) |
238 | logger.debug('Adding remote video "%s".', videoToCreateData.uuid) | 238 | // |
239 | 239 | // await db.sequelize.transaction(async t => { | |
240 | await db.sequelize.transaction(async t => { | 240 | // // We need the instance because we have to remove some other stuffs (thumbnail etc) |
241 | const sequelizeOptions = { | 241 | // const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t) |
242 | transaction: t | 242 | // await videoInstance.destroy({ transaction: t }) |
243 | } | 243 | // }) |
244 | 244 | // | |
245 | const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) | 245 | // logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid) |
246 | if (videoFromDatabase) throw new Error('UUID already exists.') | 246 | // } |
247 | 247 | // | |
248 | const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) | 248 | // async function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { |
249 | if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') | 249 | // const options = { |
250 | 250 | // arguments: [ authorAttributesToRemove, fromPod ], | |
251 | const tags = videoToCreateData.tags | 251 | // errorMessage: 'Cannot remove the remote video author with many retries.' |
252 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | 252 | // } |
253 | 253 | // | |
254 | const videoData = { | 254 | // await retryTransactionWrapper(removeRemoteVideoAuthor, options) |
255 | name: videoToCreateData.name, | 255 | // } |
256 | uuid: videoToCreateData.uuid, | 256 | // |
257 | category: videoToCreateData.category, | 257 | // async function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { |
258 | licence: videoToCreateData.licence, | 258 | // logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid) |
259 | language: videoToCreateData.language, | 259 | // |
260 | nsfw: videoToCreateData.nsfw, | 260 | // await db.sequelize.transaction(async t => { |
261 | description: videoToCreateData.truncatedDescription, | 261 | // const videoAuthor = await db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t) |
262 | channelId: videoChannel.id, | 262 | // await videoAuthor.destroy({ transaction: t }) |
263 | duration: videoToCreateData.duration, | 263 | // }) |
264 | createdAt: videoToCreateData.createdAt, | 264 | // |
265 | // FIXME: updatedAt does not seems to be considered by Sequelize | 265 | // logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid) |
266 | updatedAt: videoToCreateData.updatedAt, | 266 | // } |
267 | views: videoToCreateData.views, | 267 | // |
268 | likes: videoToCreateData.likes, | 268 | // async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { |
269 | dislikes: videoToCreateData.dislikes, | 269 | // const options = { |
270 | remote: true, | 270 | // arguments: [ videoChannelAttributesToRemove, fromPod ], |
271 | privacy: videoToCreateData.privacy | 271 | // errorMessage: 'Cannot remove the remote video channel with many retries.' |
272 | } | 272 | // } |
273 | 273 | // | |
274 | const video = db.Video.build(videoData) | 274 | // await retryTransactionWrapper(removeRemoteVideoChannel, options) |
275 | await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData) | 275 | // } |
276 | const videoCreated = await video.save(sequelizeOptions) | 276 | // |
277 | 277 | // async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { | |
278 | const tasks = [] | 278 | // logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid) |
279 | for (const fileData of videoToCreateData.files) { | 279 | // |
280 | const videoFileInstance = db.VideoFile.build({ | 280 | // await db.sequelize.transaction(async t => { |
281 | extname: fileData.extname, | 281 | // const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t) |
282 | infoHash: fileData.infoHash, | 282 | // await videoChannel.destroy({ transaction: t }) |
283 | resolution: fileData.resolution, | 283 | // }) |
284 | size: fileData.size, | 284 | // |
285 | videoId: videoCreated.id | 285 | // logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid) |
286 | }) | 286 | // } |
287 | 287 | // | |
288 | tasks.push(videoFileInstance.save(sequelizeOptions)) | 288 | // async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { |
289 | } | 289 | // const options = { |
290 | 290 | // arguments: [ reportData, fromPod ], | |
291 | await Promise.all(tasks) | 291 | // errorMessage: 'Cannot create remote abuse video with many retries.' |
292 | 292 | // } | |
293 | await videoCreated.setTags(tagInstances, sequelizeOptions) | 293 | // |
294 | }) | 294 | // await retryTransactionWrapper(reportAbuseRemoteVideo, options) |
295 | 295 | // } | |
296 | logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) | 296 | // |
297 | } | 297 | // async function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { |
298 | 298 | // logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID) | |
299 | // Handle retries on fail | 299 | // |
300 | async function updateRemoteVideoRetryWrapper (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { | 300 | // await db.sequelize.transaction(async t => { |
301 | const options = { | 301 | // const videoInstance = await fetchLocalVideoByUUID(reportData.videoUUID, t) |
302 | arguments: [ videoAttributesToUpdate, fromPod ], | 302 | // const videoAbuseData = { |
303 | errorMessage: 'Cannot update the remote video with many retries' | 303 | // reporterUsername: reportData.reporterUsername, |
304 | } | 304 | // reason: reportData.reportReason, |
305 | 305 | // reporterPodId: fromPod.id, | |
306 | await retryTransactionWrapper(updateRemoteVideo, options) | 306 | // videoId: videoInstance.id |
307 | } | 307 | // } |
308 | 308 | // | |
309 | async function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, fromPod: PodInstance) { | 309 | // await db.VideoAbuse.create(videoAbuseData) |
310 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) | 310 | // |
311 | let videoInstance: VideoInstance | 311 | // }) |
312 | let videoFieldsSave: object | 312 | // |
313 | 313 | // logger.info('Remote abuse for video uuid %s created', reportData.videoUUID) | |
314 | try { | 314 | // } |
315 | await db.sequelize.transaction(async t => { | 315 | // |
316 | const sequelizeOptions = { | 316 | // async function fetchLocalVideoByUUID (id: string, t: Sequelize.Transaction) { |
317 | transaction: t | 317 | // try { |
318 | } | 318 | // const video = await db.Video.loadLocalVideoByUUID(id, t) |
319 | 319 | // | |
320 | const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t) | 320 | // if (!video) throw new Error('Video ' + id + ' not found') |
321 | videoFieldsSave = videoInstance.toJSON() | 321 | // |
322 | const tags = videoAttributesToUpdate.tags | 322 | // return video |
323 | 323 | // } catch (err) { | |
324 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | 324 | // logger.error('Cannot load owned video from id.', { error: err.stack, id }) |
325 | 325 | // throw err | |
326 | videoInstance.set('name', videoAttributesToUpdate.name) | 326 | // } |
327 | videoInstance.set('category', videoAttributesToUpdate.category) | 327 | // } |
328 | videoInstance.set('licence', videoAttributesToUpdate.licence) | 328 | // |
329 | videoInstance.set('language', videoAttributesToUpdate.language) | 329 | // async function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) { |
330 | videoInstance.set('nsfw', videoAttributesToUpdate.nsfw) | 330 | // try { |
331 | videoInstance.set('description', videoAttributesToUpdate.truncatedDescription) | 331 | // const video = await db.Video.loadByHostAndUUID(podHost, uuid, t) |
332 | videoInstance.set('duration', videoAttributesToUpdate.duration) | 332 | // if (!video) throw new Error('Video not found') |
333 | videoInstance.set('createdAt', videoAttributesToUpdate.createdAt) | 333 | // |
334 | videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt) | 334 | // return video |
335 | videoInstance.set('views', videoAttributesToUpdate.views) | 335 | // } catch (err) { |
336 | videoInstance.set('likes', videoAttributesToUpdate.likes) | 336 | // logger.error('Cannot load video from host and uuid.', { error: err.stack, podHost, uuid }) |
337 | videoInstance.set('dislikes', videoAttributesToUpdate.dislikes) | 337 | // throw err |
338 | videoInstance.set('privacy', videoAttributesToUpdate.privacy) | 338 | // } |
339 | 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 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 4dd09917b..964db151d 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -10,7 +10,8 @@ import { | |||
10 | VIDEO_CATEGORIES, | 10 | VIDEO_CATEGORIES, |
11 | VIDEO_LICENCES, | 11 | VIDEO_LICENCES, |
12 | VIDEO_LANGUAGES, | 12 | VIDEO_LANGUAGES, |
13 | VIDEO_PRIVACIES | 13 | VIDEO_PRIVACIES, |
14 | VIDEO_MIMETYPE_EXT | ||
14 | } from '../../../initializers' | 15 | } from '../../../initializers' |
15 | import { | 16 | import { |
16 | addEventToRemoteVideo, | 17 | addEventToRemoteVideo, |
@@ -50,6 +51,7 @@ import { abuseVideoRouter } from './abuse' | |||
50 | import { blacklistRouter } from './blacklist' | 51 | import { blacklistRouter } from './blacklist' |
51 | import { rateVideoRouter } from './rate' | 52 | import { rateVideoRouter } from './rate' |
52 | import { videoChannelRouter } from './channel' | 53 | import { videoChannelRouter } from './channel' |
54 | import { getActivityPubUrl } from '../../../helpers/activitypub' | ||
53 | 55 | ||
54 | const videosRouter = express.Router() | 56 | const videosRouter = express.Router() |
55 | 57 | ||
@@ -59,19 +61,18 @@ const storage = multer.diskStorage({ | |||
59 | cb(null, CONFIG.STORAGE.VIDEOS_DIR) | 61 | cb(null, CONFIG.STORAGE.VIDEOS_DIR) |
60 | }, | 62 | }, |
61 | 63 | ||
62 | filename: (req, file, cb) => { | 64 | filename: async (req, file, cb) => { |
63 | let extension = '' | 65 | const extension = VIDEO_MIMETYPE_EXT[file.mimetype] |
64 | if (file.mimetype === 'video/webm') extension = 'webm' | 66 | let randomString = '' |
65 | else if (file.mimetype === 'video/mp4') extension = 'mp4' | 67 | |
66 | else if (file.mimetype === 'video/ogg') extension = 'ogv' | 68 | try { |
67 | generateRandomString(16) | 69 | randomString = await generateRandomString(16) |
68 | .then(randomString => { | 70 | } catch (err) { |
69 | cb(null, randomString + '.' + extension) | 71 | logger.error('Cannot generate random string for file name.', err) |
70 | }) | 72 | randomString = 'fake-random-string' |
71 | .catch(err => { | 73 | } |
72 | logger.error('Cannot generate random string for file name.', err) | 74 | |
73 | throw err | 75 | cb(null, randomString + '.' + extension) |
74 | }) | ||
75 | } | 76 | } |
76 | }) | 77 | }) |
77 | 78 | ||
@@ -190,6 +191,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi | |||
190 | channelId: res.locals.videoChannel.id | 191 | channelId: res.locals.videoChannel.id |
191 | } | 192 | } |
192 | const video = db.Video.build(videoData) | 193 | const video = db.Video.build(videoData) |
194 | video.url = getActivityPubUrl('video', video.uuid) | ||
193 | 195 | ||
194 | const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) | 196 | const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) |
195 | const videoFileHeight = await getVideoFileHeight(videoFilePath) | 197 | const videoFileHeight = await getVideoFileHeight(videoFilePath) |
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index ecb509b66..75de2278c 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -2,10 +2,48 @@ import * as url from 'url' | |||
2 | 2 | ||
3 | import { database as db } from '../initializers' | 3 | import { database as db } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { doRequest } from './requests' | 5 | import { doRequest, doRequestAndSaveToFile } from './requests' |
6 | import { isRemoteAccountValid } from './custom-validators' | 6 | import { isRemoteAccountValid } from './custom-validators' |
7 | import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' | 7 | import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' |
8 | import { ResultList } from '../../shared/models/result-list.model' | 8 | import { ResultList } from '../../shared/models/result-list.model' |
9 | import { CONFIG } from '../initializers/constants' | ||
10 | import { VideoInstance } from '../models/video/video-interface' | ||
11 | import { ActivityIconObject } from '../../shared/index' | ||
12 | import { join } from 'path' | ||
13 | |||
14 | function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) { | ||
15 | const thumbnailName = video.getThumbnailName() | ||
16 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
17 | |||
18 | const options = { | ||
19 | method: 'GET', | ||
20 | uri: icon.url | ||
21 | } | ||
22 | return doRequestAndSaveToFile(options, thumbnailPath) | ||
23 | } | ||
24 | |||
25 | function getActivityPubUrl (type: 'video' | 'videoChannel', uuid: string) { | ||
26 | if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + uuid | ||
27 | else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + uuid | ||
28 | |||
29 | return '' | ||
30 | } | ||
31 | |||
32 | async function getOrCreateAccount (accountUrl: string) { | ||
33 | let account = await db.Account.loadByUrl(accountUrl) | ||
34 | |||
35 | // We don't have this account in our database, fetch it on remote | ||
36 | if (!account) { | ||
37 | const { account } = await fetchRemoteAccountAndCreatePod(accountUrl) | ||
38 | |||
39 | if (!account) throw new Error('Cannot fetch remote account.') | ||
40 | |||
41 | // Save our new account in database | ||
42 | await account.save() | ||
43 | } | ||
44 | |||
45 | return account | ||
46 | } | ||
9 | 47 | ||
10 | async function fetchRemoteAccountAndCreatePod (accountUrl: string) { | 48 | async function fetchRemoteAccountAndCreatePod (accountUrl: string) { |
11 | const options = { | 49 | const options = { |
@@ -100,7 +138,10 @@ function activityPubCollectionPagination (url: string, page: number, result: Res | |||
100 | export { | 138 | export { |
101 | fetchRemoteAccountAndCreatePod, | 139 | fetchRemoteAccountAndCreatePod, |
102 | activityPubContextify, | 140 | activityPubContextify, |
103 | activityPubCollectionPagination | 141 | activityPubCollectionPagination, |
142 | getActivityPubUrl, | ||
143 | generateThumbnailFromUrl, | ||
144 | getOrCreateAccount | ||
104 | } | 145 | } |
105 | 146 | ||
106 | // --------------------------------------------------------------------------- | 147 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts new file mode 100644 index 000000000..dd671c4cf --- /dev/null +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -0,0 +1,34 @@ | |||
1 | import * as validator from 'validator' | ||
2 | import { | ||
3 | isVideoChannelCreateActivityValid, | ||
4 | isVideoTorrentAddActivityValid, | ||
5 | isVideoTorrentUpdateActivityValid, | ||
6 | isVideoChannelUpdateActivityValid | ||
7 | } from './videos' | ||
8 | |||
9 | function isRootActivityValid (activity: any) { | ||
10 | return Array.isArray(activity['@context']) && | ||
11 | ( | ||
12 | (activity.type === 'Collection' || activity.type === 'OrderedCollection') && | ||
13 | validator.isInt(activity.totalItems, { min: 0 }) && | ||
14 | Array.isArray(activity.items) | ||
15 | ) || | ||
16 | ( | ||
17 | validator.isURL(activity.id) && | ||
18 | validator.isURL(activity.actor) | ||
19 | ) | ||
20 | } | ||
21 | |||
22 | function isActivityValid (activity: any) { | ||
23 | return isVideoTorrentAddActivityValid(activity) || | ||
24 | isVideoChannelCreateActivityValid(activity) || | ||
25 | isVideoTorrentUpdateActivityValid(activity) || | ||
26 | isVideoChannelUpdateActivityValid(activity) | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | export { | ||
32 | isRootActivityValid, | ||
33 | isActivityValid | ||
34 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts index 800f0ddf3..0eba06a7b 100644 --- a/server/helpers/custom-validators/activitypub/index.ts +++ b/server/helpers/custom-validators/activitypub/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './activity' | ||
2 | export * from './signature' | 3 | export * from './signature' |
3 | export * from './misc' | 4 | export * from './misc' |
4 | export * from './videos' | 5 | export * from './videos' |
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 806d33483..f049f5a8c 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -12,6 +12,16 @@ function isActivityPubUrlValid (url: string) { | |||
12 | return exists(url) && validator.isURL(url, isURLOptions) | 12 | return exists(url) && validator.isURL(url, isURLOptions) |
13 | } | 13 | } |
14 | 14 | ||
15 | function isBaseActivityValid (activity: any, type: string) { | ||
16 | return Array.isArray(activity['@context']) && | ||
17 | activity.type === type && | ||
18 | validator.isURL(activity.id) && | ||
19 | validator.isURL(activity.actor) && | ||
20 | Array.isArray(activity.to) && | ||
21 | activity.to.every(t => validator.isURL(t)) | ||
22 | } | ||
23 | |||
15 | export { | 24 | export { |
16 | isActivityPubUrlValid | 25 | isActivityPubUrlValid, |
26 | isBaseActivityValid | ||
17 | } | 27 | } |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index e0ffba679..9233a1359 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,184 +1,117 @@ | |||
1 | import 'express-validator' | 1 | import * as validator from 'validator' |
2 | import { has, values } from 'lodash' | ||
3 | 2 | ||
4 | import { | 3 | import { |
5 | REQUEST_ENDPOINTS, | 4 | ACTIVITY_PUB |
6 | REQUEST_ENDPOINT_ACTIONS, | ||
7 | REQUEST_VIDEO_EVENT_TYPES | ||
8 | } from '../../../initializers' | 5 | } from '../../../initializers' |
9 | import { isArray, isDateValid, isUUIDValid } from '../misc' | 6 | import { isDateValid, isUUIDValid } from '../misc' |
10 | import { | 7 | import { |
11 | isVideoThumbnailDataValid, | ||
12 | isVideoAbuseReasonValid, | ||
13 | isVideoAbuseReporterUsernameValid, | ||
14 | isVideoViewsValid, | 8 | isVideoViewsValid, |
15 | isVideoLikesValid, | ||
16 | isVideoDislikesValid, | ||
17 | isVideoEventCountValid, | ||
18 | isRemoteVideoCategoryValid, | ||
19 | isRemoteVideoLicenceValid, | ||
20 | isRemoteVideoLanguageValid, | ||
21 | isVideoNSFWValid, | 9 | isVideoNSFWValid, |
22 | isVideoTruncatedDescriptionValid, | 10 | isVideoTruncatedDescriptionValid, |
23 | isVideoDurationValid, | 11 | isVideoDurationValid, |
24 | isVideoFileInfoHashValid, | ||
25 | isVideoNameValid, | 12 | isVideoNameValid, |
26 | isVideoTagsValid, | 13 | isVideoTagValid |
27 | isVideoFileExtnameValid, | ||
28 | isVideoFileResolutionValid | ||
29 | } from '../videos' | 14 | } from '../videos' |
30 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' | 15 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' |
31 | import { isVideoAuthorNameValid } from '../video-authors' | 16 | import { isBaseActivityValid } from './misc' |
32 | 17 | ||
33 | const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] | 18 | function isVideoTorrentAddActivityValid (activity: any) { |
34 | 19 | return isBaseActivityValid(activity, 'Add') && | |
35 | const checkers: { [ id: string ]: (obj: any) => boolean } = {} | 20 | isVideoTorrentObjectValid(activity.object) |
36 | checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo | 21 | } |
37 | checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo | 22 | |
38 | checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo | 23 | function isVideoTorrentUpdateActivityValid (activity: any) { |
39 | checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo | 24 | return isBaseActivityValid(activity, 'Update') && |
40 | checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel | 25 | isVideoTorrentObjectValid(activity.object) |
41 | checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel | ||
42 | checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel | ||
43 | checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor | ||
44 | checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor | ||
45 | |||
46 | function removeBadRequestVideos (requests: any[]) { | ||
47 | for (let i = requests.length - 1; i >= 0 ; i--) { | ||
48 | const request = requests[i] | ||
49 | const video = request.data | ||
50 | |||
51 | if ( | ||
52 | !video || | ||
53 | checkers[request.type] === undefined || | ||
54 | checkers[request.type](video) === false | ||
55 | ) { | ||
56 | requests.splice(i, 1) | ||
57 | } | ||
58 | } | ||
59 | } | 26 | } |
60 | 27 | ||
61 | function removeBadRequestVideosQadu (requests: any[]) { | 28 | function isVideoTorrentObjectValid (video: any) { |
62 | for (let i = requests.length - 1; i >= 0 ; i--) { | 29 | return video.type === 'Video' && |
63 | const request = requests[i] | 30 | isVideoNameValid(video.name) && |
64 | const video = request.data | 31 | isVideoDurationValid(video.duration) && |
65 | 32 | isUUIDValid(video.uuid) && | |
66 | if ( | 33 | setValidRemoteTags(video) && |
67 | !video || | 34 | isRemoteIdentifierValid(video.category) && |
68 | ( | 35 | isRemoteIdentifierValid(video.licence) && |
69 | isUUIDValid(video.uuid) && | 36 | isRemoteIdentifierValid(video.language) && |
70 | (has(video, 'views') === false || isVideoViewsValid(video.views)) && | 37 | isVideoViewsValid(video.video) && |
71 | (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && | 38 | isVideoNSFWValid(video.nsfw) && |
72 | (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) | 39 | isDateValid(video.published) && |
73 | ) === false | 40 | isDateValid(video.updated) && |
74 | ) { | 41 | isRemoteVideoContentValid(video.mediaType, video.content) && |
75 | requests.splice(i, 1) | 42 | isRemoteVideoIconValid(video.icon) && |
76 | } | 43 | setValidRemoteVideoUrls(video.url) |
77 | } | ||
78 | } | 44 | } |
79 | 45 | ||
80 | function removeBadRequestVideosEvents (requests: any[]) { | 46 | function isVideoChannelCreateActivityValid (activity: any) { |
81 | for (let i = requests.length - 1; i >= 0 ; i--) { | 47 | return isBaseActivityValid(activity, 'Create') && |
82 | const request = requests[i] | 48 | isVideoChannelObjectValid(activity.object) |
83 | const eventData = request.data | 49 | } |
84 | 50 | ||
85 | if ( | 51 | function isVideoChannelUpdateActivityValid (activity: any) { |
86 | !eventData || | 52 | return isBaseActivityValid(activity, 'Update') && |
87 | ( | 53 | isVideoChannelObjectValid(activity.object) |
88 | isUUIDValid(eventData.uuid) && | 54 | } |
89 | values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && | 55 | |
90 | isVideoEventCountValid(eventData.count) | 56 | function isVideoChannelObjectValid (videoChannel: any) { |
91 | ) === false | 57 | return videoChannel.type === 'VideoChannel' && |
92 | ) { | 58 | isVideoChannelNameValid(videoChannel.name) && |
93 | requests.splice(i, 1) | 59 | isVideoChannelDescriptionValid(videoChannel.description) && |
94 | } | 60 | isUUIDValid(videoChannel.uuid) |
95 | } | ||
96 | } | 61 | } |
97 | 62 | ||
98 | // --------------------------------------------------------------------------- | 63 | // --------------------------------------------------------------------------- |
99 | 64 | ||
100 | export { | 65 | export { |
101 | removeBadRequestVideos, | 66 | isVideoTorrentAddActivityValid, |
102 | removeBadRequestVideosQadu, | 67 | isVideoChannelCreateActivityValid, |
103 | removeBadRequestVideosEvents | 68 | isVideoTorrentUpdateActivityValid, |
69 | isVideoChannelUpdateActivityValid | ||
104 | } | 70 | } |
105 | 71 | ||
106 | // --------------------------------------------------------------------------- | 72 | // --------------------------------------------------------------------------- |
107 | 73 | ||
108 | function isCommonVideoAttributesValid (video: any) { | 74 | function setValidRemoteTags (video: any) { |
109 | return isDateValid(video.createdAt) && | 75 | if (Array.isArray(video.tag) === false) return false |
110 | isDateValid(video.updatedAt) && | ||
111 | isRemoteVideoCategoryValid(video.category) && | ||
112 | isRemoteVideoLicenceValid(video.licence) && | ||
113 | isRemoteVideoLanguageValid(video.language) && | ||
114 | isVideoNSFWValid(video.nsfw) && | ||
115 | isVideoTruncatedDescriptionValid(video.truncatedDescription) && | ||
116 | isVideoDurationValid(video.duration) && | ||
117 | isVideoNameValid(video.name) && | ||
118 | isVideoTagsValid(video.tags) && | ||
119 | isUUIDValid(video.uuid) && | ||
120 | isVideoViewsValid(video.views) && | ||
121 | isVideoLikesValid(video.likes) && | ||
122 | isVideoDislikesValid(video.dislikes) && | ||
123 | isArray(video.files) && | ||
124 | video.files.every(videoFile => { | ||
125 | if (!videoFile) return false | ||
126 | |||
127 | return ( | ||
128 | isVideoFileInfoHashValid(videoFile.infoHash) && | ||
129 | isVideoFileExtnameValid(videoFile.extname) && | ||
130 | isVideoFileResolutionValid(videoFile.resolution) | ||
131 | ) | ||
132 | }) | ||
133 | } | ||
134 | 76 | ||
135 | function checkAddVideo (video: any) { | 77 | const newTag = video.tag.filter(t => { |
136 | return isCommonVideoAttributesValid(video) && | 78 | return t.type === 'Hashtag' && |
137 | isUUIDValid(video.channelUUID) && | 79 | isVideoTagValid(t.name) |
138 | isVideoThumbnailDataValid(video.thumbnailData) | 80 | }) |
139 | } | ||
140 | 81 | ||
141 | function checkUpdateVideo (video: any) { | 82 | video.tag = newTag |
142 | return isCommonVideoAttributesValid(video) | 83 | return true |
143 | } | 84 | } |
144 | 85 | ||
145 | function checkRemoveVideo (video: any) { | 86 | function isRemoteIdentifierValid (data: any) { |
146 | return isUUIDValid(video.uuid) | 87 | return validator.isInt(data.identifier, { min: 0 }) |
147 | } | 88 | } |
148 | 89 | ||
149 | function checkReportVideo (abuse: any) { | 90 | function isRemoteVideoContentValid (mediaType: string, content: string) { |
150 | return isUUIDValid(abuse.videoUUID) && | 91 | return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content) |
151 | isVideoAbuseReasonValid(abuse.reportReason) && | ||
152 | isVideoAbuseReporterUsernameValid(abuse.reporterUsername) | ||
153 | } | 92 | } |
154 | 93 | ||
155 | function checkAddVideoChannel (videoChannel: any) { | 94 | function isRemoteVideoIconValid (icon: any) { |
156 | return isUUIDValid(videoChannel.uuid) && | 95 | return icon.type === 'Image' && |
157 | isVideoChannelNameValid(videoChannel.name) && | 96 | validator.isURL(icon.url) && |
158 | isVideoChannelDescriptionValid(videoChannel.description) && | 97 | icon.mediaType === 'image/jpeg' && |
159 | isDateValid(videoChannel.createdAt) && | 98 | validator.isInt(icon.width, { min: 0 }) && |
160 | isDateValid(videoChannel.updatedAt) && | 99 | validator.isInt(icon.height, { min: 0 }) |
161 | isUUIDValid(videoChannel.ownerUUID) | ||
162 | } | 100 | } |
163 | 101 | ||
164 | function checkUpdateVideoChannel (videoChannel: any) { | 102 | function setValidRemoteVideoUrls (video: any) { |
165 | return isUUIDValid(videoChannel.uuid) && | 103 | if (Array.isArray(video.url) === false) return false |
166 | isVideoChannelNameValid(videoChannel.name) && | ||
167 | isVideoChannelDescriptionValid(videoChannel.description) && | ||
168 | isDateValid(videoChannel.createdAt) && | ||
169 | isDateValid(videoChannel.updatedAt) && | ||
170 | isUUIDValid(videoChannel.ownerUUID) | ||
171 | } | ||
172 | 104 | ||
173 | function checkRemoveVideoChannel (videoChannel: any) { | 105 | const newUrl = video.url.filter(u => isRemoteVideoUrlValid(u)) |
174 | return isUUIDValid(videoChannel.uuid) | 106 | video.url = newUrl |
175 | } | ||
176 | 107 | ||
177 | function checkAddAuthor (author: any) { | 108 | return true |
178 | return isUUIDValid(author.uuid) && | ||
179 | isVideoAuthorNameValid(author.name) | ||
180 | } | 109 | } |
181 | 110 | ||
182 | function checkRemoveAuthor (author: any) { | 111 | function isRemoteVideoUrlValid (url: any) { |
183 | return isUUIDValid(author.uuid) | 112 | return url.type === 'Link' && |
113 | ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 && | ||
114 | validator.isURL(url.url) && | ||
115 | validator.isInt(url.width, { min: 0 }) && | ||
116 | validator.isInt(url.size, { min: 0 }) | ||
184 | } | 117 | } |
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts index 869b08870..58a40249b 100644 --- a/server/helpers/custom-validators/index.ts +++ b/server/helpers/custom-validators/index.ts | |||
@@ -3,6 +3,5 @@ export * from './misc' | |||
3 | export * from './pods' | 3 | export * from './pods' |
4 | export * from './pods' | 4 | export * from './pods' |
5 | export * from './users' | 5 | export * from './users' |
6 | export * from './video-authors' | ||
7 | export * from './video-channels' | 6 | export * from './video-channels' |
8 | export * from './videos' | 7 | export * from './videos' |
diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts deleted file mode 100644 index 48ca9b200..000000000 --- a/server/helpers/custom-validators/video-authors.ts +++ /dev/null | |||
@@ -1,45 +0,0 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as validator from 'validator' | ||
3 | import * as express from 'express' | ||
4 | import 'express-validator' | ||
5 | |||
6 | import { database as db } from '../../initializers' | ||
7 | import { AuthorInstance } from '../../models' | ||
8 | import { logger } from '../logger' | ||
9 | |||
10 | import { isUserUsernameValid } from './users' | ||
11 | |||
12 | function isVideoAuthorNameValid (value: string) { | ||
13 | return isUserUsernameValid(value) | ||
14 | } | ||
15 | |||
16 | function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) { | ||
17 | let promise: Promise<AuthorInstance> | ||
18 | if (validator.isInt(id)) { | ||
19 | promise = db.Author.load(+id) | ||
20 | } else { // UUID | ||
21 | promise = db.Author.loadByUUID(id) | ||
22 | } | ||
23 | |||
24 | promise.then(author => { | ||
25 | if (!author) { | ||
26 | return res.status(404) | ||
27 | .json({ error: 'Video author not found' }) | ||
28 | .end() | ||
29 | } | ||
30 | |||
31 | res.locals.author = author | ||
32 | callback() | ||
33 | }) | ||
34 | .catch(err => { | ||
35 | logger.error('Error in video author request validator.', err) | ||
36 | return res.sendStatus(500) | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | export { | ||
43 | checkVideoAuthorExists, | ||
44 | isVideoAuthorNameValid | ||
45 | } | ||
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index f3fdcaf2d..83407f17b 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -73,19 +73,26 @@ function isVideoDescriptionValid (value: string) { | |||
73 | } | 73 | } |
74 | 74 | ||
75 | function isVideoDurationValid (value: string) { | 75 | function isVideoDurationValid (value: string) { |
76 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) | 76 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
77 | return exists(value) && | ||
78 | typeof value === 'string' && | ||
79 | value.startsWith('PT') && | ||
80 | value.endsWith('S') && | ||
81 | validator.isInt(value.replace(/[^0-9]+/, ''), VIDEOS_CONSTRAINTS_FIELDS.DURATION) | ||
77 | } | 82 | } |
78 | 83 | ||
79 | function isVideoNameValid (value: string) { | 84 | function isVideoNameValid (value: string) { |
80 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) | 85 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NAME) |
81 | } | 86 | } |
82 | 87 | ||
88 | function isVideoTagValid (tag: string) { | ||
89 | return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG) | ||
90 | } | ||
91 | |||
83 | function isVideoTagsValid (tags: string[]) { | 92 | function isVideoTagsValid (tags: string[]) { |
84 | return isArray(tags) && | 93 | return isArray(tags) && |
85 | validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) && | 94 | validator.isInt(tags.length.toString(), VIDEOS_CONSTRAINTS_FIELDS.TAGS) && |
86 | tags.every(tag => { | 95 | tags.every(tag => isVideoTagValid(tag)) |
87 | return exists(tag) && validator.isLength(tag, VIDEOS_CONSTRAINTS_FIELDS.TAG) | ||
88 | }) | ||
89 | } | 96 | } |
90 | 97 | ||
91 | function isVideoThumbnailValid (value: string) { | 98 | function isVideoThumbnailValid (value: string) { |
@@ -209,6 +216,7 @@ export { | |||
209 | isRemoteVideoPrivacyValid, | 216 | isRemoteVideoPrivacyValid, |
210 | isVideoFileResolutionValid, | 217 | isVideoFileResolutionValid, |
211 | checkVideoExists, | 218 | checkVideoExists, |
219 | isVideoTagValid, | ||
212 | isRemoteVideoCategoryValid, | 220 | isRemoteVideoCategoryValid, |
213 | isRemoteVideoLicenceValid, | 221 | isRemoteVideoLicenceValid, |
214 | isRemoteVideoLanguageValid | 222 | isRemoteVideoLanguageValid |
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 8c4c983f7..31cedd768 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | import { PodInstance } from '../models' | 10 | import { PodInstance } from '../models' |
11 | import { PodSignature } from '../../shared' | 11 | import { PodSignature } from '../../shared' |
12 | import { signObject } from './peertube-crypto' | 12 | import { signObject } from './peertube-crypto' |
13 | import { createWriteStream } from 'fs' | ||
13 | 14 | ||
14 | function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { | 15 | function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { |
15 | return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { | 16 | return new Promise<{ response: request.RequestResponse, body: any }>((res, rej) => { |
@@ -17,6 +18,15 @@ function doRequest (requestOptions: request.CoreOptions & request.UriOptions) { | |||
17 | }) | 18 | }) |
18 | } | 19 | } |
19 | 20 | ||
21 | function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) { | ||
22 | return new Promise<request.RequestResponse>((res, rej) => { | ||
23 | request(requestOptions) | ||
24 | .on('response', response => res(response as request.RequestResponse)) | ||
25 | .on('error', err => rej(err)) | ||
26 | .pipe(createWriteStream(destPath)) | ||
27 | }) | ||
28 | } | ||
29 | |||
20 | type MakeRetryRequestParams = { | 30 | type MakeRetryRequestParams = { |
21 | url: string, | 31 | url: string, |
22 | method: 'GET' | 'POST', | 32 | method: 'GET' | 'POST', |
@@ -88,6 +98,7 @@ function makeSecureRequest (params: MakeSecureRequestParams) { | |||
88 | 98 | ||
89 | export { | 99 | export { |
90 | doRequest, | 100 | doRequest, |
101 | doRequestAndSaveToFile, | ||
91 | makeRetryRequest, | 102 | makeRetryRequest, |
92 | makeSecureRequest | 103 | makeSecureRequest |
93 | } | 104 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index cb838cf16..e1f877e80 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -203,6 +203,12 @@ const VIDEO_PRIVACIES = { | |||
203 | [VideoPrivacy.PRIVATE]: 'Private' | 203 | [VideoPrivacy.PRIVATE]: 'Private' |
204 | } | 204 | } |
205 | 205 | ||
206 | const VIDEO_MIMETYPE_EXT = { | ||
207 | 'video/webm': 'webm', | ||
208 | 'video/ogg': 'ogv', | ||
209 | 'video/mp4': 'mp4' | ||
210 | } | ||
211 | |||
206 | // --------------------------------------------------------------------------- | 212 | // --------------------------------------------------------------------------- |
207 | 213 | ||
208 | // Score a pod has when we create it as a friend | 214 | // Score a pod has when we create it as a friend |
@@ -212,7 +218,14 @@ const FRIEND_SCORE = { | |||
212 | } | 218 | } |
213 | 219 | ||
214 | const ACTIVITY_PUB = { | 220 | const ACTIVITY_PUB = { |
215 | COLLECTION_ITEMS_PER_PAGE: 10 | 221 | COLLECTION_ITEMS_PER_PAGE: 10, |
222 | VIDEO_URL_MIME_TYPES: [ | ||
223 | 'video/mp4', | ||
224 | 'video/webm', | ||
225 | 'video/ogg', | ||
226 | 'application/x-bittorrent', | ||
227 | 'application/x-bittorrent;x-scheme-handler/magnet' | ||
228 | ] | ||
216 | } | 229 | } |
217 | 230 | ||
218 | // --------------------------------------------------------------------------- | 231 | // --------------------------------------------------------------------------- |
@@ -245,42 +258,6 @@ const REQUESTS_VIDEO_EVENT_LIMIT_PER_POD = 50 | |||
245 | // Number of requests to retry for replay requests module | 258 | // Number of requests to retry for replay requests module |
246 | const RETRY_REQUESTS = 5 | 259 | const RETRY_REQUESTS = 5 |
247 | 260 | ||
248 | const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = { | ||
249 | VIDEOS: 'videos' | ||
250 | } | ||
251 | |||
252 | const REQUEST_ENDPOINT_ACTIONS: { | ||
253 | [ id: string ]: { | ||
254 | [ id: string ]: RemoteVideoRequestType | ||
255 | } | ||
256 | } = {} | ||
257 | REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = { | ||
258 | ADD_VIDEO: 'add-video', | ||
259 | UPDATE_VIDEO: 'update-video', | ||
260 | REMOVE_VIDEO: 'remove-video', | ||
261 | ADD_CHANNEL: 'add-channel', | ||
262 | UPDATE_CHANNEL: 'update-channel', | ||
263 | REMOVE_CHANNEL: 'remove-channel', | ||
264 | ADD_AUTHOR: 'add-author', | ||
265 | REMOVE_AUTHOR: 'remove-author', | ||
266 | REPORT_ABUSE: 'report-abuse' | ||
267 | } | ||
268 | |||
269 | const REQUEST_VIDEO_QADU_ENDPOINT = 'videos/qadu' | ||
270 | const REQUEST_VIDEO_EVENT_ENDPOINT = 'videos/events' | ||
271 | |||
272 | const REQUEST_VIDEO_QADU_TYPES: { [ id: string ]: RequestVideoQaduType } = { | ||
273 | LIKES: 'likes', | ||
274 | DISLIKES: 'dislikes', | ||
275 | VIEWS: 'views' | ||
276 | } | ||
277 | |||
278 | const REQUEST_VIDEO_EVENT_TYPES: { [ id: string ]: RequestVideoEventType } = { | ||
279 | LIKES: 'likes', | ||
280 | DISLIKES: 'dislikes', | ||
281 | VIEWS: 'views' | ||
282 | } | ||
283 | |||
284 | const REMOTE_SCHEME = { | 261 | const REMOTE_SCHEME = { |
285 | HTTP: 'https', | 262 | HTTP: 'https', |
286 | WS: 'wss' | 263 | WS: 'wss' |
@@ -306,8 +283,6 @@ let JOBS_FETCHING_INTERVAL = 60000 | |||
306 | 283 | ||
307 | // --------------------------------------------------------------------------- | 284 | // --------------------------------------------------------------------------- |
308 | 285 | ||
309 | // const SIGNATURE_ALGORITHM = 'RSA-SHA256' | ||
310 | // const SIGNATURE_ENCODING = 'hex' | ||
311 | const PRIVATE_RSA_KEY_SIZE = 2048 | 286 | const PRIVATE_RSA_KEY_SIZE = 2048 |
312 | 287 | ||
313 | // Password encryption | 288 | // Password encryption |
@@ -412,5 +387,6 @@ export { | |||
412 | VIDEO_LANGUAGES, | 387 | VIDEO_LANGUAGES, |
413 | VIDEO_PRIVACIES, | 388 | VIDEO_PRIVACIES, |
414 | VIDEO_LICENCES, | 389 | VIDEO_LICENCES, |
415 | VIDEO_RATE_TYPES | 390 | VIDEO_RATE_TYPES, |
391 | VIDEO_MIMETYPE_EXT | ||
416 | } | 392 | } |
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts new file mode 100644 index 000000000..05e77ebc3 --- /dev/null +++ b/server/lib/activitypub/misc.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import * as magnetUtil from 'magnet-uri' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { VideoTorrentObject } from '../../../shared' | ||
4 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
5 | import { database as db } from '../../initializers' | ||
6 | import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants' | ||
7 | import { VideoChannelInstance } from '../../models/video/video-channel-interface' | ||
8 | import { VideoFileAttributes } from '../../models/video/video-file-interface' | ||
9 | import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' | ||
10 | |||
11 | async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelInstance, videoObject: VideoTorrentObject, t: Sequelize.Transaction) { | ||
12 | const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoObject.uuid, videoObject.id, t) | ||
13 | if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.') | ||
14 | |||
15 | const duration = videoObject.duration.replace(/[^\d]+/, '') | ||
16 | const videoData: VideoAttributes = { | ||
17 | name: videoObject.name, | ||
18 | uuid: videoObject.uuid, | ||
19 | url: videoObject.id, | ||
20 | category: parseInt(videoObject.category.identifier, 10), | ||
21 | licence: parseInt(videoObject.licence.identifier, 10), | ||
22 | language: parseInt(videoObject.language.identifier, 10), | ||
23 | nsfw: videoObject.nsfw, | ||
24 | description: videoObject.content, | ||
25 | channelId: videoChannel.id, | ||
26 | duration: parseInt(duration, 10), | ||
27 | createdAt: videoObject.published, | ||
28 | // FIXME: updatedAt does not seems to be considered by Sequelize | ||
29 | updatedAt: videoObject.updated, | ||
30 | views: videoObject.views, | ||
31 | likes: 0, | ||
32 | dislikes: 0, | ||
33 | // likes: videoToCreateData.likes, | ||
34 | // dislikes: videoToCreateData.dislikes, | ||
35 | remote: true, | ||
36 | privacy: 1 | ||
37 | // privacy: videoToCreateData.privacy | ||
38 | } | ||
39 | |||
40 | return videoData | ||
41 | } | ||
42 | |||
43 | function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { | ||
44 | const fileUrls = videoObject.url | ||
45 | .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1) | ||
46 | |||
47 | const attributes: VideoFileAttributes[] = [] | ||
48 | for (const url of fileUrls) { | ||
49 | // Fetch associated magnet uri | ||
50 | const magnet = videoObject.url | ||
51 | .find(u => { | ||
52 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width | ||
53 | }) | ||
54 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url) | ||
55 | |||
56 | const parsed = magnetUtil.decode(magnet.url) | ||
57 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) | ||
58 | |||
59 | const attribute = { | ||
60 | extname: VIDEO_MIMETYPE_EXT[url.mimeType], | ||
61 | infoHash: parsed.infoHash, | ||
62 | resolution: url.width, | ||
63 | size: url.size, | ||
64 | videoId: videoCreated.id | ||
65 | } | ||
66 | attributes.push(attribute) | ||
67 | } | ||
68 | |||
69 | return attributes | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | export { | ||
75 | videoFileActivityUrlToDBAttributes, | ||
76 | videoActivityObjectToDBAttributes | ||
77 | } | ||
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts new file mode 100644 index 000000000..40541aca3 --- /dev/null +++ b/server/lib/activitypub/process-add.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import { VideoTorrentObject } from '../../../shared' | ||
2 | import { ActivityAdd } from '../../../shared/models/activitypub/activity' | ||
3 | import { generateThumbnailFromUrl, logger, retryTransactionWrapper, getOrCreateAccount } from '../../helpers' | ||
4 | import { database as db } from '../../initializers' | ||
5 | import { AccountInstance } from '../../models/account/account-interface' | ||
6 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | ||
7 | import Bluebird = require('bluebird') | ||
8 | |||
9 | async function processAddActivity (activity: ActivityAdd) { | ||
10 | const activityObject = activity.object | ||
11 | const activityType = activityObject.type | ||
12 | const account = await getOrCreateAccount(activity.actor) | ||
13 | |||
14 | if (activityType === 'Video') { | ||
15 | return processAddVideo(account, activity.id, activityObject as VideoTorrentObject) | ||
16 | } | ||
17 | |||
18 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | ||
19 | return Promise.resolve(undefined) | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | processAddActivity | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { | ||
31 | const options = { | ||
32 | arguments: [ account, videoChannelUrl ,video ], | ||
33 | errorMessage: 'Cannot insert the remote video with many retries.' | ||
34 | } | ||
35 | |||
36 | return retryTransactionWrapper(addRemoteVideo, options) | ||
37 | } | ||
38 | |||
39 | async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) { | ||
40 | logger.debug('Adding remote video %s.', videoToCreateData.url) | ||
41 | |||
42 | await db.sequelize.transaction(async t => { | ||
43 | const sequelizeOptions = { | ||
44 | transaction: t | ||
45 | } | ||
46 | |||
47 | const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t) | ||
48 | if (!videoChannel) throw new Error('Video channel not found.') | ||
49 | |||
50 | if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') | ||
51 | |||
52 | const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t) | ||
53 | const video = db.Video.build(videoData) | ||
54 | |||
55 | // Don't block on request | ||
56 | generateThumbnailFromUrl(video, videoToCreateData.icon) | ||
57 | .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err)) | ||
58 | |||
59 | const videoCreated = await video.save(sequelizeOptions) | ||
60 | |||
61 | const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) | ||
62 | |||
63 | const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f)) | ||
64 | await Promise.all(tasks) | ||
65 | |||
66 | const tags = videoToCreateData.tag.map(t => t.name) | ||
67 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | ||
68 | await videoCreated.setTags(tagInstances, sequelizeOptions) | ||
69 | }) | ||
70 | |||
71 | logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) | ||
72 | } | ||
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts index 114ff1848..471674ead 100644 --- a/server/lib/activitypub/process-create.ts +++ b/server/lib/activitypub/process-create.ts | |||
@@ -1,23 +1,23 @@ | |||
1 | import { | 1 | import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared' |
2 | ActivityCreate, | 2 | import { ActivityAdd } from '../../../shared/models/activitypub/activity' |
3 | VideoTorrentObject, | 3 | import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers' |
4 | VideoChannelObject | ||
5 | } from '../../../shared' | ||
6 | import { database as db } from '../../initializers' | 4 | import { database as db } from '../../initializers' |
7 | import { logger, retryTransactionWrapper } from '../../helpers' | 5 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
6 | import Bluebird = require('bluebird') | ||
7 | import { AccountInstance } from '../../models/account/account-interface' | ||
8 | import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' | ||
8 | 9 | ||
9 | function processCreateActivity (activity: ActivityCreate) { | 10 | async function processCreateActivity (activity: ActivityCreate) { |
10 | const activityObject = activity.object | 11 | const activityObject = activity.object |
11 | const activityType = activityObject.type | 12 | const activityType = activityObject.type |
13 | const account = await getOrCreateAccount(activity.actor) | ||
12 | 14 | ||
13 | if (activityType === 'Video') { | 15 | if (activityType === 'VideoChannel') { |
14 | return processCreateVideo(activityObject as VideoTorrentObject) | 16 | return processCreateVideoChannel(account, activityObject as VideoChannelObject) |
15 | } else if (activityType === 'VideoChannel') { | ||
16 | return processCreateVideoChannel(activityObject as VideoChannelObject) | ||
17 | } | 17 | } |
18 | 18 | ||
19 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 19 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
20 | return Promise.resolve() | 20 | return Promise.resolve(undefined) |
21 | } | 21 | } |
22 | 22 | ||
23 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
@@ -28,77 +28,37 @@ export { | |||
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
31 | function processCreateVideo (video: VideoTorrentObject) { | 31 | function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { |
32 | const options = { | 32 | const options = { |
33 | arguments: [ video ], | 33 | arguments: [ account, videoChannelToCreateData ], |
34 | errorMessage: 'Cannot insert the remote video with many retries.' | 34 | errorMessage: 'Cannot insert the remote video channel with many retries.' |
35 | } | 35 | } |
36 | 36 | ||
37 | return retryTransactionWrapper(addRemoteVideo, options) | 37 | return retryTransactionWrapper(addRemoteVideoChannel, options) |
38 | } | 38 | } |
39 | 39 | ||
40 | async function addRemoteVideo (videoToCreateData: VideoTorrentObject) { | 40 | async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { |
41 | logger.debug('Adding remote video %s.', videoToCreateData.url) | 41 | logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) |
42 | 42 | ||
43 | await db.sequelize.transaction(async t => { | 43 | await db.sequelize.transaction(async t => { |
44 | const sequelizeOptions = { | 44 | let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) |
45 | transaction: t | 45 | if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') |
46 | } | 46 | |
47 | 47 | const videoChannelData = { | |
48 | const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) | 48 | name: videoChannelToCreateData.name, |
49 | if (videoFromDatabase) throw new Error('UUID already exists.') | 49 | description: videoChannelToCreateData.content, |
50 | 50 | uuid: videoChannelToCreateData.uuid, | |
51 | const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) | 51 | createdAt: videoChannelToCreateData.published, |
52 | if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') | 52 | updatedAt: videoChannelToCreateData.updated, |
53 | |||
54 | const tags = videoToCreateData.tags | ||
55 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | ||
56 | |||
57 | const videoData = { | ||
58 | name: videoToCreateData.name, | ||
59 | uuid: videoToCreateData.uuid, | ||
60 | category: videoToCreateData.category, | ||
61 | licence: videoToCreateData.licence, | ||
62 | language: videoToCreateData.language, | ||
63 | nsfw: videoToCreateData.nsfw, | ||
64 | description: videoToCreateData.truncatedDescription, | ||
65 | channelId: videoChannel.id, | ||
66 | duration: videoToCreateData.duration, | ||
67 | createdAt: videoToCreateData.createdAt, | ||
68 | // FIXME: updatedAt does not seems to be considered by Sequelize | ||
69 | updatedAt: videoToCreateData.updatedAt, | ||
70 | views: videoToCreateData.views, | ||
71 | likes: videoToCreateData.likes, | ||
72 | dislikes: videoToCreateData.dislikes, | ||
73 | remote: true, | 53 | remote: true, |
74 | privacy: videoToCreateData.privacy | 54 | accountId: account.id |
75 | } | ||
76 | |||
77 | const video = db.Video.build(videoData) | ||
78 | await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData) | ||
79 | const videoCreated = await video.save(sequelizeOptions) | ||
80 | |||
81 | const tasks = [] | ||
82 | for (const fileData of videoToCreateData.files) { | ||
83 | const videoFileInstance = db.VideoFile.build({ | ||
84 | extname: fileData.extname, | ||
85 | infoHash: fileData.infoHash, | ||
86 | resolution: fileData.resolution, | ||
87 | size: fileData.size, | ||
88 | videoId: videoCreated.id | ||
89 | }) | ||
90 | |||
91 | tasks.push(videoFileInstance.save(sequelizeOptions)) | ||
92 | } | 55 | } |
93 | 56 | ||
94 | await Promise.all(tasks) | 57 | videoChannel = db.VideoChannel.build(videoChannelData) |
58 | videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) | ||
95 | 59 | ||
96 | await videoCreated.setTags(tagInstances, sequelizeOptions) | 60 | await videoChannel.save({ transaction: t }) |
97 | }) | 61 | }) |
98 | 62 | ||
99 | logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) | 63 | logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid) |
100 | } | ||
101 | |||
102 | function processCreateVideoChannel (videoChannel: VideoChannelObject) { | ||
103 | |||
104 | } | 64 | } |
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts index 187c7be7c..cd8a4b8e2 100644 --- a/server/lib/activitypub/process-update.ts +++ b/server/lib/activitypub/process-update.ts | |||
@@ -1,15 +1,25 @@ | |||
1 | import { | 1 | import { VideoChannelObject, VideoTorrentObject } from '../../../shared' |
2 | ActivityCreate, | 2 | import { ActivityUpdate } from '../../../shared/models/activitypub/activity' |
3 | VideoTorrentObject, | 3 | import { getOrCreateAccount } from '../../helpers/activitypub' |
4 | VideoChannelObject | 4 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
5 | } from '../../../shared' | 5 | import { logger } from '../../helpers/logger' |
6 | import { resetSequelizeInstance } from '../../helpers/utils' | ||
7 | import { database as db } from '../../initializers' | ||
8 | import { AccountInstance } from '../../models/account/account-interface' | ||
9 | import { VideoInstance } from '../../models/video/video-interface' | ||
10 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | ||
11 | import Bluebird = require('bluebird') | ||
12 | |||
13 | async function processUpdateActivity (activity: ActivityUpdate) { | ||
14 | const account = await getOrCreateAccount(activity.actor) | ||
6 | 15 | ||
7 | function processUpdateActivity (activity: ActivityCreate) { | ||
8 | if (activity.object.type === 'Video') { | 16 | if (activity.object.type === 'Video') { |
9 | return processUpdateVideo(activity.object) | 17 | return processUpdateVideo(account, activity.object) |
10 | } else if (activity.object.type === 'VideoChannel') { | 18 | } else if (activity.object.type === 'VideoChannel') { |
11 | return processUpdateVideoChannel(activity.object) | 19 | return processUpdateVideoChannel(account, activity.object) |
12 | } | 20 | } |
21 | |||
22 | return undefined | ||
13 | } | 23 | } |
14 | 24 | ||
15 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
@@ -20,10 +30,107 @@ export { | |||
20 | 30 | ||
21 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
22 | 32 | ||
23 | function processUpdateVideo (video: VideoTorrentObject) { | 33 | function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) { |
34 | const options = { | ||
35 | arguments: [ account, video ], | ||
36 | errorMessage: 'Cannot update the remote video with many retries' | ||
37 | } | ||
24 | 38 | ||
39 | return retryTransactionWrapper(updateRemoteVideo, options) | ||
25 | } | 40 | } |
26 | 41 | ||
27 | function processUpdateVideoChannel (videoChannel: VideoChannelObject) { | 42 | async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) { |
43 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) | ||
44 | let videoInstance: VideoInstance | ||
45 | let videoFieldsSave: object | ||
46 | |||
47 | try { | ||
48 | await db.sequelize.transaction(async t => { | ||
49 | const sequelizeOptions = { | ||
50 | transaction: t | ||
51 | } | ||
52 | |||
53 | const videoInstance = await db.Video.loadByUrl(videoAttributesToUpdate.id, t) | ||
54 | if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') | ||
55 | |||
56 | if (videoInstance.VideoChannel.Account.id !== account.id) { | ||
57 | throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url) | ||
58 | } | ||
59 | |||
60 | const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate, t) | ||
61 | videoInstance.set('name', videoData.name) | ||
62 | videoInstance.set('category', videoData.category) | ||
63 | videoInstance.set('licence', videoData.licence) | ||
64 | videoInstance.set('language', videoData.language) | ||
65 | videoInstance.set('nsfw', videoData.nsfw) | ||
66 | videoInstance.set('description', videoData.description) | ||
67 | videoInstance.set('duration', videoData.duration) | ||
68 | videoInstance.set('createdAt', videoData.createdAt) | ||
69 | videoInstance.set('updatedAt', videoData.updatedAt) | ||
70 | videoInstance.set('views', videoData.views) | ||
71 | // videoInstance.set('likes', videoData.likes) | ||
72 | // videoInstance.set('dislikes', videoData.dislikes) | ||
73 | // videoInstance.set('privacy', videoData.privacy) | ||
74 | |||
75 | await videoInstance.save(sequelizeOptions) | ||
76 | |||
77 | // Remove old video files | ||
78 | const videoFileDestroyTasks: Bluebird<void>[] = [] | ||
79 | for (const videoFile of videoInstance.VideoFiles) { | ||
80 | videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions)) | ||
81 | } | ||
82 | await Promise.all(videoFileDestroyTasks) | ||
83 | |||
84 | const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) | ||
85 | const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f)) | ||
86 | await Promise.all(tasks) | ||
87 | |||
88 | const tags = videoAttributesToUpdate.tag.map(t => t.name) | ||
89 | const tagInstances = await db.Tag.findOrCreateTags(tags, t) | ||
90 | await videoInstance.setTags(tagInstances, sequelizeOptions) | ||
91 | }) | ||
92 | |||
93 | logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) | ||
94 | } catch (err) { | ||
95 | if (videoInstance !== undefined && videoFieldsSave !== undefined) { | ||
96 | resetSequelizeInstance(videoInstance, videoFieldsSave) | ||
97 | } | ||
98 | |||
99 | // This is just a debug because we will retry the insert | ||
100 | logger.debug('Cannot update the remote video.', err) | ||
101 | throw err | ||
102 | } | ||
103 | } | ||
104 | |||
105 | async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { | ||
106 | const options = { | ||
107 | arguments: [ account, videoChannel ], | ||
108 | errorMessage: 'Cannot update the remote video channel with many retries.' | ||
109 | } | ||
110 | |||
111 | await retryTransactionWrapper(updateRemoteVideoChannel, options) | ||
112 | } | ||
113 | |||
114 | async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { | ||
115 | logger.debug('Updating remote video channel "%s".', videoChannel.uuid) | ||
116 | |||
117 | await db.sequelize.transaction(async t => { | ||
118 | const sequelizeOptions = { transaction: t } | ||
119 | |||
120 | const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id) | ||
121 | if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.') | ||
122 | |||
123 | if (videoChannelInstance.Account.id !== account.id) { | ||
124 | throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url) | ||
125 | } | ||
126 | |||
127 | videoChannelInstance.set('name', videoChannel.name) | ||
128 | videoChannelInstance.set('description', videoChannel.content) | ||
129 | videoChannelInstance.set('createdAt', videoChannel.published) | ||
130 | videoChannelInstance.set('updatedAt', videoChannel.updated) | ||
131 | |||
132 | await videoChannelInstance.save(sequelizeOptions) | ||
133 | }) | ||
28 | 134 | ||
135 | logger.info('Remote video channel with uuid %s updated', videoChannel.uuid) | ||
29 | } | 136 | } |
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts new file mode 100644 index 000000000..78a6d1444 --- /dev/null +++ b/server/middlewares/validators/activitypub/activity.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { body } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { logger, isRootActivityValid } from '../../../helpers' | ||
5 | import { checkErrors } from '../utils' | ||
6 | |||
7 | const activityPubValidator = [ | ||
8 | body('data').custom(isRootActivityValid), | ||
9 | |||
10 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
11 | logger.debug('Checking activity pub parameters', { parameters: req.body }) | ||
12 | |||
13 | checkErrors(req, res, next) | ||
14 | } | ||
15 | ] | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | export { | ||
20 | activityPubValidator | ||
21 | } | ||
diff --git a/server/middlewares/validators/activitypub/videos.ts b/server/middlewares/validators/activitypub/videos.ts deleted file mode 100644 index 497320cc1..000000000 --- a/server/middlewares/validators/activitypub/videos.ts +++ /dev/null | |||
@@ -1,61 +0,0 @@ | |||
1 | import { body } from 'express-validator/check' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { | ||
5 | logger, | ||
6 | isArray, | ||
7 | removeBadRequestVideos, | ||
8 | removeBadRequestVideosQadu, | ||
9 | removeBadRequestVideosEvents | ||
10 | } from '../../../helpers' | ||
11 | import { checkErrors } from '../utils' | ||
12 | |||
13 | const remoteVideosValidator = [ | ||
14 | body('data').custom(isArray), | ||
15 | |||
16 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
17 | logger.debug('Checking remoteVideos parameters', { parameters: req.body }) | ||
18 | |||
19 | checkErrors(req, res, () => { | ||
20 | removeBadRequestVideos(req.body.data) | ||
21 | |||
22 | return next() | ||
23 | }) | ||
24 | } | ||
25 | ] | ||
26 | |||
27 | const remoteQaduVideosValidator = [ | ||
28 | body('data').custom(isArray), | ||
29 | |||
30 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
31 | logger.debug('Checking remoteQaduVideos parameters', { parameters: req.body }) | ||
32 | |||
33 | checkErrors(req, res, () => { | ||
34 | removeBadRequestVideosQadu(req.body.data) | ||
35 | |||
36 | return next() | ||
37 | }) | ||
38 | } | ||
39 | ] | ||
40 | |||
41 | const remoteEventsVideosValidator = [ | ||
42 | body('data').custom(isArray), | ||
43 | |||
44 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
45 | logger.debug('Checking remoteEventsVideos parameters', { parameters: req.body }) | ||
46 | |||
47 | checkErrors(req, res, () => { | ||
48 | removeBadRequestVideosEvents(req.body.data) | ||
49 | |||
50 | return next() | ||
51 | }) | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | export { | ||
58 | remoteVideosValidator, | ||
59 | remoteQaduVideosValidator, | ||
60 | remoteEventsVideosValidator | ||
61 | } | ||
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts index 477f97cd4..55e772063 100644 --- a/server/models/video/video-channel-interface.ts +++ b/server/models/video/video-channel-interface.ts | |||
@@ -24,6 +24,8 @@ export namespace VideoChannelMethods { | |||
24 | export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | 24 | export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> |
25 | export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | 25 | export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> |
26 | export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance> | 26 | export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance> |
27 | export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
28 | export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance> | ||
27 | } | 29 | } |
28 | 30 | ||
29 | export interface VideoChannelClass { | 31 | export interface VideoChannelClass { |
@@ -37,6 +39,8 @@ export interface VideoChannelClass { | |||
37 | loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | 39 | loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount |
38 | loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | 40 | loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount |
39 | loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | 41 | loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos |
42 | loadByUrl: VideoChannelMethods.LoadByUrl | ||
43 | loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | ||
40 | } | 44 | } |
41 | 45 | ||
42 | export interface VideoChannelAttributes { | 46 | export interface VideoChannelAttributes { |
@@ -45,7 +49,7 @@ export interface VideoChannelAttributes { | |||
45 | name: string | 49 | name: string |
46 | description: string | 50 | description: string |
47 | remote: boolean | 51 | remote: boolean |
48 | url: string | 52 | url?: string |
49 | 53 | ||
50 | Account?: AccountInstance | 54 | Account?: AccountInstance |
51 | Videos?: VideoInstance[] | 55 | Videos?: VideoInstance[] |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index c17828f3e..93a611fa0 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -25,6 +25,8 @@ let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | |||
25 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | 25 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount |
26 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | 26 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID |
27 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | 27 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos |
28 | let loadByUrl: VideoChannelMethods.LoadByUrl | ||
29 | let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | ||
28 | 30 | ||
29 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 31 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { |
30 | VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', | 32 | VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', |
@@ -94,12 +96,14 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
94 | loadByUUID, | 96 | loadByUUID, |
95 | loadByHostAndUUID, | 97 | loadByHostAndUUID, |
96 | loadAndPopulateAccountAndVideos, | 98 | loadAndPopulateAccountAndVideos, |
97 | countByAccount | 99 | countByAccount, |
100 | loadByUrl, | ||
101 | loadByUUIDOrUrl | ||
98 | ] | 102 | ] |
99 | const instanceMethods = [ | 103 | const instanceMethods = [ |
100 | isOwned, | 104 | isOwned, |
101 | toFormattedJSON, | 105 | toFormattedJSON, |
102 | toActivityPubObject, | 106 | toActivityPubObject |
103 | ] | 107 | ] |
104 | addMethodsToModel(VideoChannel, classMethods, instanceMethods) | 108 | addMethodsToModel(VideoChannel, classMethods, instanceMethods) |
105 | 109 | ||
@@ -254,6 +258,33 @@ loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { | |||
254 | return VideoChannel.findOne(query) | 258 | return VideoChannel.findOne(query) |
255 | } | 259 | } |
256 | 260 | ||
261 | loadByUrl = function (url: string, t?: Sequelize.Transaction) { | ||
262 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | ||
263 | where: { | ||
264 | url | ||
265 | } | ||
266 | } | ||
267 | |||
268 | if (t !== undefined) query.transaction = t | ||
269 | |||
270 | return VideoChannel.findOne(query) | ||
271 | } | ||
272 | |||
273 | loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) { | ||
274 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | ||
275 | where: { | ||
276 | [Sequelize.Op.or]: [ | ||
277 | { uuid }, | ||
278 | { url } | ||
279 | ] | ||
280 | }, | ||
281 | } | ||
282 | |||
283 | if (t !== undefined) query.transaction = t | ||
284 | |||
285 | return VideoChannel.findOne(query) | ||
286 | } | ||
287 | |||
257 | loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { | 288 | loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { |
258 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 289 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { |
259 | where: { | 290 | where: { |
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index e62e25a82..a0ac43e1e 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts | |||
@@ -69,6 +69,7 @@ export namespace VideoMethods { | |||
69 | export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance> | 69 | export type LoadAndPopulateAccount = (id: number) => Bluebird<VideoInstance> |
70 | export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance> | 70 | export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird<VideoInstance> |
71 | export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance> | 71 | export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird<VideoInstance> |
72 | export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance> | ||
72 | 73 | ||
73 | export type RemoveThumbnail = (this: VideoInstance) => Promise<void> | 74 | export type RemoveThumbnail = (this: VideoInstance) => Promise<void> |
74 | export type RemovePreview = (this: VideoInstance) => Promise<void> | 75 | export type RemovePreview = (this: VideoInstance) => Promise<void> |
@@ -89,6 +90,7 @@ export interface VideoClass { | |||
89 | loadByHostAndUUID: VideoMethods.LoadByHostAndUUID | 90 | loadByHostAndUUID: VideoMethods.LoadByHostAndUUID |
90 | loadByUUID: VideoMethods.LoadByUUID | 91 | loadByUUID: VideoMethods.LoadByUUID |
91 | loadByUrl: VideoMethods.LoadByUrl | 92 | loadByUrl: VideoMethods.LoadByUrl |
93 | loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL | ||
92 | loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID | 94 | loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID |
93 | loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags | 95 | loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags |
94 | searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags | 96 | searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags |
@@ -109,7 +111,10 @@ export interface VideoAttributes { | |||
109 | likes?: number | 111 | likes?: number |
110 | dislikes?: number | 112 | dislikes?: number |
111 | remote: boolean | 113 | remote: boolean |
112 | url: string | 114 | url?: string |
115 | |||
116 | createdAt?: Date | ||
117 | updatedAt?: Date | ||
113 | 118 | ||
114 | parentId?: number | 119 | parentId?: number |
115 | channelId?: number | 120 | channelId?: number |
@@ -120,9 +125,6 @@ export interface VideoAttributes { | |||
120 | } | 125 | } |
121 | 126 | ||
122 | export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { | 127 | export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> { |
123 | createdAt: Date | ||
124 | updatedAt: Date | ||
125 | |||
126 | createPreview: VideoMethods.CreatePreview | 128 | createPreview: VideoMethods.CreatePreview |
127 | createThumbnail: VideoMethods.CreateThumbnail | 129 | createThumbnail: VideoMethods.CreateThumbnail |
128 | createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | 130 | createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash |
@@ -158,4 +160,3 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In | |||
158 | } | 160 | } |
159 | 161 | ||
160 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} | 162 | export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {} |
161 | |||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 94af1ece5..b5d333347 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -25,7 +25,8 @@ import { | |||
25 | statPromise, | 25 | statPromise, |
26 | generateImageFromVideoFile, | 26 | generateImageFromVideoFile, |
27 | transcode, | 27 | transcode, |
28 | getVideoFileHeight | 28 | getVideoFileHeight, |
29 | getActivityPubUrl | ||
29 | } from '../../helpers' | 30 | } from '../../helpers' |
30 | import { | 31 | import { |
31 | CONFIG, | 32 | CONFIG, |
@@ -88,7 +89,7 @@ let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccount | |||
88 | let listOwnedByAccount: VideoMethods.ListOwnedByAccount | 89 | let listOwnedByAccount: VideoMethods.ListOwnedByAccount |
89 | let load: VideoMethods.Load | 90 | let load: VideoMethods.Load |
90 | let loadByUUID: VideoMethods.LoadByUUID | 91 | let loadByUUID: VideoMethods.LoadByUUID |
91 | let loadByUrl: VideoMethods.LoadByUrl | 92 | let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL |
92 | let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID | 93 | let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID |
93 | let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount | 94 | let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount |
94 | let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags | 95 | let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags |
@@ -277,6 +278,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
277 | loadAndPopulateAccount, | 278 | loadAndPopulateAccount, |
278 | loadAndPopulateAccountAndPodAndTags, | 279 | loadAndPopulateAccountAndPodAndTags, |
279 | loadByHostAndUUID, | 280 | loadByHostAndUUID, |
281 | loadByUUIDOrURL, | ||
280 | loadByUUID, | 282 | loadByUUID, |
281 | loadLocalVideoByUUID, | 283 | loadLocalVideoByUUID, |
282 | loadByUUIDAndPopulateAccountAndPodAndTags, | 284 | loadByUUIDAndPopulateAccountAndPodAndTags, |
@@ -595,6 +597,7 @@ toActivityPubObject = function (this: VideoInstance) { | |||
595 | 597 | ||
596 | const videoObject: VideoTorrentObject = { | 598 | const videoObject: VideoTorrentObject = { |
597 | type: 'Video', | 599 | type: 'Video', |
600 | id: getActivityPubUrl('video', this.uuid), | ||
598 | name: this.name, | 601 | name: this.name, |
599 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | 602 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
600 | duration: 'PT' + this.duration + 'S', | 603 | duration: 'PT' + this.duration + 'S', |
@@ -731,6 +734,7 @@ getCategoryLabel = function (this: VideoInstance) { | |||
731 | 734 | ||
732 | getLicenceLabel = function (this: VideoInstance) { | 735 | getLicenceLabel = function (this: VideoInstance) { |
733 | let licenceLabel = VIDEO_LICENCES[this.licence] | 736 | let licenceLabel = VIDEO_LICENCES[this.licence] |
737 | |||
734 | // Maybe our pod is not up to date and there are new licences since our version | 738 | // Maybe our pod is not up to date and there are new licences since our version |
735 | if (!licenceLabel) licenceLabel = 'Unknown' | 739 | if (!licenceLabel) licenceLabel = 'Unknown' |
736 | 740 | ||
@@ -946,6 +950,22 @@ loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { | |||
946 | return Video.findOne(query) | 950 | return Video.findOne(query) |
947 | } | 951 | } |
948 | 952 | ||
953 | loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) { | ||
954 | const query: Sequelize.FindOptions<VideoAttributes> = { | ||
955 | where: { | ||
956 | [Sequelize.Op.or]: [ | ||
957 | { uuid }, | ||
958 | { url } | ||
959 | ] | ||
960 | }, | ||
961 | include: [ Video['sequelize'].models.VideoFile ] | ||
962 | } | ||
963 | |||
964 | if (t !== undefined) query.transaction = t | ||
965 | |||
966 | return Video.findOne(query) | ||
967 | } | ||
968 | |||
949 | loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) { | 969 | loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) { |
950 | const query: Sequelize.FindOptions<VideoAttributes> = { | 970 | const query: Sequelize.FindOptions<VideoAttributes> = { |
951 | where: { | 971 | where: { |
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 0274416b2..dc562c00a 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts | |||
@@ -7,7 +7,7 @@ import { ActivityPubSignature } from './activitypub-signature' | |||
7 | export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag | 7 | export type Activity = ActivityCreate | ActivityUpdate | ActivityFlag |
8 | 8 | ||
9 | // Flag -> report abuse | 9 | // Flag -> report abuse |
10 | export type ActivityType = 'Create' | 'Update' | 'Flag' | 10 | export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' |
11 | 11 | ||
12 | export interface BaseActivity { | 12 | export interface BaseActivity { |
13 | '@context'?: any[] | 13 | '@context'?: any[] |
@@ -20,7 +20,12 @@ export interface BaseActivity { | |||
20 | 20 | ||
21 | export interface ActivityCreate extends BaseActivity { | 21 | export interface ActivityCreate extends BaseActivity { |
22 | type: 'Create' | 22 | type: 'Create' |
23 | object: VideoTorrentObject | VideoChannelObject | 23 | object: VideoChannelObject |
24 | } | ||
25 | |||
26 | export interface ActivityAdd extends BaseActivity { | ||
27 | type: 'Add' | ||
28 | object: VideoTorrentObject | ||
24 | } | 29 | } |
25 | 30 | ||
26 | export interface ActivityUpdate extends BaseActivity { | 31 | export interface ActivityUpdate extends BaseActivity { |
diff --git a/shared/models/activitypub/objects/video-channel-object.ts b/shared/models/activitypub/objects/video-channel-object.ts index d64b4aed8..72efe42b3 100644 --- a/shared/models/activitypub/objects/video-channel-object.ts +++ b/shared/models/activitypub/objects/video-channel-object.ts | |||
@@ -2,7 +2,10 @@ import { ActivityIdentifierObject } from './common-objects' | |||
2 | 2 | ||
3 | export interface VideoChannelObject { | 3 | export interface VideoChannelObject { |
4 | type: 'VideoChannel' | 4 | type: 'VideoChannel' |
5 | id: string | ||
5 | name: string | 6 | name: string |
6 | content: string | 7 | content: string |
7 | uuid: ActivityIdentifierObject | 8 | uuid: string |
9 | published: Date | ||
10 | updated: Date | ||
8 | } | 11 | } |
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts index 00cc0a649..5685a43e0 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-torrent-object.ts | |||
@@ -7,6 +7,7 @@ import { | |||
7 | 7 | ||
8 | export interface VideoTorrentObject { | 8 | export interface VideoTorrentObject { |
9 | type: 'Video' | 9 | type: 'Video' |
10 | id: string | ||
10 | name: string | 11 | name: string |
11 | duration: string | 12 | duration: string |
12 | uuid: string | 13 | uuid: string |