Make some fields optional when uploading a video
[oweals/peertube.git] / server / models / video / video-channel.ts
1 import * as Sequelize from 'sequelize'
2 import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers'
3 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
4 import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete'
5
6 import { addMethodsToModel, getSort } from '../utils'
7 import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface'
8 import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
9 import { activityPubCollection } from '../../helpers/activitypub'
10 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11
12 let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
13 let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
14 let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
15 let isOwned: VideoChannelMethods.IsOwned
16 let countByAccount: VideoChannelMethods.CountByAccount
17 let listForApi: VideoChannelMethods.ListForApi
18 let listByAccount: VideoChannelMethods.ListByAccount
19 let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
20 let loadByUUID: VideoChannelMethods.LoadByUUID
21 let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
22 let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
23 let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
24 let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
25 let loadByUrl: VideoChannelMethods.LoadByUrl
26 let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
27
28 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
29   VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
30     {
31       uuid: {
32         type: DataTypes.UUID,
33         defaultValue: DataTypes.UUIDV4,
34         allowNull: false,
35         validate: {
36           isUUID: 4
37         }
38       },
39       name: {
40         type: DataTypes.STRING,
41         allowNull: false,
42         validate: {
43           nameValid: value => {
44             const res = isVideoChannelNameValid(value)
45             if (res === false) throw new Error('Video channel name is not valid.')
46           }
47         }
48       },
49       description: {
50         type: DataTypes.STRING,
51         allowNull: true,
52         validate: {
53           descriptionValid: value => {
54             const res = isVideoChannelDescriptionValid(value)
55             if (res === false) throw new Error('Video channel description is not valid.')
56           }
57         }
58       },
59       remote: {
60         type: DataTypes.BOOLEAN,
61         allowNull: false,
62         defaultValue: false
63       },
64       url: {
65         type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max),
66         allowNull: false,
67         validate: {
68           urlValid: value => {
69             const res = isActivityPubUrlValid(value)
70             if (res === false) throw new Error('Video channel URL is not valid.')
71           }
72         }
73       }
74     },
75     {
76       indexes: [
77         {
78           fields: [ 'accountId' ]
79         }
80       ],
81       hooks: {
82         afterDestroy
83       }
84     }
85   )
86
87   const classMethods = [
88     associate,
89
90     listForApi,
91     listByAccount,
92     loadByIdAndAccount,
93     loadAndPopulateAccount,
94     loadByUUIDAndPopulateAccount,
95     loadByUUID,
96     loadByHostAndUUID,
97     loadAndPopulateAccountAndVideos,
98     countByAccount,
99     loadByUrl,
100     loadByUUIDOrUrl
101   ]
102   const instanceMethods = [
103     isOwned,
104     toFormattedJSON,
105     toActivityPubObject
106   ]
107   addMethodsToModel(VideoChannel, classMethods, instanceMethods)
108
109   return VideoChannel
110 }
111
112 // ------------------------------ METHODS ------------------------------
113
114 isOwned = function (this: VideoChannelInstance) {
115   return this.remote === false
116 }
117
118 toFormattedJSON = function (this: VideoChannelInstance) {
119   const json = {
120     id: this.id,
121     uuid: this.uuid,
122     name: this.name,
123     description: this.description,
124     isLocal: this.isOwned(),
125     createdAt: this.createdAt,
126     updatedAt: this.updatedAt
127   }
128
129   if (this.Account !== undefined) {
130     json['owner'] = {
131       name: this.Account.name,
132       uuid: this.Account.uuid
133     }
134   }
135
136   if (Array.isArray(this.Videos)) {
137     json['videos'] = this.Videos.map(v => v.toFormattedJSON())
138   }
139
140   return json
141 }
142
143 toActivityPubObject = function (this: VideoChannelInstance) {
144   let sharesObject
145   if (Array.isArray(this.VideoChannelShares)) {
146     const shares: string[] = []
147
148     for (const videoChannelShare of this.VideoChannelShares) {
149       const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account)
150       shares.push(shareUrl)
151     }
152
153     sharesObject = activityPubCollection(shares)
154   }
155
156   const json = {
157     type: 'VideoChannel' as 'VideoChannel',
158     id: this.url,
159     uuid: this.uuid,
160     content: this.description,
161     name: this.name,
162     published: this.createdAt.toISOString(),
163     updated: this.updatedAt.toISOString(),
164     shares: sharesObject
165   }
166
167   return json
168 }
169
170 // ------------------------------ STATICS ------------------------------
171
172 function associate (models) {
173   VideoChannel.belongsTo(models.Account, {
174     foreignKey: {
175       name: 'accountId',
176       allowNull: false
177     },
178     onDelete: 'CASCADE'
179   })
180
181   VideoChannel.hasMany(models.Video, {
182     foreignKey: {
183       name: 'channelId',
184       allowNull: false
185     },
186     onDelete: 'CASCADE'
187   })
188 }
189
190 function afterDestroy (videoChannel: VideoChannelInstance) {
191   if (videoChannel.isOwned()) {
192     return sendDeleteVideoChannel(videoChannel, undefined)
193   }
194
195   return undefined
196 }
197
198 countByAccount = function (accountId: number) {
199   const query = {
200     where: {
201       accountId
202     }
203   }
204
205   return VideoChannel.count(query)
206 }
207
208 listForApi = function (start: number, count: number, sort: string) {
209   const query = {
210     offset: start,
211     limit: count,
212     order: [ getSort(sort) ],
213     include: [
214       {
215         model: VideoChannel['sequelize'].models.Account,
216         required: true,
217         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
218       }
219     ]
220   }
221
222   return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
223     return { total: count, data: rows }
224   })
225 }
226
227 listByAccount = function (accountId: number) {
228   const query = {
229     order: [ getSort('createdAt') ],
230     include: [
231       {
232         model: VideoChannel['sequelize'].models.Account,
233         where: {
234           id: accountId
235         },
236         required: true,
237         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
238       }
239     ]
240   }
241
242   return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
243     return { total: count, data: rows }
244   })
245 }
246
247 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
248   const query: Sequelize.FindOptions<VideoChannelAttributes> = {
249     where: {
250       uuid
251     }
252   }
253
254   if (t !== undefined) query.transaction = t
255
256   return VideoChannel.findOne(query)
257 }
258
259 loadByUrl = function (url: string, t?: Sequelize.Transaction) {
260   const query: Sequelize.FindOptions<VideoChannelAttributes> = {
261     where: {
262       url
263     },
264     include: [ VideoChannel['sequelize'].models.Account ]
265   }
266
267   if (t !== undefined) query.transaction = t
268
269   return VideoChannel.findOne(query)
270 }
271
272 loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) {
273   const query: Sequelize.FindOptions<VideoChannelAttributes> = {
274     where: {
275       [Sequelize.Op.or]: [
276         { uuid },
277         { url }
278       ]
279     }
280   }
281
282   if (t !== undefined) query.transaction = t
283
284   return VideoChannel.findOne(query)
285 }
286
287 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
288   const query: Sequelize.FindOptions<VideoChannelAttributes> = {
289     where: {
290       uuid
291     },
292     include: [
293       {
294         model: VideoChannel['sequelize'].models.Account,
295         include: [
296           {
297             model: VideoChannel['sequelize'].models.Server,
298             required: true,
299             where: {
300               host: fromHost
301             }
302           }
303         ]
304       }
305     ]
306   }
307
308   if (t !== undefined) query.transaction = t
309
310   return VideoChannel.findOne(query)
311 }
312
313 loadByIdAndAccount = function (id: number, accountId: number) {
314   const options = {
315     where: {
316       id,
317       accountId
318     },
319     include: [
320       {
321         model: VideoChannel['sequelize'].models.Account,
322         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
323       }
324     ]
325   }
326
327   return VideoChannel.findOne(options)
328 }
329
330 loadAndPopulateAccount = function (id: number) {
331   const options = {
332     include: [
333       {
334         model: VideoChannel['sequelize'].models.Account,
335         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
336       }
337     ]
338   }
339
340   return VideoChannel.findById(id, options)
341 }
342
343 loadByUUIDAndPopulateAccount = function (uuid: string) {
344   const options = {
345     where: {
346       uuid
347     },
348     include: [
349       {
350         model: VideoChannel['sequelize'].models.Account,
351         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
352       }
353     ]
354   }
355
356   return VideoChannel.findOne(options)
357 }
358
359 loadAndPopulateAccountAndVideos = function (id: number) {
360   const options = {
361     include: [
362       {
363         model: VideoChannel['sequelize'].models.Account,
364         include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
365       },
366       VideoChannel['sequelize'].models.Video
367     ]
368   }
369
370   return VideoChannel.findById(id, options)
371 }