]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-channel.ts
Fix tests
[github/Chocobozzz/PeerTube.git] / server / models / video / video-channel.ts
CommitLineData
3fd3ab2d 1import {
06a05d5f
C
2 AllowNull,
3 BeforeDestroy,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 Default,
9 DefaultScope,
10 ForeignKey,
6dd9de95 11 HasMany,
06a05d5f
C
12 Is,
13 Model,
14 Scopes,
f37dc0dd 15 Sequelize,
06a05d5f
C
16 Table,
17 UpdatedAt
3fd3ab2d 18} from 'sequelize-typescript'
50d6de9c 19import { ActivityPubActor } from '../../../shared/models/activitypub'
418d092a 20import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
2422c46b 21import {
06a05d5f
C
22 isVideoChannelDescriptionValid,
23 isVideoChannelNameValid,
2422c46b
C
24 isVideoChannelSupportValid
25} from '../../helpers/custom-validators/video-channels'
c5a893d5 26import { sendDeleteActor } from '../../lib/activitypub/send'
bfbd9128 27import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
f37dc0dd 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
418d092a 29import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
3fd3ab2d 30import { VideoModel } from './video'
74dc3bca 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
8a19bee1 32import { ServerModel } from '../server/server'
1735c825 33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize'
418d092a
C
34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist'
453e83ea
C
36import * as Bluebird from 'bluebird'
37import {
38 MChannelAccountDefault,
39 MChannelActor,
40 MChannelActorAccountDefault,
41 MChannelActorAccountDefaultVideos
42} from '../../typings/models/video'
f37dc0dd
C
43
44// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
1735c825 45const indexes: ModelIndexesOptions[] = [
f37dc0dd
C
46 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
47
48 {
49 fields: [ 'accountId' ]
50 },
51 {
52 fields: [ 'actorId' ]
53 }
54]
3fd3ab2d 55
418d092a 56export enum ScopeNames {
453e83ea 57 FOR_API = 'FOR_API',
d48ff09d 58 WITH_ACCOUNT = 'WITH_ACCOUNT',
50d6de9c 59 WITH_ACTOR = 'WITH_ACTOR',
418d092a
C
60 WITH_VIDEOS = 'WITH_VIDEOS',
61 SUMMARY = 'SUMMARY'
d48ff09d
C
62}
63
f37dc0dd
C
64type AvailableForListOptions = {
65 actorId: number
66}
67
bfbd9128
C
68export type SummaryOptions = {
69 withAccount?: boolean // Default: false
70 withAccountBlockerIds?: number[]
71}
72
3acc5084 73@DefaultScope(() => ({
50d6de9c
C
74 include: [
75 {
3acc5084 76 model: ActorModel,
50d6de9c
C
77 required: true
78 }
79 ]
3acc5084
C
80}))
81@Scopes(() => ({
bfbd9128 82 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
1735c825 83 const base: FindOptions = {
453e83ea 84 attributes: [ 'id', 'name', 'description', 'actorId' ],
418d092a
C
85 include: [
86 {
453e83ea 87 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
418d092a
C
88 model: ActorModel.unscoped(),
89 required: true,
90 include: [
91 {
92 attributes: [ 'host' ],
93 model: ServerModel.unscoped(),
94 required: false
95 },
96 {
97 model: AvatarModel.unscoped(),
98 required: false
99 }
100 ]
101 }
102 ]
103 }
104
bfbd9128 105 if (options.withAccount === true) {
418d092a 106 base.include.push({
bfbd9128
C
107 model: AccountModel.scope({
108 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
109 }),
418d092a
C
110 required: true
111 })
112 }
f37dc0dd 113
418d092a
C
114 return base
115 },
453e83ea 116 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
f37dc0dd 117 // Only list local channels OR channels that are on an instance followed by actorId
418d092a 118 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
f37dc0dd
C
119
120 return {
121 include: [
122 {
123 attributes: {
124 exclude: unusedActorAttributesForAPI
125 },
126 model: ActorModel,
127 where: {
1735c825 128 [Op.or]: [
c305467c 129 {
f37dc0dd
C
130 serverId: null
131 },
132 {
133 serverId: {
1735c825 134 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow)
f37dc0dd 135 }
c305467c
C
136 }
137 ]
50d6de9c 138 }
f37dc0dd
C
139 },
140 {
141 model: AccountModel,
142 required: true,
143 include: [
144 {
145 attributes: {
146 exclude: unusedActorAttributesForAPI
147 },
148 model: ActorModel, // Default scope includes avatar and server
149 required: true
150 }
151 ]
152 }
153 ]
154 }
155 },
156 [ScopeNames.WITH_ACCOUNT]: {
157 include: [
158 {
3acc5084 159 model: AccountModel,
f37dc0dd 160 required: true
d48ff09d
C
161 }
162 ]
163 },
164 [ScopeNames.WITH_VIDEOS]: {
165 include: [
3acc5084 166 VideoModel
d48ff09d 167 ]
50d6de9c
C
168 },
169 [ScopeNames.WITH_ACTOR]: {
170 include: [
3acc5084 171 ActorModel
50d6de9c 172 ]
d48ff09d 173 }
3acc5084 174}))
3fd3ab2d
C
175@Table({
176 tableName: 'videoChannel',
f37dc0dd 177 indexes
3fd3ab2d
C
178})
179export class VideoChannelModel extends Model<VideoChannelModel> {
72c7248b 180
3fd3ab2d
C
181 @AllowNull(false)
182 @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
183 @Column
184 name: string
72c7248b 185
3fd3ab2d 186 @AllowNull(true)
2422c46b 187 @Default(null)
1735c825 188 @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description', true))
a10fc78b 189 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
3fd3ab2d 190 description: string
72c7248b 191
2422c46b
C
192 @AllowNull(true)
193 @Default(null)
1735c825 194 @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support', true))
a10fc78b 195 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
2422c46b
C
196 support: string
197
3fd3ab2d
C
198 @CreatedAt
199 createdAt: Date
72c7248b 200
3fd3ab2d
C
201 @UpdatedAt
202 updatedAt: Date
4e50b6a1 203
fadf619a
C
204 @ForeignKey(() => ActorModel)
205 @Column
206 actorId: number
207
208 @BelongsTo(() => ActorModel, {
209 foreignKey: {
210 allowNull: false
211 },
212 onDelete: 'cascade'
213 })
214 Actor: ActorModel
215
3fd3ab2d
C
216 @ForeignKey(() => AccountModel)
217 @Column
218 accountId: number
4e50b6a1 219
3fd3ab2d
C
220 @BelongsTo(() => AccountModel, {
221 foreignKey: {
222 allowNull: false
223 },
6b738c7a 224 hooks: true
3fd3ab2d
C
225 })
226 Account: AccountModel
72c7248b 227
3fd3ab2d 228 @HasMany(() => VideoModel, {
72c7248b 229 foreignKey: {
3fd3ab2d 230 name: 'channelId',
72c7248b
C
231 allowNull: false
232 },
f05a1c30
C
233 onDelete: 'CASCADE',
234 hooks: true
72c7248b 235 })
3fd3ab2d 236 Videos: VideoModel[]
72c7248b 237
418d092a
C
238 @HasMany(() => VideoPlaylistModel, {
239 foreignKey: {
07b1a18a 240 allowNull: true
418d092a 241 },
df0b219d 242 onDelete: 'CASCADE',
418d092a
C
243 hooks: true
244 })
245 VideoPlaylists: VideoPlaylistModel[]
246
f05a1c30
C
247 @BeforeDestroy
248 static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
249 if (!instance.Actor) {
250 instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel
251 }
252
c5a893d5 253 if (instance.Actor.isOwned()) {
c5a893d5
C
254 return sendDeleteActor(instance.Actor, options.transaction)
255 }
256
257 return undefined
3fd3ab2d 258 }
72c7248b 259
3fd3ab2d
C
260 static countByAccount (accountId: number) {
261 const query = {
262 where: {
263 accountId
264 }
72c7248b 265 }
3fd3ab2d
C
266
267 return VideoChannelModel.count(query)
72c7248b
C
268 }
269
f37dc0dd 270 static listForApi (actorId: number, start: number, count: number, sort: string) {
3fd3ab2d
C
271 const query = {
272 offset: start,
273 limit: count,
3bb6c526 274 order: getSort(sort)
3fd3ab2d 275 }
72c7248b 276
f37dc0dd 277 const scopes = {
453e83ea 278 method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
f37dc0dd 279 }
50d6de9c 280 return VideoChannelModel
f37dc0dd
C
281 .scope(scopes)
282 .findAndCountAll(query)
283 .then(({ rows, count }) => {
284 return { total: count, data: rows }
285 })
286 }
287
453e83ea 288 static listLocalsForSitemap (sort: string): Bluebird<MChannelActor[]> {
2feebf3e
C
289 const query = {
290 attributes: [ ],
291 offset: 0,
292 order: getSort(sort),
293 include: [
294 {
295 attributes: [ 'preferredUsername', 'serverId' ],
296 model: ActorModel.unscoped(),
297 where: {
298 serverId: null
299 }
300 }
301 ]
302 }
303
304 return VideoChannelModel
305 .unscoped()
306 .findAll(query)
307 }
308
f37dc0dd
C
309 static searchForApi (options: {
310 actorId: number
311 search: string
312 start: number
313 count: number
314 sort: string
315 }) {
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))
320
321 const query = {
322 attributes: {
323 include: attributesInclude
324 },
325 offset: options.start,
326 limit: options.count,
327 order: getSort(options.sort),
328 where: {
1735c825 329 [Op.or]: [
c3c2ab1c
C
330 Sequelize.literal(
331 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
332 ),
333 Sequelize.literal(
334 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))'
f37dc0dd 335 )
c3c2ab1c 336 ]
f37dc0dd
C
337 }
338 }
339
340 const scopes = {
453e83ea 341 method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
f37dc0dd
C
342 }
343 return VideoChannelModel
344 .scope(scopes)
50d6de9c 345 .findAndCountAll(query)
3fd3ab2d
C
346 .then(({ rows, count }) => {
347 return { total: count, data: rows }
348 })
72c7248b
C
349 }
350
91b66319
C
351 static listByAccount (options: {
352 accountId: number,
353 start: number,
354 count: number,
355 sort: string
356 }) {
3fd3ab2d 357 const query = {
91b66319
C
358 offset: options.start,
359 limit: options.count,
360 order: getSort(options.sort),
3fd3ab2d
C
361 include: [
362 {
363 model: AccountModel,
364 where: {
91b66319 365 id: options.accountId
3fd3ab2d 366 },
50d6de9c 367 required: true
3fd3ab2d
C
368 }
369 ]
370 }
72c7248b 371
50d6de9c
C
372 return VideoChannelModel
373 .findAndCountAll(query)
3fd3ab2d
C
374 .then(({ rows, count }) => {
375 return { total: count, data: rows }
376 })
72c7248b
C
377 }
378
453e83ea 379 static loadByIdAndPopulateAccount (id: number): Bluebird<MChannelActorAccountDefault> {
5cf84858
C
380 return VideoChannelModel.unscoped()
381 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
9b39106d 382 .findByPk(id)
5cf84858
C
383 }
384
453e83ea 385 static loadByIdAndAccount (id: number, accountId: number): Bluebird<MChannelActorAccountDefault> {
8a19bee1 386 const query = {
3fd3ab2d
C
387 where: {
388 id,
389 accountId
d48ff09d 390 }
571389d4 391 }
3fd3ab2d 392
5cf84858 393 return VideoChannelModel.unscoped()
50d6de9c 394 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
8a19bee1 395 .findOne(query)
0d0e8dd0
C
396 }
397
453e83ea 398 static loadAndPopulateAccount (id: number): Bluebird<MChannelActorAccountDefault> {
5cf84858 399 return VideoChannelModel.unscoped()
50d6de9c 400 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
9b39106d 401 .findByPk(id)
3fd3ab2d 402 }
0d0e8dd0 403
453e83ea 404 static loadByUrlAndPopulateAccount (url: string): Bluebird<MChannelAccountDefault> {
f37dc0dd
C
405 const query = {
406 include: [
407 {
408 model: ActorModel,
409 required: true,
410 where: {
411 url
412 }
413 }
414 ]
415 }
416
417 return VideoChannelModel
418 .scope([ ScopeNames.WITH_ACCOUNT ])
8a19bee1 419 .findOne(query)
72c7248b
C
420 }
421
92bf2f62
C
422 static loadByNameWithHostAndPopulateAccount (nameWithHost: string) {
423 const [ name, host ] = nameWithHost.split('@')
424
6dd9de95 425 if (!host || host === WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
92bf2f62
C
426
427 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
428 }
429
453e83ea 430 static loadLocalByNameAndPopulateAccount (name: string): Bluebird<MChannelActorAccountDefault> {
8a19bee1 431 const query = {
3fd3ab2d 432 include: [
8a19bee1
C
433 {
434 model: ActorModel,
435 required: true,
436 where: {
437 preferredUsername: name,
438 serverId: null
439 }
440 }
3fd3ab2d
C
441 ]
442 }
72c7248b 443
5cf84858 444 return VideoChannelModel.unscoped()
8a19bee1
C
445 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
446 .findOne(query)
72c7248b
C
447 }
448
453e83ea 449 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Bluebird<MChannelActorAccountDefault> {
06a05d5f
C
450 const query = {
451 include: [
452 {
453 model: ActorModel,
454 required: true,
455 where: {
8a19bee1
C
456 preferredUsername: name
457 },
458 include: [
459 {
460 model: ServerModel,
461 required: true,
462 where: { host }
463 }
464 ]
06a05d5f
C
465 }
466 ]
467 }
468
5cf84858 469 return VideoChannelModel.unscoped()
8a19bee1
C
470 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
471 .findOne(query)
472 }
473
453e83ea 474 static loadAndPopulateAccountAndVideos (id: number): Bluebird<MChannelActorAccountDefaultVideos> {
8a19bee1
C
475 const options = {
476 include: [
477 VideoModel
478 ]
479 }
480
5cf84858 481 return VideoChannelModel.unscoped()
8a19bee1 482 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
9b39106d 483 .findByPk(id, options)
06a05d5f
C
484 }
485
2422c46b 486 toFormattedJSON (): VideoChannel {
50d6de9c 487 const actor = this.Actor.toFormattedJSON()
6b738c7a 488 const videoChannel = {
3fd3ab2d 489 id: this.id,
749c7247 490 displayName: this.getDisplayName(),
3fd3ab2d 491 description: this.description,
2422c46b 492 support: this.support,
50d6de9c 493 isLocal: this.Actor.isOwned(),
3fd3ab2d 494 createdAt: this.createdAt,
6b738c7a 495 updatedAt: this.updatedAt,
06a05d5f 496 ownerAccount: undefined
6b738c7a
C
497 }
498
a4f99a76 499 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
72c7248b 500
6b738c7a 501 return Object.assign(actor, videoChannel)
72c7248b
C
502 }
503
418d092a
C
504 toFormattedSummaryJSON (): VideoChannelSummary {
505 const actor = this.Actor.toFormattedJSON()
506
507 return {
508 id: this.id,
418d092a
C
509 name: actor.name,
510 displayName: this.getDisplayName(),
511 url: actor.url,
512 host: actor.host,
513 avatar: actor.avatar
514 }
515 }
516
50d6de9c
C
517 toActivityPubObject (): ActivityPubActor {
518 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
519
520 return Object.assign(obj, {
521 summary: this.description,
2422c46b 522 support: this.support,
50d6de9c
C
523 attributedTo: [
524 {
525 type: 'Person' as 'Person',
526 id: this.Account.Actor.url
527 }
528 ]
529 })
72c7248b 530 }
749c7247
C
531
532 getDisplayName () {
533 return this.name
534 }
744d0eca
C
535
536 isOutdated () {
537 return this.Actor.isOutdated()
538 }
72c7248b 539}