Add ability to update a video channel

pull/559/head
Chocobozzz 2018-05-11 15:10:13 +02:00
parent 9675333dec
commit 0f320037e6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
26 changed files with 296 additions and 95 deletions

View File

@ -1,6 +1,6 @@
import { Actor as ActorServer } from '../../../../../shared/models/actors/actor.model' import { Actor as ActorServer } from '../../../../../shared/models/actors/actor.model'
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
export abstract class Actor implements ActorServer { export abstract class Actor implements ActorServer {
id: number id: number
@ -41,8 +41,8 @@ export abstract class Actor implements ActorServer {
this.host = hash.host this.host = hash.host
this.followingCount = hash.followingCount this.followingCount = hash.followingCount
this.followersCount = hash.followersCount this.followersCount = hash.followersCount
this.createdAt = new Date(hash.createdAt.toString()) this.createdAt = new Date(hash.createdAt)
this.updatedAt = new Date(hash.updatedAt.toString()) this.updatedAt = new Date(hash.updatedAt)
this.avatar = hash.avatar this.avatar = hash.avatar
this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this) this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this)

View File

@ -10,7 +10,7 @@ export class VideoEdit {
tags: string[] tags: string[]
nsfw: boolean nsfw: boolean
commentsEnabled: boolean commentsEnabled: boolean
channel: number channelId: number
privacy: VideoPrivacy privacy: VideoPrivacy
support: string support: string
thumbnailfile?: any thumbnailfile?: any
@ -32,7 +32,7 @@ export class VideoEdit {
this.tags = videoDetails.tags this.tags = videoDetails.tags
this.nsfw = videoDetails.nsfw this.nsfw = videoDetails.nsfw
this.commentsEnabled = videoDetails.commentsEnabled this.commentsEnabled = videoDetails.commentsEnabled
this.channel = videoDetails.channel.id this.channelId = videoDetails.channel.id
this.privacy = videoDetails.privacy.id this.privacy = videoDetails.privacy.id
this.support = videoDetails.support this.support = videoDetails.support
this.thumbnailUrl = videoDetails.thumbnailUrl this.thumbnailUrl = videoDetails.thumbnailUrl
@ -57,7 +57,7 @@ export class VideoEdit {
tags: this.tags, tags: this.tags,
nsfw: this.nsfw, nsfw: this.nsfw,
commentsEnabled: this.commentsEnabled, commentsEnabled: this.commentsEnabled,
channelId: this.channel, channelId: this.channelId,
privacy: this.privacy privacy: this.privacy
} }
} }

View File

@ -45,6 +45,16 @@ export class Video implements VideoServerModel {
avatar: Avatar avatar: Avatar
} }
channel: {
id: number
uuid: string
name: string
displayName: string
url: string
host: string
avatar: Avatar
}
private static createDurationString (duration: number) { private static createDurationString (duration: number) {
const hours = Math.floor(duration / 3600) const hours = Math.floor(duration / 3600)
const minutes = Math.floor(duration % 3600 / 60) const minutes = Math.floor(duration % 3600 / 60)

View File

@ -67,6 +67,7 @@ export class VideoService {
language, language,
support, support,
description, description,
channelId: video.channelId,
privacy: video.privacy, privacy: video.privacy,
tags: video.tags, tags: video.tags,
nsfw: video.nsfw, nsfw: video.nsfw,

View File

@ -33,7 +33,7 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="form-group">
<label>Channel</label> <label>Channel</label>
<div class="peertube-select-disabled-container"> <div class="peertube-select-container">
<select formControlName="channelId"> <select formControlName="channelId">
<option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
</select> </select>

View File

@ -5,10 +5,6 @@
@include peertube-select-container(auto); @include peertube-select-container(auto);
} }
.peertube-select-disabled-container {
@include peertube-select-disabled-container(auto);
}
.form-group-checkbox { .form-group-checkbox {
my-help { margin-left: 5px } my-help { margin-left: 5px }
} }

View File

@ -75,7 +75,7 @@ export class VideoEditComponent implements OnInit {
this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS)) this.form.addControl('name', new FormControl('', VIDEO_NAME.VALIDATORS))
this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS)) this.form.addControl('privacy', new FormControl('', VIDEO_PRIVACY.VALIDATORS))
this.form.addControl('channelId', new FormControl({ value: '', disabled: true })) this.form.addControl('channelId', new FormControl('', VIDEO_CHANNEL.VALIDATORS))
this.form.addControl('nsfw', new FormControl(false)) this.form.addControl('nsfw', new FormControl(false))
this.form.addControl('commentsEnabled', new FormControl(true)) this.form.addControl('commentsEnabled', new FormControl(true))
this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS)) this.form.addControl('category', new FormControl('', VIDEO_CATEGORY.VALIDATORS))

