aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/activitypub/client.ts27
-rw-r--r--server/controllers/api/videos/channel.ts4
-rw-r--r--server/helpers/activitypub.ts64
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts6
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts42
-rw-r--r--server/helpers/database-utils.ts8
-rw-r--r--server/initializers/constants.ts12
-rw-r--r--server/lib/activitypub/misc.ts46
-rw-r--r--server/lib/activitypub/process-add.ts22
-rw-r--r--server/lib/activitypub/process-announce.ts29
-rw-r--r--server/lib/activitypub/process-create.ts16
-rw-r--r--server/lib/activitypub/send-request.ts40
-rw-r--r--server/middlewares/validators/video-channels.ts4
-rw-r--r--server/models/video/video-channel.ts3
-rw-r--r--shared/models/activitypub/activity.ts3
16 files changed, 216 insertions, 112 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 76049f496..7b3921770 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -4,10 +4,13 @@ import * as express from 'express'
4import { database as db } from '../../initializers' 4import { database as db } from '../../initializers'
5import { executeIfActivityPub, localAccountValidator } from '../../middlewares' 5import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
6import { pageToStartAndCount } from '../../helpers' 6import { pageToStartAndCount } from '../../helpers'
7import { AccountInstance } from '../../models' 7import { AccountInstance, VideoChannelInstance } from '../../models'
8import { activityPubCollectionPagination } from '../../helpers/activitypub' 8import { activityPubCollectionPagination } from '../../helpers/activitypub'
9import { ACTIVITY_PUB } from '../../initializers/constants' 9import { ACTIVITY_PUB } from '../../initializers/constants'
10import { asyncMiddleware } from '../../middlewares/async' 10import { asyncMiddleware } from '../../middlewares/async'
11import { videosGetValidator } from '../../middlewares/validators/videos'
12import { VideoInstance } from '../../models/video/video-interface'
13import { videoChannelsGetValidator } from '../../middlewares/validators/video-channels'
11 14
12const activityPubClientRouter = express.Router() 15const activityPubClientRouter = express.Router()
13 16
@@ -26,6 +29,16 @@ activityPubClientRouter.get('/account/:name/following',
26 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 29 executeIfActivityPub(asyncMiddleware(accountFollowingController))
27) 30)
28 31
32activityPubClientRouter.get('/videos/watch/:id',
33 executeIfActivityPub(videosGetValidator),
34 executeIfActivityPub(asyncMiddleware(videoController))
35)
36
37activityPubClientRouter.get('/video-channels/:id',
38 executeIfActivityPub(videoChannelsGetValidator),
39 executeIfActivityPub(asyncMiddleware(videoChannelController))
40)
41
29// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
30 43
31export { 44export {
@@ -63,3 +76,15 @@ async function accountFollowingController (req: express.Request, res: express.Re
63 76
64 return res.json(activityPubResult) 77 return res.json(activityPubResult)
65} 78}
79
80async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
81 const video: VideoInstance = res.locals.video
82
83 return res.json(video.toActivityPubObject())
84}
85
86async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const videoChannel: VideoChannelInstance = res.locals.videoChannel
88
89 return res.json(videoChannel.toActivityPubObject())
90}
diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts
index 656bc3129..8f3df2550 100644
--- a/server/controllers/api/videos/channel.ts
+++ b/server/controllers/api/videos/channel.ts
@@ -10,7 +10,7 @@ import {
10 paginationValidator, 10 paginationValidator,
11 setPagination, 11 setPagination,
12 setVideoChannelsSort, 12 setVideoChannelsSort,
13 videoChannelGetValidator, 13 videoChannelsGetValidator,
14 videoChannelsAddValidator, 14 videoChannelsAddValidator,
15 videoChannelsRemoveValidator, 15 videoChannelsRemoveValidator,
16 videoChannelsSortValidator, 16 videoChannelsSortValidator,
@@ -53,7 +53,7 @@ videoChannelRouter.delete('/channels/:id',
53) 53)
54 54
55videoChannelRouter.get('/channels/:id', 55videoChannelRouter.get('/channels/:id',
56 videoChannelGetValidator, 56 videoChannelsGetValidator,
57 asyncMiddleware(getVideoChannel) 57 asyncMiddleware(getVideoChannel)
58) 58)
59 59
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index b376b8ca2..c710117cd 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -4,13 +4,18 @@ import * as Sequelize from 'sequelize'
4import * as url from 'url' 4import * as url from 'url'
5import { ActivityIconObject } from '../../shared/index' 5import { ActivityIconObject } from '../../shared/index'
6import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' 6import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
7import { VideoChannelObject } from '../../shared/models/activitypub/objects/video-channel-object'
7import { ResultList } from '../../shared/models/result-list.model' 8import { ResultList } from '../../shared/models/result-list.model'
8import { database as db, REMOTE_SCHEME } from '../initializers' 9import { database as db, REMOTE_SCHEME } from '../initializers'
9import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants' 10import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
10import { sendAnnounce } from '../lib/activitypub/send-request' 11import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/misc'
12import { sendVideoAnnounce } from '../lib/activitypub/send-request'
13import { sendVideoChannelAnnounce } from '../lib/index'
14import { AccountInstance } from '../models/account/account-interface'
11import { VideoChannelInstance } from '../models/video/video-channel-interface' 15import { VideoChannelInstance } from '../models/video/video-channel-interface'
12import { VideoInstance } from '../models/video/video-interface' 16import { VideoInstance } from '../models/video/video-interface'
13import { isRemoteAccountValid } from './custom-validators' 17import { isRemoteAccountValid } from './custom-validators'
18import { isVideoChannelObjectValid } from './custom-validators/activitypub/videos'
14import { logger } from './logger' 19import { logger } from './logger'
15import { doRequest, doRequestAndSaveToFile } from './requests' 20import { doRequest, doRequestAndSaveToFile } from './requests'
16import { getServerAccount } from './utils' 21import { getServerAccount } from './utils'
@@ -34,7 +39,7 @@ async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t:
34 videoChannelId: videoChannel.id 39 videoChannelId: videoChannel.id
35 }, { transaction: t }) 40 }, { transaction: t })
36 41
37 return sendAnnounce(serverAccount, videoChannel, t) 42 return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
38} 43}
39 44
40async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) { 45async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
@@ -45,7 +50,7 @@ async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transactio
45 videoId: video.id 50 videoId: video.id
46 }, { transaction: t }) 51 }, { transaction: t })
47 52
48 return sendAnnounce(serverAccount, video, t) 53 return sendVideoAnnounce(serverAccount, video, t)
49} 54}
50 55
51function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) { 56function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) {
@@ -66,13 +71,27 @@ async function getOrCreateAccount (accountUrl: string) {
66 if (res === undefined) throw new Error('Cannot fetch remote account.') 71 if (res === undefined) throw new Error('Cannot fetch remote account.')
67 72
68 // Save our new account in database 73 // Save our new account in database
69 const account = res.account 74 account = await res.account.save()
70 await account.save()
71 } 75 }
72 76
73 return account 77 return account
74} 78}
75 79
80async function getOrCreateVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
81 let videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl)
82
83 // We don't have this account in our database, fetch it on remote
84 if (!videoChannel) {
85 videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl)
86 if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.')
87
88 // Save our new video channel in database
89 await videoChannel.save()
90 }
91
92 return videoChannel
93}
94
76async function fetchRemoteAccountAndCreateServer (accountUrl: string) { 95async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
77 const options = { 96 const options = {
78 uri: accountUrl, 97 uri: accountUrl,
@@ -131,6 +150,38 @@ async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
131 return { account, server } 150 return { account, server }
132} 151}
133 152
153async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
154 const options = {
155 uri: videoChannelUrl,
156 method: 'GET',
157 headers: {
158 'Accept': ACTIVITY_PUB_ACCEPT_HEADER
159 }
160 }
161
162 logger.info('Fetching remote video channel %s.', videoChannelUrl)
163
164 let requestResult
165 try {
166 requestResult = await doRequest(options)
167 } catch (err) {
168 logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err)
169 return undefined
170 }
171
172 const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body)
173 if (isVideoChannelObjectValid(videoChannelJSON) === false) {
174 logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON })
175 return undefined
176 }
177
178 const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount)
179 const videoChannel = db.VideoChannel.build(videoChannelAttributes)
180 videoChannel.Account = ownerAccount
181
182 return videoChannel
183}
184
134function fetchRemoteVideoPreview (video: VideoInstance) { 185function fetchRemoteVideoPreview (video: VideoInstance) {
135 // FIXME: use url 186 // FIXME: use url
136 const host = video.VideoChannel.Account.Server.host 187 const host = video.VideoChannel.Account.Server.host
@@ -200,7 +251,8 @@ export {
200 fetchRemoteVideoPreview, 251 fetchRemoteVideoPreview,
201 fetchRemoteVideoDescription, 252 fetchRemoteVideoDescription,
202 shareVideoChannelByServer, 253 shareVideoChannelByServer,
203 shareVideoByServer 254 shareVideoByServer,
255 getOrCreateVideoChannel
204} 256}
205 257
206// --------------------------------------------------------------------------- 258// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index 08e5ae0aa..8084cf7b0 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -2,8 +2,7 @@ import * as validator from 'validator'
2import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' 2import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
3import { isActivityPubUrlValid } from './misc' 3import { isActivityPubUrlValid } from './misc'
4import { 4import {
5 isVideoAnnounceValid, 5 isAnnounceValid,
6 isVideoChannelAnnounceValid,
7 isVideoChannelCreateActivityValid, 6 isVideoChannelCreateActivityValid,
8 isVideoChannelDeleteActivityValid, 7 isVideoChannelDeleteActivityValid,
9 isVideoChannelUpdateActivityValid, 8 isVideoChannelUpdateActivityValid,
@@ -37,8 +36,7 @@ function isActivityValid (activity: any) {
37 isAccountFollowActivityValid(activity) || 36 isAccountFollowActivityValid(activity) ||
38 isAccountAcceptActivityValid(activity) || 37 isAccountAcceptActivityValid(activity) ||
39 isVideoFlagValid(activity) || 38 isVideoFlagValid(activity) ||
40 isVideoAnnounceValid(activity) || 39 isAnnounceValid(activity)
41 isVideoChannelAnnounceValid(activity)
42} 40}
43 41
44// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 665a63a73..f09a764b6 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -21,7 +21,7 @@ function isActivityPubUrlValid (url: string) {
21} 21}
22 22
23function isBaseActivityValid (activity: any, type: string) { 23function isBaseActivityValid (activity: any, type: string) {
24 return Array.isArray(activity['@context']) && 24 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) &&
25 activity.type === type && 25 activity.type === type &&
26 isActivityPubUrlValid(activity.id) && 26 isActivityPubUrlValid(activity.id) &&
27 isActivityPubUrlValid(activity.actor) && 27 isActivityPubUrlValid(activity.actor) &&
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 89c49b0df..8486297ad 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -39,6 +39,7 @@ function isActivityPubVideoDurationValid (value: string) {
39 39
40function isVideoTorrentObjectValid (video: any) { 40function isVideoTorrentObjectValid (video: any) {
41 return video.type === 'Video' && 41 return video.type === 'Video' &&
42 isActivityPubUrlValid(video.id) &&
42 isVideoNameValid(video.name) && 43 isVideoNameValid(video.name) &&
43 isActivityPubVideoDurationValid(video.duration) && 44 isActivityPubVideoDurationValid(video.duration) &&
44 isUUIDValid(video.uuid) && 45 isUUIDValid(video.uuid) &&
@@ -62,14 +63,12 @@ function isVideoFlagValid (activity: any) {
62 isActivityPubUrlValid(activity.object) 63 isActivityPubUrlValid(activity.object)
63} 64}
64 65
65function isVideoAnnounceValid (activity: any) { 66function isAnnounceValid (activity: any) {
66 return isBaseActivityValid(activity, 'Announce') && 67 return isBaseActivityValid(activity, 'Announce') &&
67 isVideoTorrentObjectValid(activity.object) 68 (
68} 69 isVideoChannelCreateActivityValid(activity.object) ||
69 70 isVideoTorrentAddActivityValid(activity.object)
70function isVideoChannelAnnounceValid (activity: any) { 71 )
71 return isBaseActivityValid(activity, 'Announce') &&
72 isVideoChannelObjectValid(activity.object)
73} 72}
74 73
75function isVideoChannelCreateActivityValid (activity: any) { 74function isVideoChannelCreateActivityValid (activity: any) {
@@ -88,8 +87,11 @@ function isVideoChannelDeleteActivityValid (activity: any) {
88 87
89function isVideoChannelObjectValid (videoChannel: any) { 88function isVideoChannelObjectValid (videoChannel: any) {
90 return videoChannel.type === 'VideoChannel' && 89 return videoChannel.type === 'VideoChannel' &&
90 isActivityPubUrlValid(videoChannel.id) &&
91 isVideoChannelNameValid(videoChannel.name) && 91 isVideoChannelNameValid(videoChannel.name) &&
92 isVideoChannelDescriptionValid(videoChannel.description) && 92 isVideoChannelDescriptionValid(videoChannel.content) &&
93 isDateValid(videoChannel.published) &&
94 isDateValid(videoChannel.updated) &&
93 isUUIDValid(videoChannel.uuid) 95 isUUIDValid(videoChannel.uuid)
94} 96}
95 97
@@ -103,8 +105,8 @@ export {
103 isVideoChannelDeleteActivityValid, 105 isVideoChannelDeleteActivityValid,
104 isVideoTorrentDeleteActivityValid, 106 isVideoTorrentDeleteActivityValid,
105 isVideoFlagValid, 107 isVideoFlagValid,
106 isVideoAnnounceValid, 108 isAnnounceValid,
107 isVideoChannelAnnounceValid 109 isVideoChannelObjectValid
108} 110}
109 111
110// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
@@ -148,8 +150,20 @@ function setValidRemoteVideoUrls (video: any) {
148 150
149function isRemoteVideoUrlValid (url: any) { 151function isRemoteVideoUrlValid (url: any) {
150 return url.type === 'Link' && 152 return url.type === 'Link' &&
151 ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 && 153 (
152 isVideoUrlValid(url.url) && 154 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 &&
153 validator.isInt(url.width + '', { min: 0 }) && 155 isVideoUrlValid(url.url) &&
154 validator.isInt(url.size + '', { min: 0 }) 156 validator.isInt(url.width + '', { min: 0 }) &&
157 validator.isInt(url.size + '', { min: 0 })
158 ) ||
159 (
160 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 &&
161 isVideoUrlValid(url.url) &&
162 validator.isInt(url.width + '', { min: 0 })
163 ) ||
164 (
165 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 &&
166 validator.isLength(url.url, { min: 5 }) &&
167 validator.isInt(url.width + '', { min: 0 })
168 )
155} 169}
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts
index d62462d35..169b80065 100644
--- a/server/helpers/database-utils.ts
+++ b/server/helpers/database-utils.ts
@@ -1,10 +1,10 @@
1// TODO: import from ES6 when retry typing file will include errorFilter function 1// TODO: import from ES6 when retry typing file will include errorFilter function
2import * as retry from 'async/retry' 2import * as retry from 'async/retry'
3 3import * as Bluebird from 'bluebird'
4import { logger } from './logger' 4import { logger } from './logger'
5 5
6type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] } 6type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
7function retryTransactionWrapper (functionToRetry: (...args) => Promise<any>, options: RetryTransactionWrapperOptions) { 7function retryTransactionWrapper (functionToRetry: (...args) => Promise<any> | Bluebird<any>, options: RetryTransactionWrapperOptions) {
8 const args = options.arguments ? options.arguments : [] 8 const args = options.arguments ? options.arguments : []
9 9
10 return transactionRetryer(callback => { 10 return transactionRetryer(callback => {
@@ -13,8 +13,8 @@ function retryTransactionWrapper (functionToRetry: (...args) => Promise<any>, op
13 .catch(err => callback(err)) 13 .catch(err => callback(err))
14 }) 14 })
15 .catch(err => { 15 .catch(err => {
16 // Do not throw the error, continue the process
17 logger.error(options.errorMessage, err) 16 logger.error(options.errorMessage, err)
17 throw err
18 }) 18 })
19} 19}
20 20
@@ -28,7 +28,7 @@ function transactionRetryer (func: Function) {
28 logger.debug('Maybe retrying the transaction function.', { willRetry }) 28 logger.debug('Maybe retrying the transaction function.', { willRetry })
29 return willRetry 29 return willRetry
30 } 30 }
31 }, func, err => err ? rej(err) : res()) 31 }, func, (err, data) => err ? rej(err) : res(data))
32 }) 32 })
33} 33}
34 34
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index dca223370..eeda8347d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -227,13 +227,11 @@ const ACTIVITY_PUB_ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3
227 227
228const ACTIVITY_PUB = { 228const ACTIVITY_PUB = {
229 COLLECTION_ITEMS_PER_PAGE: 10, 229 COLLECTION_ITEMS_PER_PAGE: 10,
230 VIDEO_URL_MIME_TYPES: [ 230 URL_MIME_TYPES: {
231 'video/mp4', 231 VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
232 'video/webm', 232 TORRENT: [ 'application/x-bittorrent' ],
233 'video/ogg', 233 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
234 'application/x-bittorrent', 234 }
235 'application/x-bittorrent;x-scheme-handler/magnet'
236 ]
237} 235}
238 236
239// --------------------------------------------------------------------------- 237// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts
index 43d26c328..13838fc4c 100644
--- a/server/lib/activitypub/misc.ts
+++ b/server/lib/activitypub/misc.ts
@@ -7,6 +7,21 @@ import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants'
7import { VideoChannelInstance } from '../../models/video/video-channel-interface' 7import { VideoChannelInstance } from '../../models/video/video-channel-interface'
8import { VideoFileAttributes } from '../../models/video/video-file-interface' 8import { VideoFileAttributes } from '../../models/video/video-file-interface'
9import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' 9import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
10import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
11import { AccountInstance } from '../../models/account/account-interface'
12
13function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
14 return {
15 name: videoChannelObject.name,
16 description: videoChannelObject.content,
17 uuid: videoChannelObject.uuid,
18 url: videoChannelObject.id,
19 createdAt: new Date(videoChannelObject.published),
20 updatedAt: new Date(videoChannelObject.updated),
21 remote: true,
22 accountId: account.id
23 }
24}
10 25
11async function videoActivityObjectToDBAttributes ( 26async function videoActivityObjectToDBAttributes (
12 videoChannel: VideoChannelInstance, 27 videoChannel: VideoChannelInstance,
@@ -45,26 +60,32 @@ async function videoActivityObjectToDBAttributes (
45} 60}
46 61
47function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { 62function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
48 const fileUrls = videoObject.url 63 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
49 .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1 && u.url.startsWith('video/')) 64 const fileUrls = videoObject.url.filter(u => {
65 return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/')
66 })
67
68 if (fileUrls.length === 0) {
69 throw new Error('Cannot find video files for ' + videoCreated.url)
70 }
50 71
51 const attributes: VideoFileAttributes[] = [] 72 const attributes: VideoFileAttributes[] = []
52 for (const url of fileUrls) { 73 for (const fileUrl of fileUrls) {
53 // Fetch associated magnet uri 74 // Fetch associated magnet uri
54 const magnet = videoObject.url 75 const magnet = videoObject.url.find(u => {
55 .find(u => { 76 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width
56 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width 77 })
57 }) 78
58 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url) 79 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url)
59 80
60 const parsed = magnetUtil.decode(magnet.url) 81 const parsed = magnetUtil.decode(magnet.url)
61 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) 82 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
62 83
63 const attribute = { 84 const attribute = {
64 extname: VIDEO_MIMETYPE_EXT[url.mimeType], 85 extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType],
65 infoHash: parsed.infoHash, 86 infoHash: parsed.infoHash,
66 resolution: url.width, 87 resolution: fileUrl.width,
67 size: url.size, 88 size: fileUrl.size,
68 videoId: videoCreated.id 89 videoId: videoCreated.id
69 } 90 }
70 attributes.push(attribute) 91 attributes.push(attribute)
@@ -77,5 +98,6 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoO
77 98
78export { 99export {
79 videoFileActivityUrlToDBAttributes, 100 videoFileActivityUrlToDBAttributes,
80 videoActivityObjectToDBAttributes 101 videoActivityObjectToDBAttributes,
102 videoChannelActivityObjectToDBAttributes
81} 103}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
index 98e414dbb..df7139d46 100644
--- a/server/lib/activitypub/process-add.ts
+++ b/server/lib/activitypub/process-add.ts
@@ -5,6 +5,8 @@ import { database as db } from '../../initializers'
5import { AccountInstance } from '../../models/account/account-interface' 5import { AccountInstance } from '../../models/account/account-interface'
6import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' 6import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
7import Bluebird = require('bluebird') 7import Bluebird = require('bluebird')
8import { getOrCreateVideoChannel } from '../../helpers/activitypub'
9import { VideoChannelInstance } from '../../models/video/video-channel-interface'
8 10
9async function processAddActivity (activity: ActivityAdd) { 11async function processAddActivity (activity: ActivityAdd) {
10 const activityObject = activity.object 12 const activityObject = activity.object
@@ -12,7 +14,10 @@ async function processAddActivity (activity: ActivityAdd) {
12 const account = await getOrCreateAccount(activity.actor) 14 const account = await getOrCreateAccount(activity.actor)
13 15
14 if (activityType === 'Video') { 16 if (activityType === 'Video') {
15 return processAddVideo(account, activity.id, activityObject as VideoTorrentObject) 17 const videoChannelUrl = activity.target
18 const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
19
20 return processAddVideo(account, videoChannel, activityObject as VideoTorrentObject)
16 } 21 }
17 22
18 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 23 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@@ -27,16 +32,16 @@ export {
27 32
28// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
29 34
30function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { 35function processAddVideo (account: AccountInstance, videoChannel: VideoChannelInstance, video: VideoTorrentObject) {
31 const options = { 36 const options = {
32 arguments: [ account, videoChannelUrl, video ], 37 arguments: [ account, videoChannel, video ],
33 errorMessage: 'Cannot insert the remote video with many retries.' 38 errorMessage: 'Cannot insert the remote video with many retries.'
34 } 39 }
35 40
36 return retryTransactionWrapper(addRemoteVideo, options) 41 return retryTransactionWrapper(addRemoteVideo, options)
37} 42}
38 43
39async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) { 44function addRemoteVideo (account: AccountInstance, videoChannel: VideoChannelInstance, videoToCreateData: VideoTorrentObject) {
40 logger.debug('Adding remote video %s.', videoToCreateData.url) 45 logger.debug('Adding remote video %s.', videoToCreateData.url)
41 46
42 return db.sequelize.transaction(async t => { 47 return db.sequelize.transaction(async t => {
@@ -44,9 +49,6 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string
44 transaction: t 49 transaction: t
45 } 50 }
46 51
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.') 52 if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
51 53
52 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t) 54 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t)
@@ -59,8 +61,11 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string
59 const videoCreated = await video.save(sequelizeOptions) 61 const videoCreated = await video.save(sequelizeOptions)
60 62
61 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) 63 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
64 if (videoFileAttributes.length === 0) {
65 throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
66 }
62 67
63 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f)) 68 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t }))
64 await Promise.all(tasks) 69 await Promise.all(tasks)
65 70
66 const tags = videoToCreateData.tag.map(t => t.name) 71 const tags = videoToCreateData.tag.map(t => t.name)
@@ -71,5 +76,4 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string
71 76
72 return videoCreated 77 return videoCreated
73 }) 78 })
74
75} 79}
diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process-announce.ts
index d67958aec..f9674e095 100644
--- a/server/lib/activitypub/process-announce.ts
+++ b/server/lib/activitypub/process-announce.ts
@@ -10,38 +10,33 @@ import { VideoChannelInstance } from '../../models/video/video-channel-interface
10import { VideoInstance } from '../../models/index' 10import { VideoInstance } from '../../models/index'
11 11
12async function processAnnounceActivity (activity: ActivityAnnounce) { 12async function processAnnounceActivity (activity: ActivityAnnounce) {
13 const activityType = activity.object.type 13 const announcedActivity = activity.object
14 const accountAnnouncer = await getOrCreateAccount(activity.actor) 14 const accountAnnouncer = await getOrCreateAccount(activity.actor)
15 15
16 if (activityType === 'VideoChannel') { 16 if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
17 const activityCreate = Object.assign(activity, {
18 type: 'Create' as 'Create',
19 actor: activity.object.actor,
20 object: activity.object as VideoChannelObject
21 })
22
23 // Add share entry 17 // Add share entry
24 const videoChannel: VideoChannelInstance = await processCreateActivity(activityCreate) 18 const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
25 await db.VideoChannelShare.create({ 19 await db.VideoChannelShare.create({
26 accountId: accountAnnouncer.id, 20 accountId: accountAnnouncer.id,
27 videoChannelId: videoChannel.id 21 videoChannelId: videoChannel.id
28 }) 22 })
29 } else if (activityType === 'Video') {
30 const activityAdd = Object.assign(activity, {
31 type: 'Add' as 'Add',
32 actor: activity.object.actor,
33 object: activity.object as VideoTorrentObject
34 })
35 23
24 return undefined
25 } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
36 // Add share entry 26 // Add share entry
37 const video: VideoInstance = await processAddActivity(activityAdd) 27 const video: VideoInstance = await processAddActivity(announcedActivity)
38 await db.VideoShare.create({ 28 await db.VideoShare.create({
39 accountId: accountAnnouncer.id, 29 accountId: accountAnnouncer.id,
40 videoId: video.id 30 videoId: video.id
41 }) 31 })
32
33 return undefined
42 } 34 }
43 35
44 logger.warn('Unknown activity object type %s when announcing activity.', activityType, { activity: activity.id }) 36 logger.warn(
37 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type,
38 { activity: activity.id }
39 )
45 return Promise.resolve(undefined) 40 return Promise.resolve(undefined)
46} 41}
47 42
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
index 4e4c9f703..c4706a66b 100644
--- a/server/lib/activitypub/process-create.ts
+++ b/server/lib/activitypub/process-create.ts
@@ -4,6 +4,7 @@ import { logger, retryTransactionWrapper } from '../../helpers'
4import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' 4import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
5import { database as db } from '../../initializers' 5import { database as db } from '../../initializers'
6import { AccountInstance } from '../../models/account/account-interface' 6import { AccountInstance } from '../../models/account/account-interface'
7import { videoChannelActivityObjectToDBAttributes } from './misc'
7 8
8async function processCreateActivity (activity: ActivityCreate) { 9async function processCreateActivity (activity: ActivityCreate) {
9 const activityObject = activity.object 10 const activityObject = activity.object
@@ -37,23 +38,14 @@ function processCreateVideoChannel (account: AccountInstance, videoChannelToCrea
37 return retryTransactionWrapper(addRemoteVideoChannel, options) 38 return retryTransactionWrapper(addRemoteVideoChannel, options)
38} 39}
39 40
40async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { 41function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
41 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) 42 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
42 43
43 return db.sequelize.transaction(async t => { 44 return db.sequelize.transaction(async t => {
44 let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) 45 let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
45 if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') 46 if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
46 47
47 const videoChannelData = { 48 const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
48 name: videoChannelToCreateData.name,
49 description: videoChannelToCreateData.content,
50 uuid: videoChannelToCreateData.uuid,
51 createdAt: new Date(videoChannelToCreateData.published),
52 updatedAt: new Date(videoChannelToCreateData.updated),
53 remote: true,
54 accountId: account.id
55 }
56
57 videoChannel = db.VideoChannel.build(videoChannelData) 49 videoChannel = db.VideoChannel.build(videoChannelData)
58 videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) 50 videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
59 51
@@ -73,7 +65,7 @@ function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateDa
73 return retryTransactionWrapper(addRemoteVideoAbuse, options) 65 return retryTransactionWrapper(addRemoteVideoAbuse, options)
74} 66}
75 67
76async function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { 68function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
77 logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) 69 logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
78 70
79 return db.sequelize.transaction(async t => { 71 return db.sequelize.transaction(async t => {
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
index 664b9d826..f9b72f2a8 100644
--- a/server/lib/activitypub/send-request.ts
+++ b/server/lib/activitypub/send-request.ts
@@ -59,24 +59,21 @@ async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transac
59 return broadcastToFollowers(data, [ account ], t) 59 return broadcastToFollowers(data, [ account ], t)
60} 60}
61 61
62async function sendAnnounce (byAccount: AccountInstance, instance: VideoInstance | VideoChannelInstance, t: Sequelize.Transaction) { 62async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
63 const object = instance.toActivityPubObject() 63 const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce'
64 64 const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), true)
65 let url = '' 65
66 let objectActorUrl: string 66 const data = await announceActivityData(url, byAccount, announcedActivity)
67 if ((instance as any).VideoChannel !== undefined) { 67 return broadcastToFollowers(data, [ byAccount ], t)
68 objectActorUrl = (instance as VideoInstance).VideoChannel.Account.url 68}
69 url = getActivityPubUrl('video', instance.uuid) + '#announce'
70 } else {
71 objectActorUrl = (instance as VideoChannelInstance).Account.url
72 url = getActivityPubUrl('videoChannel', instance.uuid) + '#announce'
73 }
74 69
75 const objectWithActor = Object.assign(object, { 70async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Sequelize.Transaction) {
76 actor: objectActorUrl 71 const url = getActivityPubUrl('video', video.uuid) + '#announce'
77 })
78 72
79 const data = await announceActivityData(url, byAccount, objectWithActor) 73 const videoChannel = video.VideoChannel
74 const announcedActivity = await addActivityData(url, videoChannel.Account, videoChannel.url, video.toActivityPubObject(), true)
75
76 const data = await announceActivityData(url, byAccount, announcedActivity)
80 return broadcastToFollowers(data, [ byAccount ], t) 77 return broadcastToFollowers(data, [ byAccount ], t)
81} 78}
82 79
@@ -117,7 +114,8 @@ export {
117 sendAccept, 114 sendAccept,
118 sendFollow, 115 sendFollow,
119 sendVideoAbuse, 116 sendVideoAbuse,
120 sendAnnounce 117 sendVideoChannelAnnounce,
118 sendVideoAnnounce
121} 119}
122 120
123// --------------------------------------------------------------------------- 121// ---------------------------------------------------------------------------
@@ -159,7 +157,7 @@ async function getPublicActivityTo (account: AccountInstance) {
159 return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public') 157 return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public')
160} 158}
161 159
162async function createActivityData (url: string, byAccount: AccountInstance, object: any) { 160async function createActivityData (url: string, byAccount: AccountInstance, object: any, raw = false) {
163 const to = await getPublicActivityTo(byAccount) 161 const to = await getPublicActivityTo(byAccount)
164 const base = { 162 const base = {
165 type: 'Create', 163 type: 'Create',
@@ -169,6 +167,8 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje
169 object 167 object
170 } 168 }
171 169
170 if (raw === true) return base
171
172 return buildSignedActivity(byAccount, base) 172 return buildSignedActivity(byAccount, base)
173} 173}
174 174
@@ -195,7 +195,7 @@ async function deleteActivityData (url: string, byAccount: AccountInstance) {
195 return buildSignedActivity(byAccount, base) 195 return buildSignedActivity(byAccount, base)
196} 196}
197 197
198async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) { 198async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any, raw = false) {
199 const to = await getPublicActivityTo(byAccount) 199 const to = await getPublicActivityTo(byAccount)
200 const base = { 200 const base = {
201 type: 'Add', 201 type: 'Add',
@@ -206,6 +206,8 @@ async function addActivityData (url: string, byAccount: AccountInstance, target:
206 target 206 target
207 } 207 }
208 208
209 if (raw === true) return base
210
209 return buildSignedActivity(byAccount, base) 211 return buildSignedActivity(byAccount, base)
210} 212}
211 213
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts
index f0ead24e3..0326e05b9 100644
--- a/server/middlewares/validators/video-channels.ts
+++ b/server/middlewares/validators/video-channels.ts
@@ -83,7 +83,7 @@ const videoChannelsRemoveValidator = [
83 } 83 }
84] 84]
85 85
86const videoChannelGetValidator = [ 86const videoChannelsGetValidator = [
87 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 87 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
88 88
89 (req: express.Request, res: express.Response, next: express.NextFunction) => { 89 (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -102,7 +102,7 @@ export {
102 videoChannelsAddValidator, 102 videoChannelsAddValidator,
103 videoChannelsUpdateValidator, 103 videoChannelsUpdateValidator,
104 videoChannelsRemoveValidator, 104 videoChannelsRemoveValidator,
105 videoChannelGetValidator 105 videoChannelsGetValidator
106} 106}
107 107
108// --------------------------------------------------------------------------- 108// ---------------------------------------------------------------------------
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 1f4604f1d..f8414d4a8 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -264,7 +264,8 @@ loadByUrl = function (url: string, t?: Sequelize.Transaction) {
264 const query: Sequelize.FindOptions<VideoChannelAttributes> = { 264 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
265 where: { 265 where: {
266 url 266 url
267 } 267 },
268 include: [ VideoChannel['sequelize'].models.Account ]
268 } 269 }
269 270
270 if (t !== undefined) query.transaction = t 271 if (t !== undefined) query.transaction = t
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index b858bf759..f8e982fbb 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -24,6 +24,7 @@ export interface ActivityCreate extends BaseActivity {
24 24
25export interface ActivityAdd extends BaseActivity { 25export interface ActivityAdd extends BaseActivity {
26 type: 'Add' 26 type: 'Add'
27 target: string
27 object: VideoTorrentObject 28 object: VideoTorrentObject
28} 29}
29 30
@@ -52,5 +53,5 @@ export interface ActivityAccept extends BaseActivity {
52 53
53export interface ActivityAnnounce extends BaseActivity { 54export interface ActivityAnnounce extends BaseActivity {
54 type: 'Announce' 55 type: 'Announce'
55 object: VideoChannelObject | VideoTorrentObject 56 object: ActivityCreate | ActivityAdd
56} 57}