plugin.ts 7.5 KB

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