View File

@ -220,7 +220,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
const video = new VideoEdit() const video = new VideoEdit()
video.patch(this.form.value) video.patch(this.form.value)
video.channel = this.firstStepChannelId video.channelId = this.firstStepChannelId
video.id = this.videoUploadedIds.id video.id = this.videoUploadedIds.id
video.uuid = this.videoUploadedIds.uuid video.uuid = this.videoUploadedIds.uuid

View File

@ -9,9 +9,9 @@ import { ServerService } from '../../core'
import { AuthService } from '../../core/auth' import { AuthService } from '../../core/auth'
import { FormReactive } from '../../shared' import { FormReactive } from '../../shared'
import { ValidatorMessage } from '../../shared/forms/form-validators/validator-message' import { ValidatorMessage } from '../../shared/forms/form-validators/validator-message'
import { populateAsyncUserVideoChannels } from '../../shared/misc/utils'
import { VideoEdit } from '../../shared/video/video-edit.model' import { VideoEdit } from '../../shared/video/video-edit.model'
import { VideoService } from '../../shared/video/video.service' import { VideoService } from '../../shared/video/video.service'
import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
@Component({ @Component({
selector: 'my-videos-update', selector: 'my-videos-update',
@ -64,12 +64,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
video => { video => {
this.video = new VideoEdit(video) this.video = new VideoEdit(video)
this.userVideoChannels = [ populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
{ .catch(err => console.error(err))
id: video.channel.id,
label: video.channel.displayName
}
]
// We cannot set private a video that was not private // We cannot set private a video that was not private
if (video.privacy.id !== VideoPrivacy.PRIVATE) { if (video.privacy.id !== VideoPrivacy.PRIVATE) {

View File

@ -19,7 +19,12 @@ import {
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES VIDEO_PRIVACIES
} from '../../../initializers' } from '../../../initializers'
import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' import {
changeVideoChannelShare,
fetchRemoteVideoDescription,
getVideoActivityPubUrl,
shareVideoByServerAndChannel
} from '../../../lib/activitypub'
import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
import { JobQueue } from '../../../lib/job-queue' import { JobQueue } from '../../../lib/job-queue'
import { Redis } from '../../../lib/redis' import { Redis } from '../../../lib/redis'
@ -305,6 +310,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
const sequelizeOptions = { const sequelizeOptions = {
transaction: t transaction: t
} }
const oldVideoChannel = videoInstance.VideoChannel
if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name)
if (videoInfoToUpdate.category !== undefined) videoInstance.set('category', videoInfoToUpdate.category) if (videoInfoToUpdate.category !== undefined) videoInstance.set('category', videoInfoToUpdate.category)
@ -325,17 +331,24 @@ async function updateVideo (req: express.Request, res: express.Response) {
const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
// Video tags update?
if (videoInfoToUpdate.tags) { if (videoInfoToUpdate.tags) {
const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t) const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
await videoInstance.$set('Tags', tagInstances, sequelizeOptions) await videoInstanceUpdated.$set('Tags', tagInstances, sequelizeOptions)
videoInstance.Tags = tagInstances videoInstanceUpdated.Tags = tagInstances
}
// Video channel update?
if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel)
videoInstance.VideoChannel = res.locals.videoChannel
if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
} }
// Now we'll update the video's meta data to our friends // Now we'll update the video's meta data to our friends
if (wasPrivateVideo === false) { if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t)
await sendUpdateVideo(videoInstanceUpdated, t)
}
// Video is not private anymore, send a create action to remote servers // Video is not private anymore, send a create action to remote servers
if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) { if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {

View File

@ -14,6 +14,7 @@ import {
} from '../../initializers' } from '../../initializers'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { exists, isArray, isFileValid } from './misc' import { exists, isArray, isFileValid } from './misc'
import { VideoChannelModel } from '../../models/video/video-channel'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
@ -124,6 +125,20 @@ async function isVideoExist (id: string, res: Response) {
return true return true
} }
async function isVideoChannelOfAccountExist (channelId: number, accountId: number, res: Response) {
const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, accountId)
if (!videoChannel) {
res.status(400)
.json({ error: 'Unknown video video channel for this account.' })
.end()
return false
}
res.locals.videoChannel = videoChannel
return true
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -146,5 +161,6 @@ export {
isVideoFileSizeValid, isVideoFileSizeValid,
isVideoExist, isVideoExist,
isVideoImage, isVideoImage,
isVideoChannelOfAccountExist,
isVideoSupportValid isVideoSupportValid
} }

