]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add mute status in account and channel pages
authorChocobozzz <me@florianbigard.com>
Mon, 6 Dec 2021 15:53:00 +0000 (16:53 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 7 Dec 2021 08:46:01 +0000 (09:46 +0100)
28 files changed:
client/src/app/+accounts/accounts.component.html
client/src/app/+accounts/accounts.component.scss
client/src/app/+accounts/accounts.component.ts
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.ts
client/src/app/+video-channels/video-channels.module.ts
client/src/app/shared/shared-main/account/account.model.ts
client/src/app/shared/shared-moderation/account-block-badges.component.html [new file with mode: 0644]
client/src/app/shared/shared-moderation/account-block-badges.component.scss [new file with mode: 0644]
client/src/app/shared/shared-moderation/account-block-badges.component.ts [new file with mode: 0644]
client/src/app/shared/shared-moderation/blocklist.service.ts
client/src/app/shared/shared-moderation/index.ts
client/src/app/shared/shared-moderation/shared-moderation.module.ts
client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
server/controllers/api/blocklist.ts [new file with mode: 0644]
server/controllers/api/index.ts
server/controllers/api/users/my-subscriptions.ts
server/helpers/actors.ts [new file with mode: 0644]
server/lib/blocklist.ts
server/lib/notifier/shared/comment/comment-mention.ts
server/middlewares/validators/blocklist.ts
server/models/account/account-blocklist.ts
server/models/server/server-blocklist.ts
server/tests/api/check-params/blocklist.ts
server/tests/api/moderation/blocklist.ts
shared/extra-utils/users/blocklist-command.ts
shared/models/moderation/block-status.model.ts [new file with mode: 0644]
shared/models/moderation/index.ts

index 245edfd58ec1a174d88971a20bc2319f44a68a85..1445452507e43cc4aec0445fadc75760767ce41b 100644 (file)
               ></my-user-moderation-dropdown>
 
               <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
-              <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
-              <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
-              <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
-              <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
+
+              <my-account-block-badges [account]="account"></my-account-block-badges>
             </div>
 
             <div class="actor-handle">
index cdd00487b2706add34fdbb2681e6a9ca97239cdb..5043b98c40a192b0a563d11f289bca9d8d8ca603 100644 (file)
   }
 }
 
-my-user-moderation-dropdown,
-.badge {
-  @include margin-left(10px);
+my-user-moderation-dropdown {
+  margin: 0 10px;
 
-  position: relative;
-  top: 3px;
-}
-
-.badge {
-  font-size: 13px;
+  height: fit-content;
 }
 
 .copy-button {
@@ -64,6 +58,10 @@ my-user-moderation-dropdown,
   @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
 }
 
+.actor-display-name {
+  align-items: center;
+}
+
 .description {
   grid-column: 1 / 3;
   max-width: 1000px;
index 3cb117fccaa701daf66bdebe41560c7eb6a636fd..460f1dbf90f77d4fb676887dcbf5b8316a402ffe 100644 (file)
@@ -12,7 +12,7 @@ import {
   VideoChannelService,
   VideoService
 } from '@app/shared/shared-main'
-import { AccountReportComponent } from '@app/shared/shared-moderation'
+import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
 import { HttpStatusCode, User, UserRight } from '@shared/models'
 
 @Component({
@@ -52,6 +52,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
     private authService: AuthService,
     private videoService: VideoService,
     private markdown: MarkdownService,
+    private blocklist: BlocklistService,
     private screenService: ScreenService
   ) {
   }
@@ -159,6 +160,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
     this.updateModerationActions()
     this.loadUserIfNeeded(account)
     this.loadAccountVideosCount()
+    this.loadAccountBlockStatus()
   }
 
   private showReportModal () {
@@ -217,4 +219,9 @@ export class AccountsComponent implements OnInit, OnDestroy {
       this.accountVideosCount = res.total
     })
   }
+
+  private loadAccountBlockStatus () {
+    this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
+      .subscribe(status => this.account.updateBlockStatus(status))
+  }
 }
index 064fbb6f5b6e30a60319aa622051abe279620090..aec2e373c246a5e69bccc503a16dcab3933d18c3 100644 (file)
         <div class="section-label" i18n>OWNER ACCOUNT</div>
 
         <div class="avatar-row">
