From: Chocobozzz Date: Thu, 16 Nov 2017 14:22:39 +0000 (+0100) Subject: Server shares user videos X-Git-Tag: v0.0.1-alpha~212 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=20494f122186bb1bfd82f4c598c4744acea27b0c;p=oweals%2Fpeertube.git Server shares user videos --- diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 76049f496..7b3921770 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -4,10 +4,13 @@ import * as express from 'express' import { database as db } from '../../initializers' import { executeIfActivityPub, localAccountValidator } from '../../middlewares' import { pageToStartAndCount } from '../../helpers' -import { AccountInstance } from '../../models' +import { AccountInstance, VideoChannelInstance } from '../../models' import { activityPubCollectionPagination } from '../../helpers/activitypub' import { ACTIVITY_PUB } from '../../initializers/constants' import { asyncMiddleware } from '../../middlewares/async' +import { videosGetValidator } from '../../middlewares/validators/videos' +import { VideoInstance } from '../../models/video/video-interface' +import { videoChannelsGetValidator } from '../../middlewares/validators/video-channels' const activityPubClientRouter = express.Router() @@ -26,6 +29,16 @@ activityPubClientRouter.get('/account/:name/following', executeIfActivityPub(asyncMiddleware(accountFollowingController)) ) +activityPubClientRouter.get('/videos/watch/:id', + executeIfActivityPub(videosGetValidator), + executeIfActivityPub(asyncMiddleware(videoController)) +) + +activityPubClientRouter.get('/video-channels/:id', + executeIfActivityPub(videoChannelsGetValidator), + executeIfActivityPub(asyncMiddleware(videoChannelController)) +) + // --------------------------------------------------------------------------- export { @@ -63,3 +76,15 @@ async function accountFollowingController (req: express.Request, res: express.Re return res.json(activityPubResult) } + +async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { + const video: VideoInstance = res.locals.video + + return res.json(video.toActivityPubObject()) +} + +async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { + const videoChannel: VideoChannelInstance = res.locals.videoChannel + + return res.json(videoChannel.toActivityPubObject()) +} diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts index 656bc3129..8f3df2550 100644 --- a/server/controllers/api/videos/channel.ts +++ b/server/controllers/api/videos/channel.ts @@ -10,7 +10,7 @@ import { paginationValidator, setPagination, setVideoChannelsSort, - videoChannelGetValidator, + videoChannelsGetValidator, videoChannelsAddValidator, videoChannelsRemoveValidator, videoChannelsSortValidator, @@ -53,7 +53,7 @@ videoChannelRouter.delete('/channels/:id', ) videoChannelRouter.get('/channels/:id', - videoChannelGetValidator, + videoChannelsGetValidator, asyncMiddleware(getVideoChannel) ) diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index b376b8ca2..c710117cd 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -4,13 +4,18 @@ import * as Sequelize from 'sequelize' import * as url from 'url' import { ActivityIconObject } from '../../shared/index' import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' +import { VideoChannelObject } from '../../shared/models/activitypub/objects/video-channel-object' import { ResultList } from '../../shared/models/result-list.model' import { database as db, REMOTE_SCHEME } from '../initializers' import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants' -import { sendAnnounce } from '../lib/activitypub/send-request' +import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/misc' +import { sendVideoAnnounce } from '../lib/activitypub/send-request' +import { sendVideoChannelAnnounce } from '../lib/index' +import { AccountInstance } from '../models/account/account-interface' import { VideoChannelInstance } from '../models/video/video-channel-interface' import { VideoInstance } from '../models/video/video-interface' import { isRemoteAccountValid } from './custom-validators' +import { isVideoChannelObjectValid } from './custom-validators/activitypub/videos' import { logger } from './logger' import { doRequest, doRequestAndSaveToFile } from './requests' import { getServerAccount } from './utils' @@ -34,7 +39,7 @@ async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: videoChannelId: videoChannel.id }, { transaction: t }) - return sendAnnounce(serverAccount, videoChannel, t) + return sendVideoChannelAnnounce(serverAccount, videoChannel, t) } async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) { @@ -45,7 +50,7 @@ async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transactio videoId: video.id }, { transaction: t }) - return sendAnnounce(serverAccount, video, t) + return sendVideoAnnounce(serverAccount, video, t) } function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) { @@ -66,13 +71,27 @@ async function getOrCreateAccount (accountUrl: string) { if (res === undefined) throw new Error('Cannot fetch remote account.') // Save our new account in database - const account = res.account - await account.save() + account = await res.account.save() } return account } +async function getOrCreateVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) { + let videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl) + + // We don't have this account in our database, fetch it on remote + if (!videoChannel) { + videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl) + if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.') + + // Save our new video channel in database + await videoChannel.save() + } + + return videoChannel +} + async function fetchRemoteAccountAndCreateServer (accountUrl: string) { const options = { uri: accountUrl, @@ -131,6 +150,38 @@ async function fetchRemoteAccountAndCreateServer (accountUrl: string) { return { account, server } } +async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) { + const options = { + uri: videoChannelUrl, + method: 'GET', + headers: { + 'Accept': ACTIVITY_PUB_ACCEPT_HEADER + } + } + + logger.info('Fetching remote video channel %s.', videoChannelUrl) + + let requestResult + try { + requestResult = await doRequest(options) + } catch (err) { + logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err) + return undefined + } + + const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body) + if (isVideoChannelObjectValid(videoChannelJSON) === false) { + logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON }) + return undefined + } + + const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount) + const videoChannel = db.VideoChannel.build(videoChannelAttributes) + videoChannel.Account = ownerAccount + + return videoChannel +} + function fetchRemoteVideoPreview (video: VideoInstance) { // FIXME: use url const host = video.VideoChannel.Account.Server.host @@ -200,7 +251,8 @@ export { fetchRemoteVideoPreview, fetchRemoteVideoDescription, shareVideoChannelByServer, - shareVideoByServer + shareVideoByServer, + getOrCreateVideoChannel } // --------------------------------------------------------------------------- diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 08e5ae0aa..8084cf7b0 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -2,8 +2,7 @@ import * as validator from 'validator' import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' import { isActivityPubUrlValid } from './misc' import { - isVideoAnnounceValid, - isVideoChannelAnnounceValid, + isAnnounceValid, isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid, @@ -37,8 +36,7 @@ function isActivityValid (activity: any) { isAccountFollowActivityValid(activity) || isAccountAcceptActivityValid(activity) || isVideoFlagValid(activity) || - isVideoAnnounceValid(activity) || - isVideoChannelAnnounceValid(activity) + isAnnounceValid(activity) } // --------------------------------------------------------------------------- diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 665a63a73..f09a764b6 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts @@ -21,7 +21,7 @@ function isActivityPubUrlValid (url: string) { } function isBaseActivityValid (activity: any, type: string) { - return Array.isArray(activity['@context']) && + return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && activity.type === type && isActivityPubUrlValid(activity.id) && isActivityPubUrlValid(activity.actor) && diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 89c49b0df..8486297ad 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -39,6 +39,7 @@ function isActivityPubVideoDurationValid (value: string) { function isVideoTorrentObjectValid (video: any) { return video.type === 'Video' && + isActivityPubUrlValid(video.id) && isVideoNameValid(video.name) && isActivityPubVideoDurationValid(video.duration) && isUUIDValid(video.uuid) && @@ -62,14 +63,12 @@ function isVideoFlagValid (activity: any) { isActivityPubUrlValid(activity.object) } -function isVideoAnnounceValid (activity: any) { +function isAnnounceValid (activity: any) { return isBaseActivityValid(activity, 'Announce') && - isVideoTorrentObjectValid(activity.object) -} - -function isVideoChannelAnnounceValid (activity: any) { - return isBaseActivityValid(activity, 'Announce') && - isVideoChannelObjectValid(activity.object) + ( + isVideoChannelCreateActivityValid(activity.object) || + isVideoTorrentAddActivityValid(activity.object) + ) } function isVideoChannelCreateActivityValid (activity: any) { @@ -88,8 +87,11 @@ function isVideoChannelDeleteActivityValid (activity: any) { function isVideoChannelObjectValid (videoChannel: any) { return videoChannel.type === 'VideoChannel' && + isActivityPubUrlValid(videoChannel.id) && isVideoChannelNameValid(videoChannel.name) && - isVideoChannelDescriptionValid(videoChannel.description) && + isVideoChannelDescriptionValid(videoChannel.content) && + isDateValid(videoChannel.published) && + isDateValid(videoChannel.updated) && isUUIDValid(videoChannel.uuid) } @@ -103,8 +105,8 @@ export { isVideoChannelDeleteActivityValid, isVideoTorrentDeleteActivityValid, isVideoFlagValid, - isVideoAnnounceValid, - isVideoChannelAnnounceValid + isAnnounceValid, + isVideoChannelObjectValid } // --------------------------------------------------------------------------- @@ -148,8 +150,20 @@ function setValidRemoteVideoUrls (video: any) { function isRemoteVideoUrlValid (url: any) { return url.type === 'Link' && - ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 && - isVideoUrlValid(url.url) && - validator.isInt(url.width + '', { min: 0 }) && - validator.isInt(url.size + '', { min: 0 }) + ( + ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && + isVideoUrlValid(url.url) && + validator.isInt(url.width + '', { min: 0 }) && + validator.isInt(url.size + '', { min: 0 }) + ) || + ( + ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && + isVideoUrlValid(url.url) && + validator.isInt(url.width + '', { min: 0 }) + ) || + ( + ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && + validator.isLength(url.url, { min: 5 }) && + validator.isInt(url.width + '', { min: 0 }) + ) } diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index d62462d35..169b80065 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts @@ -1,10 +1,10 @@ // TODO: import from ES6 when retry typing file will include errorFilter function import * as retry from 'async/retry' - +import * as Bluebird from 'bluebird' import { logger } from './logger' type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] } -function retryTransactionWrapper (functionToRetry: (...args) => Promise, options: RetryTransactionWrapperOptions) { +function retryTransactionWrapper (functionToRetry: (...args) => Promise | Bluebird, options: RetryTransactionWrapperOptions) { const args = options.arguments ? options.arguments : [] return transactionRetryer(callback => { @@ -13,8 +13,8 @@ function retryTransactionWrapper (functionToRetry: (...args) => Promise, op .catch(err => callback(err)) }) .catch(err => { - // Do not throw the error, continue the process logger.error(options.errorMessage, err) + throw err }) } @@ -28,7 +28,7 @@ function transactionRetryer (func: Function) { logger.debug('Maybe retrying the transaction function.', { willRetry }) return willRetry } - }, func, err => err ? rej(err) : res()) + }, func, (err, data) => err ? rej(err) : res(data)) }) } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index dca223370..eeda8347d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -227,13 +227,11 @@ const ACTIVITY_PUB_ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3 const ACTIVITY_PUB = { COLLECTION_ITEMS_PER_PAGE: 10, - VIDEO_URL_MIME_TYPES: [ - 'video/mp4', - 'video/webm', - 'video/ogg', - 'application/x-bittorrent', - 'application/x-bittorrent;x-scheme-handler/magnet' - ] + URL_MIME_TYPES: { + VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT + TORRENT: [ 'application/x-bittorrent' ], + MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] + } } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts index 43d26c328..13838fc4c 100644 --- a/server/lib/activitypub/misc.ts +++ b/server/lib/activitypub/misc.ts @@ -7,6 +7,21 @@ import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants' import { VideoChannelInstance } from '../../models/video/video-channel-interface' import { VideoFileAttributes } from '../../models/video/video-file-interface' import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' +import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' +import { AccountInstance } from '../../models/account/account-interface' + +function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { + return { + name: videoChannelObject.name, + description: videoChannelObject.content, + uuid: videoChannelObject.uuid, + url: videoChannelObject.id, + createdAt: new Date(videoChannelObject.published), + updatedAt: new Date(videoChannelObject.updated), + remote: true, + accountId: account.id + } +} async function videoActivityObjectToDBAttributes ( videoChannel: VideoChannelInstance, @@ -45,26 +60,32 @@ async function videoActivityObjectToDBAttributes ( } function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { - const fileUrls = videoObject.url - .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1 && u.url.startsWith('video/')) + const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) + const fileUrls = videoObject.url.filter(u => { + return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') + }) + + if (fileUrls.length === 0) { + throw new Error('Cannot find video files for ' + videoCreated.url) + } const attributes: VideoFileAttributes[] = [] - for (const url of fileUrls) { + for (const fileUrl of fileUrls) { // Fetch associated magnet uri - const magnet = videoObject.url - .find(u => { - return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width - }) - if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url) + const magnet = videoObject.url.find(u => { + return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width + }) + + if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url) const parsed = magnetUtil.decode(magnet.url) if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) const attribute = { - extname: VIDEO_MIMETYPE_EXT[url.mimeType], + extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType], infoHash: parsed.infoHash, - resolution: url.width, - size: url.size, + resolution: fileUrl.width, + size: fileUrl.size, videoId: videoCreated.id } attributes.push(attribute) @@ -77,5 +98,6 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoO export { videoFileActivityUrlToDBAttributes, - videoActivityObjectToDBAttributes + videoActivityObjectToDBAttributes, + videoChannelActivityObjectToDBAttributes } diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts index 98e414dbb..df7139d46 100644 --- a/server/lib/activitypub/process-add.ts +++ b/server/lib/activitypub/process-add.ts @@ -5,6 +5,8 @@ import { database as db } from '../../initializers' import { AccountInstance } from '../../models/account/account-interface' import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' import Bluebird = require('bluebird') +import { getOrCreateVideoChannel } from '../../helpers/activitypub' +import { VideoChannelInstance } from '../../models/video/video-channel-interface' async function processAddActivity (activity: ActivityAdd) { const activityObject = activity.object @@ -12,7 +14,10 @@ async function processAddActivity (activity: ActivityAdd) { const account = await getOrCreateAccount(activity.actor) if (activityType === 'Video') { - return processAddVideo(account, activity.id, activityObject as VideoTorrentObject) + const videoChannelUrl = activity.target + const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl) + + return processAddVideo(account, videoChannel, activityObject as VideoTorrentObject) } logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) @@ -27,16 +32,16 @@ export { // --------------------------------------------------------------------------- -function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { +function processAddVideo (account: AccountInstance, videoChannel: VideoChannelInstance, video: VideoTorrentObject) { const options = { - arguments: [ account, videoChannelUrl, video ], + arguments: [ account, videoChannel, video ], errorMessage: 'Cannot insert the remote video with many retries.' } return retryTransactionWrapper(addRemoteVideo, options) } -async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) { +function addRemoteVideo (account: AccountInstance, videoChannel: VideoChannelInstance, videoToCreateData: VideoTorrentObject) { logger.debug('Adding remote video %s.', videoToCreateData.url) return db.sequelize.transaction(async t => { @@ -44,9 +49,6 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string transaction: t } - const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t) - if (!videoChannel) throw new Error('Video channel not found.') - if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t) @@ -59,8 +61,11 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string const videoCreated = await video.save(sequelizeOptions) const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) + if (videoFileAttributes.length === 0) { + throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url) + } - const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f)) + const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t })) await Promise.all(tasks) const tags = videoToCreateData.tag.map(t => t.name) @@ -71,5 +76,4 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string return videoCreated }) - } diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process-announce.ts index d67958aec..f9674e095 100644 --- a/server/lib/activitypub/process-announce.ts +++ b/server/lib/activitypub/process-announce.ts @@ -10,38 +10,33 @@ import { VideoChannelInstance } from '../../models/video/video-channel-interface import { VideoInstance } from '../../models/index' async function processAnnounceActivity (activity: ActivityAnnounce) { - const activityType = activity.object.type + const announcedActivity = activity.object const accountAnnouncer = await getOrCreateAccount(activity.actor) - if (activityType === 'VideoChannel') { - const activityCreate = Object.assign(activity, { - type: 'Create' as 'Create', - actor: activity.object.actor, - object: activity.object as VideoChannelObject - }) - + if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { // Add share entry - const videoChannel: VideoChannelInstance = await processCreateActivity(activityCreate) + const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) await db.VideoChannelShare.create({ accountId: accountAnnouncer.id, videoChannelId: videoChannel.id }) - } else if (activityType === 'Video') { - const activityAdd = Object.assign(activity, { - type: 'Add' as 'Add', - actor: activity.object.actor, - object: activity.object as VideoTorrentObject - }) + return undefined + } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') { // Add share entry - const video: VideoInstance = await processAddActivity(activityAdd) + const video: VideoInstance = await processAddActivity(announcedActivity) await db.VideoShare.create({ accountId: accountAnnouncer.id, videoId: video.id }) + + return undefined } - logger.warn('Unknown activity object type %s when announcing activity.', activityType, { activity: activity.id }) + logger.warn( + 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type, + { activity: activity.id } + ) return Promise.resolve(undefined) } diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts index 4e4c9f703..c4706a66b 100644 --- a/server/lib/activitypub/process-create.ts +++ b/server/lib/activitypub/process-create.ts @@ -4,6 +4,7 @@ import { logger, retryTransactionWrapper } from '../../helpers' import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' import { database as db } from '../../initializers' import { AccountInstance } from '../../models/account/account-interface' +import { videoChannelActivityObjectToDBAttributes } from './misc' async function processCreateActivity (activity: ActivityCreate) { const activityObject = activity.object @@ -37,23 +38,14 @@ function processCreateVideoChannel (account: AccountInstance, videoChannelToCrea return retryTransactionWrapper(addRemoteVideoChannel, options) } -async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { +function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) return db.sequelize.transaction(async t => { let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') - const videoChannelData = { - name: videoChannelToCreateData.name, - description: videoChannelToCreateData.content, - uuid: videoChannelToCreateData.uuid, - createdAt: new Date(videoChannelToCreateData.published), - updatedAt: new Date(videoChannelToCreateData.updated), - remote: true, - accountId: account.id - } - + const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) videoChannel = db.VideoChannel.build(videoChannelData) videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) @@ -73,7 +65,7 @@ function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateDa return retryTransactionWrapper(addRemoteVideoAbuse, options) } -async function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { +function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) return db.sequelize.transaction(async t => { diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts index 664b9d826..f9b72f2a8 100644 --- a/server/lib/activitypub/send-request.ts +++ b/server/lib/activitypub/send-request.ts @@ -59,24 +59,21 @@ async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transac return broadcastToFollowers(data, [ account ], t) } -async function sendAnnounce (byAccount: AccountInstance, instance: VideoInstance | VideoChannelInstance, t: Sequelize.Transaction) { - const object = instance.toActivityPubObject() - - let url = '' - let objectActorUrl: string - if ((instance as any).VideoChannel !== undefined) { - objectActorUrl = (instance as VideoInstance).VideoChannel.Account.url - url = getActivityPubUrl('video', instance.uuid) + '#announce' - } else { - objectActorUrl = (instance as VideoChannelInstance).Account.url - url = getActivityPubUrl('videoChannel', instance.uuid) + '#announce' - } +async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { + const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce' + const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), true) + + const data = await announceActivityData(url, byAccount, announcedActivity) + return broadcastToFollowers(data, [ byAccount ], t) +} - const objectWithActor = Object.assign(object, { - actor: objectActorUrl - }) +async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Sequelize.Transaction) { + const url = getActivityPubUrl('video', video.uuid) + '#announce' - const data = await announceActivityData(url, byAccount, objectWithActor) + const videoChannel = video.VideoChannel + const announcedActivity = await addActivityData(url, videoChannel.Account, videoChannel.url, video.toActivityPubObject(), true) + + const data = await announceActivityData(url, byAccount, announcedActivity) return broadcastToFollowers(data, [ byAccount ], t) } @@ -117,7 +114,8 @@ export { sendAccept, sendFollow, sendVideoAbuse, - sendAnnounce + sendVideoChannelAnnounce, + sendVideoAnnounce } // --------------------------------------------------------------------------- @@ -159,7 +157,7 @@ async function getPublicActivityTo (account: AccountInstance) { return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public') } -async function createActivityData (url: string, byAccount: AccountInstance, object: any) { +async function createActivityData (url: string, byAccount: AccountInstance, object: any, raw = false) { const to = await getPublicActivityTo(byAccount) const base = { type: 'Create', @@ -169,6 +167,8 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje object } + if (raw === true) return base + return buildSignedActivity(byAccount, base) } @@ -195,7 +195,7 @@ async function deleteActivityData (url: string, byAccount: AccountInstance) { return buildSignedActivity(byAccount, base) } -async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) { +async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any, raw = false) { const to = await getPublicActivityTo(byAccount) const base = { type: 'Add', @@ -206,6 +206,8 @@ async function addActivityData (url: string, byAccount: AccountInstance, target: target } + if (raw === true) return base + return buildSignedActivity(byAccount, base) } diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index f0ead24e3..0326e05b9 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -83,7 +83,7 @@ const videoChannelsRemoveValidator = [ } ] -const videoChannelGetValidator = [ +const videoChannelsGetValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), (req: express.Request, res: express.Response, next: express.NextFunction) => { @@ -102,7 +102,7 @@ export { videoChannelsAddValidator, videoChannelsUpdateValidator, videoChannelsRemoveValidator, - videoChannelGetValidator + videoChannelsGetValidator } // --------------------------------------------------------------------------- diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 1f4604f1d..f8414d4a8 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -264,7 +264,8 @@ loadByUrl = function (url: string, t?: Sequelize.Transaction) { const query: Sequelize.FindOptions = { where: { url - } + }, + include: [ VideoChannel['sequelize'].models.Account ] } if (t !== undefined) query.transaction = t diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index b858bf759..f8e982fbb 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -24,6 +24,7 @@ export interface ActivityCreate extends BaseActivity { export interface ActivityAdd extends BaseActivity { type: 'Add' + target: string object: VideoTorrentObject } @@ -52,5 +53,5 @@ export interface ActivityAccept extends BaseActivity { export interface ActivityAnnounce extends BaseActivity { type: 'Announce' - object: VideoChannelObject | VideoTorrentObject + object: ActivityCreate | ActivityAdd }