View File

@ -353,7 +353,7 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
return videoChannelCreated return videoChannelCreated
} }
async function refreshActorIfNeeded (actor: ActorModel) { async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
if (!actor.isOutdated()) return actor if (!actor.isOutdated()) return actor
try { try {

View File

@ -1,4 +1,4 @@
import { ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub'
import { DislikeObject } from '../../../../shared/models/activitypub/objects' import { DislikeObject } from '../../../../shared/models/activitypub/objects'
import { getActorUrl } from '../../../helpers/activitypub' import { getActorUrl } from '../../../helpers/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
@ -10,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { forwardActivity } from '../send/misc' import { forwardActivity } from '../send/misc'
import { getOrCreateAccountAndVideoAndChannel } from '../videos' import { getOrCreateAccountAndVideoAndChannel } from '../videos'
import { VideoShareModel } from '../../../models/video/video-share'
async function processUndoActivity (activity: ActivityUndo) { async function processUndoActivity (activity: ActivityUndo) {
const activityToUndo = activity.object const activityToUndo = activity.object
@ -22,6 +23,8 @@ async function processUndoActivity (activity: ActivityUndo) {
return processUndoDislike(actorUrl, activity) return processUndoDislike(actorUrl, activity)
} else if (activityToUndo.type === 'Follow') { } else if (activityToUndo.type === 'Follow') {
return processUndoFollow(actorUrl, activityToUndo) return processUndoFollow(actorUrl, activityToUndo)
} else if (activityToUndo.type === 'Announce') {
return processUndoAnnounce(actorUrl, activityToUndo)
} }
logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) 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) {
return undefined return undefined
}) })
} }
function processUndoAnnounce (actorUrl: string, announceActivity: ActivityAnnounce) {
const options = {
arguments: [ actorUrl, announceActivity ],
errorMessage: 'Cannot undo announce with many retries.'
}
return retryTransactionWrapper(undoAnnounce, options)
}
function undoAnnounce (actorUrl: string, announceActivity: ActivityAnnounce) {
return sequelizeTypescript.transaction(async t => {
const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
if (!share) throw new Error(`'Unknown video share ${announceActivity.id}.`)
await share.destroy({ transaction: t })
return undefined
})
}

View File

@ -14,7 +14,7 @@ import { VideoFileModel } from '../../../models/video/video-file'
import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
import { import {
generateThumbnailFromUrl, generateThumbnailFromUrl,
getOrCreateAccountAndVideoAndChannel, getOrCreateAccountAndVideoAndChannel, getOrCreateVideoChannel,
videoActivityObjectToDBAttributes, videoActivityObjectToDBAttributes,
videoFileActivityUrlToDBAttributes videoFileActivityUrlToDBAttributes
} from '../videos' } from '../videos'
@ -54,6 +54,10 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id) const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id)
// Fetch video channel outside the transaction
const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate)
const newVideoChannel = newVideoChannelActor.VideoChannel
logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
let videoInstance = res.video let videoInstance = res.video
let videoFieldsSave: any let videoFieldsSave: any
@ -66,12 +70,13 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
videoFieldsSave = videoInstance.toJSON() videoFieldsSave = videoInstance.toJSON()
// Check actor has the right to update the video
const videoChannel = videoInstance.VideoChannel const videoChannel = videoInstance.VideoChannel
if (videoChannel.Account.Actor.id !== actor.id) { if (videoChannel.Account.Actor.id !== actor.id) {
throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
} }
const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoAttributesToUpdate, activity.to) const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to)
videoInstance.set('name', videoData.name) videoInstance.set('name', videoData.name)
videoInstance.set('uuid', videoData.uuid) videoInstance.set('uuid', videoData.uuid)
videoInstance.set('url', videoData.url) videoInstance.set('url', videoData.url)
@ -87,6 +92,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
videoInstance.set('updatedAt', videoData.updatedAt) videoInstance.set('updatedAt', videoData.updatedAt)
videoInstance.set('views', videoData.views) videoInstance.set('views', videoData.views)
videoInstance.set('privacy', videoData.privacy) videoInstance.set('privacy', videoData.privacy)
videoInstance.set('channelId', videoData.channelId)
await videoInstance.save(sequelizeOptions) await videoInstance.save(sequelizeOptions)