-          <my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
+          <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
 
           <div class="actor-info">
             <h4>
-              <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a>
+              <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ ownerAccount.displayName }}</a>
             </h4>
 
             <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
+
+            <my-account-block-badges [account]="ownerAccount"></my-account-block-badges>
           </div>
         </div>
 
index 272fc41d95ef08e5155cb6d6281a7d243c012242..ebb991f4ee3a4fd5f63e175b12efc5fc0f5681f5 100644 (file)
@@ -4,7 +4,8 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
 import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute } from '@angular/router'
 import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
-import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
+import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
+import { BlocklistService } from '@app/shared/shared-moderation'
 import { SupportModalComponent } from '@app/shared/shared-support-modal'
 import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
 import { HttpStatusCode } from '@shared/models'
@@ -18,6 +19,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
   @ViewChild('supportModal') supportModal: SupportModalComponent
 
   videoChannel: VideoChannel
+  ownerAccount: Account
   hotkeys: Hotkey[]
   links: ListOverflowItem[] = []
   isChannelManageable = false
@@ -38,7 +40,8 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
     private restExtractor: RestExtractor,
     private hotkeysService: HotkeysService,
     private screenService: ScreenService,
-    private markdown: MarkdownService
+    private markdown: MarkdownService,
+    private blocklist: BlocklistService
   ) { }
 
   ngOnInit () {
@@ -58,8 +61,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
 
                           // After the markdown renderer to avoid layout changes
                           this.videoChannel = videoChannel
+                          this.ownerAccount = new Account(this.videoChannel.ownerAccount)
 
                           this.loadChannelVideosCount()
+                          this.loadOwnerBlockStatus()
                         })
 
     this.hotkeys = [
@@ -125,4 +130,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
       sort: '-publishedAt'
     }).subscribe(res => this.channelVideosCount = res.total)
   }
+
+  private loadOwnerBlockStatus () {
+    this.blocklist.getStatus({ accounts: [ this.ownerAccount.nameWithHostForced ], hosts: [ this.ownerAccount.host ] })
+      .subscribe(status => this.ownerAccount.updateBlockStatus(status))
+  }
 }
index 35c39cc2ec12723637f1fc3924b2e0acbcedd8c3..76aaecf83b075c2f5228bb241392ad2020b3bd71 100644 (file)
@@ -2,15 +2,16 @@ import { NgModule } from '@angular/core'
 import { SharedFormModule } from '@app/shared/shared-forms'
 import { SharedGlobalIconModule } from '@app/shared/shared-icons'
 import { SharedMainModule } from '@app/shared/shared-main'
+import { SharedModerationModule } from '@app/shared/shared-moderation'
 import { SharedSupportModal } from '@app/shared/shared-support-modal'
 import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
 import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
 import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
+import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
 import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
 import { VideoChannelsRoutingModule } from './video-channels-routing.module'
 import { VideoChannelsComponent } from './video-channels.component'
