aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+accounts/accounts.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.scss16
-rw-r--r--client/src/app/+accounts/accounts.component.ts9
-rw-r--r--client/src/app/+video-channels/video-channels.component.html6
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts14
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts6
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.html4
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.scss9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.ts11
-rw-r--r--client/src/app/shared/shared-moderation/blocklist.service.ts20
-rw-r--r--client/src/app/shared/shared-moderation/index.ts1
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts7
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts4
-rw-r--r--server/controllers/api/blocklist.ts108
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/users/my-subscriptions.ts21
-rw-r--r--server/helpers/actors.ts17
-rw-r--r--server/lib/blocklist.ts4
-rw-r--r--server/lib/notifier/shared/comment/comment-mention.ts4
-rw-r--r--server/middlewares/validators/blocklist.ts29
-rw-r--r--server/models/account/account-blocklist.ts40
-rw-r--r--server/models/server/server-blocklist.ts19
-rw-r--r--server/tests/api/check-params/blocklist.ts72
-rw-r--r--server/tests/api/moderation/blocklist.ts107
-rw-r--r--shared/extra-utils/users/blocklist-command.ts25
-rw-r--r--shared/models/moderation/block-status.model.ts15
-rw-r--r--shared/models/moderation/index.ts1
28 files changed, 533 insertions, 53 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 245edfd58..144545250 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -19,10 +19,8 @@
19 ></my-user-moderation-dropdown> 19 ></my-user-moderation-dropdown>
20 20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 22
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> 23 <my-account-block-badges [account]="account"></my-account-block-badges>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div> 24 </div>
27 25
28 <div class="actor-handle"> 26 <div class="actor-handle">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index cdd00487b..5043b98c4 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -30,16 +30,10 @@
30 } 30 }
31} 31}
32 32
33my-user-moderation-dropdown, 33my-user-moderation-dropdown {
34.badge { 34 margin: 0 10px;
35 @include margin-left(10px);
36 35
37 position: relative; 36 height: fit-content;
38 top: 3px;
39}
40
41.badge {
42 font-size: 13px;
43} 37}
44 38
45.copy-button { 39.copy-button {
@@ -64,6 +58,10 @@ my-user-moderation-dropdown,
64 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize)); 58 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
65} 59}
66 60
61.actor-display-name {
62 align-items: center;
63}
64
67.description { 65.description {
68 grid-column: 1 / 3; 66 grid-column: 1 / 3;
69 max-width: 1000px; 67 max-width: 1000px;
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 3cb117fcc..460f1dbf9 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -12,7 +12,7 @@ import {
12 VideoChannelService, 12 VideoChannelService,
13 VideoService 13 VideoService
14} from '@app/shared/shared-main' 14} from '@app/shared/shared-main'
15import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
16import { HttpStatusCode, User, UserRight } from '@shared/models' 16import { HttpStatusCode, User, UserRight } from '@shared/models'
17 17
18@Component({ 18@Component({
@@ -52,6 +52,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
52 private authService: AuthService, 52 private authService: AuthService,
53 private videoService: VideoService, 53 private videoService: VideoService,
54 private markdown: MarkdownService, 54 private markdown: MarkdownService,
55 private blocklist: BlocklistService,
55 private screenService: ScreenService 56 private screenService: ScreenService
56 ) { 57 ) {
57 } 58 }
@@ -159,6 +160,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
159 this.updateModerationActions() 160 this.updateModerationActions()
160 this.loadUserIfNeeded(account) 161 this.loadUserIfNeeded(account)
161 this.loadAccountVideosCount() 162 this.loadAccountVideosCount()
163 this.loadAccountBlockStatus()
162 } 164 }
163 165
164 private showReportModal () { 166 private showReportModal () {
@@ -217,4 +219,9 @@ export class AccountsComponent implements OnInit, OnDestroy {
217 this.accountVideosCount = res.total 219 this.accountVideosCount = res.total
218 }) 220 })
219 } 221 }
222
223 private loadAccountBlockStatus () {
224 this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
225 .subscribe(status => this.account.updateBlockStatus(status))
226 }
220} 227}
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 064fbb6f5..aec2e373c 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -23,14 +23,16 @@
23 <div class="section-label" i18n>OWNER ACCOUNT</div> 23 <div class="section-label" i18n>OWNER ACCOUNT</div>
24 24
25 <div class="avatar-row"> 25 <div class="avatar-row">
26 <my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> 26 <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
27 27
28 <div class="actor-info"> 28 <div class="actor-info">
29 <h4> 29 <h4>
30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a> 30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ ownerAccount.displayName }}</a>
31 </h4> 31 </h4>
32 32
33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> 33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
34
35 <my-account-block-badges [account]="ownerAccount"></my-account-block-badges>
34 </div> 36 </div>
35 </div> 37 </div>
36 38
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 272fc41d9..ebb991f4e 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -4,7 +4,8 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core' 6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { BlocklistService } from '@app/shared/shared-moderation'
8import { SupportModalComponent } from '@app/shared/shared-support-modal' 9import { SupportModalComponent } from '@app/shared/shared-support-modal'
9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 10import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
10import { HttpStatusCode } from '@shared/models' 11import { HttpStatusCode } from '@shared/models'
@@ -18,6 +19,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
18 @ViewChild('supportModal') supportModal: SupportModalComponent 19 @ViewChild('supportModal') supportModal: SupportModalComponent
19 20
20 videoChannel: VideoChannel 21 videoChannel: VideoChannel
22 ownerAccount: Account
21 hotkeys: Hotkey[] 23 hotkeys: Hotkey[]
22 links: ListOverflowItem[] = [] 24 links: ListOverflowItem[] = []
23 isChannelManageable = false 25 isChannelManageable = false
@@ -38,7 +40,8 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 40 private restExtractor: RestExtractor,
39 private hotkeysService: HotkeysService, 41 private hotkeysService: HotkeysService,
40 private screenService: ScreenService, 42 private screenService: ScreenService,
41 private markdown: MarkdownService 43 private markdown: MarkdownService,
44 private blocklist: BlocklistService
42 ) { } 45 ) { }
43 46
44 ngOnInit () { 47 ngOnInit () {
@@ -58,8 +61,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
58 61
59 // After the markdown renderer to avoid layout changes 62 // After the markdown renderer to avoid layout changes
60 this.videoChannel = videoChannel 63 this.videoChannel = videoChannel
64 this.ownerAccount = new Account(this.videoChannel.ownerAccount)
61 65
62 this.loadChannelVideosCount() 66 this.loadChannelVideosCount()
67 this.loadOwnerBlockStatus()
63 }) 68 })
64 69
65 this.hotkeys = [ 70 this.hotkeys = [
@@ -125,4 +130,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
125 sort: '-publishedAt' 130 sort: '-publishedAt'
126 }).subscribe(res => this.channelVideosCount = res.total) 131 }).subscribe(res => this.channelVideosCount = res.total)
127 } 132 }
133
134 private loadOwnerBlockStatus () {
135 this.blocklist.getStatus({ accounts: [ this.ownerAccount.nameWithHostForced ], hosts: [ this.ownerAccount.host ] })
136 .subscribe(status => this.ownerAccount.updateBlockStatus(status))
137 }
128} 138}
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 35c39cc2e..76aaecf83 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,15 +2,16 @@ import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation'
5import { SharedSupportModal } from '@app/shared/shared-support-modal' 6import { SharedSupportModal } from '@app/shared/shared-support-modal'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 7import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 8import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' 9import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
10import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 11import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 12import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
11import { VideoChannelsRoutingModule } from './video-channels-routing.module' 13import { VideoChannelsRoutingModule } from './video-channels-routing.module'
12import { VideoChannelsComponent } from './video-channels.component' 14import { VideoChannelsComponent } from './video-channels.component'
13import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
14 15
15@NgModule({ 16@NgModule({
16 imports: [ 17 imports: [
@@ -23,7 +24,8 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
23 SharedUserSubscriptionModule, 24 SharedUserSubscriptionModule,
24 SharedGlobalIconModule, 25 SharedGlobalIconModule,
25 SharedSupportModal, 26 SharedSupportModal,
26 SharedActorImageModule 27 SharedActorImageModule,
28 SharedModerationModule
27 ], 29 ],
28 30
29 declarations: [ 31 declarations: [
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index 92606e7fa..8b78d01a6 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, ActorImage } from '@shared/models' 1import { Account as ServerAccount, ActorImage, BlockStatus } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -49,4 +49,11 @@ export class Account extends Actor implements ServerAccount {
49 resetAvatar () { 49 resetAvatar () {
50 this.avatar = null 50 this.avatar = null
51 } 51 }
52
53 updateBlockStatus (blockStatus: BlockStatus) {
54 this.mutedByInstance = blockStatus.accounts[this.nameWithHostForced].blockedByServer
55 this.mutedByUser = blockStatus.accounts[this.nameWithHostForced].blockedByUser
56 this.mutedServerByUser = blockStatus.hosts[this.host].blockedByUser
57 this.mutedServerByInstance = blockStatus.hosts[this.host].blockedByServer
58 }
52} 59}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.html b/client/src/app/shared/shared-moderation/account-block-badges.component.html
new file mode 100644
index 000000000..feac707c2
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -0,0 +1,4 @@
1<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
2<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
3<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
4<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.scss b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
new file mode 100644
index 000000000..ccc3666aa
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
@@ -0,0 +1,9 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.badge {
5 @include margin-right(10px);
6
7 height: fit-content;
8 font-size: 12px;
9}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.ts b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
new file mode 100644
index 000000000..a72601118
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { Account } from '../shared-main'
3
4@Component({
5 selector: 'my-account-block-badges',
6 styleUrls: [ './account-block-badges.component.scss' ],
7 templateUrl: './account-block-badges.component.html'
8})
9export class AccountBlockBadgesComponent {
10 @Input() account: Account
11}
diff --git a/client/src/app/shared/shared-moderation/blocklist.service.ts b/client/src/app/shared/shared-moderation/blocklist.service.ts
index db2a8c584..f4836c6c4 100644
--- a/client/src/app/shared/shared-moderation/blocklist.service.ts
+++ b/client/src/app/shared/shared-moderation/blocklist.service.ts
@@ -3,7 +3,7 @@ import { catchError, map } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { RestExtractor, RestPagination, RestService } from '@app/core' 5import { RestExtractor, RestPagination, RestService } from '@app/core'
6import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '@shared/models' 6import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { Account } from '../shared-main' 8import { Account } from '../shared-main'
9import { AccountBlock } from './account-block.model' 9import { AccountBlock } from './account-block.model'
@@ -12,6 +12,7 @@ export enum BlocklistComponentType { Account, Instance }
12 12
13@Injectable() 13@Injectable()
14export class BlocklistService { 14export class BlocklistService {
15 static BASE_BLOCKLIST_URL = environment.apiUrl + '/api/v1/blocklist'
15 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' 16 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
16 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist' 17 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
17 18
@@ -21,6 +22,23 @@ export class BlocklistService {
21 private restService: RestService 22 private restService: RestService
22 ) { } 23 ) { }
23 24
25 /** ********************* Blocklist status ***********************/
26
27 getStatus (options: {
28 accounts?: string[]
29 hosts?: string[]
30 }) {
31 const { accounts, hosts } = options
32
33 let params = new HttpParams()
34
35 if (accounts) params = this.restService.addArrayParams(params, 'accounts', accounts)
36 if (hosts) params = this.restService.addArrayParams(params, 'hosts', hosts)
37
38 return this.authHttp.get<BlockStatus>(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
39 .pipe(catchError(err => this.restExtractor.handleError(err)))
40 }
41
24 /** ********************* User -> Account blocklist ***********************/ 42 /** ********************* User -> Account blocklist ***********************/
25 43
26 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) { 44 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index 41c910ffe..da85b2299 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,6 +1,7 @@
1export * from './report-modals' 1export * from './report-modals'
2 2
3export * from './abuse.service' 3export * from './abuse.service'
4export * from './account-block-badges.component'
4export * from './account-block.model' 5export * from './account-block.model'
5export * from './account-blocklist.component' 6export * from './account-blocklist.component'
6export * from './batch-domains-modal.component' 7export * from './batch-domains-modal.component'
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index 95213e2bd..7cadda67c 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -13,6 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
14import { VideoBlockComponent } from './video-block.component' 14import { VideoBlockComponent } from './video-block.component'
15import { VideoBlockService } from './video-block.service' 15import { VideoBlockService } from './video-block.service'
16import { AccountBlockBadgesComponent } from './account-block-badges.component'
16import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' 17import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
17 18
18@NgModule({ 19@NgModule({
@@ -31,7 +32,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
31 VideoReportComponent, 32 VideoReportComponent,
32 BatchDomainsModalComponent, 33 BatchDomainsModalComponent,
33 CommentReportComponent, 34 CommentReportComponent,
34 AccountReportComponent 35 AccountReportComponent,
36 AccountBlockBadgesComponent
35 ], 37 ],
36 38
37 exports: [ 39 exports: [
@@ -41,7 +43,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
41 VideoReportComponent, 43 VideoReportComponent,
42 BatchDomainsModalComponent, 44 BatchDomainsModalComponent,
43 CommentReportComponent, 45 CommentReportComponent,
44 AccountReportComponent 46 AccountReportComponent,
47 AccountBlockBadgesComponent
45 ], 48 ],
46 49
47 providers: [ 50 providers: [
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index b18d861d6..e2cd2cdc1 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -289,13 +289,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
289 { 289 {
290 label: $localize`Mute the instance`, 290 label: $localize`Mute the instance`,
291 description: $localize`Hide any content from that instance for you.`, 291 description: $localize`Hide any content from that instance for you.`,
292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, 292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === false,
293 handler: ({ account }) => this.blockServerByUser(account.host) 293 handler: ({ account }) => this.blockServerByUser(account.host)
294 }, 294 },
295 { 295 {
296 label: $localize`Unmute the instance`, 296 label: $localize`Unmute the instance`,
297 description: $localize`Show back content from that instance for you.`, 297 description: $localize`Show back content from that instance for you.`,
298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, 298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === true,
299 handler: ({ account }) => this.unblockServerByUser(account.host) 299 handler: ({ account }) => this.unblockServerByUser(account.host)
300 }, 300 },
301 { 301 {
diff --git a/server/controllers/api/blocklist.ts b/server/controllers/api/blocklist.ts
new file mode 100644
index 000000000..1e936ad10
--- /dev/null
+++ b/server/controllers/api/blocklist.ts
@@ -0,0 +1,108 @@
1import express from 'express'
2import { handleToNameAndHost } from '@server/helpers/actors'
3import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
4import { getServerActor } from '@server/models/application/application'
5import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
6import { MActorAccountId, MUserAccountId } from '@server/types/models'
7import { BlockStatus } from '@shared/models'
8import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
9import { logger } from '@server/helpers/logger'
10
11const blocklistRouter = express.Router()
12
13blocklistRouter.get('/status',
14 optionalAuthenticate,
15 blocklistStatusValidator,
16 asyncMiddleware(getBlocklistStatus)
17)
18
19// ---------------------------------------------------------------------------
20
21export {
22 blocklistRouter
23}
24
25// ---------------------------------------------------------------------------
26
27async function getBlocklistStatus (req: express.Request, res: express.Response) {
28 const hosts = req.query.hosts as string[]
29 const accounts = req.query.accounts as string[]
30 const user = res.locals.oauth?.token.User
31
32 const serverActor = await getServerActor()
33
34 const byAccountIds = [ serverActor.Account.id ]
35 if (user) byAccountIds.push(user.Account.id)
36
37 const status: BlockStatus = {
38 accounts: {},
39 hosts: {}
40 }
41
42 const baseOptions = {
43 byAccountIds,
44 user,
45 serverActor,
46 status
47 }
48
49 await Promise.all([
50 populateServerBlocklistStatus({ ...baseOptions, hosts }),
51 populateAccountBlocklistStatus({ ...baseOptions, accounts })
52 ])
53
54 return res.json(status)
55}
56
57async function populateServerBlocklistStatus (options: {
58 byAccountIds: number[]
59 user?: MUserAccountId
60 serverActor: MActorAccountId
61 hosts: string[]
62 status: BlockStatus
63}) {
64 const { byAccountIds, user, serverActor, hosts, status } = options
65
66 if (!hosts || hosts.length === 0) return
67
68 const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts)
69
70 logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts })
71
72 for (const host of hosts) {
73 const block = serverBlocklistStatus.find(b => b.host === host)
74
75 status.hosts[host] = getStatus(block, serverActor, user)
76 }
77}
78
79async function populateAccountBlocklistStatus (options: {
80 byAccountIds: number[]
81 user?: MUserAccountId
82 serverActor: MActorAccountId
83 accounts: string[]
84 status: BlockStatus
85}) {
86 const { byAccountIds, user, serverActor, accounts, status } = options
87
88 if (!accounts || accounts.length === 0) return
89
90 const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts)
91
92 logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts })
93
94 for (const account of accounts) {
95 const sanitizedHandle = handleToNameAndHost(account)
96
97 const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host)
98
99 status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user)
100 }
101}
102
103function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) {
104 return {
105 blockedByServer: !!(block && block.accountId === serverActor.Account.id),
106 blockedByUser: !!(block && user && block.accountId === user.Account.id)
107 }
108}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 9949b378a..5f49336b1 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -6,6 +6,7 @@ import { badRequest } from '../../helpers/express-utils'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
7import { abuseRouter } from './abuse' 7import { abuseRouter } from './abuse'
8import { accountsRouter } from './accounts' 8import { accountsRouter } from './accounts'
9import { blocklistRouter } from './blocklist'
9import { bulkRouter } from './bulk' 10import { bulkRouter } from './bulk'
10import { configRouter } from './config' 11import { configRouter } from './config'
11import { customPageRouter } from './custom-page' 12import { customPageRouter } from './custom-page'
@@ -49,6 +50,7 @@ apiRouter.use('/search', searchRouter)
49apiRouter.use('/overviews', overviewsRouter) 50apiRouter.use('/overviews', overviewsRouter)
50apiRouter.use('/plugins', pluginRouter) 51apiRouter.use('/plugins', pluginRouter)
51apiRouter.use('/custom-pages', customPageRouter) 52apiRouter.use('/custom-pages', customPageRouter)
53apiRouter.use('/blocklist', blocklistRouter)
52apiRouter.use('/ping', pong) 54apiRouter.use('/ping', pong)
53apiRouter.use('/*', badRequest) 55apiRouter.use('/*', badRequest)
54 56
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index 6799ca8c5..fb1f68635 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,5 +1,6 @@
1import 'multer' 1import 'multer'
2import express from 'express' 2import express from 'express'
3import { handlesToNameAndHost } from '@server/helpers/actors'
3import { pickCommonVideoQuery } from '@server/helpers/query' 4import { pickCommonVideoQuery } from '@server/helpers/query'
4import { sendUndoFollow } from '@server/lib/activitypub/send' 5import { sendUndoFollow } from '@server/lib/activitypub/send'
5import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' 6import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
@@ -7,7 +8,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
7import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 8import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
8import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 9import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
9import { getFormattedObjects } from '../../../helpers/utils' 10import { getFormattedObjects } from '../../../helpers/utils'
10import { WEBSERVER } from '../../../initializers/constants'
11import { sequelizeTypescript } from '../../../initializers/database' 11import { sequelizeTypescript } from '../../../initializers/database'
12import { JobQueue } from '../../../lib/job-queue' 12import { JobQueue } from '../../../lib/job-queue'
13import { 13import {
@@ -89,28 +89,23 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
89 const uris = req.query.uris as string[] 89 const uris = req.query.uris as string[]
90 const user = res.locals.oauth.token.User 90 const user = res.locals.oauth.token.User
91 91
92 const handles = uris.map(u => { 92 const sanitizedHandles = handlesToNameAndHost(uris)
93 let [ name, host ] = u.split('@')
94 if (host === WEBSERVER.HOST) host = null
95 93
96 return { name, host, uri: u } 94 const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
97 })
98
99 const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
100 95
101 const existObject: { [id: string ]: boolean } = {} 96 const existObject: { [id: string ]: boolean } = {}
102 for (const handle of handles) { 97 for (const sanitizedHandle of sanitizedHandles) {
103 const obj = results.find(r => { 98 const obj = results.find(r => {
104 const server = r.ActorFollowing.Server 99 const server = r.ActorFollowing.Server
105 100
106 return r.ActorFollowing.preferredUsername === handle.name && 101 return r.ActorFollowing.preferredUsername === sanitizedHandle.name &&
107 ( 102 (
108 (!server && !handle.host) || 103 (!server && !sanitizedHandle.host) ||
109 (server.host === handle.host) 104 (server.host === sanitizedHandle.host)
110 ) 105 )
111 }) 106 })
112 107
113 existObject[handle.uri] = obj !== undefined 108 existObject[sanitizedHandle.handle] = obj !== undefined
114 } 109 }
115 110
116 return res.json(existObject) 111 return res.json(existObject)
diff --git a/server/helpers/actors.ts b/server/helpers/actors.ts
new file mode 100644
index 000000000..c31fe6f8e
--- /dev/null
+++ b/server/helpers/actors.ts
@@ -0,0 +1,17 @@
1import { WEBSERVER } from '@server/initializers/constants'
2
3function handleToNameAndHost (handle: string) {
4 let [ name, host ] = handle.split('@')
5 if (host === WEBSERVER.HOST) host = null
6
7 return { name, host, handle }
8}
9
10function handlesToNameAndHost (handles: string[]) {
11 return handles.map(h => handleToNameAndHost(h))
12}
13
14export {
15 handleToNameAndHost,
16 handlesToNameAndHost
17}
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index d6b684015..98273a6ea 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -40,12 +40,12 @@ async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAc
40 40
41 if (userAccount) sourceAccounts.push(userAccount.id) 41 if (userAccount) sourceAccounts.push(userAccount.id)
42 42
43 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id) 43 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, targetAccount.id)
44 if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) { 44 if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) {
45 return true 45 return true
46 } 46 }
47 47
48 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId) 48 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, targetAccount.Actor.serverId)
49 if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) { 49 if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) {
50 return true 50 return true
51 } 51 }
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts
index 4f84d8dea..765cbaad9 100644
--- a/server/lib/notifier/shared/comment/comment-mention.ts
+++ b/server/lib/notifier/shared/comment/comment-mention.ts
@@ -47,8 +47,8 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
47 47
48 const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ]) 48 const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ])
49 49
50 this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId) 50 this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, this.payload.accountId)
51 this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId) 51 this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, this.payload.Account.Actor.serverId)
52 } 52 }
53 53
54 log () { 54 log () {
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index b7749e204..12980ced4 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -1,8 +1,10 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor'
4import { toArray } from '@server/helpers/custom-validators/misc'
3import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
4import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 6import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
5import { isHostValid } from '../../helpers/custom-validators/servers' 7import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
6import { logger } from '../../helpers/logger' 8import { logger } from '../../helpers/logger'
7import { WEBSERVER } from '../../initializers/constants' 9import { WEBSERVER } from '../../initializers/constants'
8import { AccountBlocklistModel } from '../../models/account/account-blocklist' 10import { AccountBlocklistModel } from '../../models/account/account-blocklist'
@@ -123,6 +125,26 @@ const unblockServerByServerValidator = [
123 } 125 }
124] 126]
125 127
128const blocklistStatusValidator = [
129 query('hosts')
130 .optional()
131 .customSanitizer(toArray)
132 .custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
133
134 query('accounts')
135 .optional()
136 .customSanitizer(toArray)
137 .custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
138
139 (req: express.Request, res: express.Response, next: express.NextFunction) => {
140 logger.debug('Checking blocklistStatusValidator parameters', { query: req.query })
141
142 if (areValidationErrors(req, res)) return
143
144 return next()
145 }
146]
147
126// --------------------------------------------------------------------------- 148// ---------------------------------------------------------------------------
127 149
128export { 150export {
@@ -131,7 +153,8 @@ export {
131 unblockAccountByAccountValidator, 153 unblockAccountByAccountValidator,
132 unblockServerByAccountValidator, 154 unblockServerByAccountValidator,
133 unblockAccountByServerValidator, 155 unblockAccountByServerValidator,
134 unblockServerByServerValidator 156 unblockServerByServerValidator,
157 blocklistStatusValidator
135} 158}
136 159
137// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index b2375b006..21983428a 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -1,11 +1,12 @@
1import { Op } from 'sequelize' 1import { Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { handlesToNameAndHost } from '@server/helpers/actors'
3import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/core-utils' 5import { AttributesOnly } from '@shared/core-utils'
5import { AccountBlock } from '../../../shared/models' 6import { AccountBlock } from '../../../shared/models'
6import { ActorModel } from '../actor/actor' 7import { ActorModel } from '../actor/actor'
7import { ServerModel } from '../server/server' 8import { ServerModel } from '../server/server'
8import { getSort, searchAttribute } from '../utils' 9import { createSafeIn, getSort, searchAttribute } from '../utils'
9import { AccountModel } from './account' 10import { AccountModel } from './account'
10 11
11enum ScopeNames { 12enum ScopeNames {
@@ -77,7 +78,7 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
77 }) 78 })
78 BlockedAccount: AccountModel 79 BlockedAccount: AccountModel
79 80
80 static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { 81 static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) {
81 const query = { 82 const query = {
82 attributes: [ 'accountId', 'id' ], 83 attributes: [ 'accountId', 'id' ],
83 where: { 84 where: {
@@ -187,6 +188,39 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
187 .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`)) 188 .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`))
188 } 189 }
189 190
191 static getBlockStatus (byAccountIds: number[], handles: string[]): Promise<{ name: string, host: string, accountId: number }[]> {
192 const sanitizedHandles = handlesToNameAndHost(handles)
193
194 const localHandles = sanitizedHandles.filter(h => !h.host)
195 .map(h => h.name)
196
197 const remoteHandles = sanitizedHandles.filter(h => !!h.host)
198 .map(h => ([ h.name, h.host ]))
199
200 const handlesWhere: string[] = []
201
202 if (localHandles.length !== 0) {
203 handlesWhere.push(`("actor"."preferredUsername" IN (:localHandles) AND "server"."id" IS NULL)`)
204 }
205
206 if (remoteHandles.length !== 0) {
207 handlesWhere.push(`(("actor"."preferredUsername", "server"."host") IN (:remoteHandles))`)
208 }
209
210 const rawQuery = `SELECT "accountBlocklist"."accountId", "actor"."preferredUsername" AS "name", "server"."host" ` +
211 `FROM "accountBlocklist" ` +
212 `INNER JOIN "account" ON "account"."id" = "accountBlocklist"."targetAccountId" ` +
213 `INNER JOIN "actor" ON "actor"."id" = "account"."actorId" ` +
214 `LEFT JOIN "server" ON "server"."id" = "actor"."serverId" ` +
215 `WHERE "accountBlocklist"."accountId" IN (${createSafeIn(AccountBlocklistModel.sequelize, byAccountIds)}) ` +
216 `AND (${handlesWhere.join(' OR ')})`
217
218 return AccountBlocklistModel.sequelize.query(rawQuery, {
219 type: QueryTypes.SELECT as QueryTypes.SELECT,
220 replacements: { byAccountIds, localHandles, remoteHandles }
221 })
222 }
223
190 toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock { 224 toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
191 return { 225 return {
192 byAccount: this.ByAccount.toFormattedJSON(), 226 byAccount: this.ByAccount.toFormattedJSON(),
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index b3579d589..092998db3 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -1,10 +1,10 @@
1import { Op } from 'sequelize' 1import { Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' 3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/core-utils' 4import { AttributesOnly } from '@shared/core-utils'
5import { ServerBlock } from '@shared/models' 5import { ServerBlock } from '@shared/models'
6import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
7import { getSort, searchAttribute } from '../utils' 7import { createSafeIn, getSort, searchAttribute } from '../utils'
8import { ServerModel } from './server' 8import { ServerModel } from './server'
9 9
10enum ScopeNames { 10enum ScopeNames {
@@ -76,7 +76,7 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
76 }) 76 })
77 BlockedServer: ServerModel 77 BlockedServer: ServerModel
78 78
79 static isServerMutedByMulti (accountIds: number[], targetServerId: number) { 79 static isServerMutedByAccounts (accountIds: number[], targetServerId: number) {
80 const query = { 80 const query = {
81 attributes: [ 'accountId', 'id' ], 81 attributes: [ 'accountId', 'id' ],
82 where: { 82 where: {
@@ -141,6 +141,19 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
141 .then(entries => entries.map(e => e.BlockedServer.host)) 141 .then(entries => entries.map(e => e.BlockedServer.host))
142 } 142 }
143 143
144 static getBlockStatus (byAccountIds: number[], hosts: string[]): Promise<{ host: string, accountId: number }[]> {
145 const rawQuery = `SELECT "server"."host", "serverBlocklist"."accountId" ` +
146 `FROM "serverBlocklist" ` +
147 `INNER JOIN "server" ON "server"."id" = "serverBlocklist"."targetServerId" ` +
148 `WHERE "server"."host" IN (:hosts) ` +
149 `AND "serverBlocklist"."accountId" IN (${createSafeIn(ServerBlocklistModel.sequelize, byAccountIds)})`
150
151 return ServerBlocklistModel.sequelize.query(rawQuery, {
152 type: QueryTypes.SELECT as QueryTypes.SELECT,
153 replacements: { hosts }
154 })
155 }
156
144 static listForApi (parameters: { 157 static listForApi (parameters: {
145 start: number 158 start: number
146 count: number 159 count: number
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index 7d5fae5cf..f72a892e2 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -481,6 +481,78 @@ describe('Test blocklist API validators', function () {
481 }) 481 })
482 }) 482 })
483 483
484 describe('When getting blocklist status', function () {
485 const path = '/api/v1/blocklist/status'
486
487 it('Should fail with a bad token', async function () {
488 await makeGetRequest({
489 url: server.url,
490 path,
491 token: 'false',
492 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
493 })
494 })
495
496 it('Should fail with a bad accounts field', async function () {
497 await makeGetRequest({
498 url: server.url,
499 path,
500 query: {
501 accounts: 1
502 },
503 expectedStatus: HttpStatusCode.BAD_REQUEST_400
504 })
505
506 await makeGetRequest({
507 url: server.url,
508 path,
509 query: {
510 accounts: [ 1 ]
511 },
512 expectedStatus: HttpStatusCode.BAD_REQUEST_400
513 })
514 })
515
516 it('Should fail with a bad hosts field', async function () {
517 await makeGetRequest({
518 url: server.url,
519 path,
520 query: {
521 hosts: 1
522 },
523 expectedStatus: HttpStatusCode.BAD_REQUEST_400
524 })
525
526 await makeGetRequest({
527 url: server.url,
528 path,
529 query: {
530 hosts: [ 1 ]
531 },
532 expectedStatus: HttpStatusCode.BAD_REQUEST_400
533 })
534 })
535
536 it('Should succeed with the correct parameters', async function () {
537 await makeGetRequest({
538 url: server.url,
539 path,
540 query: {},
541 expectedStatus: HttpStatusCode.OK_200
542 })
543
544 await makeGetRequest({
545 url: server.url,
546 path,
547 query: {
548 hosts: [ 'example.com' ],
549 accounts: [ 'john@example.com' ]
550 },
551 expectedStatus: HttpStatusCode.OK_200
552 })
553 })
554 })
555
484 after(async function () { 556 after(async function () {
485 await cleanupTests(servers) 557 await cleanupTests(servers)
486 }) 558 })
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts
index 089af8b15..b3fd8ecac 100644
--- a/server/tests/api/moderation/blocklist.ts
+++ b/server/tests/api/moderation/blocklist.ts
@@ -254,6 +254,45 @@ describe('Test blocklist', function () {
254 } 254 }
255 }) 255 })
256 256
257 it('Should get blocked status', async function () {
258 const remoteHandle = 'user2@' + servers[1].host
259 const localHandle = 'user1@' + servers[0].host
260 const unknownHandle = 'user5@' + servers[0].host
261
262 {
263 const status = await command.getStatus({ accounts: [ remoteHandle ] })
264 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
265 expect(status.accounts[remoteHandle].blockedByUser).to.be.false
266 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
267
268 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
269 }
270
271 {
272 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
273 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
274 expect(status.accounts[remoteHandle].blockedByUser).to.be.true
275 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
276
277 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
278 }
279
280 {
281 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
282 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
283
284 for (const handle of [ localHandle, remoteHandle ]) {
285 expect(status.accounts[handle].blockedByUser).to.be.true
286 expect(status.accounts[handle].blockedByServer).to.be.false
287 }
288
289 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
290 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
291
292 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
293 }
294 })
295
257 it('Should not allow a remote blocked user to comment my videos', async function () { 296 it('Should not allow a remote blocked user to comment my videos', async function () {
258 this.timeout(60000) 297 this.timeout(60000)
259 298
@@ -434,6 +473,35 @@ describe('Test blocklist', function () {
434 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 473 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
435 }) 474 })
436 475
476 it('Should get blocklist status', async function () {
477 const blockedServer = servers[1].host
478 const notBlockedServer = 'example.com'
479
480 {
481 const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
482 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
483
484 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
485 expect(status.hosts[blockedServer].blockedByUser).to.be.false
486 expect(status.hosts[blockedServer].blockedByServer).to.be.false
487
488 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
489 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
490 }
491
492 {
493 const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
494 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
495
496 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
497 expect(status.hosts[blockedServer].blockedByUser).to.be.true
498 expect(status.hosts[blockedServer].blockedByServer).to.be.false
499
500 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
501 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
502 }
503 })
504
437 it('Should unblock the remote server', async function () { 505 it('Should unblock the remote server', async function () {
438 await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port }) 506 await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port })
439 }) 507 })
@@ -575,6 +643,27 @@ describe('Test blocklist', function () {
575 } 643 }
576 }) 644 })
577 645
646 it('Should get blocked status', async function () {
647 const remoteHandle = 'user2@' + servers[1].host
648 const localHandle = 'user1@' + servers[0].host
649 const unknownHandle = 'user5@' + servers[0].host
650
651 for (const token of [ undefined, servers[0].accessToken ]) {
652 const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
653 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
654
655 for (const handle of [ localHandle, remoteHandle ]) {
656 expect(status.accounts[handle].blockedByUser).to.be.false
657 expect(status.accounts[handle].blockedByServer).to.be.true
658 }
659
660 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
661 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
662
663 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
664 }
665 })
666
578 it('Should unblock the remote account', async function () { 667 it('Should unblock the remote account', async function () {
579 await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port }) 668 await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
580 }) 669 })
@@ -620,6 +709,7 @@ describe('Test blocklist', function () {
620 }) 709 })
621 710
622 describe('When managing server blocklist', function () { 711 describe('When managing server blocklist', function () {
712
623 it('Should list all videos', async function () { 713 it('Should list all videos', async function () {
624 for (const token of [ userModeratorToken, servers[0].accessToken ]) { 714 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
625 await checkAllVideos(servers[0], token) 715 await checkAllVideos(servers[0], token)
@@ -713,6 +803,23 @@ describe('Test blocklist', function () {
713 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 803 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
714 }) 804 })
715 805
806 it('Should get blocklist status', async function () {
807 const blockedServer = servers[1].host
808 const notBlockedServer = 'example.com'
809
810 for (const token of [ undefined, servers[0].accessToken ]) {
811 const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
812 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
813
814 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
815 expect(status.hosts[blockedServer].blockedByUser).to.be.false
816 expect(status.hosts[blockedServer].blockedByServer).to.be.true
817
818 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
819 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
820 }
821 })
822
716 it('Should unblock the remote server', async function () { 823 it('Should unblock the remote server', async function () {
717 await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port }) 824 await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
718 }) 825 })
diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts
index 14491a1ae..2e7ed074d 100644
--- a/shared/extra-utils/users/blocklist-command.ts
+++ b/shared/extra-utils/users/blocklist-command.ts
@@ -1,6 +1,6 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models' 3import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared' 4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5 5
6type ListBlocklistOptions = OverrideCommandOptions & { 6type ListBlocklistOptions = OverrideCommandOptions & {
@@ -37,6 +37,29 @@ export class BlocklistCommand extends AbstractCommand {
37 37
38 // --------------------------------------------------------------------------- 38 // ---------------------------------------------------------------------------
39 39
40 getStatus (options: OverrideCommandOptions & {
41 accounts?: string[]
42 hosts?: string[]
43 }) {
44 const { accounts, hosts } = options
45
46 const path = '/api/v1/blocklist/status'
47
48 return this.getRequestBody<BlockStatus>({
49 ...options,
50
51 path,
52 query: {
53 accounts,
54 hosts
55 },
56 implicitToken: false,
57 defaultExpectedStatus: HttpStatusCode.OK_200
58 })
59 }
60
61 // ---------------------------------------------------------------------------
62
40 addToMyBlocklist (options: OverrideCommandOptions & { 63 addToMyBlocklist (options: OverrideCommandOptions & {
41 account?: string 64 account?: string
42 server?: string 65 server?: string
diff --git a/shared/models/moderation/block-status.model.ts b/shared/models/moderation/block-status.model.ts
new file mode 100644
index 000000000..597312757
--- /dev/null
+++ b/shared/models/moderation/block-status.model.ts
@@ -0,0 +1,15 @@
1export interface BlockStatus {
2 accounts: {
3 [ handle: string ]: {
4 blockedByServer: boolean
5 blockedByUser?: boolean
6 }
7 }
8
9 hosts: {
10 [ host: string ]: {
11 blockedByServer: boolean
12 blockedByUser?: boolean
13 }
14 }
15}
diff --git a/shared/models/moderation/index.ts b/shared/models/moderation/index.ts
index 8b6042e97..f8e6d351c 100644
--- a/shared/models/moderation/index.ts
+++ b/shared/models/moderation/index.ts
@@ -1,3 +1,4 @@
1export * from './abuse' 1export * from './abuse'
2export * from './block-status.model'
2export * from './account-block.model' 3export * from './account-block.model'
3export * from './server-block.model' 4export * from './server-block.model'