View File

@ -1,5 +1,12 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub' import {
ActivityAnnounce,
ActivityAudience,
ActivityCreate,
ActivityFollow,
ActivityLike,
ActivityUndo
} from '../../../../shared/models/activitypub'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -16,6 +23,8 @@ import {
import { createActivityData, createDislikeActivityData } from './send-create' import { createActivityData, createDislikeActivityData } from './send-create'
import { followActivityData } from './send-follow' import { followActivityData } from './send-follow'
import { likeActivityData } from './send-like' import { likeActivityData } from './send-like'
import { VideoShareModel } from '../../../models/video/video-share'
import { buildVideoAnnounce } from './send-announce'
async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
const me = actorFollow.ActorFollower const me = actorFollow.ActorFollower
@ -58,7 +67,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const dislikeActivity = createDislikeActivityData(byActor, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const object = await createActivityData(undoUrl, byActor, dislikeActivity, t) const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t)
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getOriginVideoAudience(video, actorsInvolvedInVideo) const audience = getOriginVideoAudience(video, actorsInvolvedInVideo)
@ -73,12 +82,24 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
} }
async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
const undoUrl = getUndoActivityPubUrl(videoShare.url)
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const object = await buildVideoAnnounce(byActor, videoShare, video, t)
const data = await undoActivityData(undoUrl, byActor, object, t)
const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
sendUndoFollow, sendUndoFollow,
sendUndoLike, sendUndoLike,
sendUndoDislike sendUndoDislike,
sendUndoAnnounce
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -86,7 +107,7 @@ export {
async function undoActivityData ( async function undoActivityData (
url: string, url: string,
byActor: ActorModel, byActor: ActorModel,
object: ActivityFollow | ActivityLike | ActivityCreate, object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
t: Transaction, t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityUndo> { ): Promise<ActivityUndo> {

View File

@ -3,16 +3,37 @@ import { VideoPrivacy } from '../../../shared/models/videos'
import { getServerActor } from '../../helpers/utils' import { getServerActor } from '../../helpers/utils'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { sendVideoAnnounce } from './send' import { sendUndoAnnounce, sendVideoAnnounce } from './send'
import { getAnnounceActivityPubUrl } from './url' import { getAnnounceActivityPubUrl } from './url'
import { VideoChannelModel } from '../../models/video/video-channel'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
if (video.privacy === VideoPrivacy.PRIVATE) return undefined if (video.privacy === VideoPrivacy.PRIVATE) return undefined
return Promise.all([
shareByServer(video, t),
shareByVideoChannel(video, t)
])
}
async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
await undoShareByVideoChannel(video, oldVideoChannel, t)
await shareByVideoChannel(video, t)
}
export {
changeVideoChannelShare,
shareVideoByServerAndChannel
}
// ---------------------------------------------------------------------------
async function shareByServer (video: VideoModel, t: Transaction) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor)
const serverSharePromise = VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: serverActor.id, actorId: serverActor.id,
videoId: video.id, videoId: video.id,
@ -27,9 +48,11 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
return undefined return undefined
}) })
}
async function shareByVideoChannel (video: VideoModel, t: Transaction) {
const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor)
const videoChannelSharePromise = VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: video.VideoChannel.actorId, actorId: video.VideoChannel.actorId,
videoId: video.id, videoId: video.id,
@ -40,17 +63,17 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
}, },
transaction: t transaction: t
}).then(([ videoChannelShare, created ]) => { }).then(([ videoChannelShare, created ]) => {
if (created) return sendVideoAnnounce(serverActor, videoChannelShare, video, t) if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
return undefined return undefined
}) })
return Promise.all([
serverSharePromise,
videoChannelSharePromise
])
} }
export { async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
shareVideoByServerAndChannel // Load old share
const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id)
await sendUndoAnnounce(oldVideoChannel.Actor, oldShare, video, t)
await oldShare.destroy({ transaction: t })
} }

