UpdatedAt
} from 'sequelize-typescript'
import { ActivityPubActor } from '../../../shared/models/activitypub'
-import { VideoChannel } from '../../../shared/models/videos'
+import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
import {
isVideoChannelDescriptionValid,
isVideoChannelNameValid,
isVideoChannelSupportValid
} from '../../helpers/custom-validators/video-channels'
import { sendDeleteActor } from '../../lib/activitypub/send'
-import { AccountModel } from '../account/account'
+import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account'
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
-import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
+import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
-import { CONSTRAINTS_FIELDS } from '../../initializers'
+import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
import { ServerModel } from '../server/server'
-import { DefineIndexesOptions } from 'sequelize'
+import { FindOptions, ModelIndexesOptions, Op } from 'sequelize'
+import { AvatarModel } from '../avatar/avatar'
+import { VideoPlaylistModel } from './video-playlist'
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
-const indexes: DefineIndexesOptions[] = [
+const indexes: ModelIndexesOptions[] = [
buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
{
}
]
-enum ScopeNames {
+export enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACTOR = 'WITH_ACTOR',
- WITH_VIDEOS = 'WITH_VIDEOS'
+ WITH_VIDEOS = 'WITH_VIDEOS',
+ SUMMARY = 'SUMMARY'
}
type AvailableForListOptions = {
actorId: number
}
-@DefaultScope({
+@DefaultScope(() => ({
include: [
{
- model: () => ActorModel,
+ model: ActorModel,
required: true
}
]
-})
-@Scopes({
- [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
- const actorIdNumber = parseInt(options.actorId + '', 10)
+}))
+@Scopes(() => ({
+ [ScopeNames.SUMMARY]: (withAccount = false) => {
+ const base: FindOptions = {
+ attributes: [ 'name', 'description', 'id', 'actorId' ],
+ include: [
+ {
+ attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
+ model: ActorModel.unscoped(),
+ required: true,
+ include: [
+ {
+ attributes: [ 'host' ],
+ model: ServerModel.unscoped(),
+ required: false
+ },
+ {
+ model: AvatarModel.unscoped(),
+ required: false
+ }
+ ]
+ }
+ ]
+ }
+
+ if (withAccount === true) {
+ base.include.push({
+ model: AccountModel.scope(AccountModelScopeNames.SUMMARY),
+ required: true
+ })
+ }
+ return base
+ },
+ [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
// Only list local channels OR channels that are on an instance followed by actorId
- const inQueryInstanceFollow = '(' +
- 'SELECT "actor"."serverId" FROM "actorFollow" ' +
- 'INNER JOIN "actor" ON actor.id= "actorFollow"."targetActorId" ' +
- 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
- ')'
+ const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
return {
include: [
},
model: ActorModel,
where: {
- [Sequelize.Op.or]: [
+ [Op.or]: [
{
serverId: null
},
{
serverId: {
- [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
+ [ Op.in ]: Sequelize.literal(inQueryInstanceFollow)
}
}
]
[ScopeNames.WITH_ACCOUNT]: {
include: [
{
- model: () => AccountModel,
+ model: AccountModel,
required: true
}
]
},
[ScopeNames.WITH_VIDEOS]: {
include: [
- () => VideoModel
+ VideoModel
]
},
[ScopeNames.WITH_ACTOR]: {
include: [
- () => ActorModel
+ ActorModel
]
}
-})
+}))
@Table({
tableName: 'videoChannel',
indexes
@AllowNull(true)
@Default(null)
- @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description'))
+ @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description', true))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
description: string
@AllowNull(true)
@Default(null)
- @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support'))
+ @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support', true))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
support: string
})
Videos: VideoModel[]
+ @HasMany(() => VideoPlaylistModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'CASCADE',
+ hooks: true
+ })
+ VideoPlaylists: VideoPlaylistModel[]
+
@BeforeDestroy
static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
if (!instance.Actor) {
limit: options.count,
order: getSort(options.sort),
where: {
- [Sequelize.Op.or]: [
+ [Op.or]: [
Sequelize.literal(
'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
),
static loadByIdAndPopulateAccount (id: number) {
return VideoChannelModel.unscoped()
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
- .findById(id)
+ .findByPk(id)
}
static loadByIdAndAccount (id: number, accountId: number) {
static loadAndPopulateAccount (id: number) {
return VideoChannelModel.unscoped()
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
- .findById(id)
+ .findByPk(id)
}
static loadByUUIDAndPopulateAccount (uuid: string) {
.findOne(query)
}
+ static loadByNameWithHostAndPopulateAccount (nameWithHost: string) {
+ const [ name, host ] = nameWithHost.split('@')
+
+ if (!host || host === WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
+
+ return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
+ }
+
static loadLocalByNameAndPopulateAccount (name: string) {
const query = {
include: [
return VideoChannelModel.unscoped()
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
- .findById(id, options)
+ .findByPk(id, options)
}
toFormattedJSON (): VideoChannel {
return Object.assign(actor, videoChannel)
}
+ toFormattedSummaryJSON (): VideoChannelSummary {
+ const actor = this.Actor.toFormattedJSON()
+
+ return {
+ id: this.id,
+ uuid: actor.uuid,
+ name: actor.name,
+ displayName: this.getDisplayName(),
+ url: actor.url,
+ host: actor.host,
+ avatar: actor.avatar
+ }
+ }
+
toActivityPubObject (): ActivityPubActor {
const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
getDisplayName () {
return this.name
}
+
+ isOutdated () {
+ return this.Actor.isOutdated()
+ }
}