1 import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
2 import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3 import { MPlugin, MPluginFormattable } from '@server/types/models'
4 import { AttributesOnly } from '@shared/typescript-utils'
5 import { PeerTubePlugin, PluginType, RegisterServerSettingOptions, SettingEntries, SettingValue } from '../../../shared/models'
7 isPluginDescriptionValid,
10 isPluginStableOrUnstableVersionValid,
11 isPluginStableVersionValid,
13 } from '../../helpers/custom-validators/plugins'
14 import { getSort, throwIfNotValid } from '../utils'
16 @DefaultScope(() => ({
18 exclude: [ 'storage' ]
26 fields: [ 'name', 'type' ],
31 export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
34 @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name'))
39 @Is('PluginType', value => throwIfNotValid(value, isPluginTypeValid, 'type'))
44 @Is('PluginVersion', value => throwIfNotValid(value, isPluginStableOrUnstableVersionValid, 'version'))
49 @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginStableVersionValid, 'version'))
63 peertubeEngine: string
66 @Is('PluginDescription', value => throwIfNotValid(value, isPluginDescriptionValid, 'description'))
71 @Is('PluginHomepage', value => throwIfNotValid(value, isPluginHomepage, 'homepage'))
76 @Column(DataType.JSONB)
80 @Column(DataType.JSONB)
89 static listEnabledPluginsAndThemes (): Promise<MPlugin[]> {
97 return PluginModel.findAll(query)
100 static loadByNpmName (npmName: string): Promise<MPlugin> {
101 const name = this.normalizePluginName(npmName)
102 const type = this.getTypeFromNpmName(npmName)
111 return PluginModel.findOne(query)
114 static getSetting (pluginName: string, pluginType: PluginType, settingName: string, registeredSettings: RegisterServerSettingOptions[]) {
116 attributes: [ 'settings' ],
123 return PluginModel.findOne(query)
125 if (!p?.settings || p.settings === undefined) {
126 const registered = registeredSettings.find(s => s.name === settingName)
127 if (!registered || registered.default === undefined) return undefined
129 return registered.default
132 return p.settings[settingName]
138 pluginType: PluginType,
139 settingNames: string[],
140 registeredSettings: RegisterServerSettingOptions[]
143 attributes: [ 'settings' ],
150 return PluginModel.findOne(query)
152 const result: SettingEntries = {}
154 for (const name of settingNames) {
155 if (!p?.settings || p.settings[name] === undefined) {
156 const registered = registeredSettings.find(s => s.name === name)
158 if (registered?.default !== undefined) {
159 result[name] = registered.default
162 result[name] = p.settings[name]
170 static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: SettingValue) {
179 [`settings.${settingName}`]: settingValue
182 return PluginModel.update(toSave, query)
183 .then(() => undefined)
186 static getData (pluginName: string, pluginType: PluginType, key: string) {
189 attributes: [ [ json('storage.' + key), 'value' ] as any ], // FIXME: typings
196 return PluginModel.findOne(query)
198 if (!c) return undefined
199 const value = c.value
202 return JSON.parse(value)
209 static storeData (pluginName: string, pluginType: PluginType, key: string, data: any) {
210 const query = 'UPDATE "plugin" SET "storage" = jsonb_set(coalesce("storage", \'{}\'), :key, :data::jsonb) ' +
211 'WHERE "name" = :pluginName AND "type" = :pluginType'
213 const jsonPath = '{' + key + '}'
216 replacements: { pluginName, pluginType, key: jsonPath, data: JSON.stringify(data) },
217 type: QueryTypes.UPDATE
220 return PluginModel.sequelize.query(query, options)
221 .then(() => undefined)
224 static listForApi (options: {
225 pluginType?: PluginType
226 uninstalled?: boolean
231 const { uninstalled = false } = options
232 const query: FindAndCountOptions = {
233 offset: options.start,
234 limit: options.count,
235 order: getSort(options.sort),
241 if (options.pluginType) query.where['type'] = options.pluginType
244 PluginModel.count(query),
245 PluginModel.findAll<MPlugin>(query)
246 ]).then(([ total, data ]) => ({ total, data }))
249 static listInstalled (): Promise<MPlugin[]> {
256 return PluginModel.findAll(query)
259 static normalizePluginName (npmName: string) {
260 return npmName.replace(/^peertube-((theme)|(plugin))-/, '')
263 static getTypeFromNpmName (npmName: string) {
264 return npmName.startsWith('peertube-plugin-')
269 static buildNpmName (name: string, type: PluginType) {
270 if (type === PluginType.THEME) return 'peertube-theme-' + name
272 return 'peertube-plugin-' + name
275 getPublicSettings (registeredSettings: RegisterServerSettingOptions[]) {
276 const result: SettingEntries = {}
277 const settings = this.settings || {}
279 for (const r of registeredSettings) {
280 if (r.private !== false) continue
282 result[r.name] = settings[r.name] ?? r.default ?? null
288 toFormattedJSON (this: MPluginFormattable): PeerTubePlugin {
292 version: this.version,
293 latestVersion: this.latestVersion,
294 enabled: this.enabled,
295 uninstalled: this.uninstalled,
296 peertubeEngine: this.peertubeEngine,
297 description: this.description,
298 homepage: this.homepage,
299 settings: this.settings,
300 createdAt: this.createdAt,
301 updatedAt: this.updatedAt