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