-import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
 
 @NgModule({
   imports: [
@@ -23,7 +24,8 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
     SharedUserSubscriptionModule,
     SharedGlobalIconModule,
     SharedSupportModal,
-    SharedActorImageModule
+    SharedActorImageModule,
+    SharedModerationModule
   ],
 
   declarations: [
index 92606e7fa9810c382253538e187767dee8731f6e..8b78d01a67c6aeecc7f1c916c76a793ef5f8cd1d 100644 (file)
@@ -1,4 +1,4 @@
-import { Account as ServerAccount, ActorImage } from '@shared/models'
+import { Account as ServerAccount, ActorImage, BlockStatus } from '@shared/models'
 import { Actor } from './actor.model'
 
 export class Account extends Actor implements ServerAccount {
@@ -49,4 +49,11 @@ export class Account extends Actor implements ServerAccount {
   resetAvatar () {
     this.avatar = null
   }
+
+  updateBlockStatus (blockStatus: BlockStatus) {
+    this.mutedByInstance = blockStatus.accounts[this.nameWithHostForced].blockedByServer
+    this.mutedByUser = blockStatus.accounts[this.nameWithHostForced].blockedByUser
+    this.mutedServerByUser = blockStatus.hosts[this.host].blockedByUser
+    this.mutedServerByInstance = blockStatus.hosts[this.host].blockedByServer
+  }
 }
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 (file)
index 0000000..feac707
--- /dev/null
@@ -0,0 +1,4 @@
+<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
+<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
+<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
+<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 (file)
index 0000000..ccc3666
--- /dev/null
@@ -0,0 +1,9 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+
+.badge {
+  @include margin-right(10px);
+
+  height: fit-content;
+  font-size: 12px;
+}
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 (file)
index 0000000..a726011
--- /dev/null
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core'
+import { Account } from '../shared-main'
+
+@Component({
+  selector: 'my-account-block-badges',
+  styleUrls: [ './account-block-badges.component.scss' ],
+  templateUrl: './account-block-badges.component.html'
+})
+export class AccountBlockBadgesComponent {
+  @Input() account: Account
+}
index db2a8c5846ea920cfda02045344f5c6f8cd6aa3b..f4836c6c4bd694893ea29cc66163ae8207f1b4cb 100644 (file)
@@ -3,7 +3,7 @@ import { catchError, map } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
-import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '@shared/models'
+import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
 import { environment } from '../../../environments/environment'
 import { Account } from '../shared-main'
 import { AccountBlock } from './account-block.model'
@@ -12,6 +12,7 @@ export enum BlocklistComponentType { Account, Instance }
 
 @Injectable()
 export class BlocklistService {
+  static BASE_BLOCKLIST_URL = environment.apiUrl + '/api/v1/blocklist'
   static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
   static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
 
@@ -21,6 +22,23 @@ export class BlocklistService {
     private restService: RestService
   ) { }
 
+  /** ********************* Blocklist status ***********************/
+
+  getStatus (options: {
+    accounts?: string[]
+    hosts?: string[]
+  }) {
+    const { accounts, hosts } = options
+
+    let params = new HttpParams()
+
+    if (accounts) params = this.restService.addArrayParams(params, 'accounts', accounts)
+    if (hosts) params = this.restService.addArrayParams(params, 'hosts', hosts)
+
+    return this.authHttp.get<BlockStatus>(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
+      .pipe(catchError(err => this.restExtractor.handleError(err)))
+  }
+
   /** ********************* User -> Account blocklist ***********************/
 
   getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
index 41c910ffe5c4cae67e54c581d93589db3f8b5051..da85b2299c5ac1c9d6bdd1697540c7d33dd29a85 100644 (file)
@@ -1,6 +1,7 @@
 export * from './report-modals'
 
 export * from './abuse.service'
+export * from './account-block-badges.component'
 export * from './account-block.model'
 export * from './account-blocklist.component'
 export * from './batch-domains-modal.component'
index 95213e2bd969376308dadbe607d80906fc8ff53f..7cadda67c83fc72932a9de3be6513587086cb00c 100644 (file)
@@ -13,6 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
 import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
 import { VideoBlockComponent } from './video-block.component'
 import { VideoBlockService } from './video-block.service'
+import { AccountBlockBadgesComponent } from './account-block-badges.component'
 import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
 
 @NgModule({
@@ -31,7 +32,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
     VideoReportComponent,
     BatchDomainsModalComponent,
     CommentReportComponent,
-    AccountReportComponent
+    AccountReportComponent,
+    AccountBlockBadgesComponent
   ],
 
   exports: [
@@ -41,7 +43,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
     VideoReportComponent,
     BatchDomainsModalComponent,
     CommentReportComponent,
-    AccountReportComponent
+    AccountReportComponent,
+    AccountBlockBadgesComponent
   ],
 
   providers: [
index b18d861d69e06d5c72776fcdcb6ec2806b190175..e2cd2cdc10255f98fd6841640453d7ae5fd38342 100644 (file)
@@ -289,13 +289,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
       {
         label: $localize`Mute the instance`,
         description: $localize`Hide any content from that instance for you.`,
-        isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
+        isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === false,
         handler: ({ account }) => this.blockServerByUser(account.host)
       },
       {
         label: $localize`Unmute the instance`,
         description: $localize`Show back content from that instance for you.`,
-        isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
+        isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === true,
         handler: ({ account }) => this.unblockServerByUser(account.host)
       },
       {
diff --git a/server/controllers/api/blocklist.ts b/server/controllers/api/blocklist.ts
new file mode 100644 (file)
index 0000000..1e936ad
--- /dev/null
@@ -0,0 +1,108 @@
+import express from 'express'
+import { handleToNameAndHost } from '@server/helpers/actors'
+import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
+import { getServerActor } from '@server/models/application/application'
+import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
+import { MActorAccountId, MUserAccountId } from '@server/types/models'
+import { BlockStatus } from '@shared/models'
+import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
+import { logger } from '@server/helpers/logger'
+
+const blocklistRouter = express.Router()
+
+blocklistRouter.get('/status',
+  optionalAuthenticate,
+  blocklistStatusValidator,
+  asyncMiddleware(getBlocklistStatus)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  blocklistRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function getBlocklistStatus (req: express.Request, res: express.Response) {
+  const hosts = req.query.hosts as string[]
+  const accounts = req.query.accounts as string[]
+  const user = res.locals.oauth?.token.User
+
+  const serverActor = await getServerActor()
+
+  const byAccountIds = [ serverActor.Account.id ]
+  if (user) byAccountIds.push(user.Account.id)
+
+  const status: BlockStatus = {
+    accounts: {},
+    hosts: {}
+  }
+
+  const baseOptions = {
+    byAccountIds,
+    user,
+    serverActor,
+    status
+  }
+
+  await Promise.all([
+    populateServerBlocklistStatus({ ...baseOptions, hosts }),
+    populateAccountBlocklistStatus({ ...baseOptions, accounts })
+  ])
+
+  return res.json(status)
+}
+
+async function populateServerBlocklistStatus (options: {
+  byAccountIds: number[]
+  user?: MUserAccountId
+  serverActor: MActorAccountId
+  hosts: string[]
+  status: BlockStatus
+}) {
+  const { byAccountIds, user, serverActor, hosts, status } = options
+
+  if (!hosts || hosts.length === 0) return
+
+  const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts)
+
+  logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts })
+
+  for (const host of hosts) {
+    const block = serverBlocklistStatus.find(b => b.host === host)
+
+    status.hosts[host] = getStatus(block, serverActor, user)
+  }
+}
+
+async function populateAccountBlocklistStatus (options: {
+  byAccountIds: number[]
+  user?: MUserAccountId
+  serverActor: MActorAccountId
+  accounts: string[]
+  status: BlockStatus
+}) {
+  const { byAccountIds, user, serverActor, accounts, status } = options
+
+  if (!accounts || accounts.length === 0) return
+
+  const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts)
+
+  logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts })
+
+  for (const account of accounts) {
+    const sanitizedHandle = handleToNameAndHost(account)
+
+    const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host)
+
+    status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user)
+  }
+}
+
+function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) {
+  return {
+    blockedByServer: !!(block && block.accountId === serverActor.Account.id),
+    blockedByUser: !!(block && user && block.accountId === user.Account.id)
+  }
+}
index 9949b378a041454b5887d89afe089b921b7120ef..5f49336b13c886ad39820c0adc98ee2cef461b94 100644 (file)
@@ -6,6 +6,7 @@ import { badRequest } from '../../helpers/express-utils'
 import { CONFIG } from '../../initializers/config'
 import { abuseRouter } from './abuse'
 import { accountsRouter } from './accounts'
+import { blocklistRouter } from './blocklist'
 import { bulkRouter } from './bulk'
 import { configRouter } from './config'
 import { customPageRouter } from './custom-page'
@@ -49,6 +50,7 @@ apiRouter.use('/search', searchRouter)
 apiRouter.use('/overviews', overviewsRouter)
 apiRouter.use('/plugins', pluginRouter)
 apiRouter.use('/custom-pages', customPageRouter)
+apiRouter.use('/blocklist', blocklistRouter)
 apiRouter.use('/ping', pong)
 apiRouter.use('/*', badRequest)
 
index 6799ca8c510bccc9d3ea7cf6405a78af065509f1..fb1f6863513441b862a5e1bac1f2a2ad0d8b61a2 100644 (file)
@@ -1,5 +1,6 @@
 import 'multer'
 import express from 'express'
+import { handlesToNameAndHost } from '@server/helpers/actors'
 import { pickCommonVideoQuery } from '@server/helpers/query'
 import { sendUndoFollow } from '@server/lib/activitypub/send'
 import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
@@ -7,7 +8,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
 import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
 import { getFormattedObjects } from '../../../helpers/utils'
-import { WEBSERVER } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { JobQueue } from '../../../lib/job-queue'
 import {
@@ -89,28 +89,23 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
   const uris = req.query.uris as string[]
   const user = res.locals.oauth.token.User
 
-  const handles = uris.map(u => {
-    let [ name, host ] = u.split('@')
-    if (host === WEBSERVER.HOST) host = null
+  const sanitizedHandles = handlesToNameAndHost(uris)
 
-    return { name, host, uri: u }
-  })
-
-  const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
+  const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
 
   const existObject: { [id: string ]: boolean } = {}
-  for (const handle of handles) {
+  for (const sanitizedHandle of sanitizedHandles) {
     const obj = results.find(r => {
       const server = r.ActorFollowing.Server
 
-      return r.ActorFollowing.preferredUsername === handle.name &&
+      return r.ActorFollowing.preferredUsername === sanitizedHandle.name &&
         (
-          (!server && !handle.host) ||
-          (server.host === handle.host)
+          (!server && !sanitizedHandle.host) ||
+          (server.host === sanitizedHandle.host)
         )
     })
 
-    existObject[handle.uri] = obj !== undefined
+    existObject[sanitizedHandle.handle] = obj !== undefined
   }
 
   return res.json(existObject)
diff --git a/server/helpers/actors.ts b/server/helpers/actors.ts
new file mode 100644 (file)
index 0000000..c31fe6f
--- /dev/null
@@ -0,0 +1,17 @@
+import { WEBSERVER } from '@server/initializers/constants'
+
+function handleToNameAndHost (handle: string) {
+  let [ name, host ] = handle.split('@')
+  if (host === WEBSERVER.HOST) host = null
+
+  return { name, host, handle }
+}
+
+function handlesToNameAndHost (handles: string[]) {
+  return handles.map(h => handleToNameAndHost(h))
+}
+
+export {
+  handleToNameAndHost,
+  handlesToNameAndHost
+}
index d6b684015cb6e3f0fcf3563ae7c28b3ffb5be408..98273a6ea0de237842c3218eace53415a3df49ae 100644 (file)
@@ -40,12 +40,12 @@ async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAc
 
   if (userAccount) sourceAccounts.push(userAccount.id)
 
-  const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id)
+  const accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, targetAccount.id)
   if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) {
     return true
   }
 
