import { database as db } from '../../initializers'
import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
import { pageToStartAndCount } from '../../helpers'
-import { AccountInstance } from '../../models'
+import { AccountInstance, VideoChannelInstance } from '../../models'
import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { ACTIVITY_PUB } from '../../initializers/constants'
import { asyncMiddleware } from '../../middlewares/async'
+import { videosGetValidator } from '../../middlewares/validators/videos'
+import { VideoInstance } from '../../models/video/video-interface'
+import { videoChannelsGetValidator } from '../../middlewares/validators/video-channels'
const activityPubClientRouter = express.Router()
executeIfActivityPub(asyncMiddleware(accountFollowingController))
)
+activityPubClientRouter.get('/videos/watch/:id',
+ executeIfActivityPub(videosGetValidator),
+ executeIfActivityPub(asyncMiddleware(videoController))
+)
+
+activityPubClientRouter.get('/video-channels/:id',
+ executeIfActivityPub(videoChannelsGetValidator),
+ executeIfActivityPub(asyncMiddleware(videoChannelController))
+)
+
// ---------------------------------------------------------------------------
export {
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())
+}
paginationValidator,
setPagination,
setVideoChannelsSort,
- videoChannelGetValidator,
+ videoChannelsGetValidator,
videoChannelsAddValidator,
videoChannelsRemoveValidator,
videoChannelsSortValidator,
)
videoChannelRouter.get('/channels/:id',
- videoChannelGetValidator,
+ videoChannelsGetValidator,
asyncMiddleware(getVideoChannel)
)
import * as url from 'url'
import { ActivityIconObject } from '../../shared/index'
import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
+import { VideoChannelObject } from '../../shared/models/activitypub/objects/video-channel-object'
import { ResultList } from '../../shared/models/result-list.model'
import { database as db, REMOTE_SCHEME } from '../initializers'
import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
-import { sendAnnounce } from '../lib/activitypub/send-request'
+import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/misc'
+import { sendVideoAnnounce } from '../lib/activitypub/send-request'
+import { sendVideoChannelAnnounce } from '../lib/index'
+import { AccountInstance } from '../models/account/account-interface'
import { VideoChannelInstance } from '../models/video/video-channel-interface'
import { VideoInstance } from '../models/video/video-interface'
import { isRemoteAccountValid } from './custom-validators'
+import { isVideoChannelObjectValid } from './custom-validators/activitypub/videos'
import { logger } from './logger'
import { doRequest, doRequestAndSaveToFile } from './requests'
import { getServerAccount } from './utils'
videoChannelId: videoChannel.id
}, { transaction: t })
- return sendAnnounce(serverAccount, videoChannel, t)
+ return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
}
async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
videoId: video.id
}, { transaction: t })
- return sendAnnounce(serverAccount, video, t)
+ return sendVideoAnnounce(serverAccount, video, t)
}
function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) {
if (res === undefined) throw new Error('Cannot fetch remote account.')
// Save our new account in database
- const account = res.account
- await account.save()
+ account = await res.account.save()
}
return account
}
+async function getOrCreateVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
+ let videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl)
+
+ // We don't have this account in our database, fetch it on remote
+ if (!videoChannel) {
+ videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl)
+ if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.')
+
+ // Save our new video channel in database
+ await videoChannel.save()
+ }
+
+ return videoChannel
+}
+
async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
const options = {
uri: accountUrl,
return { account, server }
}
+async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
+ const options = {
+ uri: videoChannelUrl,
+ method: 'GET',
+ headers: {
+ 'Accept': ACTIVITY_PUB_ACCEPT_HEADER
+ }
+ }
+
+ logger.info('Fetching remote video channel %s.', videoChannelUrl)
+
+ let requestResult
+ try {
+ requestResult = await doRequest(options)
+ } catch (err) {
+ logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err)
+ return undefined
+ }
+
+ const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body)
+ if (isVideoChannelObjectValid(videoChannelJSON) === false) {
+ logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON })
+ return undefined
+ }
+
+ const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount)
+ const videoChannel = db.VideoChannel.build(videoChannelAttributes)
+ videoChannel.Account = ownerAccount
+
+ return videoChannel
+}
+
function fetchRemoteVideoPreview (video: VideoInstance) {
// FIXME: use url
const host = video.VideoChannel.Account.Server.host
fetchRemoteVideoPreview,
fetchRemoteVideoDescription,
shareVideoChannelByServer,
- shareVideoByServer
+ shareVideoByServer,
+ getOrCreateVideoChannel
}
// ---------------------------------------------------------------------------
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
import { isActivityPubUrlValid } from './misc'
import {
- isVideoAnnounceValid,
- isVideoChannelAnnounceValid,
+ isAnnounceValid,
isVideoChannelCreateActivityValid,
isVideoChannelDeleteActivityValid,
isVideoChannelUpdateActivityValid,
isAccountFollowActivityValid(activity) ||
isAccountAcceptActivityValid(activity) ||
isVideoFlagValid(activity) ||
- isVideoAnnounceValid(activity) ||
- isVideoChannelAnnounceValid(activity)
+ isAnnounceValid(activity)
}
// ---------------------------------------------------------------------------
}
function isBaseActivityValid (activity: any, type: string) {
- return Array.isArray(activity['@context']) &&
+ return (activity['@context'] === undefined || Array.isArray(activity['@context'])) &&
activity.type === type &&
isActivityPubUrlValid(activity.id) &&
isActivityPubUrlValid(activity.actor) &&
function isVideoTorrentObjectValid (video: any) {
return video.type === 'Video' &&
+ isActivityPubUrlValid(video.id) &&
isVideoNameValid(video.name) &&
isActivityPubVideoDurationValid(video.duration) &&
isUUIDValid(video.uuid) &&
isActivityPubUrlValid(activity.object)
}
-function isVideoAnnounceValid (activity: any) {
+function isAnnounceValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
- isVideoTorrentObjectValid(activity.object)
-}
-
-function isVideoChannelAnnounceValid (activity: any) {
- return isBaseActivityValid(activity, 'Announce') &&
- isVideoChannelObjectValid(activity.object)
+ (
+ isVideoChannelCreateActivityValid(activity.object) ||
+ isVideoTorrentAddActivityValid(activity.object)
+ )
}
function isVideoChannelCreateActivityValid (activity: any) {
function isVideoChannelObjectValid (videoChannel: any) {
return videoChannel.type === 'VideoChannel' &&
+ isActivityPubUrlValid(videoChannel.id) &&
isVideoChannelNameValid(videoChannel.name) &&
- isVideoChannelDescriptionValid(videoChannel.description) &&
+ isVideoChannelDescriptionValid(videoChannel.content) &&
+ isDateValid(videoChannel.published) &&
+ isDateValid(videoChannel.updated) &&
isUUIDValid(videoChannel.uuid)
}
isVideoChannelDeleteActivityValid,
isVideoTorrentDeleteActivityValid,
isVideoFlagValid,
- isVideoAnnounceValid,
- isVideoChannelAnnounceValid
+ isAnnounceValid,
+ isVideoChannelObjectValid
}
// ---------------------------------------------------------------------------
function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' &&
- ACTIVITY_PUB.VIDEO_URL_MIME_TYPES.indexOf(url.mimeType) !== -1 &&
- isVideoUrlValid(url.url) &&
- validator.isInt(url.width + '', { min: 0 }) &&
- validator.isInt(url.size + '', { min: 0 })
+ (
+ ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 &&
+ isVideoUrlValid(url.url) &&
+ validator.isInt(url.width + '', { min: 0 }) &&
+ validator.isInt(url.size + '', { min: 0 })
+ ) ||
+ (
+ ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 &&
+ isVideoUrlValid(url.url) &&
+ validator.isInt(url.width + '', { min: 0 })
+ ) ||
+ (
+ ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 &&
+ validator.isLength(url.url, { min: 5 }) &&
+ validator.isInt(url.width + '', { min: 0 })
+ )
}
// TODO: import from ES6 when retry typing file will include errorFilter function
import * as retry from 'async/retry'
-
+import * as Bluebird from 'bluebird'
import { logger } from './logger'
type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
-function retryTransactionWrapper (functionToRetry: (...args) => Promise<any>, options: RetryTransactionWrapperOptions) {
+function retryTransactionWrapper (functionToRetry: (...args) => Promise<any> | Bluebird<any>, options: RetryTransactionWrapperOptions) {
const args = options.arguments ? options.arguments : []
return transactionRetryer(callback => {
.catch(err => callback(err))
})
.catch(err => {
- // Do not throw the error, continue the process
logger.error(options.errorMessage, err)
+ throw err
})
}
logger.debug('Maybe retrying the transaction function.', { willRetry })
return willRetry
}
- }, func, err => err ? rej(err) : res())
+ }, func, (err, data) => err ? rej(err) : res(data))
})
}
const ACTIVITY_PUB = {
COLLECTION_ITEMS_PER_PAGE: 10,
- VIDEO_URL_MIME_TYPES: [
- 'video/mp4',
- 'video/webm',
- 'video/ogg',
- 'application/x-bittorrent',
- 'application/x-bittorrent;x-scheme-handler/magnet'
- ]
+ URL_MIME_TYPES: {
+ VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
+ TORRENT: [ 'application/x-bittorrent' ],
+ MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
+ }
}
// ---------------------------------------------------------------------------
import { VideoChannelInstance } from '../../models/video/video-channel-interface'
import { VideoFileAttributes } from '../../models/video/video-file-interface'
import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
+import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
+import { AccountInstance } from '../../models/account/account-interface'
+
+function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
+ return {
+ name: videoChannelObject.name,
+ description: videoChannelObject.content,
+ uuid: videoChannelObject.uuid,
+ url: videoChannelObject.id,
+ createdAt: new Date(videoChannelObject.published),
+ updatedAt: new Date(videoChannelObject.updated),
+ remote: true,
+ accountId: account.id
+ }
+}
async function videoActivityObjectToDBAttributes (
videoChannel: VideoChannelInstance,
}
function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
- const fileUrls = videoObject.url
- .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1 && u.url.startsWith('video/'))
+ const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
+ const fileUrls = videoObject.url.filter(u => {
+ return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/')
+ })
+
+ if (fileUrls.length === 0) {
+ throw new Error('Cannot find video files for ' + videoCreated.url)
+ }
const attributes: VideoFileAttributes[] = []
- for (const url of fileUrls) {
+ for (const fileUrl of fileUrls) {
// Fetch associated magnet uri
- const magnet = videoObject.url
- .find(u => {
- return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width
- })
- if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url)
+ const magnet = videoObject.url.find(u => {
+ return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width
+ })
+
+ if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url)
const parsed = magnetUtil.decode(magnet.url)
if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
const attribute = {
- extname: VIDEO_MIMETYPE_EXT[url.mimeType],
+ extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType],
infoHash: parsed.infoHash,
- resolution: url.width,
- size: url.size,
+ resolution: fileUrl.width,
+ size: fileUrl.size,
videoId: videoCreated.id
}
attributes.push(attribute)
export {
videoFileActivityUrlToDBAttributes,
- videoActivityObjectToDBAttributes
+ videoActivityObjectToDBAttributes,
+ videoChannelActivityObjectToDBAttributes
}
import { AccountInstance } from '../../models/account/account-interface'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
import Bluebird = require('bluebird')
+import { getOrCreateVideoChannel } from '../../helpers/activitypub'
+import { VideoChannelInstance } from '../../models/video/video-channel-interface'
async function processAddActivity (activity: ActivityAdd) {
const activityObject = activity.object
const account = await getOrCreateAccount(activity.actor)
if (activityType === 'Video') {
- return processAddVideo(account, activity.id, activityObject as VideoTorrentObject)
+ const videoChannelUrl = activity.target
+ const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
+
+ return processAddVideo(account, videoChannel, activityObject as VideoTorrentObject)
}
logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
// ---------------------------------------------------------------------------
-function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
+function processAddVideo (account: AccountInstance, videoChannel: VideoChannelInstance, video: VideoTorrentObject) {
const options = {
- arguments: [ account, videoChannelUrl, video ],
+ arguments: [ account, videoChannel, video ],
errorMessage: 'Cannot insert the remote video with many retries.'
}
return retryTransactionWrapper(addRemoteVideo, options)
}
-async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) {
+function addRemoteVideo (account: AccountInstance, videoChannel: VideoChannelInstance, videoToCreateData: VideoTorrentObject) {
logger.debug('Adding remote video %s.', videoToCreateData.url)
return db.sequelize.transaction(async t => {
transaction: t
}
- const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t)
- if (!videoChannel) throw new Error('Video channel not found.')
-
if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t)
const videoCreated = await video.save(sequelizeOptions)
const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
+ if (videoFileAttributes.length === 0) {
+ throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
+ }
- const tasks: Bluebird<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)
const tags = videoToCreateData.tag.map(t => t.name)
return videoCreated
})
-
}
import { VideoInstance } from '../../models/index'
async function processAnnounceActivity (activity: ActivityAnnounce) {
- const activityType = activity.object.type
+ const announcedActivity = activity.object
const accountAnnouncer = await getOrCreateAccount(activity.actor)
- if (activityType === 'VideoChannel') {
- const activityCreate = Object.assign(activity, {
- type: 'Create' as 'Create',
- actor: activity.object.actor,
- object: activity.object as VideoChannelObject
- })
-
+ if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
// Add share entry
- const videoChannel: VideoChannelInstance = await processCreateActivity(activityCreate)
+ const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
await db.VideoChannelShare.create({
accountId: accountAnnouncer.id,
videoChannelId: videoChannel.id
})
- } else if (activityType === 'Video') {
- const activityAdd = Object.assign(activity, {
- type: 'Add' as 'Add',
- actor: activity.object.actor,
- object: activity.object as VideoTorrentObject
- })
+ return undefined
+ } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
// Add share entry
- const video: VideoInstance = await processAddActivity(activityAdd)
+ const video: VideoInstance = await processAddActivity(announcedActivity)
await db.VideoShare.create({
accountId: accountAnnouncer.id,
videoId: video.id
})
+
+ return undefined
}
- logger.warn('Unknown activity object type %s when announcing activity.', activityType, { activity: activity.id })
+ logger.warn(
+ 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type,
+ { activity: activity.id }
+ )
return Promise.resolve(undefined)
}
import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface'
+import { videoChannelActivityObjectToDBAttributes } from './misc'
async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object
return retryTransactionWrapper(addRemoteVideoChannel, options)
}
-async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
+function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
return db.sequelize.transaction(async t => {
let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
- const videoChannelData = {
- name: videoChannelToCreateData.name,
- description: videoChannelToCreateData.content,
- uuid: videoChannelToCreateData.uuid,
- createdAt: new Date(videoChannelToCreateData.published),
- updatedAt: new Date(videoChannelToCreateData.updated),
- remote: true,
- accountId: account.id
- }
-
+ const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
videoChannel = db.VideoChannel.build(videoChannelData)
videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
return retryTransactionWrapper(addRemoteVideoAbuse, options)
}
-async function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
+function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
return db.sequelize.transaction(async t => {
return broadcastToFollowers(data, [ account ], t)
}
-async function sendAnnounce (byAccount: AccountInstance, instance: VideoInstance | VideoChannelInstance, t: Sequelize.Transaction) {
- const object = instance.toActivityPubObject()
-
- let url = ''
- let objectActorUrl: string
- if ((instance as any).VideoChannel !== undefined) {
- objectActorUrl = (instance as VideoInstance).VideoChannel.Account.url
- url = getActivityPubUrl('video', instance.uuid) + '#announce'
- } else {
- objectActorUrl = (instance as VideoChannelInstance).Account.url
- url = getActivityPubUrl('videoChannel', instance.uuid) + '#announce'
- }
+async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+ const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce'
+ const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), true)
+
+ const data = await announceActivityData(url, byAccount, announcedActivity)
+ return broadcastToFollowers(data, [ byAccount ], t)
+}
- const objectWithActor = Object.assign(object, {
- actor: objectActorUrl
- })
+async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Sequelize.Transaction) {
+ const url = getActivityPubUrl('video', video.uuid) + '#announce'
- const data = await announceActivityData(url, byAccount, objectWithActor)
+ const videoChannel = video.VideoChannel
+ const announcedActivity = await addActivityData(url, videoChannel.Account, videoChannel.url, video.toActivityPubObject(), true)
+
+ const data = await announceActivityData(url, byAccount, announcedActivity)
return broadcastToFollowers(data, [ byAccount ], t)
}
sendAccept,
sendFollow,
sendVideoAbuse,
- sendAnnounce
+ sendVideoChannelAnnounce,
+ sendVideoAnnounce
}
// ---------------------------------------------------------------------------
return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public')
}
-async function createActivityData (url: string, byAccount: AccountInstance, object: any) {
+async function createActivityData (url: string, byAccount: AccountInstance, object: any, raw = false) {
const to = await getPublicActivityTo(byAccount)
const base = {
type: 'Create',
object
}
+ if (raw === true) return base
+
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 base = {
type: 'Add',
target
}
+ if (raw === true) return base
+
return buildSignedActivity(byAccount, base)
}
}
]
-const videoChannelGetValidator = [
+const videoChannelsGetValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
videoChannelsAddValidator,
videoChannelsUpdateValidator,
videoChannelsRemoveValidator,
- videoChannelGetValidator
+ videoChannelsGetValidator
}
// ---------------------------------------------------------------------------
const query: Sequelize.FindOptions<VideoChannelAttributes> = {
where: {
url
- }
+ },
+ include: [ VideoChannel['sequelize'].models.Account ]
}
if (t !== undefined) query.transaction = t
export interface ActivityAdd extends BaseActivity {
type: 'Add'
+ target: string
object: VideoTorrentObject
}
export interface ActivityAnnounce extends BaseActivity {
type: 'Announce'
- object: VideoChannelObject | VideoTorrentObject
+ object: ActivityCreate | ActivityAdd
}