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 @@
>
Banned
- Muted
- Instance muted
- Muted by your instance
- Instance muted by your instance
+
+
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 @@
}
}
-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;
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 {
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))
+ }
}
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 @@
OWNER ACCOUNT
-
+
@{{ videoChannel.ownerBy }}
+
+
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
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))
+ }
}
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'
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: [
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 @@
-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
index 000000000..feac707c2
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -0,0 +1,4 @@
+
Muted
+
Instance muted
+
Muted by your instance
+
Instance muted by your instance
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 @@
+@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
index 000000000..a72601118
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
@@ -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
+}
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'
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
(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
+ .pipe(catchError(err => this.restExtractor.handleError(err)))
+ }
+
/** ********************* User -> Account blocklist ***********************/
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 @@
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'
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'
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: [
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 {
{
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
index 000000000..1e936ad10
--- /dev/null
+++ b/server/controllers/api/blocklist.ts
@@ -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)
+ }
+}
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'
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)
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 @@
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
index 000000000..c31fe6f8e
--- /dev/null
+++ b/server/helpers/actors.ts
@@ -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
+}
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
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
}
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 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 () {
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 @@
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
}
// ---------------------------------------------------------------------------
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 @@
-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 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(),
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 @@
-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 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
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 () {
})
})
+ 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)
})
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 () {
}
})
+ 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 })
})
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 @@
/* 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({
+ ...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
index 000000000..597312757
--- /dev/null
+++ b/shared/models/moderation/block-status.model.ts
@@ -0,0 +1,15 @@
+export interface BlockStatus {
+ accounts: {
+ [ handle: string ]: {
+ blockedByServer: boolean
+ blockedByUser?: boolean
+ }
+ }
+
+ hosts: {
+ [ host: string ]: {
+ blockedByServer: boolean
+ blockedByUser?: boolean
+ }
+ }
+}
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 @@
export * from './abuse'
+export * from './block-status.model'
export * from './account-block.model'
export * from './server-block.model'