Add mute status in account and channel pages
This commit is contained in:
parent
33675a4775
commit
80badf493a
|
@ -19,10 +19,8 @@
|
|||
></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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,14 +23,16 @@
|
|||
<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>
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.badge {
|
||||
@include margin-right(10px);
|
||||
|
||||
height: fit-content;
|
||||
font-size: 12px;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export interface BlockStatus {
|
||||
accounts: {
|
||||
[ handle: string ]: {
|
||||
blockedByServer: boolean
|
||||
blockedByUser?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
hosts: {
|
||||
[ host: string ]: {
|
||||
blockedByServer: boolean
|
||||
blockedByUser?: boolean
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './abuse'
|
||||
export * from './block-status.model'
|
||||
export * from './account-block.model'
|
||||
export * from './server-block.model'
|
||||
|
|
Loading…
Reference in New Issue