Skip to content

Commit

Permalink
Plugin support for sophisticated spam handling (#6692)
Browse files Browse the repository at this point in the history
* feat(plugins): add filter:admin-user-list.bulk-actions.create.result

* feat(plugins): add filter:admin-user-moderation.actions.create.result

* feat(plugins): add filter:admin-comment-list.actions.create.result

* feat(plugin): add filter:admin-comment-list.bulk-actions.create.result

* feat(plugin): add filter:admin-abuse-list.actions.create.result

* feat(plugins): add doAction increment/decrement loader

Support for plugins to show application loader.

* feat(plugins): add doAction admin-user-list:load-data

* feat(plugins): add doAction admin-video-comment-list:load-data

* feat(plugins): add doAction admin-abuse-list:load-data

* feat(plugins): add doAction video-watch-comment-list:load-data

* cleanup and bug fixes

* fix(abuse-list-table): cleanup plugin action

* fixes after review

* UserListComponent: remove shortCacheObservable

* fix lint issues

* rename to admin-users-list:load-data

In order keep consistency with filter:admin-users-list.bulk-actions.create.result

* update plugin documentation

* move plugin actions to client-action.model.ts

* Styling

---------

Co-authored-by: Chocobozzz <[email protected]>
  • Loading branch information
kontrollanten and Chocobozzz authored Jan 28, 2025
1 parent 5d968ce commit 74b5096
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 136 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { NgClass, NgIf } from '@angular/common'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
import { AuthService, ConfirmService, LocalStorageService, Notifier, RestPagination, RestTable } from '@app/core'
import {
AuthService,
ConfirmService,
HooksService,
LocalStorageService,
Notifier,
PluginService,
RestPagination,
RestTable
} from '@app/core'
import { formatICU, getAPIHost } from '@app/helpers'
import { Actor } from '@app/shared/shared-main/account/actor.model'
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
Expand All @@ -15,6 +24,7 @@ import { User, UserRole, UserRoleType } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { SharedModule, SortMeta } from 'primeng/api'
import { TableModule } from 'primeng/table'
import { lastValueFrom } from 'rxjs'
import { ActorAvatarComponent } from '../../../../shared/shared-actor-image/actor-avatar.component'
import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../../shared/shared-forms/advanced-input-filter.component'
import { PeertubeCheckboxComponent } from '../../../../shared/shared-forms/peertube-checkbox.component'
Expand Down Expand Up @@ -70,7 +80,7 @@ type UserForList = User & {
ProgressBarComponent
]
})
export class UserListComponent extends RestTable <User> implements OnInit {
export class UserListComponent extends RestTable <User> implements OnInit, OnDestroy {
private static readonly LS_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'

@ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
Expand Down Expand Up @@ -114,7 +124,9 @@ export class UserListComponent extends RestTable <User> implements OnInit {
private auth: AuthService,
private blocklist: BlocklistService,
private userAdminService: UserAdminService,
private peertubeLocalStorage: LocalStorageService
private peertubeLocalStorage: LocalStorageService,
private hooks: HooksService,
private pluginService: PluginService
) {
super()
}
Expand All @@ -133,10 +145,12 @@ export class UserListComponent extends RestTable <User> implements OnInit {
this.saveSelectedColumns()
}

ngOnInit () {
async ngOnInit () {
this.initialize()

this.bulkActions = [
this.pluginService.addAction('admin-users-list:load-data', () => this.reloadDataInternal())

const bulkActions: DropdownAction<User[]>[][] = [
[
{
label: $localize`Delete`,
Expand Down Expand Up @@ -167,6 +181,8 @@ export class UserListComponent extends RestTable <User> implements OnInit {
]
]

this.bulkActions = await this.hooks.wrapObject(bulkActions, 'admin-users', 'filter:admin-users-list.bulk-actions.create.result')

this.columns = [
{ id: 'username', label: $localize`Username` },
{ id: 'role', label: $localize`Role` },
Expand All @@ -183,6 +199,10 @@ export class UserListComponent extends RestTable <User> implements OnInit {
this.loadSelectedColumns()
}

ngOnDestroy () {
this.pluginService.removeAction('admin-users-list:load-data')
}

loadSelectedColumns () {
const result = this.peertubeLocalStorage.getItem(UserListComponent.LS_SELECTED_COLUMNS_KEY)

Expand Down Expand Up @@ -325,34 +345,36 @@ export class UserListComponent extends RestTable <User> implements OnInit {
})
}

protected reloadDataInternal () {
this.userAdminService.getUsers({
protected async reloadDataInternal () {
const obs = this.userAdminService.getUsers({
pagination: this.pagination,
sort: this.sort,
search: this.search
}).subscribe({
next: resultList => {
this.users = resultList.data.map(u => ({
...u,
})

accountMutedStatus: {
...u.account,
try {
const resultList = await lastValueFrom(obs)

nameWithHost: Actor.CREATE_BY_STRING(u.account.name, u.account.host),
this.users = resultList.data.map(u => ({
...u,

mutedByInstance: false,
mutedByUser: false,
mutedServerByInstance: false,
mutedServerByUser: false
}
}))
this.totalRecords = resultList.total
accountMutedStatus: {
...u.account,

this.loadMutedStatus()
},
nameWithHost: Actor.CREATE_BY_STRING(u.account.name, u.account.host),

error: err => this.notifier.error(err.message)
})
mutedByInstance: false,
mutedByUser: false,
mutedServerByInstance: false,
mutedServerByUser: false
}
}))
this.totalRecords = resultList.total

this.loadMutedStatus()
} catch (err) {
this.notifier.error(err.message)
}
}

private loadMutedStatus () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NgFor, NgIf } from '@angular/common'
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AuthService, ComponentPagination, ConfirmService, Notifier, User, hasMoreItems } from '@app/core'
import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService, User } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { Syndication } from '@app/shared/shared-main/feeds/syndication.model'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
Expand All @@ -10,10 +10,10 @@ import { VideoComment } from '@app/shared/shared-video-comment/video-comment.mod
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
import { NgbDropdown, NgbDropdownButtonItem, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'
import { PeerTubeProblemDocument, ServerErrorCode, VideoCommentPolicy } from '@peertube/peertube-models'
import { Subject, Subscription } from 'rxjs'
import { lastValueFrom, Subject, Subscription } from 'rxjs'
import { InfiniteScrollerDirective } from '../../../../shared/shared-main/common/infinite-scroller.directive'
import { FeedComponent } from '../../../../shared/shared-main/feeds/feed.component'
import { LoaderComponent } from '../../../../shared/shared-main/common/loader.component'
import { FeedComponent } from '../../../../shared/shared-main/feeds/feed.component'
import { VideoCommentAddComponent } from './video-comment-add.component'
import { VideoCommentComponent } from './video-comment.component'

Expand Down Expand Up @@ -78,10 +78,13 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
private confirmService: ConfirmService,
private videoCommentService: VideoCommentService,
private activatedRoute: ActivatedRoute,
private hooks: HooksService
private hooks: HooksService,
private pluginService: PluginService
) {}

ngOnInit () {
this.pluginService.addAction('video-watch-comment-list:load-data', () => this.loadMoreThreads(true))

// Find highlighted comment in params
this.sub = this.activatedRoute.params.subscribe(
params => {
Expand All @@ -100,6 +103,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
}

ngOnDestroy () {
this.pluginService.removeAction('video-watch-comment-list:load-data')

if (this.sub) this.sub.unsubscribe()
}

Expand Down Expand Up @@ -145,7 +150,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
})
}

loadMoreThreads () {
async loadMoreThreads (reset = false) {
if (reset === true) {
this.componentPagination.currentPage = 1
}

const params = {
videoId: this.video.uuid,
videoPassword: this.videoPassword,
Expand All @@ -161,19 +170,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
'filter:api.video-watch.video-threads.list.result'
)

obs.subscribe({
next: res => {
this.comments = this.comments.concat(res.data)
this.componentPagination.totalItems = res.total
this.totalNotDeletedComments = res.totalNotDeletedComments
try {
const res = await lastValueFrom(obs)

this.onDataSubject.next(res.data)
if (reset) this.comments = []
this.comments = this.comments.concat(res.data)
this.componentPagination.totalItems = res.total
this.totalNotDeletedComments = res.totalNotDeletedComments

this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination })
},
this.onDataSubject.next(res.data)

error: err => this.notifier.error(err.message)
})
this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination })
} catch (err) {
this.notifier.error(err.message)
}
}

onCommentThreadCreated (comment: VideoComment) {
Expand Down
24 changes: 20 additions & 4 deletions client/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { forkJoin } from 'rxjs'
import { filter, first, map } from 'rxjs/operators'
import { DOCUMENT, getLocaleDirection, NgClass, NgIf, PlatformLocation } from '@angular/common'
import { AfterViewInit, Component, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'
import { AfterViewInit, Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { Event, GuardsCheckStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterLink, RouterOutlet } from '@angular/router'
import {
Expand Down Expand Up @@ -28,8 +30,6 @@ import { logger } from '@root-helpers/logger'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { SharedModule } from 'primeng/api'
import { ToastModule } from 'primeng/toast'
import { forkJoin } from 'rxjs'
import { filter, first, map } from 'rxjs/operators'
import { MenuService } from './core/menu/menu.service'
import { HeaderComponent } from './header/header.component'
import { POP_STATE_MODAL_DISMISS } from './helpers'
Expand Down Expand Up @@ -65,7 +65,7 @@ import { InstanceService } from './shared/shared-main/instance/instance.service'
ButtonComponent
]
})
export class AppComponent implements OnInit, AfterViewInit {
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private static LS_BROADCAST_MESSAGE = 'app-broadcast-message-dismissed'

@ViewChild('accountSetupWarningModal') accountSetupWarningModal: AccountSetupWarningModalComponent
Expand Down Expand Up @@ -146,12 +146,28 @@ export class AppComponent implements OnInit, AfterViewInit {

this.document.documentElement.lang = getShortLocale(this.localeId)
this.document.documentElement.dir = getLocaleDirection(this.localeId)

this.pluginService.addAction('application:increment-loader', () => {
this.loadingBar.useRef('plugins').start()

return Promise.resolve()
})
this.pluginService.addAction('application:decrement-loader', () => {
this.loadingBar.useRef('plugins').complete()

return Promise.resolve()
})
}

ngAfterViewInit () {
this.pluginService.initializeCustomModal(this.customModal)
}

ngOnDestroy () {
this.pluginService.removeAction('application:increment-loader')
this.pluginService.removeAction('application:decrement-loader')
}

// ---------------------------------------------------------------------------

isUserLoggedIn () {
Expand Down
26 changes: 26 additions & 0 deletions client/src/app/core/plugins/plugin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { getDevLocale, isOnDevLocale } from '@app/helpers'
import { CustomModalComponent } from '@app/modal/custom-modal.component'
import { getCompleteLocale, getKeys, isDefaultLocale, peertubeTranslate } from '@peertube/peertube-core-utils'
import {
ClientDoActionCallback,
ClientDoActionName,
ClientHook,
ClientHookName,
PluginClientScope,
Expand All @@ -28,6 +30,7 @@ import {
import { PluginInfo, PluginsManager } from '@root-helpers/plugins-manager'
import { environment } from '../../../environments/environment'
import { RegisterClientHelpers } from '../../../types/register-client-option.model'
import { logger } from '@root-helpers/logger'

type FormFields = {
video: {
Expand Down Expand Up @@ -58,6 +61,8 @@ export class PluginService implements ClientHook {

private pluginsManager: PluginsManager

private actions = new Map<ClientDoActionName, ClientDoActionCallback>()

constructor (
private authService: AuthService,
private notifier: Notifier,
Expand All @@ -71,13 +76,22 @@ export class PluginService implements ClientHook {
this.loadTranslations()

this.pluginsManager = new PluginsManager({
doAction: this.doAction.bind(this),
peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this),
onFormFields: this.onFormFields.bind(this),
onSettingsScripts: this.onSettingsScripts.bind(this),
onClientRoute: this.onClientRoute.bind(this)
})
}

addAction (actionName: ClientDoActionName, callback: ClientDoActionCallback) {
this.actions.set(actionName, callback)
}

removeAction (actionName: ClientDoActionName) {
this.actions.delete(actionName)
}

initializePlugins () {
this.pluginsManager.loadPluginsList(this.server.getHTMLConfig())

Expand Down Expand Up @@ -184,6 +198,18 @@ export class PluginService implements ClientHook {
return firstValueFrom(obs)
}

private doAction (actionName: ClientDoActionName) {
if (!this.actions.has(actionName)) {
logger.warn(`Plugin tried to do unknown action: ${actionName}`)
}

try {
return this.actions.get(actionName)()
} catch (err: any) {
logger.warn(`Cannot run action ${actionName}`, err)
}
}

private onFormFields (
pluginInfo: PluginInfo,
commonOptions: RegisterClientFormFieldOptions,
Expand Down
Loading

0 comments on commit 74b5096

Please sign in to comment.