509 lines
12 KiB
TypeScript
509 lines
12 KiB
TypeScript
import {
|
|
ResultList,
|
|
VideoChannelActivityAction,
|
|
VideoChannelActivityTarget,
|
|
type VideoChannelActivity,
|
|
type VideoChannelActivityActionType,
|
|
type VideoChannelActivityDetails,
|
|
type VideoChannelActivityTargetType
|
|
} from '@peertube/peertube-models'
|
|
import { uuidToShort } from '@peertube/peertube-node-utils'
|
|
import { VIDEO_CHANNEL_ACTIVITY_ACTIONS, VIDEO_CHANNEL_ACTIVITY_TARGETS } from '@server/initializers/constants.js'
|
|
import { MChannelId, MChannelSync, MUserAccountId, MVideo, MVideoImport, MVideoPlaylist } from '@server/types/models/index.js'
|
|
import { MChannelActivityFormattable } from '@server/types/models/video/video-channel-activity.js'
|
|
import { Op, Transaction } from 'sequelize'
|
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
|
import { AccountModel } from '../account/account.js'
|
|
import { ActorModel } from '../actor/actor.js'
|
|
import { getSort, SequelizeModel } from '../shared/index.js'
|
|
import { VideoChannelSyncModel } from './video-channel-sync.js'
|
|
import { VideoChannelModel } from './video-channel.js'
|
|
import { VideoImportModel } from './video-import.js'
|
|
import { VideoPlaylistModel } from './video-playlist.js'
|
|
import { VideoModel } from './video.js'
|
|
|
|
interface VideoChannelActivityData {
|
|
video?: {
|
|
id: number
|
|
name: string
|
|
uuid: string
|
|
url: string
|
|
isLive: boolean
|
|
}
|
|
|
|
videoImport?: {
|
|
id: number
|
|
name: string
|
|
uuid: string
|
|
url: string
|
|
targetUrl: string
|
|
}
|
|
|
|
playlist?: {
|
|
id: number
|
|
name: string
|
|
uuid: string
|
|
url: string
|
|
}
|
|
|
|
channel?: {
|
|
id: number
|
|
name: string
|
|
displayName: string
|
|
url: string
|
|
}
|
|
|
|
channelSync?: {
|
|
id: number
|
|
externalChannelUrl: string
|
|
}
|
|
}
|
|
|
|
@Table({
|
|
tableName: 'videoChannelActivity',
|
|
indexes: [
|
|
{
|
|
fields: [ 'videoChannelId' ]
|
|
},
|
|
{
|
|
fields: [ 'accountId' ]
|
|
},
|
|
{
|
|
fields: [ 'videoId' ],
|
|
where: {
|
|
videoId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
},
|
|
{
|
|
fields: [ 'videoPlaylistId' ],
|
|
where: {
|
|
videoPlaylistId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
},
|
|
{
|
|
fields: [ 'videoChannelSyncId' ],
|
|
where: {
|
|
videoChannelSyncId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
},
|
|
{
|
|
fields: [ 'videoImportId' ],
|
|
where: {
|
|
videoImportId: {
|
|
[Op.ne]: null
|
|
}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
export class VideoChannelActivityModel extends SequelizeModel<VideoChannelActivityModel> {
|
|
@AllowNull(false)
|
|
@Column
|
|
declare action: VideoChannelActivityActionType
|
|
|
|
@AllowNull(false)
|
|
@Column
|
|
declare targetType: VideoChannelActivityTargetType
|
|
|
|
// Store association data in case they are deleted
|
|
@AllowNull(false)
|
|
@Column(DataType.JSONB)
|
|
declare data: VideoChannelActivityData
|
|
|
|
// More information about the action
|
|
@AllowNull(true)
|
|
@Column(DataType.JSONB)
|
|
declare details: VideoChannelActivityDetails
|
|
|
|
@CreatedAt
|
|
declare createdAt: Date
|
|
|
|
@UpdatedAt
|
|
declare updatedAt: Date
|
|
|
|
@ForeignKey(() => VideoChannelModel)
|
|
@Column
|
|
declare videoChannelId: number
|
|
|
|
@BelongsTo(() => VideoChannelModel, {
|
|
foreignKey: {
|
|
allowNull: false
|
|
},
|
|
onDelete: 'cascade'
|
|
})
|
|
declare VideoChannel: Awaited<VideoChannelModel>
|
|
|
|
@ForeignKey(() => AccountModel)
|
|
@Column
|
|
declare accountId: number
|
|
|
|
@BelongsTo(() => AccountModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
declare Account: Awaited<AccountModel>
|
|
|
|
@ForeignKey(() => VideoModel)
|
|
@Column
|
|
declare videoId: number
|
|
|
|
@BelongsTo(() => VideoModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
declare Video: Awaited<VideoModel>
|
|
|
|
@ForeignKey(() => VideoPlaylistModel)
|
|
@Column
|
|
declare videoPlaylistId: number
|
|
|
|
@BelongsTo(() => VideoPlaylistModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
declare VideoPlaylist: Awaited<VideoPlaylistModel>
|
|
|
|
@ForeignKey(() => VideoChannelSyncModel)
|
|
@Column
|
|
declare videoChannelSyncId: number
|
|
|
|
@BelongsTo(() => VideoChannelSyncModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
declare VideoChannelSync: Awaited<VideoChannelSyncModel>
|
|
|
|
@ForeignKey(() => VideoImportModel)
|
|
@Column
|
|
declare videoImportId: number
|
|
|
|
@BelongsTo(() => VideoImportModel, {
|
|
foreignKey: {
|
|
allowNull: true
|
|
},
|
|
onDelete: 'set null'
|
|
})
|
|
declare VideoImport: Awaited<VideoImportModel>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static async listForAPI (options: {
|
|
channelId: number
|
|
start: number
|
|
count: number
|
|
sort: string
|
|
}): Promise<ResultList<MChannelActivityFormattable>> {
|
|
const where = { videoChannelId: options.channelId }
|
|
|
|
const countQuery = VideoChannelActivityModel.count({ where })
|
|
const dataQuery = VideoChannelActivityModel.findAll({
|
|
where,
|
|
offset: options.start,
|
|
limit: options.count,
|
|
order: getSort(options.sort),
|
|
include: [
|
|
{
|
|
model: VideoChannelModel.unscoped(),
|
|
required: true,
|
|
include: [
|
|
{
|
|
attributes: [ 'preferredUsername', 'url' ],
|
|
model: ActorModel.unscoped()
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: AccountModel,
|
|
required: false
|
|
},
|
|
{
|
|
model: VideoModel.unscoped(),
|
|
required: false
|
|
},
|
|
{
|
|
model: VideoPlaylistModel.unscoped(),
|
|
required: false
|
|
},
|
|
{
|
|
model: VideoChannelSyncModel.unscoped(),
|
|
required: false
|
|
},
|
|
{
|
|
model: VideoImportModel.unscoped(),
|
|
required: false,
|
|
include: [
|
|
{
|
|
model: VideoModel.unscoped(),
|
|
required: false
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
|
|
const [ total, data ] = await Promise.all([ countQuery, dataQuery ])
|
|
|
|
return { total, data }
|
|
}
|
|
|
|
static async addVideoActivity (options: {
|
|
action: VideoChannelActivityActionType
|
|
user: MUserAccountId
|
|
channel: MChannelId
|
|
video: MVideo
|
|
transaction: Transaction
|
|
}) {
|
|
const { action, user, channel, video, transaction } = options
|
|
|
|
return this.create({
|
|
action,
|
|
targetType: VideoChannelActivityTarget.VIDEO,
|
|
data: {
|
|
video: {
|
|
id: video.id,
|
|
name: video.name,
|
|
uuid: video.uuid,
|
|
url: video.url,
|
|
isLive: video.isLive
|
|
}
|
|
},
|
|
details: null,
|
|
accountId: user.Account.id,
|
|
videoChannelId: channel.id,
|
|
videoId: action !== VideoChannelActivityAction.DELETE
|
|
? video.id
|
|
: null
|
|
}, { transaction })
|
|
}
|
|
|
|
static async addVideoImportActivity (options: {
|
|
action: VideoChannelActivityActionType
|
|
user: MUserAccountId
|
|
channel: MChannelId
|
|
videoImport: MVideoImport
|
|
video: MVideo
|
|
transaction: Transaction
|
|
}) {
|
|
const { action, user, channel, video, videoImport, transaction } = options
|
|
|
|
return this.create({
|
|
action,
|
|
targetType: VideoChannelActivityTarget.VIDEO_IMPORT,
|
|
data: {
|
|
videoImport: {
|
|
id: video.id,
|
|
name: video.name,
|
|
uuid: video.uuid,
|
|
url: video.url,
|
|
targetUrl: videoImport.targetUrl
|
|
}
|
|
},
|
|
details: null,
|
|
accountId: user.Account.id,
|
|
videoChannelId: channel.id,
|
|
videoImportId: action !== VideoChannelActivityAction.DELETE
|
|
? videoImport.id
|
|
: null
|
|
}, { transaction })
|
|
}
|
|
|
|
static async addChannelActivity (options: {
|
|
action: VideoChannelActivityActionType
|
|
user: MUserAccountId
|
|
channel: MChannelActivityFormattable['VideoChannel']
|
|
transaction: Transaction
|
|
}) {
|
|
const { action, user, channel, transaction } = options
|
|
|
|
return this.create({
|
|
action,
|
|
targetType: VideoChannelActivityTarget.CHANNEL,
|
|
data: {
|
|
channel: {
|
|
id: channel.id,
|
|
name: channel.Actor.preferredUsername,
|
|
displayName: channel.name,
|
|
url: channel.Actor.url
|
|
}
|
|
},
|
|
details: null,
|
|
accountId: user.Account.id,
|
|
videoChannelId: channel.id
|
|
}, { transaction })
|
|
}
|
|
|
|
static async addPlaylistActivity (options: {
|
|
action: VideoChannelActivityActionType
|
|
user: MUserAccountId
|
|
channel: MChannelId
|
|
playlist: MVideoPlaylist
|
|
transaction: Transaction
|
|
}) {
|
|
const { action, user, channel, playlist, transaction } = options
|
|
|
|
return this.create({
|
|
action,
|
|
targetType: VideoChannelActivityTarget.PLAYLIST,
|
|
data: {
|
|
playlist: {
|
|
id: playlist.id,
|
|
name: playlist.name,
|
|
uuid: playlist.uuid,
|
|
url: playlist.url
|
|
}
|
|
},
|
|
details: null,
|
|
accountId: user.Account.id,
|
|
videoChannelId: channel.id,
|
|
videoPlaylistId: action !== VideoChannelActivityAction.DELETE
|
|
? playlist.id
|
|
: null
|
|
}, { transaction })
|
|
}
|
|
|
|
static async addChannelSyncActivity (options: {
|
|
action: VideoChannelActivityActionType
|
|
user: MUserAccountId
|
|
channel: MChannelId
|
|
sync: MChannelSync
|
|
transaction: Transaction
|
|
}) {
|
|
const { action, user, channel, sync, transaction } = options
|
|
|
|
return this.create({
|
|
action,
|
|
targetType: VideoChannelActivityTarget.CHANNEL_SYNC,
|
|
data: {
|
|
channelSync: {
|
|
id: sync.id,
|
|
externalChannelUrl: sync.externalChannelUrl
|
|
}
|
|
},
|
|
details: null,
|
|
accountId: user.Account.id,
|
|
videoChannelId: channel.id,
|
|
videoChannelSyncId: action !== VideoChannelActivityAction.DELETE
|
|
? sync.id
|
|
: null
|
|
}, { transaction })
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
toFormattedJSON (this: MChannelActivityFormattable): VideoChannelActivity {
|
|
return {
|
|
id: this.id,
|
|
|
|
account: this.Account
|
|
? this.Account.toFormattedSummaryJSON()
|
|
: null,
|
|
|
|
action: {
|
|
id: this.action,
|
|
label: VIDEO_CHANNEL_ACTIVITY_ACTIONS[this.action]
|
|
},
|
|
|
|
targetType: {
|
|
id: this.targetType,
|
|
label: VIDEO_CHANNEL_ACTIVITY_TARGETS[this.targetType]
|
|
},
|
|
|
|
video: this.formatVideo(),
|
|
playlist: this.formatPlaylist(),
|
|
channel: this.formatChannel(),
|
|
channelSync: this.formatSync(),
|
|
videoImport: this.formatVideoImport(),
|
|
|
|
details: this.details,
|
|
createdAt: this.createdAt
|
|
}
|
|
}
|
|
|
|
formatChannel (this: MChannelActivityFormattable): VideoChannelActivity['channel'] {
|
|
return {
|
|
id: this.VideoChannel.id,
|
|
name: this.VideoChannel.Actor.preferredUsername,
|
|
displayName: this.VideoChannel.name,
|
|
url: this.VideoChannel.Actor.url
|
|
}
|
|
}
|
|
|
|
formatVideo (this: MChannelActivityFormattable): VideoChannelActivity['video'] {
|
|
if (this.targetType !== VideoChannelActivityTarget.VIDEO) return null
|
|
|
|
const target = this.Video || this.data.video
|
|
|
|
return {
|
|
id: target.id,
|
|
name: target.name,
|
|
uuid: target.uuid,
|
|
shortUUID: uuidToShort(target.uuid),
|
|
isLive: target.isLive,
|
|
url: target.url
|
|
}
|
|
}
|
|
|
|
formatVideoImport (this: MChannelActivityFormattable): VideoChannelActivity['videoImport'] {
|
|
if (this.targetType !== VideoChannelActivityTarget.VIDEO_IMPORT) return null
|
|
|
|
if (this.VideoImport && this.VideoImport.Video) {
|
|
return {
|
|
id: this.VideoImport.id,
|
|
name: this.VideoImport.Video.name,
|
|
uuid: this.VideoImport.Video.uuid,
|
|
shortUUID: uuidToShort(this.VideoImport.Video.uuid),
|
|
url: this.VideoImport.Video.url,
|
|
targetUrl: this.VideoImport.targetUrl
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: this.data.videoImport.id,
|
|
name: this.data.videoImport.name,
|
|
uuid: this.data.videoImport.uuid,
|
|
shortUUID: uuidToShort(this.data.videoImport.uuid),
|
|
url: this.data.videoImport.url,
|
|
targetUrl: this.data.videoImport.targetUrl
|
|
}
|
|
}
|
|
|
|
formatPlaylist (this: MChannelActivityFormattable): VideoChannelActivity['playlist'] {
|
|
if (this.targetType !== VideoChannelActivityTarget.PLAYLIST) return null
|
|
|
|
const target = this.VideoPlaylist || this.data.playlist
|
|
|
|
return {
|
|
id: target.id,
|
|
name: target.name,
|
|
uuid: target.uuid,
|
|
shortUUID: uuidToShort(target.uuid),
|
|
url: target.url
|
|
}
|
|
}
|
|
|
|
formatSync (): VideoChannelActivity['channelSync'] {
|
|
if (this.targetType !== VideoChannelActivityTarget.CHANNEL_SYNC) return null
|
|
|
|
const target = this.VideoChannelSync || this.data.channelSync
|
|
|
|
return {
|
|
id: target.id,
|
|
externalChannelUrl: target.externalChannelUrl
|
|
}
|
|
}
|
|
}
|