diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 036e794d2..60dbcdf1d 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html @@ -10,8 +10,13 @@
{{ account.nameWithHost }}
Banned + Muted + Instance muted - +
{{ account.followersCount }} subscribers
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 287ab3e46..0374b70ef 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html @@ -9,7 +9,7 @@ Created Video State - + diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html index 0585e0490..ff4543b97 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html @@ -8,7 +8,7 @@ Video name Sensitive Date - + diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index afa9ccfe4..eb8d30e17 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html @@ -60,8 +60,10 @@ - {{ user.username }} - (banned) + + {{ user.username }} + (banned) + {{ user.email }} {{ user.videoQuotaUsed }} / {{ user.videoQuota }} diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html new file mode 100644 index 000000000..a96a11f5e --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html @@ -0,0 +1,26 @@ +
+
Muted accounts
+
+ + + + + + Account + Muted at + + + + + + {{ accountBlock.blockedAccount.nameWithHost }} + {{ accountBlock.createdAt }} + + + + + + diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts new file mode 100644 index 000000000..fbad28410 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from '@angular/core' +import { NotificationsService } from 'angular2-notifications' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { RestPagination, RestTable } from '@app/shared' +import { SortMeta } from 'primeng/components/common/sortmeta' +import { BlocklistService, AccountBlock } from '@app/shared/blocklist' + +@Component({ + selector: 'my-account-blocklist', + styleUrls: [ './my-account-blocklist.component.scss' ], + templateUrl: './my-account-blocklist.component.html' +}) +export class MyAccountBlocklistComponent extends RestTable implements OnInit { + blockedAccounts: AccountBlock[] = [] + totalRecords = 0 + rowsPerPage = 10 + sort: SortMeta = { field: 'createdAt', order: -1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + constructor ( + private notificationsService: NotificationsService, + private blocklistService: BlocklistService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.initialize() + } + + unblockAccount (accountBlock: AccountBlock) { + const blockedAccount = accountBlock.blockedAccount + + this.blocklistService.unblockAccountByUser(blockedAccount) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }) + ) + + this.loadData() + } + ) + } + + protected loadData () { + return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort) + .subscribe( + resultList => { + this.blockedAccounts = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } +} diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html new file mode 100644 index 000000000..680334740 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html @@ -0,0 +1,27 @@ +
+
Muted instances
+
+ + + + + + Instance + Muted at + + + + + + + {{ serverBlock.blockedServer.host }} + {{ serverBlock.createdAt }} + + + + + + diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts new file mode 100644 index 000000000..b994c2c99 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts @@ -0,0 +1,60 @@ +import { Component, OnInit } from '@angular/core' +import { NotificationsService } from 'angular2-notifications' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { RestPagination, RestTable } from '@app/shared' +import { SortMeta } from 'primeng/components/common/sortmeta' +import { ServerBlock } from '../../../../../shared' +import { BlocklistService } from '@app/shared/blocklist' + +@Component({ + selector: 'my-account-server-blocklist', + styleUrls: [ './my-account-server-blocklist.component.scss' ], + templateUrl: './my-account-server-blocklist.component.html' +}) +export class MyAccountServerBlocklistComponent extends RestTable implements OnInit { + blockedAccounts: ServerBlock[] = [] + totalRecords = 0 + rowsPerPage = 10 + sort: SortMeta = { field: 'createdAt', order: -1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + constructor ( + private notificationsService: NotificationsService, + private blocklistService: BlocklistService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.initialize() + } + + unblockServer (serverBlock: ServerBlock) { + const host = serverBlock.blockedServer.host + + this.blocklistService.unblockServerByUser(host) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Instance {{host}} unmuted.', { host }) + ) + + this.loadData() + } + ) + } + + protected loadData () { + return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort) + .subscribe( + resultList => { + this.blockedAccounts = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } +} diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 4b2168e35..49f9c94a7 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts @@ -11,6 +11,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' +import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' +import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' const myAccountRoutes: Routes = [ { @@ -94,6 +96,24 @@ const myAccountRoutes: Routes = [ title: 'Ownership changes' } } + }, + { + path: 'blocklist/accounts', + component: MyAccountBlocklistComponent, + data: { + meta: { + title: 'Accounts blocklist' + } + } + }, + { + path: 'blocklist/servers', + component: MyAccountServerBlocklistComponent, + data: { + meta: { + title: 'Instances blocklist' + } + } } ] } diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index b602fd69f..41333c25a 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html @@ -19,7 +19,21 @@ - Ownership changes +
+ + Misc + - {{ miscLabel }} + + +
+ Muted accounts + + Muted instances + + Ownership changes +
+
+
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss index 20b2639b5..6243c6dcf 100644 --- a/client/src/app/+my-account/my-account.component.scss +++ b/client/src/app/+my-account/my-account.component.scss @@ -1,4 +1,4 @@ -.my-library { +.my-library, .misc { span[role=button] { cursor: pointer; } diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index bad60a8fb..d728caf07 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -13,6 +13,7 @@ import { Subscription } from 'rxjs' export class MyAccountComponent implements OnInit, OnDestroy { libraryLabel = '' + miscLabel = '' private routeSub: Subscription @@ -23,11 +24,11 @@ export class MyAccountComponent implements OnInit, OnDestroy { ) {} ngOnInit () { - this.updateLibraryLabel(this.router.url) + this.updateLabels(this.router.url) this.routeSub = this.router.events .pipe(filter(event => event instanceof NavigationStart)) - .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) + .subscribe((event: NavigationStart) => this.updateLabels(event.url)) } ngOnDestroy () { @@ -40,7 +41,7 @@ export class MyAccountComponent implements OnInit, OnDestroy { return importConfig.http.enabled || importConfig.torrent.enabled } - private updateLibraryLabel (url: string) { + private updateLabels (url: string) { const [ path ] = url.split('?') if (path.startsWith('/my-account/video-channels')) { @@ -54,5 +55,13 @@ export class MyAccountComponent implements OnInit, OnDestroy { } else { this.libraryLabel = '' } + + if (path.startsWith('/my-account/blocklist/accounts')) { + this.miscLabel = this.i18n('Muted accounts') + } else if (path.startsWith('/my-account/blocklist/servers')) { + this.miscLabel = this.i18n('Muted instances') + } else { + this.miscLabel = '' + } } } diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index ad21162a8..017ebd57d 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -19,6 +19,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' +import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' +import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' @NgModule({ imports: [ @@ -45,7 +47,9 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub ActorAvatarInfoComponent, MyAccountVideoImportsComponent, MyAccountDangerZoneComponent, - MyAccountSubscriptionsComponent + MyAccountSubscriptionsComponent, + MyAccountBlocklistComponent, + MyAccountServerBlocklistComponent ], exports: [ diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts index 42f2cfeaf..0aba9428a 100644 --- a/client/src/app/shared/account/account.model.ts +++ b/client/src/app/shared/account/account.model.ts @@ -5,6 +5,8 @@ export class Account extends Actor implements ServerAccount { displayName: string description: string nameWithHost: string + muted: boolean + mutedServer: boolean userId?: number @@ -15,5 +17,8 @@ export class Account extends Actor implements ServerAccount { this.description = hash.description this.userId = hash.userId this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) + + this.muted = false + this.mutedServer = false } } diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts new file mode 100644 index 000000000..336680f65 --- /dev/null +++ b/client/src/app/shared/blocklist/account-block.model.ts @@ -0,0 +1,14 @@ +import { AccountBlock as AccountBlockServer } from '../../../../../shared' +import { Account } from '../account/account.model' + +export class AccountBlock implements AccountBlockServer { + byAccount: Account + blockedAccount: Account + createdAt: Date | string + + constructor (block: AccountBlockServer) { + this.byAccount = new Account(block.byAccount) + this.blockedAccount = new Account(block.blockedAccount) + this.createdAt = block.createdAt + } +} \ No newline at end of file diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts new file mode 100644 index 000000000..d9c318258 --- /dev/null +++ b/client/src/app/shared/blocklist/blocklist.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core' +import { environment } from '../../../environments/environment' +import { HttpClient, HttpParams } from '@angular/common/http' +import { RestExtractor, RestPagination, RestService } from '../rest' +import { SortMeta } from 'primeng/api' +import { catchError, map } from 'rxjs/operators' +import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared' +import { Account } from '@app/shared/account/account.model' +import { AccountBlock } from '@app/shared/blocklist/account-block.model' + +@Injectable() +export class BlocklistService { + static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) { } + + /*********************** User -> Account blocklist ***********************/ + + getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) { + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp.get>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + blockAccountByUser (account: Account) { + const body = { accountName: account.nameWithHost } + + return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + unblockAccountByUser (account: Account) { + const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost + + return this.authHttp.delete(path) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + /*********************** User -> Server blocklist ***********************/ + + getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) { + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp.get>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + blockServerByUser (host: string) { + const body = { host } + + return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + unblockServerByUser (host: string) { + const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host + + return this.authHttp.delete(path) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + private formatAccountBlock (accountBlock: AccountBlockServer) { + return new AccountBlock(accountBlock) + } +} diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts new file mode 100644 index 000000000..8cf6a55f7 --- /dev/null +++ b/client/src/app/shared/blocklist/index.ts @@ -0,0 +1,2 @@ +export * from './blocklist.service' +export * from './account-block.model' \ No newline at end of file diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html index 111627424..48230d6d8 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html @@ -9,13 +9,13 @@
\ No newline at end of file diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index 0a9aa7b04..92c4d1d2c 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss @@ -46,5 +46,10 @@ .dropdown-item { cursor: pointer; color: #000 !important; + + a, span { + display: block; + width: 100%; + } } } \ No newline at end of file diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html index 01db7cd4a..7367a7e59 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html @@ -1,5 +1,8 @@ - + - + \ No newline at end of file diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts index 105c99d8b..2f4a55f37 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' +import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' import { NotificationsService } from 'angular2-notifications' import { I18n } from '@ngx-translate/i18n-polyfill' import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' @@ -6,16 +6,20 @@ import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.com import { UserService } from '@app/shared/users' import { AuthService, ConfirmService } from '@app/core' import { User, UserRight } from '../../../../../shared/models/users' +import { Account } from '@app/shared/account/account.model' +import { BlocklistService } from '@app/shared/blocklist' @Component({ selector: 'my-user-moderation-dropdown', templateUrl: './user-moderation-dropdown.component.html', styleUrls: [ './user-moderation-dropdown.component.scss' ] }) -export class UserModerationDropdownComponent implements OnInit { +export class UserModerationDropdownComponent implements OnChanges { @ViewChild('userBanModal') userBanModal: UserBanModalComponent @Input() user: User + @Input() account: Account + @Input() buttonSize: 'normal' | 'small' = 'normal' @Input() placement = 'left' @@ -29,10 +33,11 @@ export class UserModerationDropdownComponent implements OnInit { private notificationsService: NotificationsService, private confirmService: ConfirmService, private userService: UserService, + private blocklistService: BlocklistService, private i18n: I18n ) { } - ngOnInit () { + ngOnChanges () { this.buildActions() } @@ -92,6 +97,74 @@ export class UserModerationDropdownComponent implements OnInit { ) } + blockAccountByUser (account: Account) { + this.blocklistService.blockAccountByUser(account) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost }) + ) + + this.account.muted = true + this.userChanged.emit() + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + + unblockAccountByUser (account: Account) { + this.blocklistService.unblockAccountByUser(account) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost }) + ) + + this.account.muted = false + this.userChanged.emit() + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + + blockServerByUser (host: string) { + this.blocklistService.blockServerByUser(host) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Instance {{host}} muted.', { host }) + ) + + this.account.mutedServer = true + this.userChanged.emit() + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + + unblockServerByUser (host: string) { + this.blocklistService.unblockServerByUser(host) + .subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Instance {{host}} unmuted.', { host }) + ) + + this.account.mutedServer = false + this.userChanged.emit() + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } + getRouterUserEditLink (user: User) { return [ '/admin', 'users', 'update', user.id ] } @@ -102,25 +175,53 @@ export class UserModerationDropdownComponent implements OnInit { if (this.authService.isLoggedIn()) { const authUser = this.authService.getUser() - if (authUser.hasRight(UserRight.MANAGE_USERS)) { + if (this.user && authUser.id === this.user.id) return + + if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) { this.userActions = this.userActions.concat([ { label: this.i18n('Edit'), - linkBuilder: this.getRouterUserEditLink + linkBuilder: ({ user }) => this.getRouterUserEditLink(user) }, { label: this.i18n('Delete'), - handler: user => this.removeUser(user) + handler: ({ user }) => this.removeUser(user) }, { label: this.i18n('Ban'), - handler: user => this.openBanUserModal(user), - isDisplayed: user => !user.blocked + handler: ({ user }) => this.openBanUserModal(user), + isDisplayed: ({ user }) => !user.muted }, { label: this.i18n('Unban'), - handler: user => this.unbanUser(user), - isDisplayed: user => user.blocked + handler: ({ user }) => this.unbanUser(user), + isDisplayed: ({ user }) => user.muted + } + ]) + } + + // User actions on accounts/servers + if (this.account) { + this.userActions = this.userActions.concat([ + { + label: this.i18n('Mute this account'), + isDisplayed: ({ account }) => account.muted === false, + handler: ({ account }) => this.blockAccountByUser(account) + }, + { + label: this.i18n('Unmute this account'), + isDisplayed: ({ account }) => account.muted === true, + handler: ({ account }) => this.unblockAccountByUser(account) + }, + { + label: this.i18n('Mute the instance'), + isDisplayed: ({ account }) => !account.userId && account.mutedServer === false, + handler: ({ account }) => this.blockServerByUser(account.host) + }, + { + label: this.i18n('Unmute the instance'), + isDisplayed: ({ account }) => !account.userId && account.mutedServer === true, + handler: ({ account }) => this.unblockServerByUser(account.host) } ]) } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 9647a7966..40e05fcc7 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -58,6 +58,7 @@ import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-fe import { OverviewService } from '@app/shared/overview' import { UserBanModalComponent } from '@app/shared/moderation' import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' +import { BlocklistService } from '@app/shared/blocklist' @NgModule({ imports: [ @@ -172,6 +173,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod OverviewService, VideoChangeOwnershipValidatorsService, VideoAcceptOwnershipValidatorsService, + BlocklistService, I18nPrimengCalendarService, ScreenService, diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts index e955ffde9..95a4105ec 100644 --- a/server/controllers/api/users/my-blocklist.ts +++ b/server/controllers/api/users/my-blocklist.ts @@ -6,7 +6,6 @@ import { asyncRetryTransactionMiddleware, authenticate, paginationValidator, - serverGetValidator, setDefaultPagination, setDefaultSort, unblockAccountByAccountValidator @@ -14,6 +13,7 @@ import { import { accountsBlocklistSortValidator, blockAccountByAccountValidator, + blockServerByAccountValidator, serversBlocklistSortValidator, unblockServerByAccountValidator } from '../../../middlewares/validators' @@ -58,7 +58,7 @@ myBlocklistRouter.get('/me/blocklist/servers', myBlocklistRouter.post('/me/blocklist/servers', authenticate, - asyncMiddleware(serverGetValidator), + asyncMiddleware(blockServerByAccountValidator), asyncRetryTransactionMiddleware(blockServer) ) diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts index 394c24537..1633e500c 100644 --- a/server/lib/blocklist.ts +++ b/server/lib/blocklist.ts @@ -4,7 +4,7 @@ import { ServerBlocklistModel } from '../models/server/server-blocklist' function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { return sequelizeTypescript.transaction(async t => { - return AccountBlocklistModel.create({ + return AccountBlocklistModel.upsert({ accountId: byAccountId, targetAccountId: targetAccountId }, { transaction: t }) @@ -13,7 +13,7 @@ function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { function addServerInBlocklist (byAccountId: number, targetServerId: number) { return sequelizeTypescript.transaction(async t => { - return ServerBlocklistModel.create({ + return ServerBlocklistModel.upsert({ accountId: byAccountId, targetServerId }, { transaction: t }) diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts index 9dbd5e512..25c054d6b 100644 --- a/server/middlewares/validators/blocklist.ts +++ b/server/middlewares/validators/blocklist.ts @@ -1,4 +1,4 @@ -import { param, body } from 'express-validator/check' +import { body, param } from 'express-validator/check' import * as express from 'express' import { logger } from '../../helpers/logger' import { areValidationErrors } from './utils' @@ -7,6 +7,8 @@ import { UserModel } from '../../models/account/user' import { AccountBlocklistModel } from '../../models/account/account-blocklist' import { isHostValid } from '../../helpers/custom-validators/servers' import { ServerBlocklistModel } from '../../models/server/server-blocklist' +import { ServerModel } from '../../models/server/server' +import { CONFIG } from '../../initializers' const blockAccountByAccountValidator = [ body('accountName').exists().withMessage('Should have an account name with host'), @@ -17,6 +19,17 @@ const blockAccountByAccountValidator = [ if (areValidationErrors(req, res)) return if (!await isAccountNameWithHostExist(req.body.accountName, res)) return + const user = res.locals.oauth.token.User as UserModel + const accountToBlock = res.locals.account + + if (user.Account.id === accountToBlock.id) { + res.status(409) + .send({ error: 'You cannot block yourself.' }) + .end() + + return + } + return next() } ] @@ -38,6 +51,35 @@ const unblockAccountByAccountValidator = [ } ] +const blockServerByAccountValidator = [ + body('host').custom(isHostValid).withMessage('Should have a valid host'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking serverGetValidator parameters', { parameters: req.body }) + + if (areValidationErrors(req, res)) return + + const host: string = req.body.host + + if (host === CONFIG.WEBSERVER.HOST) { + return res.status(409) + .send({ error: 'You cannot block your own server.' }) + .end() + } + + const server = await ServerModel.loadByHost(host) + if (!server) { + return res.status(404) + .send({ error: 'Server host not found.' }) + .end() + } + + res.locals.server = server + + return next() + } +] + const unblockServerByAccountValidator = [ param('host').custom(isHostValid).withMessage('Should have an account name with host'), @@ -56,6 +98,7 @@ const unblockServerByAccountValidator = [ // --------------------------------------------------------------------------- export { + blockServerByAccountValidator, blockAccountByAccountValidator, unblockAccountByAccountValidator, unblockServerByAccountValidator diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index bacd122e8..fa2819235 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -18,7 +18,7 @@ enum ScopeNames { { model: () => AccountModel, required: true, - as: 'AccountBlocked' + as: 'BlockedAccount' } ] } @@ -67,10 +67,10 @@ export class AccountBlocklistModel extends Model { name: 'targetAccountId', allowNull: false }, - as: 'AccountBlocked', + as: 'BlockedAccount', onDelete: 'CASCADE' }) - AccountBlocked: AccountModel + BlockedAccount: AccountModel static loadByAccountAndTarget (accountId: number, targetAccountId: number) { const query = { @@ -104,7 +104,7 @@ export class AccountBlocklistModel extends Model { toFormattedJSON (): AccountBlock { return { byAccount: this.ByAccount.toFormattedJSON(), - accountBlocked: this.AccountBlocked.toFormattedJSON(), + blockedAccount: this.BlockedAccount.toFormattedJSON(), createdAt: this.createdAt } } diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 705ed2c6b..450f27152 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts @@ -72,7 +72,7 @@ export class ServerBlocklistModel extends Model { }, onDelete: 'CASCADE' }) - ServerBlocked: ServerModel + BlockedServer: ServerModel static loadByAccountAndHost (accountId: number, host: string) { const query = { @@ -114,7 +114,7 @@ export class ServerBlocklistModel extends Model { toFormattedJSON (): ServerBlock { return { byAccount: this.ByAccount.toFormattedJSON(), - serverBlocked: this.ServerBlocked.toFormattedJSON(), + blockedServer: this.BlockedServer.toFormattedJSON(), createdAt: this.createdAt } } diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts index 8117c46a6..d24d9323f 100644 --- a/server/tests/api/check-params/blocklist.ts +++ b/server/tests/api/check-params/blocklist.ts @@ -85,6 +85,16 @@ describe('Test blocklist API validators', function () { }) }) + it('Should fail to block ourselves', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'root' }, + statusCodeExpected: 409 + }) + }) + it('Should succeed with the correct params', async function () { await makePostBodyRequest({ url: server.url, @@ -170,6 +180,16 @@ describe('Test blocklist API validators', function () { }) }) + it('Should fail with our own server', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: 'localhost:9001' }, + statusCodeExpected: 409 + }) + }) + it('Should succeed with the correct params', async function () { await makePostBodyRequest({ url: server.url, diff --git a/server/tests/api/users/account-blocklist.ts b/server/tests/api/users/account-blocklist.ts index 00ad51461..026971331 100644 --- a/server/tests/api/users/account-blocklist.ts +++ b/server/tests/api/users/account-blocklist.ts @@ -183,9 +183,9 @@ describe('Test accounts blocklist', function () { const block = blocks[0] expect(block.byAccount.displayName).to.equal('root') expect(block.byAccount.name).to.equal('root') - expect(block.accountBlocked.displayName).to.equal('user2') - expect(block.accountBlocked.name).to.equal('user2') - expect(block.accountBlocked.host).to.equal('localhost:9002') + expect(block.blockedAccount.displayName).to.equal('user2') + expect(block.blockedAccount.name).to.equal('user2') + expect(block.blockedAccount.host).to.equal('localhost:9002') } { @@ -197,9 +197,9 @@ describe('Test accounts blocklist', function () { const block = blocks[0] expect(block.byAccount.displayName).to.equal('root') expect(block.byAccount.name).to.equal('root') - expect(block.accountBlocked.displayName).to.equal('user1') - expect(block.accountBlocked.name).to.equal('user1') - expect(block.accountBlocked.host).to.equal('localhost:9001') + expect(block.blockedAccount.displayName).to.equal('user1') + expect(block.blockedAccount.name).to.equal('user1') + expect(block.blockedAccount.host).to.equal('localhost:9001') } }) @@ -267,7 +267,7 @@ describe('Test accounts blocklist', function () { const block = blocks[0] expect(block.byAccount.displayName).to.equal('root') expect(block.byAccount.name).to.equal('root') - expect(block.serverBlocked.host).to.equal('localhost:9002') + expect(block.blockedServer.host).to.equal('localhost:9002') }) it('Should unblock the remote server', async function () { diff --git a/shared/models/blocklist/account-block.model.ts b/shared/models/blocklist/account-block.model.ts index d6f8840c5..a942ed614 100644 --- a/shared/models/blocklist/account-block.model.ts +++ b/shared/models/blocklist/account-block.model.ts @@ -2,6 +2,6 @@ import { Account } from '../actors' export interface AccountBlock { byAccount: Account - accountBlocked: Account + blockedAccount: Account createdAt: Date | string } diff --git a/shared/models/blocklist/server-block.model.ts b/shared/models/blocklist/server-block.model.ts index efba672bd..a8b8af0b7 100644 --- a/shared/models/blocklist/server-block.model.ts +++ b/shared/models/blocklist/server-block.model.ts @@ -2,7 +2,7 @@ import { Account } from '../actors' export interface ServerBlock { byAccount: Account - serverBlocked: { + blockedServer: { host: string } createdAt: Date | string