mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-08-17 09:20:26 +03:00
feat(plugins): install/update/uninstall plugins in jobs
* To avoid long running operations in HTTP requests. * To not collidate with other install/update/uninstall operations.
This commit is contained in:
parent
014cdb5981
commit
c6a11145fe
@ -11,18 +11,21 @@
|
||||
<my-edit-button
|
||||
*ngIf="!isTheme(plugin)" [ptRouterLink]="getShowRouterLink(plugin)" label="Settings" i18n-label
|
||||
[responsiveLabel]="true"
|
||||
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
|
||||
></my-edit-button>
|
||||
|
||||
<my-button
|
||||
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
|
||||
[attr.disabled]="isUpdating(plugin) || isUninstalling(plugin)"
|
||||
class="update-button" *ngIf="isUpdateAvailable(plugin)" (click)="update(plugin)"
|
||||
[label]="getUpdateLabel(plugin)" icon="refresh" [responsiveLabel]="true"
|
||||
></my-button>
|
||||
|
||||
<my-delete-button
|
||||
[loading]="willUpdate(plugin)"
|
||||
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
|
||||
></my-button>
|
||||
|
||||
<my-delete-button
|
||||
(click)="uninstall(plugin)"
|
||||
label="Uninstall" i18n-label [responsiveLabel]="true"
|
||||
[disabled]="isUpdating(plugin) || isUninstalling(plugin)"
|
||||
[loading]="willUninstall(plugin)"
|
||||
[disabled]="willUpdate(plugin) || willUninstall(plugin)"
|
||||
></my-delete-button>
|
||||
</div>
|
||||
</my-plugin-card>
|
||||
|
@ -8,4 +8,8 @@
|
||||
my-edit-button,
|
||||
my-button {
|
||||
@include margin-right(10px);
|
||||
|
||||
&[disabled=true] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Subject, Subscription, filter } from 'rxjs'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
|
||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PeerTubeSocket } from '@app/core'
|
||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||
import { compareSemVer } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubePlugin, PluginType, PluginType_Type } from '@peertube/peertube-models'
|
||||
import { PeerTubePlugin, PluginManagePayload, PluginType, PluginType_Type, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delete-button.component'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component'
|
||||
@ -13,6 +13,8 @@ import { PluginCardComponent } from '../shared/plugin-card.component'
|
||||
import { InfiniteScrollerDirective } from '../../../shared/shared-main/angular/infinite-scroller.directive'
|
||||
import { NgIf, NgFor } from '@angular/common'
|
||||
import { PluginNavigationComponent } from '../shared/plugin-navigation.component'
|
||||
import { JobService } from '@app/+admin/system'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
|
||||
@Component({
|
||||
selector: 'my-plugin-list-installed',
|
||||
@ -30,7 +32,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component
|
||||
DeleteButtonComponent
|
||||
]
|
||||
})
|
||||
export class PluginListInstalledComponent implements OnInit {
|
||||
export class PluginListInstalledComponent implements OnInit, OnDestroy {
|
||||
pluginType: PluginType_Type
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
@ -41,18 +43,22 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
sort = 'name'
|
||||
|
||||
plugins: PeerTubePlugin[] = []
|
||||
updating: { [name: string]: boolean } = {}
|
||||
uninstalling: { [name: string]: boolean } = {}
|
||||
toBeUpdated: { [name: string]: boolean } = {}
|
||||
toBeUninstalled: { [name: string]: boolean } = {}
|
||||
|
||||
onDataSubject = new Subject<any[]>()
|
||||
|
||||
private notificationSub: Subscription
|
||||
|
||||
constructor (
|
||||
private pluginService: PluginService,
|
||||
private pluginApiService: PluginApiService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private jobService: JobService,
|
||||
private peertubeSocket: PeerTubeSocket
|
||||
) {
|
||||
}
|
||||
|
||||
@ -63,6 +69,43 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
this.router.navigate([], { queryParams, replaceUrl: true })
|
||||
}
|
||||
|
||||
this.jobService.listUnfinishedJobs({
|
||||
jobType: 'plugin-manage',
|
||||
pagination: {
|
||||
count: 10,
|
||||
start: 0
|
||||
},
|
||||
sort: {
|
||||
field: 'createdAt',
|
||||
order: -1
|
||||
}
|
||||
}).subscribe({
|
||||
next: resultList => {
|
||||
const jobs = resultList.data
|
||||
|
||||
jobs.forEach((job) => {
|
||||
let payload: PluginManagePayload
|
||||
|
||||
try {
|
||||
payload = JSON.parse(job.data)
|
||||
} catch (err) {}
|
||||
|
||||
if (payload.action === 'update') {
|
||||
this.toBeUpdated[payload.npmName] = true
|
||||
}
|
||||
|
||||
if (payload.action === 'uninstall') {
|
||||
this.toBeUninstalled[payload.npmName] = true
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
error: err => {
|
||||
logger.error('Could not fetch status of installed plugins.', { err })
|
||||
this.notifier.error($localize`Could not fetch status of installed plugins.`)
|
||||
}
|
||||
})
|
||||
|
||||
this.route.queryParams.subscribe(query => {
|
||||
if (!query['pluginType']) return
|
||||
|
||||
@ -70,6 +113,12 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
|
||||
this.reloadPlugins()
|
||||
})
|
||||
|
||||
this.subscribeToNotifications()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.notificationSub) this.notificationSub.unsubscribe()
|
||||
}
|
||||
|
||||
reloadPlugins () {
|
||||
@ -117,12 +166,12 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
return $localize`Update to ${plugin.latestVersion}`
|
||||
}
|
||||
|
||||
isUpdating (plugin: PeerTubePlugin) {
|
||||
return !!this.updating[this.getPluginKey(plugin)]
|
||||
willUpdate (plugin: PeerTubePlugin) {
|
||||
return !!this.toBeUpdated[this.getPluginKey(plugin)]
|
||||
}
|
||||
|
||||
isUninstalling (plugin: PeerTubePlugin) {
|
||||
return !!this.uninstalling[this.getPluginKey(plugin)]
|
||||
willUninstall (plugin: PeerTubePlugin) {
|
||||
return !!this.toBeUninstalled[this.getPluginKey(plugin)]
|
||||
}
|
||||
|
||||
isTheme (plugin: PeerTubePlugin) {
|
||||
@ -131,7 +180,7 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
|
||||
async uninstall (plugin: PeerTubePlugin) {
|
||||
const pluginKey = this.getPluginKey(plugin)
|
||||
if (this.uninstalling[pluginKey]) return
|
||||
if (this.toBeUninstalled[pluginKey]) return
|
||||
|
||||
const res = await this.confirmService.confirm(
|
||||
$localize`Do you really want to uninstall ${plugin.name}?`,
|
||||
@ -139,29 +188,24 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
this.uninstalling[pluginKey] = true
|
||||
this.toBeUninstalled[pluginKey] = true
|
||||
|
||||
this.pluginApiService.uninstall(plugin.name, plugin.type)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`${plugin.name} uninstalled.`)
|
||||
|
||||
this.plugins = this.plugins.filter(p => p.name !== plugin.name)
|
||||
this.pagination.totalItems--
|
||||
|
||||
this.uninstalling[pluginKey] = false
|
||||
this.notifier.success($localize`${plugin.name} will be uninstalled.`)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.notifier.error(err.message)
|
||||
this.uninstalling[pluginKey] = false
|
||||
this.toBeUninstalled[pluginKey] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async update (plugin: PeerTubePlugin) {
|
||||
const pluginKey = this.getPluginKey(plugin)
|
||||
if (this.updating[pluginKey]) return
|
||||
if (this.toBeUpdated[pluginKey]) return
|
||||
|
||||
if (this.isMajorUpgrade(plugin)) {
|
||||
const res = await this.confirmService.confirm(
|
||||
@ -173,22 +217,18 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
if (res === false) return
|
||||
}
|
||||
|
||||
this.updating[pluginKey] = true
|
||||
this.toBeUpdated[pluginKey] = true
|
||||
|
||||
this.pluginApiService.update(plugin.name, plugin.type)
|
||||
.pipe()
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.updating[pluginKey] = false
|
||||
|
||||
this.notifier.success($localize`${plugin.name} updated.`)
|
||||
|
||||
Object.assign(plugin, res)
|
||||
this.notifier.success($localize`${plugin.name} will be updated.`)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.notifier.error(err.message)
|
||||
this.updating[pluginKey] = false
|
||||
this.toBeUpdated[pluginKey] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -201,8 +241,36 @@ export class PluginListInstalledComponent implements OnInit {
|
||||
return this.pluginApiService.getPluginOrThemeHref(this.pluginType, name)
|
||||
}
|
||||
|
||||
private getPluginKey (plugin: PeerTubePlugin) {
|
||||
return plugin.name + plugin.type
|
||||
private async subscribeToNotifications () {
|
||||
const obs = await this.peertubeSocket.getMyNotificationsSocket()
|
||||
|
||||
this.notificationSub = obs
|
||||
.pipe(
|
||||
filter(d => d.notification?.type === UserNotificationType.PLUGIN_MANAGE_FINISHED)
|
||||
).subscribe(data => {
|
||||
const pluginName = data.notification.plugin?.name
|
||||
|
||||
if (pluginName) {
|
||||
const npmName = this.getPluginKey(data.notification.plugin)
|
||||
|
||||
if (this.toBeUninstalled[npmName]) {
|
||||
this.toBeUninstalled[npmName] = false
|
||||
|
||||
if (!data.notification.hasOperationFailed) {
|
||||
this.plugins = this.plugins.filter(p => p.name !== pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.toBeUpdated[npmName]) {
|
||||
this.toBeUpdated[npmName] = false
|
||||
this.reloadPlugins()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getPluginKey (plugin: Pick<PeerTubePlugin, 'name' | 'type'>) {
|
||||
return this.pluginService.nameToNpmName(plugin.name, plugin.type)
|
||||
}
|
||||
|
||||
private isMajorUpgrade (plugin: PeerTubePlugin) {
|
||||
|
@ -1,9 +1,5 @@
|
||||
<my-plugin-navigation [pluginType]="pluginType"></my-plugin-navigation>
|
||||
|
||||
<div class="alert pt-alert-primary" i18n *ngIf="pluginInstalled">
|
||||
To load your new installed plugins or themes, refresh the page.
|
||||
</div>
|
||||
|
||||
<div class="result-and-search">
|
||||
<ng-container *ngIf="!search">
|
||||
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
|
||||
@ -51,8 +47,9 @@
|
||||
|
||||
<my-button
|
||||
*ngIf="plugin.installed === false" (click)="install(plugin)"
|
||||
[loading]="isInstalling(plugin)" label="Install" [responsiveLabel]="true"
|
||||
icon="cloud-download" [attr.disabled]="isInstalling(plugin)"
|
||||
label="Install" [responsiveLabel]="true"
|
||||
[loading]="willInstall(plugin)"
|
||||
icon="cloud-download" [attr.disabled]="willInstall(plugin)"
|
||||
></my-button>
|
||||
</div>
|
||||
|
||||
|
@ -29,3 +29,7 @@
|
||||
.alert {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
my-button[disabled=true] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
|
||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
|
||||
import { PeerTubePluginIndex, PluginType, PluginType_Type } from '@peertube/peertube-models'
|
||||
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PeerTubeSocket, PluginService } from '@app/core'
|
||||
import { PeerTubePluginIndex, PluginManagePayload, PluginType, PluginType_Type, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component'
|
||||
@ -14,6 +14,7 @@ import { AutofocusDirective } from '../../../shared/shared-main/angular/autofocu
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { NgIf, NgFor } from '@angular/common'
|
||||
import { PluginNavigationComponent } from '../shared/plugin-navigation.component'
|
||||
import { JobService } from '@app/+admin/system'
|
||||
|
||||
@Component({
|
||||
selector: 'my-plugin-search',
|
||||
@ -32,7 +33,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component
|
||||
ButtonComponent
|
||||
]
|
||||
})
|
||||
export class PluginSearchComponent implements OnInit {
|
||||
export class PluginSearchComponent implements OnInit, OnDestroy {
|
||||
pluginType: PluginType_Type
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
@ -46,12 +47,13 @@ export class PluginSearchComponent implements OnInit {
|
||||
isSearching = false
|
||||
|
||||
plugins: PeerTubePluginIndex[] = []
|
||||
installing: { [name: string]: boolean } = {}
|
||||
toBeInstalled: { [name: string]: boolean } = {}
|
||||
pluginInstalled = false
|
||||
|
||||
onDataSubject = new Subject<any[]>()
|
||||
|
||||
private searchSubject = new Subject<string>()
|
||||
private notificationSub: Subscription
|
||||
|
||||
constructor (
|
||||
private pluginService: PluginService,
|
||||
@ -59,7 +61,9 @@ export class PluginSearchComponent implements OnInit {
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private jobService: JobService,
|
||||
private peertubeSocket: PeerTubeSocket
|
||||
) {
|
||||
}
|
||||
|
||||
@ -70,6 +74,39 @@ export class PluginSearchComponent implements OnInit {
|
||||
this.router.navigate([], { queryParams })
|
||||
}
|
||||
|
||||
this.jobService.listUnfinishedJobs({
|
||||
jobType: 'plugin-manage',
|
||||
pagination: {
|
||||
count: 10,
|
||||
start: 0
|
||||
},
|
||||
sort: {
|
||||
field: 'createdAt',
|
||||
order: -1
|
||||
}
|
||||
}).subscribe({
|
||||
next: resultList => {
|
||||
const jobs = resultList.data
|
||||
|
||||
jobs.forEach((job) => {
|
||||
let payload: PluginManagePayload
|
||||
|
||||
try {
|
||||
payload = JSON.parse(job.data)
|
||||
} catch (err) {}
|
||||
|
||||
if (payload.action === 'install') {
|
||||
this.toBeInstalled[payload.npmName] = true
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
error: err => {
|
||||
logger.error('Could not fetch status of installed plugins.', { err })
|
||||
this.notifier.error($localize`Could not fetch status of installed plugins.`)
|
||||
}
|
||||
})
|
||||
|
||||
this.route.queryParams.subscribe(query => {
|
||||
if (!query['pluginType']) return
|
||||
|
||||
@ -85,6 +122,12 @@ export class PluginSearchComponent implements OnInit {
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(search => this.router.navigate([], { queryParams: { search }, queryParamsHandling: 'merge' }))
|
||||
|
||||
this.subscribeToNotifications()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.notificationSub) this.notificationSub.unsubscribe()
|
||||
}
|
||||
|
||||
onSearchChange (event: Event) {
|
||||
@ -131,8 +174,8 @@ export class PluginSearchComponent implements OnInit {
|
||||
this.loadMorePlugins()
|
||||
}
|
||||
|
||||
isInstalling (plugin: PeerTubePluginIndex) {
|
||||
return !!this.installing[plugin.npmName]
|
||||
willInstall (plugin: PeerTubePluginIndex) {
|
||||
return !!this.toBeInstalled[plugin.npmName]
|
||||
}
|
||||
|
||||
getShowRouterLink (plugin: PeerTubePluginIndex) {
|
||||
@ -144,7 +187,7 @@ export class PluginSearchComponent implements OnInit {
|
||||
}
|
||||
|
||||
async install (plugin: PeerTubePluginIndex) {
|
||||
if (this.installing[plugin.npmName]) return
|
||||
if (this.toBeInstalled[plugin.npmName]) return
|
||||
|
||||
const res = await this.confirmService.confirm(
|
||||
$localize`Please only install plugins or themes you trust, since they can execute any code on your instance.`,
|
||||
@ -152,24 +195,46 @@ export class PluginSearchComponent implements OnInit {
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
this.installing[plugin.npmName] = true
|
||||
this.toBeInstalled[plugin.npmName] = true
|
||||
|
||||
this.pluginApiService.install(plugin.npmName)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.installing[plugin.npmName] = false
|
||||
this.pluginInstalled = true
|
||||
|
||||
this.notifier.success($localize`${plugin.name} installed.`)
|
||||
|
||||
plugin.installed = true
|
||||
this.notifier.success($localize`${plugin.name} will be installed.`)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.installing[plugin.npmName] = false
|
||||
this.toBeInstalled[plugin.npmName] = false
|
||||
|
||||
this.notifier.error(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async subscribeToNotifications () {
|
||||
const obs = await this.peertubeSocket.getMyNotificationsSocket()
|
||||
|
||||
this.notificationSub = obs
|
||||
.pipe(
|
||||
filter(d => d.notification?.type === UserNotificationType.PLUGIN_MANAGE_FINISHED)
|
||||
).subscribe(data => {
|
||||
const pluginName = data.notification.plugin?.name
|
||||
|
||||
if (pluginName) {
|
||||
const npmName = this.pluginService.nameToNpmName(data.notification.plugin.name, data.notification.plugin.type)
|
||||
|
||||
if (this.toBeInstalled[npmName]) {
|
||||
this.toBeInstalled[npmName] = false
|
||||
|
||||
if (!data.notification.hasOperationFailed) {
|
||||
const plugin = this.plugins.find(p => p.name === pluginName)
|
||||
|
||||
if (plugin) {
|
||||
plugin.installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -20,19 +20,20 @@ export class JobService {
|
||||
) {}
|
||||
|
||||
listJobs (options: {
|
||||
jobState?: JobStateClient
|
||||
jobStates?: JobStateClient[]
|
||||
jobType: JobTypeClient
|
||||
pagination: RestPagination
|
||||
sort: SortMeta
|
||||
}): Observable<ResultList<Job>> {
|
||||
const { jobState, jobType, pagination, sort } = options
|
||||
const { jobStates, jobType, pagination, sort } = options
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (jobType !== 'all') params = params.append('jobType', jobType)
|
||||
if (jobStates) params = params.append('states', jobStates.join(','))
|
||||
|
||||
return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL + `/${jobState || ''}`, { params })
|
||||
return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res, [ 'createdAt', 'processedOn', 'finishedOn' ], 'precise')),
|
||||
map(res => this.restExtractor.applyToResultListData(res, this.prettyPrintData.bind(this))),
|
||||
@ -41,6 +42,17 @@ export class JobService {
|
||||
)
|
||||
}
|
||||
|
||||
listUnfinishedJobs (options: {
|
||||
jobType: JobTypeClient
|
||||
pagination: RestPagination
|
||||
sort: SortMeta
|
||||
}): Observable<ResultList<Job>> {
|
||||
return this.listJobs({
|
||||
...options,
|
||||
jobStates: [ 'active', 'waiting', 'delayed', 'paused' ]
|
||||
})
|
||||
}
|
||||
|
||||
private prettyPrintData (obj: Job) {
|
||||
const data = JSON.stringify(obj.data, null, 2)
|
||||
|
||||
|
@ -61,6 +61,7 @@ export class JobsComponent extends RestTable implements OnInit {
|
||||
'move-to-file-system',
|
||||
'move-to-object-storage',
|
||||
'notify',
|
||||
'plugin-manage',
|
||||
'transcoding-job-builder',
|
||||
'video-channel-import',
|
||||
'video-file-import',
|
||||
@ -154,7 +155,7 @@ export class JobsComponent extends RestTable implements OnInit {
|
||||
|
||||
this.jobsService
|
||||
.listJobs({
|
||||
jobState,
|
||||
jobStates: [ jobState ],
|
||||
jobType: this.jobType,
|
||||
pagination: this.pagination,
|
||||
sort: this.sort
|
||||
|
@ -49,7 +49,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
||||
abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
|
||||
newPeerTubeVersion: $localize`A new PeerTube version is available`,
|
||||
newPluginVersion: $localize`One of your plugin/theme has a new available version`,
|
||||
myVideoStudioEditionFinished: $localize`Video studio edition has finished`
|
||||
myVideoStudioEditionFinished: $localize`Video studio edition has finished`,
|
||||
pluginManageFinished: $localize`Plugin or theme has been installed, updated or uninstalled`
|
||||
}
|
||||
this.notificationSettingGroups = [
|
||||
{
|
||||
@ -89,7 +90,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
|
||||
'newInstanceFollower',
|
||||
'autoInstanceFollowing',
|
||||
'newPeerTubeVersion',
|
||||
'newPluginVersion'
|
||||
'newPluginVersion',
|
||||
'pluginManageFinished'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Subject } from 'rxjs'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { ManagerOptions, Socket, SocketOptions } from 'socket.io-client'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { LiveVideoEventPayload, LiveVideoEventType, UserNotification as UserNotificationServer } from '@peertube/peertube-models'
|
||||
@ -12,6 +12,7 @@ export class PeerTubeSocket {
|
||||
private io: (uri: string, opts?: Partial<ManagerOptions & SocketOptions>) => Socket
|
||||
|
||||
private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>()
|
||||
private notificationObs: Observable<{ type: NotificationEvent, notification?: UserNotificationServer }>
|
||||
private liveVideosSubject = new Subject<{ type: LiveVideoEventType, payload: LiveVideoEventPayload }>()
|
||||
|
||||
private notificationSocket: Socket
|
||||
@ -24,7 +25,11 @@ export class PeerTubeSocket {
|
||||
async getMyNotificationsSocket () {
|
||||
await this.initNotificationSocket()
|
||||
|
||||
return this.notificationSubject.asObservable()
|
||||
if (!this.notificationObs) {
|
||||
this.notificationObs = this.notificationSubject.asObservable()
|
||||
}
|
||||
|
||||
return this.notificationObs
|
||||
}
|
||||
|
||||
getLiveVideosObservable () {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
|
||||
import { ButtonComponent } from './button.component'
|
||||
|
||||
@Component({
|
||||
@ -7,6 +7,7 @@ import { ButtonComponent } from './button.component'
|
||||
<my-button
|
||||
icon="delete" className="grey-button"
|
||||
[disabled]="disabled" [label]="label" [title]="title"
|
||||
[loading]="loading"
|
||||
[responsiveLabel]="responsiveLabel"
|
||||
></my-button>
|
||||
`,
|
||||
@ -18,6 +19,7 @@ export class DeleteButtonComponent implements OnInit {
|
||||
@Input() title: string
|
||||
@Input() responsiveLabel = false
|
||||
@Input() disabled: boolean
|
||||
@Input({ transform: booleanAttribute }) loading = false
|
||||
|
||||
ngOnInit () {
|
||||
if (this.label === undefined && !this.title) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
|
||||
import { ButtonComponent } from './button.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-edit-button',
|
||||
template: `
|
||||
<my-button
|
||||
icon="edit" [label]="label" [title]="title" [responsiveLabel]="responsiveLabel"
|
||||
icon="edit" [disabled]="disabled" [label]="label" [title]="title" [responsiveLabel]="responsiveLabel"
|
||||
[ptRouterLink]="ptRouterLink"
|
||||
></my-button>
|
||||
`,
|
||||
@ -13,6 +13,7 @@ import { ButtonComponent } from './button.component'
|
||||
imports: [ ButtonComponent ]
|
||||
})
|
||||
export class EditButtonComponent implements OnInit {
|
||||
@Input({ transform: booleanAttribute }) disabled = false
|
||||
@Input() label: string
|
||||
@Input() title: string
|
||||
@Input() ptRouterLink: string[] | string = []
|
||||
|
@ -20,6 +20,7 @@ export class UserNotification implements UserNotificationServer {
|
||||
id: number
|
||||
type: UserNotificationType_Type
|
||||
read: boolean
|
||||
hasOperationFailed: boolean
|
||||
|
||||
video?: VideoInfo & {
|
||||
channel: ActorInfo & { avatarUrl?: string }
|
||||
@ -119,10 +120,13 @@ export class UserNotification implements UserNotificationServer {
|
||||
pluginUrl?: string
|
||||
pluginQueryParams?: { [id: string]: string } = {}
|
||||
|
||||
jobUrl?: string
|
||||
|
||||
constructor (hash: UserNotificationServer, user: AuthUser) {
|
||||
this.id = hash.id
|
||||
this.type = hash.type
|
||||
this.read = hash.read
|
||||
this.hasOperationFailed = hash.hasOperationFailed
|
||||
|
||||
// We assume that some fields exist
|
||||
// To prevent a notification popup crash in case of bug, wrap it inside a try/catch
|
||||
@ -250,6 +254,15 @@ export class UserNotification implements UserNotificationServer {
|
||||
this.pluginQueryParams.pluginType = this.plugin.type + ''
|
||||
break
|
||||
|
||||
case UserNotificationType.PLUGIN_MANAGE_FINISHED:
|
||||
this.pluginUrl = '/admin/plugins/list-installed'
|
||||
this.jobUrl = '/admin/system/jobs'
|
||||
|
||||
if (this.plugin) {
|
||||
this.pluginQueryParams.pluginType = this.plugin.type + ''
|
||||
}
|
||||
break
|
||||
|
||||
case UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED:
|
||||
this.videoUrl = this.buildVideoUrl(this.video)
|
||||
break
|
||||
|
@ -239,6 +239,24 @@
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="22"> <!-- UserNotificationType.MANGE_PLUGIN_FINISHED -->
|
||||
<my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div *ngIf="!notification.hasOperationFailed" class="message" i18n>
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">
|
||||
<span *ngIf="notification.plugin">The plugin/theme {{ notification.plugin.name }}</span>
|
||||
<span *ngIf="!notification.plugin">A plugin/theme</span>
|
||||
</a> has been installed, updated or uninstalled.
|
||||
</div>
|
||||
|
||||
<div *ngIf="notification.hasOperationFailed" class="message" i18n>
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">
|
||||
<span *ngIf="notification.plugin">The plugin/theme {{ notification.plugin.name }}</span>
|
||||
<span *ngIf="!notification.plugin">A plugin/theme</span>
|
||||
</a> has failed to be installed, updated or uninstalled.
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
||||
|
||||
|
@ -22,6 +22,7 @@ export type JobType =
|
||||
| 'move-to-object-storage'
|
||||
| 'move-to-file-system'
|
||||
| 'notify'
|
||||
| 'plugin-manage'
|
||||
| 'video-channel-import'
|
||||
| 'video-file-import'
|
||||
| 'video-import'
|
||||
@ -87,6 +88,18 @@ export type RefreshPayload = {
|
||||
|
||||
export type EmailPayload = SendEmailOptions
|
||||
|
||||
export type PluginManagePayload = {
|
||||
action: 'install' | 'update'
|
||||
npmName: string
|
||||
path?: string
|
||||
version?: string
|
||||
userId: number
|
||||
} | {
|
||||
action: 'uninstall'
|
||||
npmName: string
|
||||
userId: number
|
||||
}
|
||||
|
||||
export type VideoFileImportPayload = {
|
||||
videoUUID: string
|
||||
filePath: string
|
||||
|
@ -29,6 +29,7 @@ export interface UserNotificationSetting {
|
||||
|
||||
newPeerTubeVersion: UserNotificationSettingValueType
|
||||
newPluginVersion: UserNotificationSettingValueType
|
||||
pluginManageFinished: UserNotificationSettingValueType
|
||||
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValueType
|
||||
}
|
||||
|
@ -36,7 +36,9 @@ export const UserNotificationType = {
|
||||
|
||||
NEW_USER_REGISTRATION_REQUEST: 20,
|
||||
|
||||
NEW_LIVE_FROM_SUBSCRIPTION: 21
|
||||
NEW_LIVE_FROM_SUBSCRIPTION: 21,
|
||||
|
||||
PLUGIN_MANAGE_FINISHED: 22
|
||||
} as const
|
||||
|
||||
export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType]
|
||||
@ -67,6 +69,7 @@ export interface UserNotification {
|
||||
id: number
|
||||
type: UserNotificationType_Type
|
||||
read: boolean
|
||||
hasOperationFailed: boolean
|
||||
|
||||
video?: VideoInfo & {
|
||||
channel: ActorInfo
|
||||
|
@ -170,7 +170,7 @@ export class PluginsCommand extends AbstractCommand {
|
||||
path: apiPath,
|
||||
fields: { npmName, path, pluginVersion },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
defaultExpectedStatus: HttpStatusCode.CREATED_201
|
||||
})
|
||||
}
|
||||
|
||||
@ -187,7 +187,7 @@ export class PluginsCommand extends AbstractCommand {
|
||||
path: apiPath,
|
||||
fields: { npmName, path },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
defaultExpectedStatus: HttpStatusCode.CREATED_201
|
||||
})
|
||||
}
|
||||
|
||||
@ -203,7 +203,7 @@ export class PluginsCommand extends AbstractCommand {
|
||||
path: apiPath,
|
||||
fields: { npmName },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
defaultExpectedStatus: HttpStatusCode.CREATED_201
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,8 @@ import {
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test server plugins API validators', function () {
|
||||
@ -42,14 +43,16 @@ describe('Test server plugins API validators', function () {
|
||||
userAccessToken = await server.login.getAccessToken(user)
|
||||
|
||||
{
|
||||
const res = await server.plugins.install({ npmName: npmPlugin })
|
||||
const plugin = res.body as PeerTubePlugin
|
||||
await server.plugins.install({ npmName: npmPlugin })
|
||||
await waitJobs(server)
|
||||
const plugin = await server.plugins.get({ npmName: npmPlugin })
|
||||
npmVersion = plugin.version
|
||||
}
|
||||
|
||||
{
|
||||
const res = await server.plugins.install({ npmName: themePlugin })
|
||||
const plugin = res.body as PeerTubePlugin
|
||||
await server.plugins.install({ npmName: themePlugin })
|
||||
await waitJobs(server)
|
||||
const plugin = await server.plugins.get({ npmName: themePlugin }) as PeerTubePlugin
|
||||
themeVersion = plugin.version
|
||||
}
|
||||
})
|
||||
|
@ -171,7 +171,8 @@ describe('Test user notifications API validators', function () {
|
||||
abuseStateChange: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB
|
||||
newPluginVersion: UserNotificationSettingValue.WEB,
|
||||
pluginManageFinished: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
it('Should fail with missing fields', async function () {
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
|
||||
import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js'
|
||||
@ -55,6 +55,7 @@ describe('Test admin notifications', function () {
|
||||
|
||||
await server.plugins.install({ npmName: 'peertube-plugin-hello-world' })
|
||||
await server.plugins.install({ npmName: 'peertube-theme-background-red' })
|
||||
await waitJobs(server)
|
||||
|
||||
sqlCommand = new SQLCommand(server)
|
||||
})
|
||||
|
@ -12,7 +12,8 @@ import {
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||
import { testHelloWorldRegisteredSettings } from '@tests/shared/plugins.js'
|
||||
@ -96,6 +97,7 @@ describe('Test plugins', function () {
|
||||
|
||||
await command.install({ npmName: 'peertube-plugin-hello-world' })
|
||||
await command.install({ npmName: 'peertube-theme-background-red' })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should have the plugin loaded in the configuration', async function () {
|
||||
@ -274,6 +276,7 @@ describe('Test plugins', function () {
|
||||
|
||||
{
|
||||
await command.update({ npmName: `peertube-${type}-${name}` })
|
||||
await waitJobs(server)
|
||||
|
||||
const plugin = await getPluginFromAPI()
|
||||
expect(plugin.version).to.equal(oldVersion)
|
||||
@ -291,6 +294,7 @@ describe('Test plugins', function () {
|
||||
|
||||
it('Should uninstall the plugin', async function () {
|
||||
await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
|
||||
await waitJobs(server)
|
||||
|
||||
const body = await command.list({ pluginType: PluginType.PLUGIN })
|
||||
expect(body.total).to.equal(0)
|
||||
@ -310,6 +314,7 @@ describe('Test plugins', function () {
|
||||
|
||||
it('Should uninstall the theme', async function () {
|
||||
await command.uninstall({ npmName: 'peertube-theme-background-red' })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should have updated the configuration', async function () {
|
||||
@ -342,6 +347,7 @@ describe('Test plugins', function () {
|
||||
path: PluginsCommand.getPluginTestPath('-broken'),
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
await waitJobs(server)
|
||||
|
||||
await check()
|
||||
|
||||
@ -360,6 +366,7 @@ describe('Test plugins', function () {
|
||||
}
|
||||
|
||||
await command.install({ path: PluginsCommand.getPluginTestPath('-native') })
|
||||
await waitJobs(server)
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
|
@ -6,7 +6,8 @@ import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer, PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test users', function () {
|
||||
@ -34,6 +35,7 @@ describe('Test users', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ npmName: 'peertube-theme-background-red' })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
describe('Creating a user', function () {
|
||||
|
@ -24,6 +24,7 @@ describe('Official plugin Akismet', function () {
|
||||
await servers[0].plugins.install({
|
||||
npmName: 'peertube-plugin-akismet'
|
||||
})
|
||||
await waitJobs(servers[0])
|
||||
|
||||
if (!process.env.AKISMET_KEY) throw new Error('Missing AKISMET_KEY from env')
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
|
||||
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
describe('Official plugin auth-ldap', function () {
|
||||
@ -16,6 +16,7 @@ describe('Official plugin auth-ldap', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ npmName: 'peertube-plugin-auth-ldap' })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should not login with without LDAP settings', async function () {
|
||||
@ -104,6 +105,7 @@ describe('Official plugin auth-ldap', function () {
|
||||
|
||||
it('Should not login if the plugin is uninstalled', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-auth-ldap' })
|
||||
await waitJobs(server)
|
||||
|
||||
await server.login.login({
|
||||
user: { username: 'fry@planetexpress.com', password: 'fry' },
|
||||
|
@ -9,7 +9,8 @@ import {
|
||||
doubleFollow,
|
||||
killallServers,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { MockBlocklist } from '../shared/mock-servers/index.js'
|
||||
|
||||
@ -38,6 +39,7 @@ describe('Official plugin auto-block videos', function () {
|
||||
for (const server of servers) {
|
||||
await server.plugins.install({ npmName: 'peertube-plugin-auto-block-videos' })
|
||||
}
|
||||
await waitJobs(servers)
|
||||
|
||||
blocklistServer = new MockBlocklist()
|
||||
port = await blocklistServer.initialize()
|
||||
|
@ -10,7 +10,8 @@ import {
|
||||
killallServers,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { MockBlocklist } from '../shared/mock-servers/index.js'
|
||||
|
||||
@ -29,6 +30,7 @@ describe('Official plugin auto-mute', function () {
|
||||
for (const server of servers) {
|
||||
await server.plugins.install({ npmName: 'peertube-plugin-auto-mute' })
|
||||
}
|
||||
await waitJobs(servers)
|
||||
|
||||
blocklistServer = new MockBlocklist()
|
||||
port = await blocklistServer.initialize()
|
||||
|
@ -24,6 +24,7 @@ describe('Official plugin Privacy Remover', function () {
|
||||
await servers[0].plugins.install({
|
||||
npmName: 'peertube-plugin-privacy-remover'
|
||||
})
|
||||
await waitJobs(servers[0])
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
@ -112,6 +112,7 @@ describe('Test syndication feeds', () => {
|
||||
await waitJobs([ ...servers, serverHLSOnly ])
|
||||
|
||||
await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-podcast-custom-tags') })
|
||||
await waitJobs(servers[0])
|
||||
})
|
||||
|
||||
describe('All feed', function () {
|
||||
@ -792,6 +793,7 @@ describe('Test syndication feeds', () => {
|
||||
|
||||
after(async function () {
|
||||
await servers[0].plugins.uninstall({ npmName: 'peertube-plugin-test-podcast-custom-tags' })
|
||||
await waitJobs(servers[0])
|
||||
|
||||
await cleanupTests([ ...servers, serverHLSOnly ])
|
||||
})
|
||||
|
@ -31,6 +31,7 @@ describe('Test plugin action hooks', function () {
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
|
||||
await waitJobs(servers[0])
|
||||
|
||||
await killallServers([ servers[0] ])
|
||||
|
||||
|
@ -9,7 +9,8 @@ import {
|
||||
decodeQueryString,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function loginExternal (options: {
|
||||
@ -71,6 +72,8 @@ describe('Test external auth plugins', function () {
|
||||
for (const suffix of [ 'one', 'two', 'three' ]) {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) })
|
||||
}
|
||||
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should display the correct configuration', async function () {
|
||||
@ -327,6 +330,7 @@ describe('Test external auth plugins', function () {
|
||||
|
||||
it('Should uninstall the plugin one and do not login Cyan', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' })
|
||||
await waitJobs(server)
|
||||
|
||||
await loginExternal({
|
||||
server,
|
||||
|
@ -44,6 +44,9 @@ describe('Test plugin filter hooks', function () {
|
||||
|
||||
await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
|
||||
await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
|
||||
|
||||
await waitJobs(servers[0])
|
||||
|
||||
{
|
||||
({ uuid: videoPlaylistUUID } = await servers[0].playlists.create({
|
||||
attributes: {
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
makeHTMLRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test plugins HTML injection', function () {
|
||||
@ -39,6 +40,7 @@ describe('Test plugins HTML injection', function () {
|
||||
this.timeout(30000)
|
||||
|
||||
await command.install({ npmName: 'peertube-plugin-hello-world' })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should have the correct global css', async function () {
|
||||
@ -55,6 +57,7 @@ describe('Test plugins HTML injection', function () {
|
||||
|
||||
it('Should have an empty global css on uninstall', async function () {
|
||||
await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
|
||||
await waitJobs(server)
|
||||
|
||||
{
|
||||
const text = await command.getCSS()
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test id and pass auth plugins', function () {
|
||||
@ -30,6 +31,7 @@ describe('Test id and pass auth plugins', function () {
|
||||
for (const suffix of [ 'one', 'two', 'three' ]) {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-id-pass-auth-' + suffix) })
|
||||
}
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should display the correct configuration', async function () {
|
||||
@ -213,6 +215,7 @@ describe('Test id and pass auth plugins', function () {
|
||||
|
||||
it('Should uninstall the plugin one and do not login existing Crash', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-id-pass-auth-one' })
|
||||
await waitJobs(server)
|
||||
|
||||
await server.login.login({
|
||||
user: { username: 'crash', password: 'crash password' },
|
||||
|
@ -41,6 +41,7 @@ describe('Test plugin helpers', function () {
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-four') })
|
||||
await waitJobs(servers[0])
|
||||
})
|
||||
|
||||
describe('Logger', function () {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
makePostBodyRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
@ -26,6 +27,7 @@ describe('Test plugin helpers', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-five') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should answer "pong"', async function () {
|
||||
@ -100,6 +102,7 @@ describe('Test plugin helpers', function () {
|
||||
|
||||
it('Should remove the plugin and remove the routes', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-five' })
|
||||
await waitJobs(server)
|
||||
|
||||
for (const path of basePaths) {
|
||||
await makeGetRequest({
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test plugin settings', function () {
|
||||
@ -24,6 +25,7 @@ describe('Test plugin settings', function () {
|
||||
await command.install({
|
||||
path: PluginsCommand.getPluginTestPath()
|
||||
})
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should not have duplicate settings', async function () {
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test plugin storage', function () {
|
||||
@ -24,6 +25,7 @@ describe('Test plugin storage', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
describe('DB storage', function () {
|
||||
@ -76,6 +78,7 @@ describe('Test plugin storage', function () {
|
||||
|
||||
it('Should still have the file after an uninstallation', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-six' })
|
||||
await waitJobs(server)
|
||||
|
||||
const content = await getFileContent()
|
||||
expect(content).to.equal('Prince Ali')
|
||||
@ -83,6 +86,7 @@ describe('Test plugin storage', function () {
|
||||
|
||||
it('Should still have the file after the reinstallation', async function () {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
|
||||
await waitJobs(server)
|
||||
|
||||
const content = await getFileContent()
|
||||
expect(content).to.equal('Prince Ali')
|
||||
|
@ -105,6 +105,7 @@ describe('Test transcoding plugins', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-one') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should have the appropriate available profiles', async function () {
|
||||
@ -219,6 +220,7 @@ describe('Test transcoding plugins', function () {
|
||||
this.timeout(240000)
|
||||
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-transcoding-one' })
|
||||
await waitJobs(server)
|
||||
|
||||
const config = await server.config.getConfig()
|
||||
|
||||
@ -238,6 +240,7 @@ describe('Test transcoding plugins', function () {
|
||||
|
||||
before(async function () {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-two') })
|
||||
await waitJobs(server)
|
||||
|
||||
await updateConf(server, 'test-vod-profile', 'test-live-profile')
|
||||
})
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
@ -23,6 +24,7 @@ describe('Test plugins module unloading', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should return a numeric value', async function () {
|
||||
@ -48,6 +50,7 @@ describe('Test plugins module unloading', function () {
|
||||
|
||||
it('Should uninstall the plugin and free the route', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-unloading' })
|
||||
await waitJobs(server)
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
@ -58,6 +61,7 @@ describe('Test plugins module unloading', function () {
|
||||
|
||||
it('Should return a different numeric value', async function () {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
|
||||
await waitJobs(server)
|
||||
|
||||
const res = await makeGetRequest({
|
||||
url: server.url,
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
function buildWebSocket (server: PeerTubeServer, path: string) {
|
||||
@ -42,6 +43,7 @@ describe('Test plugin websocket', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-websocket') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should not connect to the websocket without the appropriate path', async function () {
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test plugin translations', function () {
|
||||
@ -23,6 +24,7 @@ describe('Test plugin translations', function () {
|
||||
|
||||
await command.install({ path: PluginsCommand.getPluginTestPath() })
|
||||
await command.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should not have translations for locale pt', async function () {
|
||||
@ -56,6 +58,7 @@ describe('Test plugin translations', function () {
|
||||
|
||||
it('Should remove the plugin and remove the locales', async function () {
|
||||
await command.uninstall({ npmName: 'peertube-plugin-test-filter-translations' })
|
||||
await waitJobs(server)
|
||||
|
||||
{
|
||||
const body = await command.getTranslations({ locale: 'fr-FR' })
|
||||
|
@ -7,7 +7,8 @@ import {
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
PluginsCommand,
|
||||
setAccessTokensToServers
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
|
||||
@ -21,6 +22,7 @@ describe('Test plugin altering video constants', function () {
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
|
||||
await waitJobs(server)
|
||||
})
|
||||
|
||||
it('Should have updated languages', async function () {
|
||||
@ -93,6 +95,7 @@ describe('Test plugin altering video constants', function () {
|
||||
|
||||
it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () {
|
||||
await server.plugins.uninstall({ npmName: 'peertube-plugin-test-video-constants' })
|
||||
await waitJobs(server)
|
||||
|
||||
{
|
||||
const languages = await server.videos.getLanguages()
|
||||
@ -145,6 +148,7 @@ describe('Test plugin altering video constants', function () {
|
||||
|
||||
it('Should be able to reset categories', async function () {
|
||||
await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
|
||||
await waitJobs(server)
|
||||
|
||||
{
|
||||
const categories = await server.videos.getCategories()
|
||||
|
@ -54,7 +54,8 @@ function getAllNotificationsSettings (): UserNotificationSetting {
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
pluginManageFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| Failed to #{action} #{pluginType} #{pluginName}
|
||||
|
||||
block content
|
||||
p
|
||||
| PeerTube failed to #{action} #{pluginType} #{pluginName}. To find more information about the issue you can visit #[a(href=jobsUrl) the PeerTube admin interface].
|
@ -0,0 +1,8 @@
|
||||
extends ../common/greetings
|
||||
|
||||
block title
|
||||
| The #{pluginType} #{pluginName} has been #{actionPerfect}
|
||||
|
||||
block content
|
||||
p
|
||||
| To see all installed plugin and themes you can visit #[a(href=pluginUrl) the PeerTube admin interface].
|
@ -32,7 +32,7 @@ jobsRouter.post('/resume',
|
||||
resumeJobQueue
|
||||
)
|
||||
|
||||
jobsRouter.get('/:state?',
|
||||
jobsRouter.get('/',
|
||||
openapiOperationDoc({ operationId: 'getJobs' }),
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_JOBS),
|
||||
@ -65,31 +65,31 @@ function resumeJobQueue (req: express.Request, res: express.Response) {
|
||||
}
|
||||
|
||||
async function listJobs (req: express.Request, res: express.Response) {
|
||||
const state = req.params.state as JobState
|
||||
const states = (req.query.states || '').split(',').filter(s => !!s) as JobState[]
|
||||
const asc = req.query.sort === 'createdAt'
|
||||
const jobType = req.query.jobType
|
||||
|
||||
const jobs = await JobQueue.Instance.listForApi({
|
||||
state,
|
||||
states,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
asc,
|
||||
jobType
|
||||
})
|
||||
const total = await JobQueue.Instance.count(state, jobType)
|
||||
const total = await JobQueue.Instance.count(states, jobType)
|
||||
|
||||
const result: ResultList<Job> = {
|
||||
total,
|
||||
data: await Promise.all(jobs.map(j => formatJob(j, state)))
|
||||
data: await Promise.all(jobs.map(j => formatJob(j)))
|
||||
}
|
||||
|
||||
return res.json(result)
|
||||
}
|
||||
|
||||
async function formatJob (job: BullJob, state?: JobState): Promise<Job> {
|
||||
async function formatJob (job: BullJob): Promise<Job> {
|
||||
return {
|
||||
id: job.id,
|
||||
state: state || await job.getState(),
|
||||
state: await job.getState(),
|
||||
type: job.queueName as JobType,
|
||||
data: job.data,
|
||||
parent: job.parent
|
||||
|
@ -33,6 +33,8 @@ import {
|
||||
RegisteredServerSettings,
|
||||
UserRight
|
||||
} from '@peertube/peertube-models'
|
||||
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue/job-queue.js'
|
||||
import { basename } from 'path'
|
||||
|
||||
const pluginRouter = express.Router()
|
||||
|
||||
@ -144,45 +146,74 @@ function getPlugin (req: express.Request, res: express.Response) {
|
||||
|
||||
async function installPlugin (req: express.Request, res: express.Response) {
|
||||
const body: InstallOrUpdatePlugin = req.body
|
||||
|
||||
const fromDisk = !!body.path
|
||||
const toInstall = body.npmName || body.path
|
||||
const npmName = body.npmName ?? basename(body.path)
|
||||
|
||||
const pluginVersion = body.pluginVersion && body.npmName
|
||||
? body.pluginVersion
|
||||
: undefined
|
||||
const options = {
|
||||
type: 'plugin-manage',
|
||||
payload: {
|
||||
action: 'install',
|
||||
npmName,
|
||||
path: body.path,
|
||||
version: pluginVersion,
|
||||
userId: res.locals.oauth.token.user.id
|
||||
}
|
||||
} as CreateJobArgument
|
||||
|
||||
try {
|
||||
const plugin = await PluginManager.Instance.install({ toInstall, version: pluginVersion, fromDisk })
|
||||
await JobQueue.Instance.createJob(options)
|
||||
|
||||
return res.json(plugin.toFormattedJSON())
|
||||
return res.status(HttpStatusCode.CREATED_201).end()
|
||||
} catch (err) {
|
||||
logger.warn('Cannot install plugin %s.', toInstall, { err })
|
||||
return res.fail({ message: 'Cannot install plugin ' + toInstall })
|
||||
logger.error('Cannot create plugin-install job.', { err, options })
|
||||
return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePlugin (req: express.Request, res: express.Response) {
|
||||
const body: InstallOrUpdatePlugin = req.body
|
||||
const npmName = body.npmName ?? basename(body.path)
|
||||
|
||||
const options = {
|
||||
type: 'plugin-manage',
|
||||
payload: {
|
||||
action: 'update',
|
||||
npmName,
|
||||
path: body.path,
|
||||
userId: res.locals.oauth.token.user.id
|
||||
}
|
||||
} as CreateJobArgument
|
||||
|
||||
const fromDisk = !!body.path
|
||||
const toUpdate = body.npmName || body.path
|
||||
try {
|
||||
const plugin = await PluginManager.Instance.update(toUpdate, fromDisk)
|
||||
await JobQueue.Instance.createJob(options)
|
||||
|
||||
return res.json(plugin.toFormattedJSON())
|
||||
return res.status(HttpStatusCode.CREATED_201).end()
|
||||
} catch (err) {
|
||||
logger.warn('Cannot update plugin %s.', toUpdate, { err })
|
||||
return res.fail({ message: 'Cannot update plugin ' + toUpdate })
|
||||
logger.error('Cannot create plugin-install job.', { err, options })
|
||||
return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallPlugin (req: express.Request, res: express.Response) {
|
||||
const body: ManagePlugin = req.body
|
||||
|
||||
await PluginManager.Instance.uninstall({ npmName: body.npmName })
|
||||
try {
|
||||
await JobQueue.Instance.createJob({
|
||||
type: 'plugin-manage',
|
||||
payload: {
|
||||
action: 'uninstall',
|
||||
npmName: body.npmName,
|
||||
userId: res.locals.oauth.token.user.id
|
||||
}
|
||||
})
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
return res.status(HttpStatusCode.CREATED_201).end()
|
||||
} catch (err) {
|
||||
logger.error('Cannot create plugin-uninstall job for %s.', body.npmName, { err })
|
||||
return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
|
||||
}
|
||||
}
|
||||
|
||||
function getPublicPluginSettings (req: express.Request, res: express.Response) {
|
||||
|
@ -76,6 +76,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
|
||||
abuseStateChange: body.abuseStateChange,
|
||||
newPeerTubeVersion: body.newPeerTubeVersion,
|
||||
newPluginVersion: body.newPluginVersion,
|
||||
pluginManageFinished: body.pluginManageFinished,
|
||||
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ import { cpus } from 'os'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 850
|
||||
const LAST_MIGRATION_VERSION = 860
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -213,7 +213,8 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = {
|
||||
'notify': 1,
|
||||
'federate-video': 1,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
'import-user-archive': 1,
|
||||
'plugin-manage': 1
|
||||
}
|
||||
// Excluded keys are jobs that can be configured by admins
|
||||
const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
|
||||
@ -241,7 +242,8 @@ const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-im
|
||||
'notify': 5,
|
||||
'federate-video': 3,
|
||||
'create-user-export': 1,
|
||||
'import-user-archive': 1
|
||||
'import-user-archive': 1,
|
||||
'plugin-manage': 1
|
||||
}
|
||||
const JOB_TTL: { [id in JobType]: number } = {
|
||||
'activitypub-http-broadcast': 60000 * 10, // 10 minutes
|
||||
@ -270,7 +272,8 @@ const JOB_TTL: { [id in JobType]: number } = {
|
||||
'notify': 60000 * 5, // 5 minutes
|
||||
'federate-video': 60000 * 5, // 5 minutes,
|
||||
'create-user-export': 60000 * 60 * 24, // 24 hours
|
||||
'import-user-archive': 60000 * 60 * 24 // 24 hours
|
||||
'import-user-archive': 60000 * 60 * 24, // 24 hours
|
||||
'plugin-manage': 60000 * 10 // 10 minutes
|
||||
}
|
||||
const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
|
||||
'videos-views-stats': {
|
||||
|
@ -0,0 +1,42 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
const { transaction } = utils
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('userNotificationSetting', 'pluginManageFinished', data, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "userNotificationSetting" SET "pluginManageFinished" = 1'
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('userNotificationSetting', 'pluginManageFinished', data, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down () {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
db: any
|
||||
}): Promise<void> {
|
||||
const { transaction } = utils
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: null,
|
||||
allowNull: true
|
||||
}
|
||||
await utils.queryInterface.addColumn('userNotification', 'hasOperationFailed', data, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const query = 'UPDATE "userNotification" SET "hasOperationFailed" = false'
|
||||
await utils.sequelize.query(query, { transaction })
|
||||
}
|
||||
|
||||
{
|
||||
const data = {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: null,
|
||||
allowNull: false
|
||||
}
|
||||
await utils.queryInterface.changeColumn('userNotification', 'hasOperationFailed', data, { transaction })
|
||||
}
|
||||
}
|
||||
|
||||
function down () {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
64
server/core/lib/job-queue/handlers/plugin-manage.ts
Normal file
64
server/core/lib/job-queue/handlers/plugin-manage.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { Job } from 'bullmq'
|
||||
import { PluginManagePayload } from '@peertube/peertube-models'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { PluginManager } from '@server/lib/plugins/plugin-manager.js'
|
||||
import { Notifier } from '@server/lib/notifier/index.js'
|
||||
import { PluginModel } from '@server/models/server/plugin.js'
|
||||
|
||||
async function processPluginManage (job: Job) {
|
||||
const payload = job.data as PluginManagePayload
|
||||
let hasError = false
|
||||
let pluginId: number
|
||||
logger.info('Processing plugin manage in job %s.', job.id)
|
||||
|
||||
switch (payload.action) {
|
||||
case 'install': {
|
||||
const toInstall = payload.path || payload.npmName
|
||||
const fromDisk = !!payload.path
|
||||
|
||||
try {
|
||||
const plugin = await PluginManager.Instance.install({
|
||||
fromDisk,
|
||||
toInstall,
|
||||
version: payload.version
|
||||
})
|
||||
pluginId = plugin.id
|
||||
} catch (err) {
|
||||
hasError = true
|
||||
logger.warn('Cannot install plugin %s.', toInstall, { err })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'update': {
|
||||
const toUpdate = payload.path || payload.npmName
|
||||
const fromDisk = !!payload.path
|
||||
|
||||
try {
|
||||
const plugin = await PluginManager.Instance.update(toUpdate, fromDisk)
|
||||
pluginId = plugin.id
|
||||
} catch (err) {
|
||||
hasError = true
|
||||
logger.warn('Cannot update plugin %s.', toUpdate, { err })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'uninstall':
|
||||
try {
|
||||
const plugin = await PluginModel.loadByNpmName(payload.npmName)
|
||||
pluginId = plugin.id
|
||||
await PluginManager.Instance.uninstall(payload)
|
||||
} catch (err) {
|
||||
hasError = true
|
||||
logger.warn('Cannot uninstall plugin %s.', payload.npmName, { err })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
Notifier.Instance.notifyOfPluginManageFinished(payload, pluginId, hasError)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
processPluginManage
|
||||
}
|
@ -29,6 +29,7 @@ import {
|
||||
ManageVideoTorrentPayload,
|
||||
MoveStoragePayload,
|
||||
NotifyPayload,
|
||||
PluginManagePayload,
|
||||
RefreshPayload,
|
||||
TranscodingJobBuilderPayload,
|
||||
VideoChannelImportPayload,
|
||||
@ -75,6 +76,7 @@ import { processVideosViewsStats } from './handlers/video-views-stats.js'
|
||||
import { onMoveToFileSystemFailure, processMoveToFileSystem } from './handlers/move-to-file-system.js'
|
||||
import { processCreateUserExport } from './handlers/create-user-export.js'
|
||||
import { processImportUserArchive } from './handlers/import-user-archive.js'
|
||||
import { processPluginManage } from './handlers/plugin-manage.js'
|
||||
|
||||
export type CreateJobArgument =
|
||||
{ type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
|
||||
@ -104,7 +106,8 @@ export type CreateJobArgument =
|
||||
{ type: 'federate-video', payload: FederateVideoPayload } |
|
||||
{ type: 'create-user-export', payload: CreateUserExportPayload } |
|
||||
{ type: 'generate-video-storyboard', payload: GenerateStoryboardPayload } |
|
||||
{ type: 'import-user-archive', payload: ImportUserArchivePayload }
|
||||
{ type: 'import-user-archive', payload: ImportUserArchivePayload } |
|
||||
{ type: 'plugin-manage', payload: PluginManagePayload }
|
||||
|
||||
export type CreateJobOptions = {
|
||||
delay?: number
|
||||
@ -129,6 +132,7 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
|
||||
'move-to-object-storage': processMoveToObjectStorage,
|
||||
'move-to-file-system': processMoveToFileSystem,
|
||||
'notify': processNotify,
|
||||
'plugin-manage': processPluginManage,
|
||||
'video-channel-import': processVideoChannelImport,
|
||||
'video-file-import': processVideoFileImport,
|
||||
'video-import': processVideoImport,
|
||||
@ -164,6 +168,7 @@ const jobTypes: JobType[] = [
|
||||
'move-to-object-storage',
|
||||
'move-to-file-system',
|
||||
'notify',
|
||||
'plugin-manage',
|
||||
'transcoding-job-builder',
|
||||
'video-channel-import',
|
||||
'video-file-import',
|
||||
@ -413,15 +418,15 @@ class JobQueue {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async listForApi (options: {
|
||||
state?: JobState
|
||||
states: JobState[]
|
||||
start: number
|
||||
count: number
|
||||
asc?: boolean
|
||||
jobType: JobType
|
||||
}): Promise<Job[]> {
|
||||
const { state, start, count, asc, jobType } = options
|
||||
const { states, start, count, asc, jobType } = options
|
||||
|
||||
const states = this.buildStateFilter(state)
|
||||
const totalStates = this.buildStateFilter(states)
|
||||
const filteredJobTypes = this.buildTypeFilter(jobType)
|
||||
|
||||
let results: Job[] = []
|
||||
@ -434,7 +439,7 @@ class JobQueue {
|
||||
continue
|
||||
}
|
||||
|
||||
let jobs = await queue.getJobs(states, 0, start + count, asc)
|
||||
let jobs = await queue.getJobs(totalStates, 0, start + count, asc)
|
||||
|
||||
// FIXME: we have sometimes undefined values https://github.com/taskforcesh/bullmq/issues/248
|
||||
jobs = jobs.filter(j => !!j)
|
||||
@ -454,8 +459,8 @@ class JobQueue {
|
||||
return results.slice(start, start + count)
|
||||
}
|
||||
|
||||
async count (state: JobState, jobType?: JobType): Promise<number> {
|
||||
const states = this.buildStateFilter(state)
|
||||
async count (states: JobState[], jobType?: JobType): Promise<number> {
|
||||
const totalStates = this.buildStateFilter(states)
|
||||
const filteredJobTypes = this.buildTypeFilter(jobType)
|
||||
|
||||
let total = 0
|
||||
@ -469,7 +474,7 @@ class JobQueue {
|
||||
|
||||
const counts = await queue.getJobCounts()
|
||||
|
||||
for (const s of states) {
|
||||
for (const s of totalStates) {
|
||||
total += counts[s]
|
||||
}
|
||||
}
|
||||
@ -477,18 +482,18 @@ class JobQueue {
|
||||
return total
|
||||
}
|
||||
|
||||
private buildStateFilter (state?: JobState) {
|
||||
if (!state) return Array.from(jobStates)
|
||||
private buildStateFilter (states: JobState[]) {
|
||||
if (states.length === 0) return Array.from(jobStates)
|
||||
|
||||
const states = [ state ]
|
||||
const totalStates = states
|
||||
|
||||
// Include parent and prioritized if filtering on waiting
|
||||
if (state === 'waiting') {
|
||||
states.push('waiting-children')
|
||||
states.push('prioritized')
|
||||
if (states.includes('waiting')) {
|
||||
totalStates.push('waiting-children')
|
||||
totalStates.push('prioritized')
|
||||
}
|
||||
|
||||
return states
|
||||
return totalStates
|
||||
}
|
||||
|
||||
private buildTypeFilter (jobType?: JobType) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserNotificationSettingValue, UserNotificationSettingValueType } from '@peertube/peertube-models'
|
||||
import { PluginManagePayload, UserNotificationSettingValue, UserNotificationSettingValueType } from '@peertube/peertube-models'
|
||||
import { MRegistration, MUser, MUserDefault } from '@server/types/models/user/index.js'
|
||||
import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist.js'
|
||||
import { logger, loggerTagsFactory } from '../../helpers/logger.js'
|
||||
@ -43,6 +43,7 @@ import {
|
||||
StudioEditionFinishedForOwner,
|
||||
UnblacklistForOwner
|
||||
} from './shared/index.js'
|
||||
import { PluginManageFinished } from './shared/plugin-manage/plugin-manage-finished.js'
|
||||
|
||||
const lTags = loggerTagsFactory('notifier')
|
||||
|
||||
@ -69,7 +70,8 @@ class Notifier {
|
||||
newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ],
|
||||
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
||||
newPluginVersion: [ NewPluginVersionForAdmins ],
|
||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ]
|
||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
|
||||
pluginManageFinished: [ PluginManageFinished ]
|
||||
}
|
||||
|
||||
private static instance: Notifier
|
||||
@ -266,6 +268,15 @@ class Notifier {
|
||||
.catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }))
|
||||
}
|
||||
|
||||
notifyOfPluginManageFinished (pluginManagePayload: PluginManagePayload, pluginId: number, hasError: boolean) {
|
||||
const models = this.notificationModels.pluginManageFinished
|
||||
|
||||
logger.debug('Notify on new plugin manage finished', { ...pluginManagePayload, ...lTags() })
|
||||
|
||||
this.sendNotifications(models, { hasError, pluginManagePayload, pluginId })
|
||||
.catch(err => logger.error('Cannot notify on plugin manage finished.', { err }))
|
||||
}
|
||||
|
||||
notifyOfFinishedVideoStudioEdition (video: MVideoFullLight) {
|
||||
const models = this.notificationModels.videoStudioEditionFinished
|
||||
|
||||
@ -297,7 +308,7 @@ class Notifier {
|
||||
if (webNotificationEnabled) {
|
||||
await notification.save()
|
||||
|
||||
PeerTubeSocket.Instance.sendNotification(user.id, notification)
|
||||
await PeerTubeSocket.Instance.sendNotification(user.id, notification)
|
||||
}
|
||||
|
||||
if (emailNotificationEnabled) {
|
||||
@ -307,9 +318,11 @@ class Notifier {
|
||||
Hooks.runAction('action:notifier.notification.created', { webNotificationEnabled, emailNotificationEnabled, user, notification })
|
||||
}
|
||||
|
||||
for (const to of toEmails) {
|
||||
const payload = await object.createEmail(to)
|
||||
JobQueue.Instance.createJobAsync({ type: 'email', payload })
|
||||
if (object.createEmail) {
|
||||
for (const to of toEmails) {
|
||||
const payload = await object.createEmail(to)
|
||||
JobQueue.Instance.createJobAsync({ type: 'email', payload })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,83 @@
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||
import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models/index.js'
|
||||
import { PluginManagePayload, PluginType, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { AbstractNotification } from '../common/abstract-notification.js'
|
||||
import { PluginModel } from '@server/models/server/plugin.js'
|
||||
import { WEBSERVER } from '@server/initializers/constants.js'
|
||||
|
||||
export type PluginManageFinishedPayload = {
|
||||
pluginManagePayload: PluginManagePayload
|
||||
hasError: boolean
|
||||
pluginId: number
|
||||
}
|
||||
|
||||
export class PluginManageFinished extends AbstractNotification <PluginManageFinishedPayload> {
|
||||
private user: MUserDefault
|
||||
|
||||
async prepare () {
|
||||
this.user = await UserModel.loadByIdFull(this.payload.pluginManagePayload.userId)
|
||||
}
|
||||
|
||||
log () {
|
||||
logger.info('Notifying user %s its plugin %s is finished.', this.user.username, this.payload.pluginManagePayload.action)
|
||||
}
|
||||
|
||||
getSetting (user: MUserWithNotificationSetting) {
|
||||
return user.NotificationSetting.pluginManageFinished
|
||||
}
|
||||
|
||||
getTargetUsers () {
|
||||
if (!this.user) return []
|
||||
|
||||
return [ this.user ]
|
||||
}
|
||||
|
||||
createNotification (user: MUserWithNotificationSetting) {
|
||||
const notification = UserNotificationModel.build<UserNotificationModelForApi>({
|
||||
type: UserNotificationType.PLUGIN_MANAGE_FINISHED,
|
||||
pluginId: this.payload.pluginId,
|
||||
userId: user.id,
|
||||
hasOperationFailed: this.payload.hasError
|
||||
})
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
createEmail (to: string) {
|
||||
const { hasError, pluginManagePayload } = this.payload
|
||||
const { action, npmName } = pluginManagePayload
|
||||
const pluginType = PluginModel.getTypeFromNpmName(npmName) === PluginType.PLUGIN ? 'plugin' : 'theme'
|
||||
const pluginName = PluginModel.normalizePluginName(npmName)
|
||||
const jobsUrl = WEBSERVER.URL + '/admin/system/jobs'
|
||||
|
||||
if (hasError) {
|
||||
return {
|
||||
template: 'plugin-manage-failed',
|
||||
to,
|
||||
subject: `Failed to ${action} ${pluginType} ${pluginName}`,
|
||||
locals: {
|
||||
action,
|
||||
pluginName,
|
||||
pluginType,
|
||||
jobsUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + pluginType
|
||||
const actionPerfect = action === 'update' ? 'updated' : action + 'ed'
|
||||
|
||||
return {
|
||||
template: 'plugin-manage',
|
||||
to,
|
||||
subject: `The ${pluginType} ${pluginName} has been ${actionPerfect}`,
|
||||
locals: {
|
||||
actionPerfect,
|
||||
pluginName,
|
||||
pluginType,
|
||||
pluginUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import { UserNotificationModelForApi } from '@server/types/models/user/index.js'
|
||||
import { LiveVideoEventPayload, LiveVideoEventType } from '@peertube/peertube-models'
|
||||
import { logger } from '../helpers/logger.js'
|
||||
import { authenticateRunnerSocket, authenticateSocket } from '../middlewares/index.js'
|
||||
import { PluginModel } from '@server/models/server/plugin.js'
|
||||
|
||||
class PeerTubeSocket {
|
||||
|
||||
@ -76,12 +77,16 @@ class PeerTubeSocket {
|
||||
})
|
||||
}
|
||||
|
||||
sendNotification (userId: number, notification: UserNotificationModelForApi) {
|
||||
async sendNotification (userId: number, notification: UserNotificationModelForApi) {
|
||||
const sockets = this.userNotificationSockets[userId]
|
||||
if (!sockets) return
|
||||
|
||||
logger.debug('Sending user notification to user %d.', userId)
|
||||
|
||||
await notification.reload({
|
||||
include: [ PluginModel ]
|
||||
})
|
||||
|
||||
const notificationMessage = notification.toFormattedJSON()
|
||||
for (const socket of sockets) {
|
||||
socket.emit('new-notification', notificationMessage)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import express from 'express'
|
||||
import { Server } from 'http'
|
||||
import { join } from 'path'
|
||||
import { buildLogger } from '@server/helpers/logger.js'
|
||||
import { buildLogger, logger } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { WEBSERVER } from '@server/initializers/constants.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
@ -254,6 +254,7 @@ function buildSocketHelpers () {
|
||||
return {
|
||||
sendNotification: (userId: number, notification: UserNotificationModelForApi) => {
|
||||
PeerTubeSocket.Instance.sendNotification(userId, notification)
|
||||
.catch(err => logger.error('Failed to send notification on behalf of plugin.', { err }))
|
||||
},
|
||||
sendVideoLiveNewState: (video: MVideo) => {
|
||||
PeerTubeSocket.Instance.sendVideoLiveNewState(video)
|
||||
|
@ -93,7 +93,8 @@ export class UserSettingsImporter extends AbstractUserImporter <UserSettingsExpo
|
||||
autoInstanceFollowing: settingsImportData.autoInstanceFollowing,
|
||||
newPeerTubeVersion: settingsImportData.newPeerTubeVersion,
|
||||
newPluginVersion: settingsImportData.newPluginVersion,
|
||||
myVideoStudioEditionFinished: settingsImportData.myVideoStudioEditionFinished
|
||||
myVideoStudioEditionFinished: settingsImportData.myVideoStudioEditionFinished,
|
||||
pluginManageFinished: settingsImportData.pluginManageFinished
|
||||
}
|
||||
|
||||
await UserNotificationSettingModel.updateUserSettings(values, this.user.id)
|
||||
|
@ -274,7 +274,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB,
|
||||
pluginManageFinished: UserNotificationSettingValue.WEB
|
||||
}
|
||||
|
||||
return UserNotificationSettingModel.create(values, { transaction: t })
|
||||
|
@ -75,6 +75,7 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||
"UserNotificationModel"."id",
|
||||
"UserNotificationModel"."type",
|
||||
"UserNotificationModel"."read",
|
||||
"UserNotificationModel"."hasOperationFailed",
|
||||
"UserNotificationModel"."createdAt",
|
||||
"UserNotificationModel"."updatedAt",
|
||||
"Video"."id" AS "Video.id",
|
||||
|
@ -181,6 +181,15 @@ export class UserNotificationSettingModel extends SequelizeModel<UserNotificatio
|
||||
@Column
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingPluginManageFinished',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'pluginManageFinished')
|
||||
)
|
||||
@Column
|
||||
pluginManageFinished: UserNotificationSettingValueType
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
@ -233,7 +242,8 @@ export class UserNotificationSettingModel extends SequelizeModel<UserNotificatio
|
||||
abuseStateChange: this.abuseStateChange,
|
||||
newPeerTubeVersion: this.newPeerTubeVersion,
|
||||
myVideoStudioEditionFinished: this.myVideoStudioEditionFinished,
|
||||
newPluginVersion: this.newPluginVersion
|
||||
newPluginVersion: this.newPluginVersion,
|
||||
pluginManageFinished: this.pluginManageFinished
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,6 +260,12 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
})
|
||||
UserRegistration: Awaited<UserRegistrationModel>
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(false)
|
||||
@Is('HasOperationFailed', value => throwIfNotValid(value, isBooleanValid, 'hasOperationFailed'))
|
||||
@Column
|
||||
hasOperationFailed: boolean
|
||||
|
||||
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||
const where = { userId }
|
||||
|
||||
@ -445,6 +451,7 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
read: this.read,
|
||||
hasOperationFailed: this.hasOperationFailed,
|
||||
video,
|
||||
videoImport,
|
||||
comment,
|
||||
|
Loading…
Reference in New Issue
Block a user