-  const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId)
+  const instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, targetAccount.Actor.serverId)
   if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) {
     return true
   }
index 4f84d8dea2a84c5ce2ee5338d874b7228cb53cce..765cbaad95fecff738cabd260eadaa4f5329c765 100644 (file)
@@ -47,8 +47,8 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
 
     const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ])
 
-    this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId)
-    this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId)
+    this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, this.payload.accountId)
+    this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, this.payload.Account.Actor.serverId)
   }
 
   log () {
index b7749e204c8bac8bc6ba164a78876ddcc04bb36f..12980ced4d722818f7e9344c3cb2e34115a7555a 100644 (file)
@@ -1,8 +1,10 @@
 import express from 'express'
-import { body, param } from 'express-validator'
+import { body, param, query } from 'express-validator'
+import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor'
+import { toArray } from '@server/helpers/custom-validators/misc'
 import { getServerActor } from '@server/models/application/application'
 import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
-import { isHostValid } from '../../helpers/custom-validators/servers'
+import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
 import { logger } from '../../helpers/logger'
 import { WEBSERVER } from '../../initializers/constants'
 import { AccountBlocklistModel } from '../../models/account/account-blocklist'
@@ -123,6 +125,26 @@ const unblockServerByServerValidator = [
   }
 ]
 
