18 } from 'sequelize-typescript'
19 import { ActivityPubActor } from '../../../shared/models/activitypub'
20 import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
22 isVideoChannelDescriptionValid,
23 isVideoChannelNameValid,
24 isVideoChannelSupportValid
25 } from '../../helpers/custom-validators/video-channels'
26 import { sendDeleteActor } from '../../lib/activitypub/send'
27 import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
28 import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29 import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
30 import { VideoModel } from './video'
31 import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32 import { ServerModel } from '../server/server'
33 import { FindOptions, ModelIndexesOptions, Op } from 'sequelize'
34 import { AvatarModel } from '../avatar/avatar'
35 import { VideoPlaylistModel } from './video-playlist'
36 import * as Bluebird from 'bluebird'
38 MChannelAccountDefault,
40 MChannelActorAccountDefault,
41 MChannelActorAccountDefaultVideos
42 } from '../../typings/models/video'
44 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
45 const indexes: ModelIndexesOptions[] = [
46 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
49 fields: [ 'accountId' ]
56 export enum ScopeNames {
58 WITH_ACCOUNT = 'WITH_ACCOUNT',
59 WITH_ACTOR = 'WITH_ACTOR',
60 WITH_VIDEOS = 'WITH_VIDEOS',
64 type AvailableForListOptions = {
68 export type SummaryOptions = {
69 withAccount?: boolean // Default: false
70 withAccountBlockerIds?: number[]
73 @DefaultScope(() => ({
82 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
83 const base: FindOptions = {
84 attributes: [ 'id', 'name', 'description', 'actorId' ],
87 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
88 model: ActorModel.unscoped(),
92 attributes: [ 'host' ],
93 model: ServerModel.unscoped(),
97 model: AvatarModel.unscoped(),
105 if (options.withAccount === true) {
107 model: AccountModel.scope({
108 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
116 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
117 // Only list local channels OR channels that are on an instance followed by actorId
118 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
124 exclude: unusedActorAttributesForAPI
134 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow)
146 exclude: unusedActorAttributesForAPI
148 model: ActorModel, // Default scope includes avatar and server
156 [ScopeNames.WITH_ACCOUNT]: {
164 [ScopeNames.WITH_VIDEOS]: {
169 [ScopeNames.WITH_ACTOR]: {
176 tableName: 'videoChannel',
179 export class VideoChannelModel extends Model<VideoChannelModel> {
182 @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
188 @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description', true))
189 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
194 @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support', true))
195 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
204 @ForeignKey(() => ActorModel)
208 @BelongsTo(() => ActorModel, {
216 @ForeignKey(() => AccountModel)
220 @BelongsTo(() => AccountModel, {
226 Account: AccountModel
228 @HasMany(() => VideoModel, {
238 @HasMany(() => VideoPlaylistModel, {
245 VideoPlaylists: VideoPlaylistModel[]
248 static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
249 if (!instance.Actor) {
250 instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel
253 if (instance.Actor.isOwned()) {
254 return sendDeleteActor(instance.Actor, options.transaction)
260 static countByAccount (accountId: number) {
267 return VideoChannelModel.count(query)
270 static listForApi (actorId: number, start: number, count: number, sort: string) {
278 method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
280 return VideoChannelModel
282 .findAndCountAll(query)
283 .then(({ rows, count }) => {
284 return { total: count, data: rows }
288 static listLocalsForSitemap (sort: string): Bluebird<MChannelActor[]> {
292 order: getSort(sort),
295 attributes: [ 'preferredUsername', 'serverId' ],
296 model: ActorModel.unscoped(),
304 return VideoChannelModel
309 static searchForApi (options: {
316 const attributesInclude = []
317 const escapedSearch = VideoModel.sequelize.escape(options.search)
318 const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
319 attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
323 include: attributesInclude
325 offset: options.start,
326 limit: options.count,
327 order: getSort(options.sort),
331 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
334 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))'
341 method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
343 return VideoChannelModel
345 .findAndCountAll(query)
346 .then(({ rows, count }) => {
347 return { total: count, data: rows }
351 static listByAccount (options: {
358 offset: options.start,
359 limit: options.count,
360 order: getSort(options.sort),
365 id: options.accountId
372 return VideoChannelModel
373 .findAndCountAll(query)
374 .then(({ rows, count }) => {
375 return { total: count, data: rows }
379 static loadByIdAndPopulateAccount (id: number): Bluebird<MChannelActorAccountDefault> {
380 return VideoChannelModel.unscoped()
381 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
385 static loadByIdAndAccount (id: number, accountId: number): Bluebird<MChannelActorAccountDefault> {
393 return VideoChannelModel.unscoped()
394 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
398 static loadAndPopulateAccount (id: number): Bluebird<MChannelActorAccountDefault> {
399 return VideoChannelModel.unscoped()
400 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
404 static loadByUrlAndPopulateAccount (url: string): Bluebird<MChannelAccountDefault> {
417 return VideoChannelModel
418 .scope([ ScopeNames.WITH_ACCOUNT ])
422 static loadByNameWithHostAndPopulateAccount (nameWithHost: string) {
423 const [ name, host ] = nameWithHost.split('@')
425 if (!host || host === WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
427 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
430 static loadLocalByNameAndPopulateAccount (name: string): Bluebird<MChannelActorAccountDefault> {
437 preferredUsername: name,
444 return VideoChannelModel.unscoped()
445 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
449 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Bluebird<MChannelActorAccountDefault> {
456 preferredUsername: name
469 return VideoChannelModel.unscoped()
470 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
474 static loadAndPopulateAccountAndVideos (id: number): Bluebird<MChannelActorAccountDefaultVideos> {
481 return VideoChannelModel.unscoped()
482 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
483 .findByPk(id, options)
486 toFormattedJSON (): VideoChannel {
487 const actor = this.Actor.toFormattedJSON()
488 const videoChannel = {
490 displayName: this.getDisplayName(),
491 description: this.description,
492 support: this.support,
493 isLocal: this.Actor.isOwned(),
494 createdAt: this.createdAt,
495 updatedAt: this.updatedAt,
496 ownerAccount: undefined
499 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
501 return Object.assign(actor, videoChannel)
504 toFormattedSummaryJSON (): VideoChannelSummary {
505 const actor = this.Actor.toFormattedJSON()
510 displayName: this.getDisplayName(),
517 toActivityPubObject (): ActivityPubActor {
518 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
520 return Object.assign(obj, {
521 summary: this.description,
522 support: this.support,
525 type: 'Person' as 'Person',
526 id: this.Account.Actor.url
537 return this.Actor.isOutdated()