diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index eee89e2fd..41272bca0 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -1,22 +1,26 @@ // Intercept ActivityPub client requests import * as express from 'express' +import { pageToStartAndCount } from '../../helpers' +import { activityPubCollectionPagination } from '../../helpers/activitypub' import { database as db } from '../../initializers' -import { executeIfActivityPub, localAccountValidator } from '../../middlewares' -import { pageToStartAndCount } from '../../helpers' -import { AccountInstance, VideoChannelInstance } from '../../models' -import { activityPubCollectionPagination } from '../../helpers/activitypub' import { ACTIVITY_PUB, CONFIG } from '../../initializers/constants' +import { buildVideoChannelAnnounceToFollowers } from '../../lib/activitypub/send/send-announce' +import { buildVideoAnnounceToFollowers } from '../../lib/index' +import { executeIfActivityPub, localAccountValidator } from '../../middlewares' import { asyncMiddleware } from '../../middlewares/async' -import { videosGetValidator } from '../../middlewares/validators/videos' +import { videoChannelsGetValidator, videoChannelsShareValidator } from '../../middlewares/validators/video-channels' +import { videosGetValidator, videosShareValidator } from '../../middlewares/validators/videos' +import { AccountInstance, VideoChannelInstance } from '../../models' +import { VideoChannelShareInstance } from '../../models/video/video-channel-share-interface' import { VideoInstance } from '../../models/video/video-interface' -import { videoChannelsGetValidator } from '../../middlewares/validators/video-channels' +import { VideoShareInstance } from '../../models/video/video-share-interface' const activityPubClientRouter = express.Router() activityPubClientRouter.get('/account/:name', executeIfActivityPub(localAccountValidator), - executeIfActivityPub(asyncMiddleware(accountController)) + executeIfActivityPub(accountController) ) activityPubClientRouter.get('/account/:name/followers', @@ -31,7 +35,12 @@ activityPubClientRouter.get('/account/:name/following', activityPubClientRouter.get('/videos/watch/:id', executeIfActivityPub(videosGetValidator), - executeIfActivityPub(asyncMiddleware(videoController)) + executeIfActivityPub(videoController) +) + +activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', + executeIfActivityPub(asyncMiddleware(videosShareValidator)), + executeIfActivityPub(asyncMiddleware(videoAnnounceController)) ) activityPubClientRouter.get('/video-channels/:id', @@ -39,6 +48,11 @@ activityPubClientRouter.get('/video-channels/:id', executeIfActivityPub(asyncMiddleware(videoChannelController)) ) +activityPubClientRouter.get('/video-channels/:id/announces/:accountId', + executeIfActivityPub(asyncMiddleware(videoChannelsShareValidator)), + executeIfActivityPub(asyncMiddleware(videoChannelAnnounceController)) +) + // --------------------------------------------------------------------------- export { @@ -47,7 +61,7 @@ export { // --------------------------------------------------------------------------- -async function accountController (req: express.Request, res: express.Response, next: express.NextFunction) { +function accountController (req: express.Request, res: express.Response, next: express.NextFunction) { const account: AccountInstance = res.locals.account return res.json(account.toActivityPubObject()).end() @@ -77,12 +91,26 @@ 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) { +function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { const video: VideoInstance = res.locals.video return res.json(video.toActivityPubObject()) } +async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { + const share = res.locals.videoShare as VideoShareInstance + const object = await buildVideoAnnounceToFollowers(share.Account, res.locals.video, undefined) + + return res.json(object) +} + +async function videoChannelAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { + const share = res.locals.videoChannelShare as VideoChannelShareInstance + const object = await buildVideoChannelAnnounceToFollowers(share.Account, share.VideoChannel, undefined) + + return res.json(object) +} + async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { const videoChannel: VideoChannelInstance = res.locals.videoChannel diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts index fe0fc650a..a6d7f2b82 100644 --- a/server/helpers/custom-validators/accounts.ts +++ b/server/helpers/custom-validators/accounts.ts @@ -1,4 +1,4 @@ -import * as Promise from 'bluebird' +import * as Bluebird from 'bluebird' import * as express from 'express' import 'express-validator' import * as validator from 'validator' @@ -11,33 +11,45 @@ function isAccountNameValid (value: string) { return isUserUsernameValid(value) } -function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) { - let promise: Promise - if (validator.isInt(id)) { +function checkAccountIdExists (id: number | string, res: express.Response, callback: (err: Error, account: AccountInstance) => any) { + let promise: Bluebird + + if (validator.isInt('' + id)) { promise = db.Account.load(+id) } else { // UUID - promise = db.Account.loadByUUID(id) + promise = db.Account.loadByUUID('' + id) } - promise.then(account => { + return checkAccountExists(promise, res, callback) +} + +function checkLocalAccountNameExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => any) { + const p = db.Account.loadLocalByName(name) + + return checkAccountExists(p, res, callback) +} + +function checkAccountExists (p: Bluebird, res: express.Response, callback: (err: Error, account: AccountInstance) => any) { + p.then(account => { if (!account) { return res.status(404) - .json({ error: 'Video account not found' }) + .send({ error: 'Account not found' }) .end() } res.locals.account = account - callback() - }) - .catch(err => { - logger.error('Error in video account request validator.', err) - return res.sendStatus(500) + return callback(null, account) }) + .catch(err => { + logger.error('Error in account request validator.', err) + return res.sendStatus(500) + }) } // --------------------------------------------------------------------------- export { - checkVideoAccountExists, + checkAccountIdExists, + checkLocalAccountNameExists, isAccountNameValid } diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts index 5de01f74b..267d987fc 100644 --- a/server/helpers/custom-validators/video-channels.ts +++ b/server/helpers/custom-validators/video-channels.ts @@ -1,14 +1,14 @@ -import * as Promise from 'bluebird' -import * as validator from 'validator' +import * as Bluebird from 'bluebird' import * as express from 'express' import 'express-validator' import 'multer' +import * as validator from 'validator' -import { database as db, CONSTRAINTS_FIELDS } from '../../initializers' +import { CONSTRAINTS_FIELDS, database as db } from '../../initializers' import { VideoChannelInstance } from '../../models' import { logger } from '../logger' -import { exists } from './misc' import { isActivityPubUrlValid } from './index' +import { exists } from './misc' const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS @@ -25,7 +25,7 @@ function isVideoChannelNameValid (value: string) { } function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) { - let promise: Promise + let promise: Bluebird if (validator.isInt(id)) { promise = db.VideoChannel.loadAndPopulateAccount(+id) } else { // UUID @@ -48,11 +48,32 @@ function checkVideoChannelExists (id: string, res: express.Response, callback: ( }) } +async function isVideoChannelExistsPromise (id: string, res: express.Response) { + let videoChannel: VideoChannelInstance + if (validator.isInt(id)) { + videoChannel = await db.VideoChannel.loadAndPopulateAccount(+id) + } else { // UUID + videoChannel = await db.VideoChannel.loadByUUIDAndPopulateAccount(id) + } + + if (!videoChannel) { + res.status(404) + .json({ error: 'Video channel not found' }) + .end() + + return false + } + + res.locals.videoChannel = videoChannel + return true +} + // --------------------------------------------------------------------------- export { isVideoChannelDescriptionValid, - isVideoChannelNameValid, checkVideoChannelExists, + isVideoChannelNameValid, + isVideoChannelExistsPromise, isVideoChannelUrlValid } diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 205d8c62f..276354626 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -130,6 +130,27 @@ function checkVideoExists (id: string, res: Response, callback: () => void) { }) } +async function isVideoExistsPromise (id: string, res: Response) { + let video: VideoInstance + + if (validator.isInt(id)) { + video = await db.Video.loadAndPopulateAccountAndServerAndTags(+id) + } else { // UUID + video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(id) + } + + if (!video) { + res.status(404) + .json({ error: 'Video not found' }) + .end() + + return false + } + + res.locals.video = video + return true +} + // --------------------------------------------------------------------------- export { @@ -152,5 +173,6 @@ export { isVideoPrivacyValid, isVideoFileResolutionValid, isVideoFileSizeValid, - checkVideoExists + checkVideoExists, + isVideoExistsPromise } diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts index eefbe2884..f20e588ab 100644 --- a/server/lib/activitypub/process/misc.ts +++ b/server/lib/activitypub/process/misc.ts @@ -1,13 +1,16 @@ import * as magnetUtil from 'magnet-uri' import { VideoTorrentObject } from '../../../../shared' import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object' +import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos' +import { doRequest } from '../../../helpers/requests' +import { database as db } from '../../../initializers' import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants' import { AccountInstance } from '../../../models/account/account-interface' 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 { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' +import { getOrCreateAccountAndServer } from '../account' function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { return { @@ -97,10 +100,60 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoO return attributes } +async function addVideoShares (instance: VideoInstance, shares: string[]) { + for (const share of shares) { + // Fetch url + const json = await doRequest({ + uri: share, + json: true + }) + const actor = json['actor'] + if (!actor) continue + + const account = await getOrCreateAccountAndServer(actor) + + const entry = { + accountId: account.id, + videoId: instance.id + } + + await db.VideoShare.findOrCreate({ + where: entry, + defaults: entry + }) + } +} + +async function addVideoChannelShares (instance: VideoChannelInstance, shares: string[]) { + for (const share of shares) { + // Fetch url + const json = await doRequest({ + uri: share, + json: true + }) + const actor = json['actor'] + if (!actor) continue + + const account = await getOrCreateAccountAndServer(actor) + + const entry = { + accountId: account.id, + videoChannelId: instance.id + } + + await db.VideoChannelShare.findOrCreate({ + where: entry, + defaults: entry + }) + } +} + // --------------------------------------------------------------------------- export { videoFileActivityUrlToDBAttributes, videoActivityObjectToDBAttributes, - videoChannelActivityObjectToDBAttributes + videoChannelActivityObjectToDBAttributes, + addVideoChannelShares, + addVideoShares } diff --git a/server/lib/activitypub/process/process-add.ts b/server/lib/activitypub/process/process-add.ts index 98280b9f0..e6bf63eb2 100644 --- a/server/lib/activitypub/process/process-add.ts +++ b/server/lib/activitypub/process/process-add.ts @@ -11,7 +11,7 @@ import { VideoInstance } from '../../../models/video/video-interface' import { getOrCreateAccountAndServer } from '../account' import { getOrCreateVideoChannel } from '../video-channels' import { generateThumbnailFromUrl } from '../videos' -import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' +import { addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' async function processAddActivity (activity: ActivityAdd) { const activityObject = activity.object @@ -37,12 +37,10 @@ export { // --------------------------------------------------------------------------- -async function processAddVideo ( - account: AccountInstance, - activity: ActivityAdd, - videoChannel: VideoChannelInstance, - videoToCreateData: VideoTorrentObject -) { +async function processAddVideo (account: AccountInstance, + activity: ActivityAdd, + videoChannel: VideoChannelInstance, + videoToCreateData: VideoTorrentObject) { const options = { arguments: [ account, activity, videoChannel, videoToCreateData ], errorMessage: 'Cannot insert the remote video with many retries.' @@ -59,6 +57,10 @@ async function processAddVideo ( await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike') } + if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) { + await addVideoShares(video, videoToCreateData.shares.orderedItems) + } + return video } diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index d8532d3a1..2aa9ad5c7 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts @@ -1,34 +1,23 @@ -import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity' +import { ActivityAdd, ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub/activity' +import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { database as db } from '../../../initializers/index' +import { AccountInstance } from '../../../models/account/account-interface' import { VideoInstance } from '../../../models/index' import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { getOrCreateAccountAndServer } from '../account' +import { forwardActivity } from '../send/misc' import { processAddActivity } from './process-add' import { processCreateActivity } from './process-create' -import { getOrCreateAccountAndServer } from '../account' async function processAnnounceActivity (activity: ActivityAnnounce) { const announcedActivity = activity.object const accountAnnouncer = await getOrCreateAccountAndServer(activity.actor) if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { - // Add share entry - const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) - await db.VideoChannelShare.create({ - accountId: accountAnnouncer.id, - videoChannelId: videoChannel.id - }) - - return undefined + return processVideoChannelShare(accountAnnouncer, activity) } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') { - // Add share entry - const video: VideoInstance = await processAddActivity(announcedActivity) - await db.VideoShare.create({ - accountId: accountAnnouncer.id, - videoId: video.id - }) - - return undefined + return processVideoShare(accountAnnouncer, activity) } logger.warn( @@ -44,3 +33,78 @@ async function processAnnounceActivity (activity: ActivityAnnounce) { export { processAnnounceActivity } + +// --------------------------------------------------------------------------- + +function processVideoChannelShare (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) { + const options = { + arguments: [ accountAnnouncer, activity ], + errorMessage: 'Cannot share the video channel with many retries.' + } + + return retryTransactionWrapper(shareVideoChannel, options) +} + +async function shareVideoChannel (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) { + const announcedActivity = activity.object as ActivityCreate + + return db.sequelize.transaction(async t => { + // Add share entry + const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) + const share = { + accountId: accountAnnouncer.id, + videoChannelId: videoChannel.id + } + + const [ , created ] = await db.VideoChannelShare.findOrCreate({ + where: share, + defaults: share, + transaction: t + }) + + if (videoChannel.isOwned() && created === true) { + // Don't resend the activity to the sender + const exceptions = [ accountAnnouncer ] + await forwardActivity(activity, t, exceptions) + } + + return undefined + }) +} + +function processVideoShare (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) { + const options = { + arguments: [ accountAnnouncer, activity ], + errorMessage: 'Cannot share the video with many retries.' + } + + return retryTransactionWrapper(shareVideo, options) +} + +function shareVideo (accountAnnouncer: AccountInstance, activity: ActivityAnnounce) { + const announcedActivity = activity.object as ActivityAdd + + return db.sequelize.transaction(async t => { + // Add share entry + const video: VideoInstance = await processAddActivity(announcedActivity) + + const share = { + accountId: accountAnnouncer.id, + videoId: video.id + } + + const [ , created ] = await db.VideoShare.findOrCreate({ + where: share, + defaults: share, + transaction: t + }) + + if (video.isOwned() && created === true) { + // Don't resend the activity to the sender + const exceptions = [ accountAnnouncer ] + await forwardActivity(activity, t, exceptions) + } + + return undefined + }) +} diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1f982598b..c88082bbf 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -8,7 +8,7 @@ import { AccountInstance } from '../../../models/account/account-interface' import { getOrCreateAccountAndServer } from '../account' import { forwardActivity } from '../send/misc' import { getVideoChannelActivityPubUrl } from '../url' -import { videoChannelActivityObjectToDBAttributes } from './misc' +import { addVideoChannelShares, videoChannelActivityObjectToDBAttributes } from './misc' async function processCreateActivity (activity: ActivityCreate) { const activityObject = activity.object @@ -92,13 +92,19 @@ async function processCreateView (byAccount: AccountInstance, activity: Activity } } -function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { +async function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { const options = { arguments: [ account, videoChannelToCreateData ], errorMessage: 'Cannot insert the remote video channel with many retries.' } - return retryTransactionWrapper(addRemoteVideoChannel, options) + const videoChannel = await retryTransactionWrapper(addRemoteVideoChannel, options) + + if (videoChannelToCreateData.shares && Array.isArray(videoChannelToCreateData.shares.orderedItems)) { + await addVideoChannelShares(videoChannel, videoChannelToCreateData.shares.orderedItems) + } + + return videoChannel } function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index 444c1cbd6..fd1add68e 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts @@ -1,13 +1,14 @@ import { Transaction } from 'sequelize' +import { Activity } from '../../../../shared/models/activitypub/activity' import { logger } from '../../../helpers/logger' import { ACTIVITY_PUB, database as db } from '../../../initializers' import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/index' +import { VideoInstance } from '../../../models/video/video-interface' import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' -import { VideoInstance } from '../../../models/video/video-interface' -import { Activity } from '../../../../shared/models/activitypub/activity' async function forwardActivity ( activity: Activity, @@ -85,9 +86,16 @@ function getOriginVideoAudience (video: VideoInstance, accountsInvolvedInVideo: } } -function getVideoFollowersAudience (accountsInvolvedInVideo: AccountInstance[]) { +function getOriginVideoChannelAudience (videoChannel: VideoChannelInstance, accountsInvolved: AccountInstance[]) { return { - to: accountsInvolvedInVideo.map(a => a.followersUrl), + to: [ videoChannel.Account.url ], + cc: accountsInvolved.map(a => a.followersUrl) + } +} + +function getObjectFollowersAudience (accountsInvolvedInObject: AccountInstance[]) { + return { + to: accountsInvolvedInObject.map(a => a.followersUrl), cc: [] } } @@ -99,6 +107,13 @@ async function getAccountsInvolvedInVideo (video: VideoInstance) { return accountsToForwardView } +async function getAccountsInvolvedInVideoChannel (videoChannel: VideoChannelInstance) { + const accountsToForwardView = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) + accountsToForwardView.push(videoChannel.Account) + + return accountsToForwardView +} + async function getAudience (accountSender: AccountInstance, isPublic = true) { const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() @@ -131,10 +146,12 @@ async function computeFollowerUris (toAccountFollower: AccountInstance[], follow export { broadcastToFollowers, + getOriginVideoChannelAudience, unicastTo, getAudience, getOriginVideoAudience, getAccountsInvolvedInVideo, - getVideoFollowersAudience, + getAccountsInvolvedInVideoChannel, + getObjectFollowersAudience, forwardActivity } diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index b8ea51bc0..efc23af46 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts @@ -1,34 +1,96 @@ import { Transaction } from 'sequelize' import { ActivityAdd } from '../../../../shared/index' -import { ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub/activity' +import { ActivityAnnounce, ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub/activity' import { AccountInstance, VideoInstance } from '../../../models' import { VideoChannelInstance } from '../../../models/video/video-channel-interface' import { getAnnounceActivityPubUrl } from '../url' -import { broadcastToFollowers } from './misc' +import { + broadcastToFollowers, + getAccountsInvolvedInVideo, + getAccountsInvolvedInVideoChannel, + getAudience, + getObjectFollowersAudience, + getOriginVideoAudience, + getOriginVideoChannelAudience, + unicastTo +} from './misc' import { addActivityData } from './send-add' import { createActivityData } from './send-create' -async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { +async function buildVideoAnnounceToFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { const url = getAnnounceActivityPubUrl(video.url, byAccount) const videoChannel = video.VideoChannel const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) - const data = await announceActivityData(url, byAccount, announcedActivity) + const accountsToForwardView = await getAccountsInvolvedInVideo(video) + const audience = getObjectFollowersAudience(accountsToForwardView) + const data = await announceActivityData(url, byAccount, announcedActivity, audience) + + return data +} + +async function sendVideoAnnounceToFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const data = await buildVideoAnnounceToFollowers(byAccount, video, t) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) } -async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { +async function sendVideoAnnounceToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(video.url, byAccount) + + const videoChannel = video.VideoChannel + const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) + + const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) + const audience = getOriginVideoAudience(video, accountsInvolvedInVideo) + const data = await createActivityData(url, byAccount, announcedActivity, audience) + + return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t) +} + +async function buildVideoChannelAnnounceToFollowers (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount) const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) - const data = await announceActivityData(url, byAccount, announcedActivity) + const accountsToForwardView = await getAccountsInvolvedInVideoChannel(videoChannel) + const audience = getObjectFollowersAudience(accountsToForwardView) + const data = await announceActivityData(url, byAccount, announcedActivity, audience) + + return data +} + +async function sendVideoChannelAnnounceToFollowers (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { + const data = await buildVideoChannelAnnounceToFollowers(byAccount, videoChannel, t) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) } -async function announceActivityData (url: string, byAccount: AccountInstance, object: ActivityCreate | ActivityAdd) { +async function sendVideoChannelAnnounceToOrigin (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount) + const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) + + const accountsInvolvedInVideo = await getAccountsInvolvedInVideoChannel(videoChannel) + const audience = getOriginVideoChannelAudience(videoChannel, accountsInvolvedInVideo) + const data = await createActivityData(url, byAccount, announcedActivity, audience) + + return unicastTo(data, byAccount, videoChannel.Account.sharedInboxUrl, t) +} + +async function announceActivityData ( + url: string, + byAccount: AccountInstance, + object: ActivityCreate | ActivityAdd, + audience?: ActivityAudience +) { + if (!audience) { + audience = await getAudience(byAccount) + } + const activity: ActivityAnnounce = { type: 'Announce', + to: audience.to, + cc: audience.cc, id: url, actor: byAccount.url, object @@ -40,7 +102,11 @@ async function announceActivityData (url: string, byAccount: AccountInstance, ob // --------------------------------------------------------------------------- export { - sendVideoAnnounce, - sendVideoChannelAnnounce, - announceActivityData + sendVideoAnnounceToFollowers, + sendVideoChannelAnnounceToFollowers, + sendVideoAnnounceToOrigin, + sendVideoChannelAnnounceToOrigin, + announceActivityData, + buildVideoAnnounceToFollowers, + buildVideoChannelAnnounceToFollowers } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 113d89233..bf66606c1 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -9,7 +9,7 @@ import { getAccountsInvolvedInVideo, getAudience, getOriginVideoAudience, - getVideoFollowersAudience, + getObjectFollowersAudience, unicastTo } from './misc' @@ -47,7 +47,7 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video const viewActivity = createViewActivityData(byAccount, video) const accountsToForwardView = await getAccountsInvolvedInVideo(video) - const audience = getVideoFollowersAudience(accountsToForwardView) + const audience = getObjectFollowersAudience(accountsToForwardView) const data = await createActivityData(url, byAccount, viewActivity, audience) // Use the server account to send the view, because it could be an unregistered account @@ -72,7 +72,7 @@ async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, vi const dislikeActivity = createDislikeActivityData(byAccount, video) const accountsToForwardView = await getAccountsInvolvedInVideo(video) - const audience = getVideoFollowersAudience(accountsToForwardView) + const audience = getObjectFollowersAudience(accountsToForwardView) const data = await createActivityData(url, byAccount, dislikeActivity, audience) const followersException = [ byAccount ] diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 8ca775bf3..41b879b8a 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -7,7 +7,7 @@ import { getAccountsInvolvedInVideo, getAudience, getOriginVideoAudience, - getVideoFollowersAudience, + getObjectFollowersAudience, unicastTo } from './misc' @@ -25,7 +25,7 @@ async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: Vide const url = getVideoLikeActivityPubUrl(byAccount, video) const accountsInvolvedInVideo = await getAccountsInvolvedInVideo(video) - const audience = getVideoFollowersAudience(accountsInvolvedInVideo) + const audience = getObjectFollowersAudience(accountsInvolvedInVideo) const data = await likeActivityData(url, byAccount, video, audience) const toAccountsFollowers = await getAccountsInvolvedInVideo(video) diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 79fc113f0..9b732df40 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -10,7 +10,7 @@ import { AccountInstance } from '../../../models' import { AccountFollowInstance } from '../../../models/account/account-follow-interface' import { VideoInstance } from '../../../models/video/video-interface' import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' -import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getVideoFollowersAudience, unicastTo } from './misc' +import { broadcastToFollowers, getAccountsInvolvedInVideo, getAudience, getObjectFollowersAudience, unicastTo } from './misc' import { createActivityData, createDislikeActivityData } from './send-create' import { followActivityData } from './send-follow' import { likeActivityData } from './send-like' @@ -43,7 +43,7 @@ async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video: const undoUrl = getUndoActivityPubUrl(likeUrl) const toAccountsFollowers = await getAccountsInvolvedInVideo(video) - const audience = getVideoFollowersAudience(toAccountsFollowers) + const audience = getObjectFollowersAudience(toAccountsFollowers) const object = await likeActivityData(likeUrl, byAccount, video) const data = await undoActivityData(undoUrl, byAccount, object, audience) diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 689e200a6..e14b0f50c 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts @@ -3,7 +3,7 @@ import { getServerAccount } from '../../helpers/utils' import { database as db } from '../../initializers' import { VideoChannelInstance } from '../../models/index' import { VideoInstance } from '../../models/video/video-interface' -import { sendVideoAnnounce, sendVideoChannelAnnounce } from './send/send-announce' +import { sendVideoAnnounceToFollowers, sendVideoChannelAnnounceToFollowers } from './send/send-announce' async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Transaction) { const serverAccount = await getServerAccount() @@ -13,7 +13,7 @@ async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: videoChannelId: videoChannel.id }, { transaction: t }) - return sendVideoChannelAnnounce(serverAccount, videoChannel, t) + return sendVideoChannelAnnounceToFollowers(serverAccount, videoChannel, t) } async function shareVideoByServer (video: VideoInstance, t: Transaction) { @@ -24,7 +24,7 @@ async function shareVideoByServer (video: VideoInstance, t: Transaction) { videoId: video.id }, { transaction: t }) - return sendVideoAnnounce(serverAccount, video, t) + return sendVideoAnnounceToFollowers(serverAccount, video, t) } export { diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 17395a99b..6475c4218 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -22,37 +22,37 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) { } function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) { - return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString() + return video.url + '/views/' + byAccount.uuid + '/' + new Date().toISOString() } function getVideoLikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) { - return byAccount.url + '#likes/' + video.id + return byAccount.url + '/likes/' + video.id } function getVideoDislikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) { - return byAccount.url + '#dislikes/' + video.id + return byAccount.url + '/dislikes/' + video.id } function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) { const me = accountFollow.AccountFollower const following = accountFollow.AccountFollowing - return me.url + '#follows/' + following.id + return me.url + '/follows/' + following.id } function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) { const follower = accountFollow.AccountFollower const me = accountFollow.AccountFollowing - return follower.url + '#accepts/follows/' + me.id + return follower.url + '/accepts/follows/' + me.id } function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) { - return originalUrl + '#announces/' + byAccount.id + return originalUrl + '/announces/' + byAccount.id } function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) { - return originalUrl + '#updates/' + updatedAt + return originalUrl + '/updates/' + updatedAt } function getUndoActivityPubUrl (originalUrl: string) { diff --git a/server/middlewares/async.ts b/server/middlewares/async.ts index 29ebd169d..9692f9be7 100644 --- a/server/middlewares/async.ts +++ b/server/middlewares/async.ts @@ -1,10 +1,18 @@ -import { Request, Response, NextFunction } from 'express' +import { Request, Response, NextFunction, RequestHandler } from 'express' +import { eachSeries } from 'async' // Syntactic sugar to avoid try/catch in express controllers // Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 -function asyncMiddleware (fn: (req: Request, res: Response, next: NextFunction) => Promise) { +function asyncMiddleware (fun: RequestHandler | RequestHandler[]) { return (req: Request, res: Response, next: NextFunction) => { - return Promise.resolve(fn(req, res, next)) + if (Array.isArray(fun) === true) { + return eachSeries(fun as RequestHandler[], (f, cb) => { + Promise.resolve(f(req, res, cb)) + .catch(next) + }, next) + } + + return Promise.resolve((fun as RequestHandler)(req, res, next)) .catch(next) } } diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts index 07ae76b63..47ed6a7bb 100644 --- a/server/middlewares/validators/account.ts +++ b/server/middlewares/validators/account.ts @@ -1,9 +1,7 @@ import * as express from 'express' import { param } from 'express-validator/check' import { logger } from '../../helpers' -import { isAccountNameValid } from '../../helpers/custom-validators/accounts' -import { database as db } from '../../initializers/database' -import { AccountInstance } from '../../models' +import { checkLocalAccountNameExists, isAccountNameValid } from '../../helpers/custom-validators/accounts' import { checkErrors } from './utils' const localAccountValidator = [ @@ -13,7 +11,7 @@ const localAccountValidator = [ logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) checkErrors(req, res, () => { - checkLocalAccountExists(req.params.name, res, next) + checkLocalAccountNameExists(req.params.name, res, next) }) } ] @@ -23,23 +21,3 @@ const localAccountValidator = [ export { localAccountValidator } - -// --------------------------------------------------------------------------- - -function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) { - db.Account.loadLocalByName(name) - .then(account => { - if (!account) { - return res.status(404) - .send({ error: 'Account not found' }) - .end() - } - - res.locals.account = account - return callback(null, account) - }) - .catch(err => { - logger.error('Error in account request validator.', err) - return res.sendStatus(500) - }) -} diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts index ea107bbe8..77a1a0d4b 100644 --- a/server/middlewares/validators/utils.ts +++ b/server/middlewares/validators/utils.ts @@ -14,8 +14,22 @@ function checkErrors (req: express.Request, res: express.Response, next: express return next() } +function areValidationErrors (req: express.Request, res: express.Response) { + const errors = validationResult(req) + + if (!errors.isEmpty()) { + logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) + res.status(400).json({ errors: errors.mapped() }) + + return true + } + + return false +} + // --------------------------------------------------------------------------- export { - checkErrors + checkErrors, + areValidationErrors } diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index c6fd3b59d..f30fbf0dc 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -1,13 +1,19 @@ import * as express from 'express' import { body, param } from 'express-validator/check' import { UserRight } from '../../../shared' -import { checkVideoAccountExists } from '../../helpers/custom-validators/accounts' -import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' -import { checkVideoChannelExists, isIdOrUUIDValid } from '../../helpers/index' +import { checkAccountIdExists } from '../../helpers/custom-validators/accounts' +import { isIdValid } from '../../helpers/custom-validators/misc' +import { + checkVideoChannelExists, + isVideoChannelDescriptionValid, + isVideoChannelExistsPromise, + isVideoChannelNameValid +} from '../../helpers/custom-validators/video-channels' +import { isIdOrUUIDValid } from '../../helpers/index' import { logger } from '../../helpers/logger' import { database as db } from '../../initializers' import { UserInstance } from '../../models' -import { checkErrors } from './utils' +import { areValidationErrors, checkErrors } from './utils' const listVideoAccountChannelsValidator = [ param('accountId').custom(isIdOrUUIDValid).withMessage('Should have a valid account id'), @@ -16,7 +22,7 @@ const listVideoAccountChannelsValidator = [ logger.debug('Checking listVideoAccountChannelsValidator parameters', { parameters: req.body }) checkErrors(req, res, () => { - checkVideoAccountExists(req.params.accountId, res, next) + checkAccountIdExists(req.params.accountId, res, next) }) } ] @@ -90,6 +96,28 @@ const videoChannelsGetValidator = [ } ] +const videoChannelsShareValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelShare parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoChannelExistsPromise(req.params.id, res)) return + + const share = await db.VideoChannelShare.load(res.locals.video.id, req.params.accountId) + if (!share) { + return res.status(404) + .end() + } + + res.locals.videoChannelShare = share + + return next() + } +] + // --------------------------------------------------------------------------- export { @@ -97,7 +125,8 @@ export { videoChannelsAddValidator, videoChannelsUpdateValidator, videoChannelsRemoveValidator, - videoChannelsGetValidator + videoChannelsGetValidator, + videoChannelsShareValidator } // --------------------------------------------------------------------------- diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index df0eb7b96..5ffc85210 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -24,7 +24,8 @@ import { CONSTRAINTS_FIELDS, SEARCHABLE_COLUMNS } from '../../initializers' import { database as db } from '../../initializers/database' import { UserInstance } from '../../models/account/user-interface' import { authenticate } from '../oauth' -import { checkErrors } from './utils' +import { areValidationErrors, checkErrors } from './utils' +import { isVideoExistsPromise } from '../../helpers/index' const videosAddValidator = [ body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( @@ -230,6 +231,28 @@ const videoRateValidator = [ } ] +const videosShareValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoShare parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isVideoExistsPromise(req.params.id, res)) return + + const share = await db.VideoShare.load(req.params.accountId, res.locals.video.id) + if (!share) { + return res.status(404) + .end() + } + + res.locals.videoShare = share + + return next() + } +] + // --------------------------------------------------------------------------- export { @@ -238,6 +261,7 @@ export { videosGetValidator, videosRemoveValidator, videosSearchValidator, + videosShareValidator, videoAbuseReportValidator, diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts index b409d1db3..21f81e901 100644 --- a/server/models/video/video-channel-interface.ts +++ b/server/models/video/video-channel-interface.ts @@ -6,6 +6,7 @@ import { VideoChannelObject } from '../../../shared/models/activitypub/objects/v import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' import { AccountInstance } from '../account/account-interface' import { VideoInstance } from './video-interface' +import { VideoChannelShareInstance } from './video-channel-share-interface' export namespace VideoChannelMethods { export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel @@ -47,6 +48,7 @@ export interface VideoChannelAttributes { Account?: AccountInstance Videos?: VideoInstance[] + VideoChannelShares?: VideoChannelShareInstance[] } export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance { diff --git a/server/models/video/video-channel-share-interface.ts b/server/models/video/video-channel-share-interface.ts index 8bb531af2..bcb3a0e24 100644 --- a/server/models/video/video-channel-share-interface.ts +++ b/server/models/video/video-channel-share-interface.ts @@ -5,10 +5,12 @@ import { VideoChannelInstance } from './video-channel-interface' export namespace VideoChannelShareMethods { export type LoadAccountsByShare = (videoChannelId: number) => Bluebird + export type Load = (accountId: number, videoId: number) => Bluebird } export interface VideoChannelShareClass { loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare + load: VideoChannelShareMethods.Load } export interface VideoChannelShareAttributes { diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts index 01f84c806..e47c0dae7 100644 --- a/server/models/video/video-channel-share.ts +++ b/server/models/video/video-channel-share.ts @@ -5,6 +5,7 @@ import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelSha let VideoChannelShare: Sequelize.Model let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare +let load: VideoChannelShareMethods.Load export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { VideoChannelShare = sequelize.define('VideoChannelShare', @@ -23,6 +24,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da const classMethods = [ associate, + load, loadAccountsByShare ] addMethodsToModel(VideoChannelShare, classMethods) @@ -50,6 +52,19 @@ function associate (models) { }) } +load = function (accountId: number, videoChannelId: number) { + return VideoChannelShare.findOne({ + where: { + accountId, + videoChannelId + }, + include: [ + VideoChannelShare['sequelize'].models.Account, + VideoChannelShare['sequelize'].models.VideoChannel + ] + }) +} + loadAccountsByShare = function (videoChannelId: number) { const query = { where: { diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 64130310d..e11268b2c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -6,6 +6,8 @@ import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' import { addMethodsToModel, getSort } from '../utils' import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' +import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' +import { activityPubCollection } from '../../helpers/activitypub' let VideoChannel: Sequelize.Model let toFormattedJSON: VideoChannelMethods.ToFormattedJSON @@ -139,6 +141,18 @@ toFormattedJSON = function (this: VideoChannelInstance) { } toActivityPubObject = function (this: VideoChannelInstance) { + let sharesObject + if (Array.isArray(this.VideoChannelShares)) { + const shares: string[] = [] + + for (const videoChannelShare of this.VideoChannelShares) { + const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) + shares.push(shareUrl) + } + + sharesObject = activityPubCollection(shares) + } + const json = { type: 'VideoChannel' as 'VideoChannel', id: this.url, @@ -146,7 +160,8 @@ toActivityPubObject = function (this: VideoChannelInstance) { content: this.description, name: this.name, published: this.createdAt.toISOString(), - updated: this.updatedAt.toISOString() + updated: this.updatedAt.toISOString(), + shares: sharesObject } return json diff --git a/server/models/video/video-share-interface.ts b/server/models/video/video-share-interface.ts index 569568842..ad23444b6 100644 --- a/server/models/video/video-share-interface.ts +++ b/server/models/video/video-share-interface.ts @@ -1,14 +1,16 @@ +import * as Bluebird from 'bluebird' import * as Sequelize from 'sequelize' import { AccountInstance } from '../account/account-interface' import { VideoInstance } from './video-interface' -import * as Bluebird from 'bluebird' export namespace VideoShareMethods { - export type LoadAccountsByShare = (videoChannelId: number) => Bluebird + export type LoadAccountsByShare = (videoId: number) => Bluebird + export type Load = (accountId: number, videoId: number) => Bluebird } export interface VideoShareClass { loadAccountsByShare: VideoShareMethods.LoadAccountsByShare + load: VideoShareMethods.Load } export interface VideoShareAttributes { diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 22ac31a4a..fe5d56d42 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -5,6 +5,7 @@ import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './v let VideoShare: Sequelize.Model let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare +let load: VideoShareMethods.Load export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { VideoShare = sequelize.define('VideoShare', @@ -23,7 +24,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da const classMethods = [ associate, - loadAccountsByShare + loadAccountsByShare, + load ] addMethodsToModel(VideoShare, classMethods) @@ -50,6 +52,18 @@ function associate (models) { }) } +load = function (accountId: number, videoId: number) { + return VideoShare.findOne({ + where: { + accountId, + videoId + }, + include: [ + VideoShare['sequelize'].models.Account + ] + }) +} + loadAccountsByShare = function (videoId: number) { const query = { where: { diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 457bfce77..4956b57ee 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -32,6 +32,7 @@ import { isVideoNameValid, isVideoLicenceValid, isVideoNSFWValid, isVideoDescrip import { logger } from '../../helpers/logger' import { generateImageFromVideoFile, transcode, getVideoFileHeight } from '../../helpers/ffmpeg-utils' import { createTorrentPromise, writeFilePromise, unlinkPromise, renamePromise, statPromise } from '../../helpers/core-utils' +import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' let Video: Sequelize.Model let getOriginalFile: VideoMethods.GetOriginalFile @@ -573,6 +574,18 @@ toActivityPubObject = function (this: VideoInstance) { dislikesObject = activityPubCollection(dislikes) } + let sharesObject + if (Array.isArray(this.VideoShares)) { + const shares: string[] = [] + + for (const videoShare of this.VideoShares) { + const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) + shares.push(shareUrl) + } + + sharesObject = activityPubCollection(shares) + } + const url = [] for (const file of this.VideoFiles) { url.push({ @@ -630,7 +643,8 @@ toActivityPubObject = function (this: VideoInstance) { }, url, likes: likesObject, - dislikes: dislikesObject + dislikes: dislikesObject, + shares: sharesObject } return videoObject @@ -823,7 +837,8 @@ listAllAndSharedByAccountForOutbox = function (accountId: number, start: number, accountId } ] - } + }, + include: [ Video['sequelize'].models.Account ] }, { model: Video['sequelize'].models.VideoChannel, diff --git a/shared/models/activitypub/objects/video-channel-object.ts b/shared/models/activitypub/objects/video-channel-object.ts index c9325b5df..dcce8696b 100644 --- a/shared/models/activitypub/objects/video-channel-object.ts +++ b/shared/models/activitypub/objects/video-channel-object.ts @@ -1,3 +1,5 @@ +import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection' + export interface VideoChannelObject { type: 'VideoChannel' id: string @@ -7,4 +9,5 @@ export interface VideoChannelObject { published: string updated: string actor?: string + shares?: ActivityPubOrderedCollection } diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts index a4e032d04..a15ec7142 100644 --- a/shared/models/activitypub/objects/video-torrent-object.ts +++ b/shared/models/activitypub/objects/video-torrent-object.ts @@ -27,4 +27,5 @@ export interface VideoTorrentObject { actor?: string likes?: ActivityPubOrderedCollection dislikes?: ActivityPubOrderedCollection + shares?: ActivityPubOrderedCollection }