+const blocklistStatusValidator = [
+  query('hosts')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
+
+  query('accounts')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking blocklistStatusValidator parameters', { query: req.query })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -131,7 +153,8 @@ export {
   unblockAccountByAccountValidator,
   unblockServerByAccountValidator,
   unblockAccountByServerValidator,
-  unblockServerByServerValidator
+  unblockServerByServerValidator,
+  blocklistStatusValidator
 }
 
 // ---------------------------------------------------------------------------
index b2375b00648ac8a79d2aa0e05750a64744c7321d..21983428a7111cc162e5516b6ff45adb90b4251d 100644 (file)
@@ -1,11 +1,12 @@
-import { Op } from 'sequelize'
+import { Op, QueryTypes } from 'sequelize'
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { handlesToNameAndHost } from '@server/helpers/actors'
 import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
 import { AttributesOnly } from '@shared/core-utils'
 import { AccountBlock } from '../../../shared/models'
 import { ActorModel } from '../actor/actor'
 import { ServerModel } from '../server/server'
-import { getSort, searchAttribute } from '../utils'
+import { createSafeIn, getSort, searchAttribute } from '../utils'
 import { AccountModel } from './account'
 
 enum ScopeNames {
@@ -77,7 +78,7 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
   })
   BlockedAccount: AccountModel
 
