diff options
author | Chocobozzz <me@florianbigard.com> | 2018-05-11 15:10:13 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-05-11 15:25:51 +0200 |
commit | 0f320037e689b2778959c12ddd4ce790f6e4ae4f (patch) | |
tree | 425acaa4345442388901c833275bb76b42a8a268 | |
parent | 9675333decd0b89b73a4fc67b39272f7296bfe3f (diff) | |
download | PeerTube-0f320037e689b2778959c12ddd4ce790f6e4ae4f.tar.gz PeerTube-0f320037e689b2778959c12ddd4ce790f6e4ae4f.tar.zst PeerTube-0f320037e689b2778959c12ddd4ce790f6e4ae4f.zip |
Add ability to update a video channel
26 files changed, 296 insertions, 95 deletions
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index 56ff780b7..37d84cb6e 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Actor as ActorServer } from '../../../../../shared/models/actors/actor.model' | 1 | import { Actor as ActorServer } from '../../../../../shared/models/actors/actor.model' |
2 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | ||
3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 2 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
3 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | ||
4 | 4 | ||
5 | export abstract class Actor implements ActorServer { | 5 | export abstract class Actor implements ActorServer { |
6 | id: number | 6 | id: number |
@@ -41,8 +41,8 @@ export abstract class Actor implements ActorServer { | |||
41 | this.host = hash.host | 41 | this.host = hash.host |
42 | this.followingCount = hash.followingCount | 42 | this.followingCount = hash.followingCount |
43 | this.followersCount = hash.followersCount | 43 | this.followersCount = hash.followersCount |
44 | this.createdAt = new Date(hash.createdAt.toString()) | 44 | this.createdAt = new Date(hash.createdAt) |
45 | this.updatedAt = new Date(hash.updatedAt.toString()) | 45 | this.updatedAt = new Date(hash.updatedAt) |
46 | this.avatar = hash.avatar | 46 | this.avatar = hash.avatar |
47 | 47 | ||
48 | this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this) | 48 | this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this) |
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 39826d71e..ad2929db5 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -10,7 +10,7 @@ export class VideoEdit { | |||
10 | tags: string[] | 10 | tags: string[] |
11 | nsfw: boolean | 11 | nsfw: boolean |
12 | commentsEnabled: boolean | 12 | commentsEnabled: boolean |
13 | channel: number | 13 | channelId: number |
14 | privacy: VideoPrivacy | 14 | privacy: VideoPrivacy |
15 | support: string | 15 | support: string |
16 | thumbnailfile?: any | 16 | thumbnailfile?: any |
@@ -32,7 +32,7 @@ export class VideoEdit { | |||
32 | this.tags = videoDetails.tags | 32 | this.tags = videoDetails.tags |
33 | this.nsfw = videoDetails.nsfw | 33 | this.nsfw = videoDetails.nsfw |
34 | this.commentsEnabled = videoDetails.commentsEnabled | 34 | this.commentsEnabled = videoDetails.commentsEnabled |
35 | this.channel = videoDetails.channel.id | 35 | this.channelId = videoDetails.channel.id |
36 | this.privacy = videoDetails.privacy.id | 36 | this.privacy = videoDetails.privacy.id |
37 | this.support = videoDetails.support | 37 | this.support = videoDetails.support |
38 | this.thumbnailUrl = videoDetails.thumbnailUrl | 38 | this.thumbnailUrl = videoDetails.thumbnailUrl |
@@ -57,7 +57,7 @@ export class VideoEdit { | |||
57 | tags: this.tags, | 57 | tags: this.tags, |
58 | nsfw: this.nsfw, | 58 | nsfw: this.nsfw, |
59 | commentsEnabled: this.commentsEnabled, | 59 | commentsEnabled: this.commentsEnabled, |
60 | channelId: this.channel, | 60 | channelId: this.channelId, |
61 | privacy: this.privacy | 61 | privacy: this.privacy |
62 | } | 62 | } |
63 | } | 63 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index f56eecaeb..48d562f9c 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -45,6 +45,16 @@ export class Video implements VideoServerModel { | |||
45 | avatar: Avatar | 45 | avatar: Avatar |
46 | } | 46 | } |
47 | 47 | ||
48 | channel: { | ||
49 | id: number | ||
50 | uuid: string | ||
51 | name: string | ||
52 | displayName: string | ||
53 | url: string | ||
54 | host: string | ||
55 | avatar: Avatar | ||
56 | } | ||
57 | |||
48 | private static createDurationString (duration: number) { | 58 | private static createDurationString (duration: number) { |
49 | const hours = Math.floor(duration / 3600) | 59 | const hours = Math.floor(duration / 3600) |
50 | const minutes = Math.floor(duration % 3600 / 60) | 60 | const minutes = Math.floor(duration % 3600 / 60) |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index b45777c55..cd8539b41 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -67,6 +67,7 @@ export class VideoService { | |||
67 | language, | 67 | language, |
68 | support, | 68 | support, |
69 | description, | 69 | description, |
70 | channelId: video.channelId, | ||
70 | privacy: video.privacy, | 71 | privacy: video.privacy, |
71 | tags: video.tags, | 72 | tags: video.tags, |
72 | nsfw: video.nsfw, | 73 | nsfw: video.nsfw, |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 9cd3454a0..77b554ad5 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -33,7 +33,7 @@ | |||
33 | <div class="col-md-4"> | 33 | <div class="col-md-4"> |
34 | <div class="form-group"> | 34 | <div class="form-group"> |
35 | <label>Channel</label> | 35 | <label>Channel</label> |
36 | <div class="peertube-select-disabled-container"> | 36 | <div class="peertube-select-container"> |
37 | <select formControlName="channelId"> | 37 | <select formControlName="channelId"> |
38 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 38 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> |
39 | </select> | 39 | </select> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index cf64ff589..58ed5ab98 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -5,10 +5,6 @@ | |||
5 | @include peertube-select-container(auto); | 5 | @include peertube-select-container(auto); |
6 | } | 6 | } |
7 | 7 | ||
8 | .peertube-select-disabled-container { | ||
9 | @include peertube-select-disabled-container(auto); | ||
10 | } | ||
11 | |||
12 | .form-group-checkbox { | 8 | .form-group-checkbox { |
13 | my-help { margin-left: 5px } | 9 | my-help { margin-left: 5px } |
14 | } | 10 | } |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 6ab1a4a24..77e984855 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts | |||
@@ -75,7 +75,7 @@ export class VideoEditComponent implements OnInit { | |||
75 | 75 | ||
76 | this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) | 76 | this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) |
77 | this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) | 77 | this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) |
78 | this.form.addControl('channelId', new FormControl({ value: '', disabled: true })) | 78 | this.form.addControl('channelId', new FormControl('', VIDEO_CHANNEL.VALIDATORS)) |
79 | this.form.addControl('nsfw', new FormControl(false)) | 79 | this.form.addControl('nsfw', new FormControl(false)) |
80 | this.form.addControl('commentsEnabled', new FormControl(true)) | 80 | this.form.addControl('commentsEnabled', new FormControl(true)) |
81 | this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) | 81 | this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) |
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index ae5548897..fa967018d 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts | |||
@@ -220,7 +220,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy | |||
220 | 220 | ||
221 | const video = new VideoEdit() | 221 | const video = new VideoEdit() |
222 | video.patch(this.form.value) | 222 | video.patch(this.form.value) |
223 | video.channel = this.firstStepChannelId | 223 | video.channelId = this.firstStepChannelId |
224 | video.id = this.videoUploadedIds.id | 224 | video.id = this.videoUploadedIds.id |
225 | video.uuid = this.videoUploadedIds.uuid | 225 | video.uuid = this.videoUploadedIds.uuid |
226 | 226 | ||
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 6cd204f72..73e2764c6 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts | |||
@@ -9,9 +9,9 @@ import { ServerService } from '../../core' | |||
9 | import { AuthService } from '../../core/auth' | 9 | import { AuthService } from '../../core/auth' |
10 | import { FormReactive } from '../../shared' | 10 | import { FormReactive } from '../../shared' |
11 | import { ValidatorMessage } from '../../shared/forms/form-validators/validator-message' | 11 | import { ValidatorMessage } from '../../shared/forms/form-validators/validator-message' |
12 | import { populateAsyncUserVideoChannels } from '../../shared/misc/utils' | ||
13 | import { VideoEdit } from '../../shared/video/video-edit.model' | 12 | import { VideoEdit } from '../../shared/video/video-edit.model' |
14 | import { VideoService } from '../../shared/video/video.service' | 13 | import { VideoService } from '../../shared/video/video.service' |
14 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | ||
15 | 15 | ||
16 | @Component({ | 16 | @Component({ |
17 | selector: 'my-videos-update', | 17 | selector: 'my-videos-update', |
@@ -64,12 +64,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
64 | video => { | 64 | video => { |
65 | this.video = new VideoEdit(video) | 65 | this.video = new VideoEdit(video) |
66 | 66 | ||
67 | this.userVideoChannels = [ | 67 | populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) |
68 | { | 68 | .catch(err => console.error(err)) |
69 | id: video.channel.id, | ||
70 | label: video.channel.displayName | ||
71 | } | ||
72 | ] | ||
73 | 69 | ||
74 | // We cannot set private a video that was not private | 70 | // We cannot set private a video that was not private |
75 | if (video.privacy.id !== VideoPrivacy.PRIVATE) { | 71 | if (video.privacy.id !== VideoPrivacy.PRIVATE) { |
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' |
22 | import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' | 22 | import { |
23 | changeVideoChannelShare, | ||
24 | fetchRemoteVideoDescription, | ||
25 | getVideoActivityPubUrl, | ||
26 | shareVideoByServerAndChannel | ||
27 | } from '../../../lib/activitypub' | ||
23 | import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' | 28 | import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' |
24 | import { JobQueue } from '../../../lib/job-queue' | 29 | import { JobQueue } from '../../../lib/job-queue' |
25 | import { Redis } from '../../../lib/redis' | 30 | import { 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' |
15 | import { VideoModel } from '../../models/video/video' | 15 | import { VideoModel } from '../../models/video/video' |
16 | import { exists, isArray, isFileValid } from './misc' | 16 | import { exists, isArray, isFileValid } from './misc' |
17 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
17 | 18 | ||
18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 19 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
19 | const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES | 20 | const 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 | ||
128 | async 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 | ||
129 | export { | 144 | export { |
@@ -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 | ||
356 | async function refreshActorIfNeeded (actor: ActorModel) { | 356 | async 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 @@ | |||
1 | import { ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' | 1 | import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' |
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | 2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' |
3 | import { getActorUrl } from '../../../helpers/activitypub' | 3 | import { getActorUrl } from '../../../helpers/activitypub' |
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
@@ -10,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
10 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 10 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
11 | import { forwardActivity } from '../send/misc' | 11 | import { forwardActivity } from '../send/misc' |
12 | import { getOrCreateAccountAndVideoAndChannel } from '../videos' | 12 | import { getOrCreateAccountAndVideoAndChannel } from '../videos' |
13 | import { VideoShareModel } from '../../../models/video/video-share' | ||
13 | 14 | ||
14 | async function processUndoActivity (activity: ActivityUndo) { | 15 | async 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 | |||
130 | function 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 | |||
139 | function 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' | |||
14 | import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' | 14 | import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' |
15 | import { | 15 | import { |
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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { ActivityAudience, ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' | 2 | import { |
3 | ActivityAnnounce, | ||
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityFollow, | ||
7 | ActivityLike, | ||
8 | ActivityUndo | ||
9 | } from '../../../../shared/models/activitypub' | ||
3 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
5 | import { VideoModel } from '../../../models/video/video' | 12 | import { VideoModel } from '../../../models/video/video' |
@@ -16,6 +23,8 @@ import { | |||
16 | import { createActivityData, createDislikeActivityData } from './send-create' | 23 | import { createActivityData, createDislikeActivityData } from './send-create' |
17 | import { followActivityData } from './send-follow' | 24 | import { followActivityData } from './send-follow' |
18 | import { likeActivityData } from './send-like' | 25 | import { likeActivityData } from './send-like' |
26 | import { VideoShareModel } from '../../../models/video/video-share' | ||
27 | import { buildVideoAnnounce } from './send-announce' | ||
19 | 28 | ||
20 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { | 29 | async 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 | ||
85 | async 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 | ||
78 | export { | 98 | export { |
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 { | |||
86 | async function undoActivityData ( | 107 | async 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' | |||
3 | import { getServerActor } from '../../helpers/utils' | 3 | import { getServerActor } from '../../helpers/utils' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { VideoShareModel } from '../../models/video/video-share' | 5 | import { VideoShareModel } from '../../models/video/video-share' |
6 | import { sendVideoAnnounce } from './send' | 6 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
7 | import { getAnnounceActivityPubUrl } from './url' | 7 | import { getAnnounceActivityPubUrl } from './url' |
8 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
8 | 9 | ||
9 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 10 | async 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 | |||
19 | async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { | ||
20 | await undoShareByVideoChannel(video, oldVideoChannel, t) | ||
21 | |||
22 | await shareByVideoChannel(video, t) | ||
23 | } | ||
24 | |||
25 | export { | ||
26 | changeVideoChannelShare, | ||
27 | shareVideoByServerAndChannel | ||
28 | } | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async 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 | ||
53 | async 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 | ||
54 | export { | 72 | async 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 | ||
140 | function 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 | |||
140 | async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) { | 147 | async 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 | |||
6 | import { | 6 | import { |
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' | |||
23 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 24 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
24 | import { UserModel } from '../../models/account/user' | 25 | import { UserModel } from '../../models/account/user' |
25 | import { VideoModel } from '../../models/video/video' | 26 | import { VideoModel } from '../../models/video/video' |
26 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
27 | import { VideoShareModel } from '../../models/video/video-share' | 27 | import { VideoShareModel } from '../../models/video/video-share' |
28 | import { authenticate } from '../oauth' | 28 | import { authenticate } from '../oauth' |
29 | import { areValidationErrors } from './utils' | 29 | import { 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 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { User } from '../../../../shared/index' | 5 | import { User, Video } from '../../../../shared/index' |
6 | import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, uploadVideo, wait } from '../../utils' | 6 | import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, updateVideo, uploadVideo, wait } from '../../utils' |
7 | import { | 7 | import { |
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 '../' |
18 | import { VideoPrivacy } from '../../../../shared/models/videos' | 18 | import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos' |
19 | import { readdirPromise } from '../../../helpers/core-utils' | 19 | import { readdirPromise } from '../../../helpers/core-utils' |
20 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' | 20 | import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' |
21 | import { dateIsValid, webtorrentAdd } from '../index' | 21 | import { 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) |
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index f555f0118..46e883e5f 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts | |||
@@ -64,7 +64,7 @@ export interface ActivityAnnounce extends BaseActivity { | |||
64 | 64 | ||
65 | export interface ActivityUndo extends BaseActivity { | 65 | export interface ActivityUndo extends BaseActivity { |
66 | type: 'Undo', | 66 | type: 'Undo', |
67 | object: ActivityFollow | ActivityLike | ActivityCreate | 67 | object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce |
68 | } | 68 | } |
69 | 69 | ||
70 | export interface ActivityLike extends BaseActivity { | 70 | export interface ActivityLike extends BaseActivity { |
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts index f91616519..6b3b1b47c 100644 --- a/shared/models/actors/actor.model.ts +++ b/shared/models/actors/actor.model.ts | |||
@@ -8,7 +8,7 @@ export interface Actor { | |||
8 | host: string | 8 | host: string |
9 | followingCount: number | 9 | followingCount: number |
10 | followersCount: number | 10 | followersCount: number |
11 | createdAt: Date | 11 | createdAt: Date | string |
12 | updatedAt: Date | 12 | updatedAt: Date | string |
13 | avatar: Avatar | 13 | avatar: Avatar |
14 | } | 14 | } |
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts index 3a205bb83..c368d8464 100644 --- a/shared/models/videos/video-update.model.ts +++ b/shared/models/videos/video-update.model.ts | |||
@@ -11,6 +11,7 @@ export interface VideoUpdate { | |||
11 | tags?: string[] | 11 | tags?: string[] |
12 | commentsEnabled?: boolean | 12 | commentsEnabled?: boolean |
13 | nsfw?: boolean | 13 | nsfw?: boolean |
14 | channelId?: number | ||
14 | thumbnailfile?: Blob | 15 | thumbnailfile?: Blob |
15 | previewfile?: Blob | 16 | previewfile?: Blob |
16 | } | 17 | } |
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index ba1881da3..eb40e82de 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -48,6 +48,16 @@ export interface Video { | |||
48 | host: string | 48 | host: string |
49 | avatar: Avatar | 49 | avatar: Avatar |
50 | } | 50 | } |
51 | |||
52 | channel: { | ||
53 | id: number | ||
54 | uuid: string | ||
55 | name: string | ||
56 | displayName: string | ||
57 | url: string | ||
58 | host: string | ||
59 | avatar: Avatar | ||
60 | } | ||
51 | } | 61 | } |
52 | 62 | ||
53 | export interface VideoDetails extends Video { | 63 | export interface VideoDetails extends Video { |