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