-  static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
+  static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) {
     const query = {
       attributes: [ 'accountId', 'id' ],
       where: {
@@ -187,6 +188,39 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
       .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`))
   }
 
+  static getBlockStatus (byAccountIds: number[], handles: string[]): Promise<{ name: string, host: string, accountId: number }[]> {
+    const sanitizedHandles = handlesToNameAndHost(handles)
+
+    const localHandles = sanitizedHandles.filter(h => !h.host)
+                                         .map(h => h.name)
+
+    const remoteHandles = sanitizedHandles.filter(h => !!h.host)
+                                          .map(h => ([ h.name, h.host ]))
+
+    const handlesWhere: string[] = []
+
+    if (localHandles.length !== 0) {
+      handlesWhere.push(`("actor"."preferredUsername" IN (:localHandles) AND "server"."id" IS NULL)`)
+    }
+
+    if (remoteHandles.length !== 0) {
+      handlesWhere.push(`(("actor"."preferredUsername", "server"."host") IN (:remoteHandles))`)
+    }
+
+    const rawQuery = `SELECT "accountBlocklist"."accountId", "actor"."preferredUsername" AS "name", "server"."host" ` +
+      `FROM "accountBlocklist" ` +
+      `INNER JOIN "account" ON "account"."id" = "accountBlocklist"."targetAccountId" ` +
+      `INNER JOIN "actor" ON "actor"."id" = "account"."actorId" ` +
+      `LEFT JOIN "server" ON "server"."id" = "actor"."serverId" ` +
+      `WHERE "accountBlocklist"."accountId" IN (${createSafeIn(AccountBlocklistModel.sequelize, byAccountIds)}) ` +
+      `AND (${handlesWhere.join(' OR ')})`
+
+    return AccountBlocklistModel.sequelize.query(rawQuery, {
+      type: QueryTypes.SELECT as QueryTypes.SELECT,
+      replacements: { byAccountIds, localHandles, remoteHandles }
+    })
+  }
+
   toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
     return {
       byAccount: this.ByAccount.toFormattedJSON(),
index b3579d5896f41f1bf596436abb4a67e484d4d463..092998db33c6682ee1cbe0bdf17c0e18f4ad1b20 100644 (file)
@@ -1,10 +1,10 @@
-import { Op } from 'sequelize'
+import { Op, QueryTypes } from 'sequelize'
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
 import { AttributesOnly } from '@shared/core-utils'
 import { ServerBlock } from '@shared/models'
 import { AccountModel } from '../account/account'
-import { getSort, searchAttribute } from '../utils'
+import { createSafeIn, getSort, searchAttribute } from '../utils'
 import { ServerModel } from './server'
 
 enum ScopeNames {
@@ -76,7 +76,7 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
   })
   BlockedServer: ServerModel
 
-  static isServerMutedByMulti (accountIds: number[], targetServerId: number) {
+  static isServerMutedByAccounts (accountIds: number[], targetServerId: number) {
     const query = {
       attributes: [ 'accountId', 'id' ],
       where: {
@@ -141,6 +141,19 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
       .then(entries => entries.map(e => e.BlockedServer.host))
   }
 
+  static getBlockStatus (byAccountIds: number[], hosts: string[]): Promise<{ host: string, accountId: number }[]> {
+    const rawQuery = `SELECT "server"."host", "serverBlocklist"."accountId" ` +
+      `FROM "serverBlocklist" ` +
+      `INNER JOIN "server" ON "server"."id" = "serverBlocklist"."targetServerId" ` +
+      `WHERE "server"."host" IN (:hosts) ` +
+      `AND "serverBlocklist"."accountId" IN (${createSafeIn(ServerBlocklistModel.sequelize, byAccountIds)})`
+
+    return ServerBlocklistModel.sequelize.query(rawQuery, {
+      type: QueryTypes.SELECT as QueryTypes.SELECT,
+      replacements: { hosts }
+    })
+  }
+
   static listForApi (parameters: {
     start: number
     count: number
index 7d5fae5cfb917dd27e43ff66ffe041af4baeeb84..f72a892e2dc7baec1e990f5277e460291b6e6df9 100644 (file)
@@ -481,6 +481,78 @@ describe('Test blocklist API validators', function () {
     })
   })
 
+  describe('When getting blocklist status', function () {
+    const path = '/api/v1/blocklist/status'
+
+    it('Should fail with a bad token', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path,
+        token: 'false',
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
+    })
+
+    it('Should fail with a bad accounts field', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {
+          accounts: 1
+        },
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
+      })
+
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {
+          accounts: [ 1 ]
+        },
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
+      })
+    })
+
+    it('Should fail with a bad hosts field', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {
+          hosts: 1
+        },
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
+      })
+
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {
+          hosts: [ 1 ]
+        },
+        expectedStatus: HttpStatusCode.BAD_REQUEST_400
+      })
+    })
+
+    it('Should succeed with the correct parameters', async function () {
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {},
+        expectedStatus: HttpStatusCode.OK_200
+      })
+
+      await makeGetRequest({
+        url: server.url,
+        path,
+        query: {
+          hosts: [ 'example.com' ],
+          accounts: [ 'john@example.com' ]
+        },
+        expectedStatus: HttpStatusCode.OK_200
+      })
+    })
+  })
+
   after(async function () {
     await cleanupTests(servers)
   })
index 089af8b159c0f45e198a8082ff4242c15277064c..b3fd8ecac8cfcf0894e32ff2a24a36bd88886c62 100644 (file)
@@ -254,6 +254,45 @@ describe('Test blocklist', function () {
         }
       })
 
+      it('Should get blocked status', async function () {
+        const remoteHandle = 'user2@' + servers[1].host
+        const localHandle = 'user1@' + servers[0].host
+        const unknownHandle = 'user5@' + servers[0].host
+
+        {
+          const status = await command.getStatus({ accounts: [ remoteHandle ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(1)
+          expect(status.accounts[remoteHandle].blockedByUser).to.be.false
+          expect(status.accounts[remoteHandle].blockedByServer).to.be.false
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(0)
+        }
+
+        {
+          const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(1)
+          expect(status.accounts[remoteHandle].blockedByUser).to.be.true
+          expect(status.accounts[remoteHandle].blockedByServer).to.be.false
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(0)
+        }
+
+        {
+          const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(3)
+
+          for (const handle of [ localHandle, remoteHandle ]) {
+            expect(status.accounts[handle].blockedByUser).to.be.true
+            expect(status.accounts[handle].blockedByServer).to.be.false
+          }
+
+          expect(status.accounts[unknownHandle].blockedByUser).to.be.false
+          expect(status.accounts[unknownHandle].blockedByServer).to.be.false
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(0)
+        }
+      })
+
       it('Should not allow a remote blocked user to comment my videos', async function () {
         this.timeout(60000)
 
@@ -434,6 +473,35 @@ describe('Test blocklist', function () {
         expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
       })
 
+      it('Should get blocklist status', async function () {
+        const blockedServer = servers[1].host
+        const notBlockedServer = 'example.com'
+
+        {
+          const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(0)
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(2)
+          expect(status.hosts[blockedServer].blockedByUser).to.be.false
+          expect(status.hosts[blockedServer].blockedByServer).to.be.false
+
+          expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
+          expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
+        }
+
+        {
+          const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(0)
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(2)
+          expect(status.hosts[blockedServer].blockedByUser).to.be.true
+          expect(status.hosts[blockedServer].blockedByServer).to.be.false
+
+          expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
+          expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
+        }
+      })
+
       it('Should unblock the remote server', async function () {
         await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port })
       })
@@ -575,6 +643,27 @@ describe('Test blocklist', function () {
         }
       })
 
+      it('Should get blocked status', async function () {
+        const remoteHandle = 'user2@' + servers[1].host
+        const localHandle = 'user1@' + servers[0].host
+        const unknownHandle = 'user5@' + servers[0].host
+
+        for (const token of [ undefined, servers[0].accessToken ]) {
+          const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(3)
+
+          for (const handle of [ localHandle, remoteHandle ]) {
+            expect(status.accounts[handle].blockedByUser).to.be.false
+            expect(status.accounts[handle].blockedByServer).to.be.true
+          }
+
+          expect(status.accounts[unknownHandle].blockedByUser).to.be.false
+          expect(status.accounts[unknownHandle].blockedByServer).to.be.false
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(0)
+        }
+      })
+
       it('Should unblock the remote account', async function () {
         await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
       })
@@ -620,6 +709,7 @@ describe('Test blocklist', function () {
     })
 
     describe('When managing server blocklist', function () {
+
       it('Should list all videos', async function () {
         for (const token of [ userModeratorToken, servers[0].accessToken ]) {
           await checkAllVideos(servers[0], token)
@@ -713,6 +803,23 @@ describe('Test blocklist', function () {
         expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
       })
 
+      it('Should get blocklist status', async function () {
+        const blockedServer = servers[1].host
+        const notBlockedServer = 'example.com'
+
+        for (const token of [ undefined, servers[0].accessToken ]) {
+          const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
+          expect(Object.keys(status.accounts)).to.have.lengthOf(0)
+
+          expect(Object.keys(status.hosts)).to.have.lengthOf(2)
+          expect(status.hosts[blockedServer].blockedByUser).to.be.false
+          expect(status.hosts[blockedServer].blockedByServer).to.be.true
+
+          expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
+          expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
+        }
+      })
+
       it('Should unblock the remote server', async function () {
         await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
       })
index 14491a1aee596c744b8b88f8f21f538f7903ebcc..2e7ed074dc3f36df77d6d42df5238fc6d75eef67 100644 (file)
@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
+import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
 
 type ListBlocklistOptions = OverrideCommandOptions & {
@@ -37,6 +37,29 @@ export class BlocklistCommand extends AbstractCommand {
 
   // ---------------------------------------------------------------------------
 
+  getStatus (options: OverrideCommandOptions & {
+    accounts?: string[]
+    hosts?: string[]
+  }) {
+    const { accounts, hosts } = options
+
+    const path = '/api/v1/blocklist/status'
+
+    return this.getRequestBody<BlockStatus>({
+      ...options,
+
+      path,
+      query: {
+        accounts,
+        hosts
+      },
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.OK_200
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
   addToMyBlocklist (options: OverrideCommandOptions & {
     account?: string
     server?: string
diff --git a/shared/models/moderation/block-status.model.ts b/shared/models/moderation/block-status.model.ts
new file mode 100644 (file)
index 0000000..5973127
--- /dev/null
@@ -0,0 +1,15 @@
+export interface BlockStatus {
+  accounts: {
+    [ handle: string ]: {
+      blockedByServer: boolean
+      blockedByUser?: boolean
+    }
+  }
+
+  hosts: {
+    [ host: string ]: {
+      blockedByServer: boolean
+      blockedByUser?: boolean
+    }
+  }
+}
index 8b6042e9790b55fb986e42fcac258de1b8cb63d6..f8e6d351cf2adb0b145ee9cf11439b250bd45af2 100644 (file)
@@ -1,3 +1,4 @@
 export * from './abuse'
+export * from './block-status.model'
 export * from './account-block.model'
 export * from './server-block.model'