Split types and typings
[oweals/peertube.git] / server / models / server / plugin.ts
1 import * as Bluebird from 'bluebird'
2 import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
3 import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4 import { MPlugin, MPluginFormattable } from '@server/types/models'
5 import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
6 import { PluginType } from '../../../shared/models/plugins/plugin.type'
7 import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
8 import {
9   isPluginDescriptionValid,
10   isPluginHomepage,
11   isPluginNameValid,
12   isPluginTypeValid,
13   isPluginVersionValid
14 } from '../../helpers/custom-validators/plugins'
15 import { getSort, throwIfNotValid } from '../utils'
16
17 @DefaultScope(() => ({
18   attributes: {
19     exclude: [ 'storage' ]
20   }
21 }))
22
23 @Table({
24   tableName: 'plugin',
25   indexes: [
26     {
27       fields: [ 'name', 'type' ],
28       unique: true
29     }
30   ]
31 })
32 export class PluginModel extends Model<PluginModel> {
33
34   @AllowNull(false)
35   @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name'))
36   @Column
37   name: string
38
39   @AllowNull(false)
40   @Is('PluginType', value => throwIfNotValid(value, isPluginTypeValid, 'type'))
41   @Column
42   type: number
43
44   @AllowNull(false)
45   @Is('PluginVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version'))
46   @Column
47   version: string
48
49   @AllowNull(true)
50   @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version'))
51   @Column
52   latestVersion: string
53
54   @AllowNull(false)
55   @Column
56   enabled: boolean
57
58   @AllowNull(false)
59   @Column
60   uninstalled: boolean
61
62   @AllowNull(false)
63   @Column
64   peertubeEngine: string
65
66   @AllowNull(true)
67   @Is('PluginDescription', value => throwIfNotValid(value, isPluginDescriptionValid, 'description'))
68   @Column
69   description: string
70
71   @AllowNull(false)
72   @Is('PluginHomepage', value => throwIfNotValid(value, isPluginHomepage, 'homepage'))
73   @Column
74   homepage: string
75
76   @AllowNull(true)
77   @Column(DataType.JSONB)
78   settings: any
79
80   @AllowNull(true)
81   @Column(DataType.JSONB)
82   storage: any
83
84   @CreatedAt
85   createdAt: Date
86
87   @UpdatedAt
88   updatedAt: Date
89
90   static listEnabledPluginsAndThemes (): Bluebird<MPlugin[]> {
91     const query = {
92       where: {
93         enabled: true,
94         uninstalled: false
95       }
96     }
97
98     return PluginModel.findAll(query)
99   }
100
101   static loadByNpmName (npmName: string): Bluebird<MPlugin> {
102     const name = this.normalizePluginName(npmName)
103     const type = this.getTypeFromNpmName(npmName)
104
105     const query = {
106       where: {
107         name,
108         type
109       }
110     }
111
112     return PluginModel.findOne(query)
113   }
114
115   static getSetting (pluginName: string, pluginType: PluginType, settingName: string, registeredSettings: RegisterServerSettingOptions[]) {
116     const query = {
117       attributes: [ 'settings' ],
118       where: {
119         name: pluginName,
120         type: pluginType
121       }
122     }
123
124     return PluginModel.findOne(query)
125       .then(p => {
126         if (!p || !p.settings || p.settings === undefined) {
127           const registered = registeredSettings.find(s => s.name === settingName)
128           if (!registered || registered.default === undefined) return undefined
129
130           return registered.default
131         }
132
133         return p.settings[settingName]
134       })
135   }
136
137   static getSettings (
138     pluginName: string,
139     pluginType: PluginType,
140     settingNames: string[],
141     registeredSettings: RegisterServerSettingOptions[]
142   ) {
143     const query = {
144       attributes: [ 'settings' ],
145       where: {
146         name: pluginName,
147         type: pluginType
148       }
149     }
150
151     return PluginModel.findOne(query)
152       .then(p => {
153         const result: { [settingName: string ]: string | boolean } = {}
154
155         for (const name of settingNames) {
156           if (!p || !p.settings || p.settings[name] === undefined) {
157             const registered = registeredSettings.find(s => s.name === name)
158
159             if (registered?.default !== undefined) {
160               result[name] = registered.default
161             }
162           } else {
163             result[name] = p.settings[name]
164           }
165         }
166
167         return result
168       })
169   }
170
171   static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: string) {
172     const query = {
173       where: {
174         name: pluginName,
175         type: pluginType
176       }
177     }
178
179     const toSave = {
180       [`settings.${settingName}`]: settingValue
181     }
182
183     return PluginModel.update(toSave, query)
184       .then(() => undefined)
185   }
186
187   static getData (pluginName: string, pluginType: PluginType, key: string) {
188     const query = {
189       raw: true,
190       attributes: [ [ json('storage.' + key), 'value' ] as any ], // FIXME: typings
191       where: {
192         name: pluginName,
193         type: pluginType
194       }
195     }
196
197     return PluginModel.findOne(query)
198       .then((c: any) => {
199         if (!c) return undefined
200         const value = c.value
201
202         if (typeof value === 'string' && value.startsWith('{')) {
203           try {
204             return JSON.parse(value)
205           } catch {
206             return value
207           }
208         }
209
210         return c.value
211       })
212   }
213
214   static storeData (pluginName: string, pluginType: PluginType, key: string, data: any) {
215     const query = 'UPDATE "plugin" SET "storage" = jsonb_set(coalesce("storage", \'{}\'), :key, :data::jsonb) ' +
216     'WHERE "name" = :pluginName AND "type" = :pluginType'
217
218     const jsonPath = '{' + key + '}'
219
220     const options = {
221       replacements: { pluginName, pluginType, key: jsonPath, data: JSON.stringify(data) },
222       type: QueryTypes.UPDATE
223     }
224
225     return PluginModel.sequelize.query(query, options)
226                       .then(() => undefined)
227   }
228
229   static listForApi (options: {
230     pluginType?: PluginType
231     uninstalled?: boolean
232     start: number
233     count: number
234     sort: string
235   }) {
236     const { uninstalled = false } = options
237     const query: FindAndCountOptions = {
238       offset: options.start,
239       limit: options.count,
240       order: getSort(options.sort),
241       where: {
242         uninstalled
243       }
244     }
245
246     if (options.pluginType) query.where['type'] = options.pluginType
247
248     return PluginModel
249       .findAndCountAll<MPlugin>(query)
250       .then(({ rows, count }) => {
251         return { total: count, data: rows }
252       })
253   }
254
255   static listInstalled (): Bluebird<MPlugin[]> {
256     const query = {
257       where: {
258         uninstalled: false
259       }
260     }
261
262     return PluginModel.findAll(query)
263   }
264
265   static normalizePluginName (npmName: string) {
266     return npmName.replace(/^peertube-((theme)|(plugin))-/, '')
267   }
268
269   static getTypeFromNpmName (npmName: string) {
270     return npmName.startsWith('peertube-plugin-')
271       ? PluginType.PLUGIN
272       : PluginType.THEME
273   }
274
275   static buildNpmName (name: string, type: PluginType) {
276     if (type === PluginType.THEME) return 'peertube-theme-' + name
277
278     return 'peertube-plugin-' + name
279   }
280
281   getPublicSettings (registeredSettings: RegisterServerSettingOptions[]) {
282     const result: { [ name: string ]: string } = {}
283     const settings = this.settings || {}
284
285     for (const r of registeredSettings) {
286       if (r.private !== false) continue
287
288       result[r.name] = settings[r.name] || r.default || null
289     }
290
291     return result
292   }
293
294   toFormattedJSON (this: MPluginFormattable): PeerTubePlugin {
295     return {
296       name: this.name,
297       type: this.type,
298       version: this.version,
299       latestVersion: this.latestVersion,
300       enabled: this.enabled,
301       uninstalled: this.uninstalled,
302       peertubeEngine: this.peertubeEngine,
303       description: this.description,
304       homepage: this.homepage,
305       settings: this.settings,
306       createdAt: this.createdAt,
307       updatedAt: this.updatedAt
308     }
309   }
310
311 }