aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/index.ts25
-rw-r--r--server/helpers/custom-validators/videos.ts16
-rw-r--r--server/lib/activitypub/actor.ts2
-rw-r--r--server/lib/activitypub/process/process-undo.ts25
-rw-r--r--server/lib/activitypub/process/process-update.ts10
-rw-r--r--server/lib/activitypub/send/send-undo.ts29
-rw-r--r--server/lib/activitypub/share.ts45
-rw-r--r--server/lib/activitypub/videos.ts13
-rw-r--r--server/middlewares/validators/videos.ts27
-rw-r--r--server/models/video/video-share.ts9
-rw-r--r--server/models/video/video.ts37
-rw-r--r--server/tests/api/videos/video-channels.ts78
-rw-r--r--server/tests/utils/videos/videos.ts15
13 files changed, 259 insertions, 72 deletions
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 4b3198a74..c07430e6c 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -19,7 +19,12 @@ import {
19 VIDEO_MIMETYPE_EXT, 19 VIDEO_MIMETYPE_EXT,
20 VIDEO_PRIVACIES 20 VIDEO_PRIVACIES
21} from '../../../initializers' 21} from '../../../initializers'
22import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' 22import {
23 changeVideoChannelShare,
24 fetchRemoteVideoDescription,
25 getVideoActivityPubUrl,
26 shareVideoByServerAndChannel
27} from '../../../lib/activitypub'
23import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' 28import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
24import { JobQueue } from '../../../lib/job-queue' 29import { JobQueue } from '../../../lib/job-queue'
25import { Redis } from '../../../lib/redis' 30import { Redis } from '../../../lib/redis'
@@ -305,6 +310,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
305 const sequelizeOptions = { 310 const sequelizeOptions = {
306 transaction: t 311 transaction: t
307 } 312 }
313 const oldVideoChannel = videoInstance.VideoChannel
308 314
309 if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) 315 if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name)
310 if (videoInfoToUpdate.category !== undefined) videoInstance.set('category', videoInfoToUpdate.category) 316 if (videoInfoToUpdate.category !== undefined) videoInstance.set('category', videoInfoToUpdate.category)
@@ -325,18 +331,25 @@ async function updateVideo (req: express.Request, res: express.Response) {
325 331
326 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 332 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
327 333
334 // Video tags update?
328 if (videoInfoToUpdate.tags) { 335 if (videoInfoToUpdate.tags) {
329 const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t) 336 const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
330 337
331 await videoInstance.$set('Tags', tagInstances, sequelizeOptions) 338 await videoInstanceUpdated.$set('Tags', tagInstances, sequelizeOptions)
332 videoInstance.Tags = tagInstances 339 videoInstanceUpdated.Tags = tagInstances
333 } 340 }
334 341
335 // Now we'll update the video's meta data to our friends 342 // Video channel update?
336 if (wasPrivateVideo === false) { 343 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
337 await sendUpdateVideo(videoInstanceUpdated, t) 344 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel)
345 videoInstance.VideoChannel = res.locals.videoChannel
346
347 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
338 } 348 }
339 349
350 // Now we'll update the video's meta data to our friends
351 if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t)
352
340 // Video is not private anymore, send a create action to remote servers 353 // Video is not private anymore, send a create action to remote servers
341 if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) { 354 if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
342 await sendCreateVideo(videoInstanceUpdated, t) 355 await sendCreateVideo(videoInstanceUpdated, t)
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 23d2d8ac6..c35db49ac 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -14,6 +14,7 @@ import {
14} from '../../initializers' 14} from '../../initializers'
15import { VideoModel } from '../../models/video/video' 15import { VideoModel } from '../../models/video/video'
16import { exists, isArray, isFileValid } from './misc' 16import { exists, isArray, isFileValid } from './misc'
17import { VideoChannelModel } from '../../models/video/video-channel'
17 18
18const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 19const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
19const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 20const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
@@ -124,6 +125,20 @@ async function isVideoExist (id: string, res: Response) {
124 return true 125 return true
125} 126}
126 127
128async function isVideoChannelOfAccountExist (channelId: number, accountId: number, res: Response) {
129 const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, accountId)
130 if (!videoChannel) {
131 res.status(400)
132 .json({ error: 'Unknown video video channel for this account.' })
133 .end()
134
135 return false
136 }
137
138 res.locals.videoChannel = videoChannel
139 return true
140}
141
127// --------------------------------------------------------------------------- 142// ---------------------------------------------------------------------------
128 143
129export { 144export {
@@ -146,5 +161,6 @@ export {
146 isVideoFileSizeValid, 161 isVideoFileSizeValid,
147 isVideoExist, 162 isVideoExist,
148 isVideoImage, 163 isVideoImage,
164 isVideoChannelOfAccountExist,
149 isVideoSupportValid 165 isVideoSupportValid
150} 166}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index b0cf9bb17..5773fc34f 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -353,7 +353,7 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
353 return videoChannelCreated 353 return videoChannelCreated
354} 354}
355 355
356async function refreshActorIfNeeded (actor: ActorModel) { 356async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
357 if (!actor.isOutdated()) return actor 357 if (!actor.isOutdated()) return actor
358 358
359 try { 359 try {
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 565e70289..9b024d15f 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -1,4 +1,4 @@
1import { ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' 1import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { getActorUrl } from '../../../helpers/activitypub' 3import { getActorUrl } from '../../../helpers/activitypub'
4import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -10,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
10import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 10import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
11import { forwardActivity } from '../send/misc' 11import { forwardActivity } from '../send/misc'
12import { getOrCreateAccountAndVideoAndChannel } from '../videos' 12import { getOrCreateAccountAndVideoAndChannel } from '../videos'
13import { VideoShareModel } from '../../../models/video/video-share'
13 14
14async function processUndoActivity (activity: ActivityUndo) { 15async function processUndoActivity (activity: ActivityUndo) {
15 const activityToUndo = activity.object 16 const activityToUndo = activity.object
@@ -22,6 +23,8 @@ async function processUndoActivity (activity: ActivityUndo) {
22 return processUndoDislike(actorUrl, activity) 23 return processUndoDislike(actorUrl, activity)
23 } else if (activityToUndo.type === 'Follow') { 24 } else if (activityToUndo.type === 'Follow') {
24 return processUndoFollow(actorUrl, activityToUndo) 25 return processUndoFollow(actorUrl, activityToUndo)
26 } else if (activityToUndo.type === 'Announce') {
27 return processUndoAnnounce(actorUrl, activityToUndo)
25 } 28 }
26 29
27 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) 30 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
@@ -123,3 +126,23 @@ function undoFollow (actorUrl: string, followActivity: ActivityFollow) {
123 return undefined 126 return undefined
124 }) 127 })
125} 128}
129
130function processUndoAnnounce (actorUrl: string, announceActivity: ActivityAnnounce) {
131 const options = {
132 arguments: [ actorUrl, announceActivity ],
133 errorMessage: 'Cannot undo announce with many retries.'
134 }
135
136 return retryTransactionWrapper(undoAnnounce, options)
137}
138
139function undoAnnounce (actorUrl: string, announceActivity: ActivityAnnounce) {
140 return sequelizeTypescript.transaction(async t => {
141 const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
142 if (!share) throw new Error(`'Unknown video share ${announceActivity.id}.`)
143
144 await share.destroy({ transaction: t })
145
146 return undefined
147 })
148}
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 51e3cc4e3..0dd657c2b 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -14,7 +14,7 @@ import { VideoFileModel } from '../../../models/video/video-file'
14import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' 14import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
15import { 15import {
16 generateThumbnailFromUrl, 16 generateThumbnailFromUrl,
17 getOrCreateAccountAndVideoAndChannel, 17 getOrCreateAccountAndVideoAndChannel, getOrCreateVideoChannel,
18 videoActivityObjectToDBAttributes, 18 videoActivityObjectToDBAttributes,
19 videoFileActivityUrlToDBAttributes 19 videoFileActivityUrlToDBAttributes
20} from '../videos' 20} from '../videos'
@@ -54,6 +54,10 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
54 54
55 const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id) 55 const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id)
56 56
57 // Fetch video channel outside the transaction
58 const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate)
59 const newVideoChannel = newVideoChannelActor.VideoChannel
60
57 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 61 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
58 let videoInstance = res.video 62 let videoInstance = res.video
59 let videoFieldsSave: any 63 let videoFieldsSave: any
@@ -66,12 +70,13 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
66 70
67 videoFieldsSave = videoInstance.toJSON() 71 videoFieldsSave = videoInstance.toJSON()
68 72
73 // Check actor has the right to update the video
69 const videoChannel = videoInstance.VideoChannel 74 const videoChannel = videoInstance.VideoChannel
70 if (videoChannel.Account.Actor.id !== actor.id) { 75 if (videoChannel.Account.Actor.id !== actor.id) {
71 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) 76 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
72 } 77 }
73 78
74 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoAttributesToUpdate, activity.to) 79 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to)
75 videoInstance.set('name', videoData.name) 80 videoInstance.set('name', videoData.name)
76 videoInstance.set('uuid', videoData.uuid) 81 videoInstance.set('uuid', videoData.uuid)
77 videoInstance.set('url', videoData.url) 82 videoInstance.set('url', videoData.url)
@@ -87,6 +92,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
87 videoInstance.set('updatedAt', videoData.updatedAt) 92 videoInstance.set('updatedAt', videoData.updatedAt)
88 videoInstance.set('views', videoData.views) 93 videoInstance.set('views', videoData.views)
89 videoInstance.set('privacy', videoData.privacy) 94 videoInstance.set('privacy', videoData.privacy)
95 videoInstance.set('channelId', videoData.channelId)
90 96
91 await videoInstance.save(sequelizeOptions) 97 await videoInstance.save(sequelizeOptions)
92 98
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index bd49d452e..adee2192f 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -1,5 +1,12 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' 2import {
3 ActivityAnnounce,
4 ActivityAudience,
5 ActivityCreate,
6 ActivityFollow,
7 ActivityLike,
8 ActivityUndo
9} from '../../../../shared/models/activitypub'
3import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
4import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 11import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
5import { VideoModel } from '../../../models/video/video' 12import { VideoModel } from '../../../models/video/video'
@@ -16,6 +23,8 @@ import {
16import { createActivityData, createDislikeActivityData } from './send-create' 23import { createActivityData, createDislikeActivityData } from './send-create'
17import { followActivityData } from './send-follow' 24import { followActivityData } from './send-follow'
18import { likeActivityData } from './send-like' 25import { likeActivityData } from './send-like'
26import { VideoShareModel } from '../../../models/video/video-share'
27import { buildVideoAnnounce } from './send-announce'
19 28
20async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { 29async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
21 const me = actorFollow.ActorFollower 30 const me = actorFollow.ActorFollower
@@ -58,7 +67,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
58 67
59 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 68 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
60 const dislikeActivity = createDislikeActivityData(byActor, video) 69 const dislikeActivity = createDislikeActivityData(byActor, video)
61 const object = await createActivityData(undoUrl, byActor, dislikeActivity, t) 70 const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t)
62 71
63 if (video.isOwned() === false) { 72 if (video.isOwned() === false) {
64 const audience = getOriginVideoAudience(video, actorsInvolvedInVideo) 73 const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
@@ -73,12 +82,24 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
73 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 82 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
74} 83}
75 84
85async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
86 const undoUrl = getUndoActivityPubUrl(videoShare.url)
87
88 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
89 const object = await buildVideoAnnounce(byActor, videoShare, video, t)
90 const data = await undoActivityData(undoUrl, byActor, object, t)
91
92 const followersException = [ byActor ]
93 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
94}
95
76// --------------------------------------------------------------------------- 96// ---------------------------------------------------------------------------
77 97
78export { 98export {
79 sendUndoFollow, 99 sendUndoFollow,
80 sendUndoLike, 100 sendUndoLike,
81 sendUndoDislike 101 sendUndoDislike,
102 sendUndoAnnounce
82} 103}
83 104
84// --------------------------------------------------------------------------- 105// ---------------------------------------------------------------------------
@@ -86,7 +107,7 @@ export {
86async function undoActivityData ( 107async function undoActivityData (
87 url: string, 108 url: string,
88 byActor: ActorModel, 109 byActor: ActorModel,
89 object: ActivityFollow | ActivityLike | ActivityCreate, 110 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
90 t: Transaction, 111 t: Transaction,
91 audience?: ActivityAudience 112 audience?: ActivityAudience
92): Promise<ActivityUndo> { 113): Promise<ActivityUndo> {
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index f256f8d21..698414867 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -3,16 +3,37 @@ import { VideoPrivacy } from '../../../shared/models/videos'
3import { getServerActor } from '../../helpers/utils' 3import { getServerActor } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { VideoShareModel } from '../../models/video/video-share' 5import { VideoShareModel } from '../../models/video/video-share'
6import { sendVideoAnnounce } from './send' 6import { sendUndoAnnounce, sendVideoAnnounce } from './send'
7import { getAnnounceActivityPubUrl } from './url' 7import { getAnnounceActivityPubUrl } from './url'
8import { VideoChannelModel } from '../../models/video/video-channel'
8 9
9async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 10async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
10 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 11 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
11 12
13 return Promise.all([
14 shareByServer(video, t),
15 shareByVideoChannel(video, t)
16 ])
17}
18
19async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
20 await undoShareByVideoChannel(video, oldVideoChannel, t)
21
22 await shareByVideoChannel(video, t)
23}
24
25export {
26 changeVideoChannelShare,
27 shareVideoByServerAndChannel
28}
29
30// ---------------------------------------------------------------------------
31
32async function shareByServer (video: VideoModel, t: Transaction) {
12 const serverActor = await getServerActor() 33 const serverActor = await getServerActor()
13 34
14 const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) 35 const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor)
15 const serverSharePromise = VideoShareModel.findOrCreate({ 36 return VideoShareModel.findOrCreate({
16 defaults: { 37 defaults: {
17 actorId: serverActor.id, 38 actorId: serverActor.id,
18 videoId: video.id, 39 videoId: video.id,
@@ -27,9 +48,11 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
27 48
28 return undefined 49 return undefined
29 }) 50 })
51}
30 52
53async function shareByVideoChannel (video: VideoModel, t: Transaction) {
31 const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) 54 const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor)
32 const videoChannelSharePromise = VideoShareModel.findOrCreate({ 55 return VideoShareModel.findOrCreate({
33 defaults: { 56 defaults: {
34 actorId: video.VideoChannel.actorId, 57 actorId: video.VideoChannel.actorId,
35 videoId: video.id, 58 videoId: video.id,
@@ -40,17 +63,17 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
40 }, 63 },
41 transaction: t 64 transaction: t
42 }).then(([ videoChannelShare, created ]) => { 65 }).then(([ videoChannelShare, created ]) => {
43 if (created) return sendVideoAnnounce(serverActor, videoChannelShare, video, t) 66 if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
44 67
45 return undefined 68 return undefined
46 }) 69 })
47
48 return Promise.all([
49 serverSharePromise,
50 videoChannelSharePromise
51 ])
52} 70}
53 71
54export { 72async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
55 shareVideoByServerAndChannel 73 // Load old share
74 const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
75 if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id)
76
77 await sendUndoAnnounce(oldVideoChannel.Actor, oldShare, video, t)
78 await oldShare.destroy({ transaction: t })
56} 79}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index b81acbb35..2899acff3 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -137,6 +137,13 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObje
137 return attributes 137 return attributes
138} 138}
139 139
140function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
141 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
142 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
143
144 return getOrCreateActorAndServerAndModel(channel.id)
145}
146
140async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) { 147async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) {
141 logger.debug('Adding remote video %s.', videoObject.id) 148 logger.debug('Adding remote video %s.', videoObject.id)
142 149
@@ -199,10 +206,7 @@ async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentOb
199 actor = await getOrCreateActorAndServerAndModel(actorObj.id) 206 actor = await getOrCreateActorAndServerAndModel(actorObj.id)
200 } 207 }
201 208
202 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 209 const channelActor = await getOrCreateVideoChannel(videoObject)
203 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
204
205 const channelActor = await getOrCreateActorAndServerAndModel(channel.id)
206 210
207 const options = { 211 const options = {
208 arguments: [ videoObject, channelActor ], 212 arguments: [ videoObject, channelActor ],
@@ -301,6 +305,7 @@ export {
301 videoActivityObjectToDBAttributes, 305 videoActivityObjectToDBAttributes,
302 videoFileActivityUrlToDBAttributes, 306 videoFileActivityUrlToDBAttributes,
303 getOrCreateVideo, 307 getOrCreateVideo,
308 getOrCreateVideoChannel,
304 addVideoShares} 309 addVideoShares}
305 310
306// --------------------------------------------------------------------------- 311// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index b93dccc50..aa2afb068 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -6,6 +6,7 @@ import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, t
6import { 6import {
7 isVideoAbuseReasonValid, 7 isVideoAbuseReasonValid,
8 isVideoCategoryValid, 8 isVideoCategoryValid,
9 isVideoChannelOfAccountExist,
9 isVideoDescriptionValid, 10 isVideoDescriptionValid,
10 isVideoExist, 11 isVideoExist,
11 isVideoFile, 12 isVideoFile,
@@ -23,7 +24,6 @@ import { logger } from '../../helpers/logger'
23import { CONSTRAINTS_FIELDS } from '../../initializers' 24import { CONSTRAINTS_FIELDS } from '../../initializers'
24import { UserModel } from '../../models/account/user' 25import { UserModel } from '../../models/account/user'
25import { VideoModel } from '../../models/video/video' 26import { VideoModel } from '../../models/video/video'
26import { VideoChannelModel } from '../../models/video/video-channel'
27import { VideoShareModel } from '../../models/video/video-share' 27import { VideoShareModel } from '../../models/video/video-share'
28import { authenticate } from '../oauth' 28import { authenticate } from '../oauth'
29import { areValidationErrors } from './utils' 29import { areValidationErrors } from './utils'
@@ -75,7 +75,10 @@ const videosAddValidator = [
75 .optional() 75 .optional()
76 .toInt() 76 .toInt()
77 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), 77 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
78 body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), 78 body('channelId')
79 .toInt()
80 .custom(isIdValid)
81 .withMessage('Should have correct video channel id'),
79 82
80 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 83 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
81 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 84 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
@@ -86,16 +89,7 @@ const videosAddValidator = [
86 const videoFile: Express.Multer.File = req.files['videofile'][0] 89 const videoFile: Express.Multer.File = req.files['videofile'][0]
87 const user = res.locals.oauth.token.User 90 const user = res.locals.oauth.token.User
88 91
89 const videoChannel = await VideoChannelModel.loadByIdAndAccount(req.body.channelId, user.Account.id) 92 if (!await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
90 if (!videoChannel) {
91 res.status(400)
92 .json({ error: 'Unknown video video channel for this account.' })
93 .end()
94
95 return
96 }
97
98 res.locals.videoChannel = videoChannel
99 93
100 const isAble = await user.isAbleToUploadVideo(videoFile) 94 const isAble = await user.isAbleToUploadVideo(videoFile)
101 if (isAble === false) { 95 if (isAble === false) {
@@ -173,6 +167,10 @@ const videosUpdateValidator = [
173 .optional() 167 .optional()
174 .toBoolean() 168 .toBoolean()
175 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), 169 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
170 body('channelId')
171 .optional()
172 .toInt()
173 .custom(isIdValid).withMessage('Should have correct video channel id'),
176 174
177 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 175 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
178 logger.debug('Checking videosUpdate parameters', { parameters: req.body }) 176 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
@@ -184,7 +182,8 @@ const videosUpdateValidator = [
184 const video = res.locals.video 182 const video = res.locals.video
185 183
186 // Check if the user who did the request is able to update the video 184 // Check if the user who did the request is able to update the video
187 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return 185 const user = res.locals.oauth.token.User
186 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
188 187
189 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) { 188 if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
190 return res.status(409) 189 return res.status(409)
@@ -192,6 +191,8 @@ const videosUpdateValidator = [
192 .end() 191 .end()
193 } 192 }
194 193
194 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
195
195 return next() 196 return next()
196 } 197 }
197] 198]
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index 6f770957f..602cc69b9 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -98,6 +98,15 @@ export class VideoShareModel extends Model<VideoShareModel> {
98 }) 98 })
99 } 99 }
100 100
101 static loadByUrl (url: string, t: Sequelize.Transaction) {
102 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
103 where: {
104 url
105 },
106 transaction: t
107 })
108 }
109
101 static loadActorsByShare (videoId: number, t: Sequelize.Transaction) { 110 static loadActorsByShare (videoId: number, t: Sequelize.Transaction) {
102 const query = { 111 const query = {
103 where: { 112 where: {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index ea466fccd..fe8c30655 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -130,11 +130,27 @@ enum ScopeNames {
130 } 130 }
131 131
132 const videoChannelInclude = { 132 const videoChannelInclude = {
133 attributes: [ 'name', 'description' ], 133 attributes: [ 'name', 'description', 'id' ],
134 model: VideoChannelModel.unscoped(), 134 model: VideoChannelModel.unscoped(),
135 required: true, 135 required: true,
136 where: {}, 136 where: {},
137 include: [ 137 include: [
138 {
139 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
140 model: ActorModel.unscoped(),
141 required: true,
142 include: [
143 {
144 attributes: [ 'host' ],
145 model: ServerModel.unscoped(),
146 required: false
147 },
148 {
149 model: AvatarModel.unscoped(),
150 required: false
151 }
152 ]
153 },
138 accountInclude 154 accountInclude
139 ] 155 ]
140 } 156 }
@@ -771,12 +787,17 @@ export class VideoModel extends Model<VideoModel> {
771 } 787 }
772 }, 788 },
773 { 789 {
774 preferredUsername: Sequelize.where(Sequelize.col('preferredUsername'), { 790 preferredUsernameChannel: Sequelize.where(Sequelize.col('VideoChannel->Actor.preferredUsername'), {
775 [ Sequelize.Op.iLike ]: '%' + value + '%' 791 [ Sequelize.Op.iLike ]: '%' + value + '%'
776 }) 792 })
777 }, 793 },
778 { 794 {
779 host: Sequelize.where(Sequelize.col('host'), { 795 preferredUsernameAccount: Sequelize.where(Sequelize.col('VideoChannel->Account->Actor.preferredUsername'), {
796 [ Sequelize.Op.iLike ]: '%' + value + '%'
797 })
798 },
799 {
800 host: Sequelize.where(Sequelize.col('VideoChannel->Account->Actor->Server.host'), {
780 [ Sequelize.Op.iLike ]: '%' + value + '%' 801 [ Sequelize.Op.iLike ]: '%' + value + '%'
781 }) 802 })
782 } 803 }
@@ -1043,6 +1064,7 @@ export class VideoModel extends Model<VideoModel> {
1043 1064
1044 toFormattedJSON (): Video { 1065 toFormattedJSON (): Video {
1045 const formattedAccount = this.VideoChannel.Account.toFormattedJSON() 1066 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
1067 const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
1046 1068
1047 return { 1069 return {
1048 id: this.id, 1070 id: this.id,
@@ -1085,6 +1107,15 @@ export class VideoModel extends Model<VideoModel> {
1085 url: formattedAccount.url, 1107 url: formattedAccount.url,
1086 host: formattedAccount.host, 1108 host: formattedAccount.host,
1087 avatar: formattedAccount.avatar 1109 avatar: formattedAccount.avatar
1110 },
1111 channel: {
1112 id: formattedVideoChannel.id,
1113 uuid: formattedVideoChannel.uuid,
1114 name: formattedVideoChannel.name,
1115 displayName: formattedVideoChannel.displayName,
1116 url: formattedVideoChannel.url,
1117 host: formattedVideoChannel.host,
1118 avatar: formattedVideoChannel.avatar
1088 } 1119 }
1089 } 1120 }
1090 } 1121 }
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 585b6a2b5..35c418f7c 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -2,8 +2,8 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User } from '../../../../shared/index' 5import { User, Video } from '../../../../shared/index'
6import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, uploadVideo, wait } from '../../utils' 6import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, updateVideo, uploadVideo, wait } from '../../utils'
7import { 7import {
8 addVideoChannel, 8 addVideoChannel,
9 deleteVideoChannel, 9 deleteVideoChannel,
@@ -25,8 +25,11 @@ describe('Test video channels', function () {
25 let servers: ServerInfo[] 25 let servers: ServerInfo[]
26 let userInfo: User 26 let userInfo: User
27 let accountUUID: string 27 let accountUUID: string
28 let videoChannelId: number 28 let firstVideoChannelId: number
29 let videoChannelUUID: string 29 let firstVideoChannelUUID: string
30 let secondVideoChannelId: number
31 let secondVideoChannelUUID: string
32 let videoUUID: string
30 33
31 before(async function () { 34 before(async function () {
32 this.timeout(30000) 35 this.timeout(30000)
@@ -42,6 +45,9 @@ describe('Test video channels', function () {
42 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 45 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
43 const user: User = res.body 46 const user: User = res.body
44 accountUUID = user.account.uuid 47 accountUUID = user.account.uuid
48
49 firstVideoChannelId = user.videoChannels[0].id
50 firstVideoChannelUUID = user.videoChannels[0].uuid
45 } 51 }
46 52
47 await wait(5000) 53 await wait(5000)
@@ -58,17 +64,22 @@ describe('Test video channels', function () {
58 it('Should create another video channel', async function () { 64 it('Should create another video channel', async function () {
59 this.timeout(10000) 65 this.timeout(10000)
60 66
61 const videoChannel = { 67 {
62 displayName: 'second video channel', 68 const videoChannel = {
63 description: 'super video channel description', 69 displayName: 'second video channel',
64 support: 'super video channel support text' 70 description: 'super video channel description',
71 support: 'super video channel support text'
72 }
73 const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel)
74 secondVideoChannelId = res.body.videoChannel.id
75 secondVideoChannelUUID = res.body.videoChannel.uuid
65 } 76 }
66 const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
67 videoChannelId = res.body.videoChannel.id
68 videoChannelUUID = res.body.videoChannel.uuid
69 77
70 // The channel is 1 is propagated to servers 2 78 // The channel is 1 is propagated to servers 2
71 await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my video name', channelId: videoChannelId }) 79 {
80 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'my video name', channelId: secondVideoChannelId })
81 videoUUID = res.body.video.uuid
82 }
72 83
73 await wait(3000) 84 await wait(3000)
74 }) 85 })
@@ -130,7 +141,7 @@ describe('Test video channels', function () {
130 support: 'video channel support text updated' 141 support: 'video channel support text updated'
131 } 142 }
132 143
133 await updateVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId, videoChannelAttributes) 144 await updateVideoChannel(servers[0].url, servers[0].accessToken, secondVideoChannelId, videoChannelAttributes)
134 145
135 await wait(3000) 146 await wait(3000)
136 }) 147 })
@@ -149,7 +160,7 @@ describe('Test video channels', function () {
149 }) 160 })
150 161
151 it('Should get video channel', async function () { 162 it('Should get video channel', async function () {
152 const res = await getVideoChannel(servers[0].url, videoChannelId) 163 const res = await getVideoChannel(servers[0].url, secondVideoChannelId)
153 164
154 const videoChannel = res.body 165 const videoChannel = res.body
155 expect(videoChannel.displayName).to.equal('video channel updated') 166 expect(videoChannel.displayName).to.equal('video channel updated')
@@ -157,20 +168,45 @@ describe('Test video channels', function () {
157 expect(videoChannel.support).to.equal('video channel support text updated') 168 expect(videoChannel.support).to.equal('video channel support text updated')
158 }) 169 })
159 170
160 it('Should list the video channel videos', async function () { 171 it('Should list the second video channel videos', async function () {
161 this.timeout(10000) 172 this.timeout(10000)
162 173
163 for (const server of servers) { 174 for (const server of servers) {
164 const res = await getVideoChannelVideos(server.url, server.accessToken, videoChannelUUID, 0, 5) 175 const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondVideoChannelUUID, 0, 5)
165 expect(res.body.total).to.equal(1) 176 expect(res1.body.total).to.equal(1)
166 expect(res.body.data).to.be.an('array') 177 expect(res1.body.data).to.be.an('array')
167 expect(res.body.data).to.have.lengthOf(1) 178 expect(res1.body.data).to.have.lengthOf(1)
168 expect(res.body.data[0].name).to.equal('my video name') 179 expect(res1.body.data[0].name).to.equal('my video name')
180 }
181 })
182
183 it('Should change the video channel of a video', async function () {
184 this.timeout(10000)
185
186 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { channelId: firstVideoChannelId })
187
188 await wait(5000)
189 })
190
191 it('Should list the first video channel videos', async function () {
192 this.timeout(10000)
193
194 for (const server of servers) {
195 const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondVideoChannelUUID, 0, 5)
196 expect(res1.body.total).to.equal(0)
197
198 const res2 = await getVideoChannelVideos(server.url, server.accessToken, firstVideoChannelUUID, 0, 5)
199 expect(res2.body.total).to.equal(1)
200
201 const videos: Video[] = res2.body.data
202 expect(videos).to.be.an('array')
203 expect(videos).to.have.lengthOf(1)
204 expect(videos[0].name).to.equal('my video name')
169 } 205 }
170 }) 206 })
171 207
172 it('Should delete video channel', async function () { 208 it('Should delete video channel', async function () {
173 await deleteVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId) 209 await deleteVideoChannel(servers[0].url, servers[0].accessToken, secondVideoChannelId)
174 }) 210 })
175 211
176 it('Should have video channel deleted', async function () { 212 it('Should have video channel deleted', async function () {
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index 870dfd21f..07c4ffc77 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -15,7 +15,7 @@ import {
15 ServerInfo, 15 ServerInfo,
16 testImage 16 testImage
17} from '../' 17} from '../'
18import { VideoPrivacy } from '../../../../shared/models/videos' 18import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
19import { readdirPromise } from '../../../helpers/core-utils' 19import { readdirPromise } from '../../../helpers/core-utils'
20import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' 20import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers'
21import { dateIsValid, webtorrentAdd } from '../index' 21import { dateIsValid, webtorrentAdd } from '../index'
@@ -385,6 +385,7 @@ function updateVideo (url: string, accessToken: string, id: number | string, att
385 if (attributes.description) body['description'] = attributes.description 385 if (attributes.description) body['description'] = attributes.description
386 if (attributes.tags) body['tags'] = attributes.tags 386 if (attributes.tags) body['tags'] = attributes.tags
387 if (attributes.privacy) body['privacy'] = attributes.privacy 387 if (attributes.privacy) body['privacy'] = attributes.privacy
388 if (attributes.channelId) body['channelId'] = attributes.channelId
388 389
389 // Upload request 390 // Upload request
390 if (attributes.thumbnailfile || attributes.previewfile) { 391 if (attributes.thumbnailfile || attributes.previewfile) {
@@ -489,6 +490,8 @@ async function completeVideoCheck (
489 expect(video.account.uuid).to.be.a('string') 490 expect(video.account.uuid).to.be.a('string')
490 expect(video.account.host).to.equal(attributes.account.host) 491 expect(video.account.host).to.equal(attributes.account.host)
491 expect(video.account.name).to.equal(attributes.account.name) 492 expect(video.account.name).to.equal(attributes.account.name)
493 expect(video.channel.displayName).to.equal(attributes.channel.name)
494 expect(video.channel.name).to.have.lengthOf(36)
492 expect(video.likes).to.equal(attributes.likes) 495 expect(video.likes).to.equal(attributes.likes)
493 expect(video.dislikes).to.equal(attributes.dislikes) 496 expect(video.dislikes).to.equal(attributes.dislikes)
494 expect(video.isLocal).to.equal(attributes.isLocal) 497 expect(video.isLocal).to.equal(attributes.isLocal)
@@ -498,19 +501,19 @@ async function completeVideoCheck (
498 expect(dateIsValid(video.updatedAt)).to.be.true 501 expect(dateIsValid(video.updatedAt)).to.be.true
499 502
500 const res = await getVideo(url, video.uuid) 503 const res = await getVideo(url, video.uuid)
501 const videoDetails = res.body 504 const videoDetails: VideoDetails = res.body
502 505
503 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 506 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
504 expect(videoDetails.tags).to.deep.equal(attributes.tags) 507 expect(videoDetails.tags).to.deep.equal(attributes.tags)
505 expect(videoDetails.account.name).to.equal(attributes.account.name) 508 expect(videoDetails.account.name).to.equal(attributes.account.name)
506 expect(videoDetails.account.host).to.equal(attributes.account.host) 509 expect(videoDetails.account.host).to.equal(attributes.account.host)
507 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
508
509 expect(videoDetails.channel.displayName).to.equal(attributes.channel.name) 510 expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
510 expect(videoDetails.channel.name).to.have.lengthOf(36) 511 expect(videoDetails.channel.name).to.have.lengthOf(36)
512 expect(videoDetails.channel.host).to.equal(attributes.account.host)
511 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) 513 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
512 expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true 514 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
513 expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true 515 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
516 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
514 517
515 for (const attributeFile of attributes.files) { 518 for (const attributeFile of attributes.files) {
516 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution) 519 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)