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