View File

@ -137,6 +137,13 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObje
return attributes return attributes
} }
function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
const channel = videoObject.attributedTo.find(a => a.type === 'Group')
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
return getOrCreateActorAndServerAndModel(channel.id)
}
async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) { async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) {
logger.debug('Adding remote video %s.', videoObject.id) logger.debug('Adding remote video %s.', videoObject.id)
@ -199,10 +206,7 @@ async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentOb
actor = await getOrCreateActorAndServerAndModel(actorObj.id) actor = await getOrCreateActorAndServerAndModel(actorObj.id)
} }
const channel = videoObject.attributedTo.find(a => a.type === 'Group') const channelActor = await getOrCreateVideoChannel(videoObject)
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
const channelActor = await getOrCreateActorAndServerAndModel(channel.id)
const options = { const options = {
arguments: [ videoObject, channelActor ], arguments: [ videoObject, channelActor ],
@ -301,6 +305,7 @@ export {
videoActivityObjectToDBAttributes, videoActivityObjectToDBAttributes,
videoFileActivityUrlToDBAttributes, videoFileActivityUrlToDBAttributes,
getOrCreateVideo, getOrCreateVideo,
getOrCreateVideoChannel,
addVideoShares} addVideoShares}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -6,6 +6,7 @@ import { isBooleanValid, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntOrNull, t
import { import {
isVideoAbuseReasonValid, isVideoAbuseReasonValid,
isVideoCategoryValid, isVideoCategoryValid,
isVideoChannelOfAccountExist,
isVideoDescriptionValid, isVideoDescriptionValid,
isVideoExist, isVideoExist,
isVideoFile, isVideoFile,
@ -23,7 +24,6 @@ import { logger } from '../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS } from '../../initializers'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoChannelModel } from '../../models/video/video-channel'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { authenticate } from '../oauth' import { authenticate } from '../oauth'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
@ -75,7 +75,10 @@ const videosAddValidator = [
.optional() .optional()
.toInt() .toInt()
.custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'), .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), body('channelId')
.toInt()
.custom(isIdValid)
.withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
@ -86,16 +89,7 @@ const videosAddValidator = [
const videoFile: Express.Multer.File = req.files['videofile'][0] const videoFile: Express.Multer.File = req.files['videofile'][0]
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
const videoChannel = await VideoChannelModel.loadByIdAndAccount(req.body.channelId, user.Account.id) if (!await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
if (!videoChannel) {
res.status(400)
.json({ error: 'Unknown video video channel for this account.' })
.end()
return
}
res.locals.videoChannel = videoChannel
const isAble = await user.isAbleToUploadVideo(videoFile) const isAble = await user.isAbleToUploadVideo(videoFile)
if (isAble === false) { if (isAble === false) {
@ -173,6 +167,10 @@ const videosUpdateValidator = [
.optional() .optional()
.toBoolean() .toBoolean()
.custom(isBooleanValid).withMessage('Should have comments enabled boolean'), .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
body('channelId')
.optional()
.toInt()
.custom(isIdValid).withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videosUpdate parameters', { parameters: req.body }) logger.debug('Checking videosUpdate parameters', { parameters: req.body })
@ -184,7 +182,8 @@ const videosUpdateValidator = [
const video = res.locals.video const video = res.locals.video
// Check if the user who did the request is able to update the video // Check if the user who did the request is able to update the video
if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return const user = res.locals.oauth.token.User
if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) { if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
return res.status(409) return res.status(409)
@ -192,6 +191,8 @@ const videosUpdateValidator = [
.end() .end()
} }
if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user.Account.id, res)) return
return next() return next()
} }
] ]

View File

@ -98,6 +98,15 @@ export class VideoShareModel extends Model<VideoShareModel> {
}) })
} }
static loadByUrl (url: string, t: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: {
url
},
transaction: t
})
}
static loadActorsByShare (videoId: number, t: Sequelize.Transaction) { static loadActorsByShare (videoId: number, t: Sequelize.Transaction) {
const query = { const query = {
where: { where: {

View File

@ -130,11 +130,27 @@ enum ScopeNames {
} }
const videoChannelInclude = { const videoChannelInclude = {
attributes: [ 'name', 'description' ], attributes: [ 'name', 'description', 'id' ],
model: VideoChannelModel.unscoped(), model: VideoChannelModel.unscoped(),
required: true, required: true,
where: {}, where: {},
include: [ include: [
{
attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(),
required: true,
include: [
{
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
},
{
model: AvatarModel.unscoped(),
required: false
}
]
},
accountInclude accountInclude
] ]
} }
@ -771,12 +787,17 @@ export class VideoModel extends Model<VideoModel> {
} }
}, },
{ {
preferredUsername: Sequelize.where(Sequelize.col('preferredUsername'), { preferredUsernameChannel: Sequelize.where(Sequelize.col('VideoChannel->Actor.preferredUsername'), {
[ Sequelize.Op.iLike ]: '%' + value + '%' [ Sequelize.Op.iLike ]: '%' + value + '%'
}) })
}, },
{ {
host: Sequelize.where(Sequelize.col('host'), { preferredUsernameAccount: Sequelize.where(Sequelize.col('VideoChannel->Account->Actor.preferredUsername'), {
[ Sequelize.Op.iLike ]: '%' + value + '%'
})
},
{
host: Sequelize.where(Sequelize.col('VideoChannel->Account->Actor->Server.host'), {
[ Sequelize.Op.iLike ]: '%' + value + '%' [ Sequelize.Op.iLike ]: '%' + value + '%'
}) })
} }
@ -1043,6 +1064,7 @@ export class VideoModel extends Model<VideoModel> {
toFormattedJSON (): Video { toFormattedJSON (): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON() const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
return { return {
id: this.id, id: this.id,
@ -1085,6 +1107,15 @@ export class VideoModel extends Model<VideoModel> {
url: formattedAccount.url, url: formattedAccount.url,
host: formattedAccount.host, host: formattedAccount.host,
avatar: formattedAccount.avatar avatar: formattedAccount.avatar
},
channel: {
id: formattedVideoChannel.id,
uuid: formattedVideoChannel.uuid,
name: formattedVideoChannel.name,
displayName: formattedVideoChannel.displayName,
url: formattedVideoChannel.url,
host: formattedVideoChannel.host,
avatar: formattedVideoChannel.avatar
} }
} }
} }

View File

@ -2,8 +2,8 @@
import * as chai from 'chai' import * as chai from 'chai'
import 'mocha' import 'mocha'
import { User } from '../../../../shared/index' import { User, Video } from '../../../../shared/index'
import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, uploadVideo, wait } from '../../utils' import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, updateVideo, uploadVideo, wait } from '../../utils'
import { import {
addVideoChannel, addVideoChannel,
deleteVideoChannel, deleteVideoChannel,
@ -25,8 +25,11 @@ describe('Test video channels', function () {
let servers: ServerInfo[] let servers: ServerInfo[]
let userInfo: User let userInfo: User
let accountUUID: string let accountUUID: string
let videoChannelId: number let firstVideoChannelId: number
let videoChannelUUID: string let firstVideoChannelUUID: string
let secondVideoChannelId: number
let secondVideoChannelUUID: string
let videoUUID: string
before(async function () { before(async function () {
this.timeout(30000) this.timeout(30000)
@ -42,6 +45,9 @@ describe('Test video channels', function () {
const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
const user: User = res.body const user: User = res.body
accountUUID = user.account.uuid accountUUID = user.account.uuid
firstVideoChannelId = user.videoChannels[0].id
firstVideoChannelUUID = user.videoChannels[0].uuid
} }
await wait(5000) await wait(5000)
@ -58,17 +64,22 @@ describe('Test video channels', function () {
it('Should create another video channel', async function () { it('Should create another video channel', async function () {
this.timeout(10000) this.timeout(10000)
const videoChannel = { {
displayName: 'second video channel', const videoChannel = {
description: 'super video channel description', displayName: 'second video channel',
support: 'super video channel support text' description: 'super video channel description',
support: 'super video channel support text'
}
const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel)
secondVideoChannelId = res.body.videoChannel.id
secondVideoChannelUUID = res.body.videoChannel.uuid
} }
const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
videoChannelId = res.body.videoChannel.id
videoChannelUUID = res.body.videoChannel.uuid
// The channel is 1 is propagated to servers 2 // The channel is 1 is propagated to servers 2
await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my video name', channelId: videoChannelId }) {
const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'my video name', channelId: secondVideoChannelId })
videoUUID = res.body.video.uuid
}
await wait(3000) await wait(3000)
}) })
@ -130,7 +141,7 @@ describe('Test video channels', function () {
support: 'video channel support text updated' support: 'video channel support text updated'
} }
await updateVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId, videoChannelAttributes) await updateVideoChannel(servers[0].url, servers[0].accessToken, secondVideoChannelId, videoChannelAttributes)
await wait(3000) await wait(3000)
}) })
@ -149,7 +160,7 @@ describe('Test video channels', function () {
}) })
it('Should get video channel', async function () { it('Should get video channel', async function () {
const res = await getVideoChannel(servers[0].url, videoChannelId) const res = await getVideoChannel(servers[0].url, secondVideoChannelId)
const videoChannel = res.body const videoChannel = res.body
expect(videoChannel.displayName).to.equal('video channel updated') expect(videoChannel.displayName).to.equal('video channel updated')
@ -157,20 +168,45 @@ describe('Test video channels', function () {
expect(videoChannel.support).to.equal('video channel support text updated') expect(videoChannel.support).to.equal('video channel support text updated')
}) })
it('Should list the video channel videos', async function () { it('Should list the second video channel videos', async function () {
this.timeout(10000) this.timeout(10000)
for (const server of servers) { for (const server of servers) {
const res = await getVideoChannelVideos(server.url, server.accessToken, videoChannelUUID, 0, 5) const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondVideoChannelUUID, 0, 5)
expect(res.body.total).to.equal(1) expect(res1.body.total).to.equal(1)
expect(res.body.data).to.be.an('array') expect(res1.body.data).to.be.an('array')
expect(res.body.data).to.have.lengthOf(1) expect(res1.body.data).to.have.lengthOf(1)
expect(res.body.data[0].name).to.equal('my video name') expect(res1.body.data[0].name).to.equal('my video name')
}
})
it('Should change the video channel of a video', async function () {
this.timeout(10000)
await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { channelId: firstVideoChannelId })
await wait(5000)
})
it('Should list the first video channel videos', async function () {
this.timeout(10000)
for (const server of servers) {
const res1 = await getVideoChannelVideos(server.url, server.accessToken, secondVideoChannelUUID, 0, 5)
expect(res1.body.total).to.equal(0)
const res2 = await getVideoChannelVideos(server.url, server.accessToken, firstVideoChannelUUID, 0, 5)
expect(res2.body.total).to.equal(1)
const videos: Video[] = res2.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(1)
expect(videos[0].name).to.equal('my video name')
} }
}) })
it('Should delete video channel', async function () { it('Should delete video channel', async function () {
await deleteVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId) await deleteVideoChannel(servers[0].url, servers[0].accessToken, secondVideoChannelId)
}) })
it('Should have video channel deleted', async function () { it('Should have video channel deleted', async function () {

View File

@ -15,7 +15,7 @@ import {
ServerInfo, ServerInfo,
testImage testImage
} from '../' } from '../'
import { VideoPrivacy } from '../../../../shared/models/videos' import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos'
import { readdirPromise } from '../../../helpers/core-utils' import { readdirPromise } from '../../../helpers/core-utils'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers' import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers'
import { dateIsValid, webtorrentAdd } from '../index' import { dateIsValid, webtorrentAdd } from '../index'
@ -385,6 +385,7 @@ function updateVideo (url: string, accessToken: string, id: number | string, att
if (attributes.description) body['description'] = attributes.description if (attributes.description) body['description'] = attributes.description
if (attributes.tags) body['tags'] = attributes.tags if (attributes.tags) body['tags'] = attributes.tags
if (attributes.privacy) body['privacy'] = attributes.privacy if (attributes.privacy) body['privacy'] = attributes.privacy
if (attributes.channelId) body['channelId'] = attributes.channelId
// Upload request // Upload request
if (attributes.thumbnailfile || attributes.previewfile) { if (attributes.thumbnailfile || attributes.previewfile) {
@ -489,6 +490,8 @@ async function completeVideoCheck (
expect(video.account.uuid).to.be.a('string') expect(video.account.uuid).to.be.a('string')
expect(video.account.host).to.equal(attributes.account.host) expect(video.account.host).to.equal(attributes.account.host)
expect(video.account.name).to.equal(attributes.account.name) expect(video.account.name).to.equal(attributes.account.name)
expect(video.channel.displayName).to.equal(attributes.channel.name)
expect(video.channel.name).to.have.lengthOf(36)
expect(video.likes).to.equal(attributes.likes) expect(video.likes).to.equal(attributes.likes)
expect(video.dislikes).to.equal(attributes.dislikes) expect(video.dislikes).to.equal(attributes.dislikes)
expect(video.isLocal).to.equal(attributes.isLocal) expect(video.isLocal).to.equal(attributes.isLocal)
@ -498,19 +501,19 @@ async function completeVideoCheck (
expect(dateIsValid(video.updatedAt)).to.be.true expect(dateIsValid(video.updatedAt)).to.be.true
const res = await getVideo(url, video.uuid) const res = await getVideo(url, video.uuid)
const videoDetails = res.body const videoDetails: VideoDetails = res.body
expect(videoDetails.files).to.have.lengthOf(attributes.files.length) expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
expect(videoDetails.tags).to.deep.equal(attributes.tags) expect(videoDetails.tags).to.deep.equal(attributes.tags)
expect(videoDetails.account.name).to.equal(attributes.account.name) expect(videoDetails.account.name).to.equal(attributes.account.name)
expect(videoDetails.account.host).to.equal(attributes.account.host) expect(videoDetails.account.host).to.equal(attributes.account.host)
expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
expect(videoDetails.channel.displayName).to.equal(attributes.channel.name) expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
expect(videoDetails.channel.name).to.have.lengthOf(36) expect(videoDetails.channel.name).to.have.lengthOf(36)
expect(videoDetails.channel.host).to.equal(attributes.account.host)
expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
expect(dateIsValid(videoDetails.channel.createdAt)).to.be.true expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
expect(dateIsValid(videoDetails.channel.updatedAt)).to.be.true expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
for (const attributeFile of attributes.files) { for (const attributeFile of attributes.files) {
const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution) const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)

View File

@ -64,7 +64,7 @@ export interface ActivityAnnounce extends BaseActivity {
export interface ActivityUndo extends BaseActivity { export interface ActivityUndo extends BaseActivity {
type: 'Undo', type: 'Undo',
object: ActivityFollow | ActivityLike | ActivityCreate object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce
} }
export interface ActivityLike extends BaseActivity { export interface ActivityLike extends BaseActivity {

View File

@ -8,7 +8,7 @@ export interface Actor {
host: string host: string
followingCount: number followingCount: number
followersCount: number followersCount: number
createdAt: Date createdAt: Date | string
updatedAt: Date updatedAt: Date | string
avatar: Avatar avatar: Avatar
} }

View File

@ -11,6 +11,7 @@ export interface VideoUpdate {
tags?: string[] tags?: string[]
commentsEnabled?: boolean commentsEnabled?: boolean
nsfw?: boolean nsfw?: boolean
channelId?: number
thumbnailfile?: Blob thumbnailfile?: Blob
previewfile?: Blob previewfile?: Blob
} }

View File

@ -48,6 +48,16 @@ export interface Video {
host: string host: string
avatar: Avatar avatar: Avatar
} }
channel: {
id: number
uuid: string
name: string
displayName: string
url: string
host: string
avatar: Avatar
}
} }
export interface VideoDetails extends Video { export interface VideoDetails extends Video {