// Intercept ActivityPub client requests
import * as express from 'express'
-
-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 { database as db } from '../../initializers'
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',
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',
executeIfActivityPub(asyncMiddleware(videoChannelController))
)
+activityPubClientRouter.get('/video-channels/:id/announces/:accountId',
+ executeIfActivityPub(asyncMiddleware(videoChannelsShareValidator)),
+ executeIfActivityPub(asyncMiddleware(videoChannelAnnounceController))
+)
+
// ---------------------------------------------------------------------------
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()
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
-import * as Promise from 'bluebird'
+import * as Bluebird from 'bluebird'
import * as express from 'express'
import 'express-validator'
import * as validator from 'validator'
return isUserUsernameValid(value)
}
-function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
- let promise: Promise<AccountInstance>
- if (validator.isInt(id)) {
+function checkAccountIdExists (id: number | string, res: express.Response, callback: (err: Error, account: AccountInstance) => any) {
+ let promise: Bluebird<AccountInstance>
+
+ 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<AccountInstance>, 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
}
-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
}
function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) {
- let promise: Promise<VideoChannelInstance>
+ let promise: Bluebird<VideoChannelInstance>
if (validator.isInt(id)) {
promise = db.VideoChannel.loadAndPopulateAccount(+id)
} else { // UUID
})
}
+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
}
})
}
+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 {
isVideoPrivacyValid,
isVideoFileResolutionValid,
isVideoFileSizeValid,
- checkVideoExists
+ checkVideoExists,
+ isVideoExistsPromise
}
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 {
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
}
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
// ---------------------------------------------------------------------------
-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.'
await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike')
}
+ if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) {
+ await addVideoShares(video, videoToCreateData.shares.orderedItems)
+ }
+
return video
}
-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(
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
+ })
+}
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
}
}
-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) {
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,
}
}
-function getVideoFollowersAudience (accountsInvolvedInVideo: AccountInstance[]) {
+function getOriginVideoChannelAudience (videoChannel: VideoChannelInstance, accountsInvolved: AccountInstance[]) {
+ return {
+ to: [ videoChannel.Account.url ],
+ cc: accountsInvolved.map(a => a.followersUrl)
+ }
+}
+
+function getObjectFollowersAudience (accountsInvolvedInObject: AccountInstance[]) {
return {
- to: accountsInvolvedInVideo.map(a => a.followersUrl),
+ to: accountsInvolvedInObject.map(a => a.followersUrl),
cc: []
}
}
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()
export {
broadcastToFollowers,
+ getOriginVideoChannelAudience,
unicastTo,
getAudience,
getOriginVideoAudience,
getAccountsInvolvedInVideo,
- getVideoFollowersAudience,
+ getAccountsInvolvedInVideoChannel,
+ getObjectFollowersAudience,
forwardActivity
}
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
// ---------------------------------------------------------------------------
export {
- sendVideoAnnounce,
- sendVideoChannelAnnounce,
- announceActivityData
+ sendVideoAnnounceToFollowers,
+ sendVideoChannelAnnounceToFollowers,
+ sendVideoAnnounceToOrigin,
+ sendVideoChannelAnnounceToOrigin,
+ announceActivityData,
+ buildVideoAnnounceToFollowers,
+ buildVideoChannelAnnounceToFollowers
}
getAccountsInvolvedInVideo,
getAudience,
getOriginVideoAudience,
- getVideoFollowersAudience,
+ getObjectFollowersAudience,
unicastTo
} from './misc'
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
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 ]
getAccountsInvolvedInVideo,
getAudience,
getOriginVideoAudience,
- getVideoFollowersAudience,
+ getObjectFollowersAudience,
unicastTo
} from './misc'
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)
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'
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)
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()
videoChannelId: videoChannel.id
}, { transaction: t })
- return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
+ return sendVideoChannelAnnounceToFollowers(serverAccount, videoChannel, t)
}
async function shareVideoByServer (video: VideoInstance, t: Transaction) {
videoId: video.id
}, { transaction: t })
- return sendVideoAnnounce(serverAccount, video, t)
+ return sendVideoAnnounceToFollowers(serverAccount, video, t)
}
export {
}
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) {
-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<any>) {
+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)
}
}
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 = [
logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
checkErrors(req, res, () => {
- checkLocalAccountExists(req.params.name, res, next)
+ checkLocalAccountNameExists(req.params.name, res, next)
})
}
]
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)
- })
-}
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
}
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'),
logger.debug('Checking listVideoAccountChannelsValidator parameters', { parameters: req.body })
checkErrors(req, res, () => {
- checkVideoAccountExists(req.params.accountId, res, next)
+ checkAccountIdExists(req.params.accountId, res, next)
})
}
]
}
]
+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 {
videoChannelsAddValidator,
videoChannelsUpdateValidator,
videoChannelsRemoveValidator,
- videoChannelsGetValidator
+ videoChannelsGetValidator,
+ videoChannelsShareValidator
}
// ---------------------------------------------------------------------------
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(
}
]
+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 {
videosGetValidator,
videosRemoveValidator,
videosSearchValidator,
+ videosShareValidator,
videoAbuseReportValidator,
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
Account?: AccountInstance
Videos?: VideoInstance[]
+ VideoChannelShares?: VideoChannelShareInstance[]
}
export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> {
export namespace VideoChannelShareMethods {
export type LoadAccountsByShare = (videoChannelId: number) => Bluebird<AccountInstance[]>
+ export type Load = (accountId: number, videoId: number) => Bluebird<VideoChannelShareInstance>
}
export interface VideoChannelShareClass {
loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
+ load: VideoChannelShareMethods.Load
}
export interface VideoChannelShareAttributes {
let VideoChannelShare: Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes>
let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
+let load: VideoChannelShareMethods.Load
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoChannelShare = sequelize.define<VideoChannelShareInstance, VideoChannelShareAttributes>('VideoChannelShare',
const classMethods = [
associate,
+ load,
loadAccountsByShare
]
addMethodsToModel(VideoChannelShare, classMethods)
})
}
+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: {
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<VideoChannelInstance, VideoChannelAttributes>
let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
}
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,
content: this.description,
name: this.name,
published: this.createdAt.toISOString(),
- updated: this.updatedAt.toISOString()
+ updated: this.updatedAt.toISOString(),
+ shares: sharesObject
}
return json
+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<AccountInstance[]>
+ export type LoadAccountsByShare = (videoId: number) => Bluebird<AccountInstance[]>
+ export type Load = (accountId: number, videoId: number) => Bluebird<VideoShareInstance>
}
export interface VideoShareClass {
loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
+ load: VideoShareMethods.Load
}
export interface VideoShareAttributes {
let VideoShare: Sequelize.Model<VideoShareInstance, VideoShareAttributes>
let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
+let load: VideoShareMethods.Load
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoShare = sequelize.define<VideoShareInstance, VideoShareAttributes>('VideoShare',
const classMethods = [
associate,
- loadAccountsByShare
+ loadAccountsByShare,
+ load
]
addMethodsToModel(VideoShare, classMethods)
})
}
+load = function (accountId: number, videoId: number) {
+ return VideoShare.findOne({
+ where: {
+ accountId,
+ videoId
+ },
+ include: [
+ VideoShare['sequelize'].models.Account
+ ]
+ })
+}
+
loadAccountsByShare = function (videoId: number) {
const query = {
where: {
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<VideoInstance, VideoAttributes>
let getOriginalFile: VideoMethods.GetOriginalFile
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({
},
url,
likes: likesObject,
- dislikes: dislikesObject
+ dislikes: dislikesObject,
+ shares: sharesObject
}
return videoObject
accountId
}
]
- }
+ },
+ include: [ Video['sequelize'].models.Account ]
},
{
model: Video['sequelize'].models.VideoChannel,
+import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
+
export interface VideoChannelObject {
type: 'VideoChannel'
id: string
published: string
updated: string
actor?: string
+ shares?: ActivityPubOrderedCollection<string>
}
actor?: string
likes?: ActivityPubOrderedCollection<string>
dislikes?: ActivityPubOrderedCollection<string>
+ shares?: ActivityPubOrderedCollection<string>
}