Add abuse and registration requests stats
This commit is contained in:
parent
fbe47a9f8e
commit
db69d9491e
|
@ -23,7 +23,7 @@
|
||||||
<div i18n>on {{ instanceName }}</div>
|
<div i18n>on {{ instanceName }}</div>
|
||||||
</my-signup-step-title>
|
</my-signup-step-title>
|
||||||
|
|
||||||
<my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
|
<my-register-step-about [serverStats]="serverStats" [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
|
||||||
|
|
||||||
<div class="step-buttons">
|
<div class="step-buttons">
|
||||||
<a i18n class="skip-step underline-orange" routerLink="/login">
|
<a i18n class="skip-step underline-orange" routerLink="/login">
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { CdkStep } from '@angular/cdk/stepper'
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AuthService } from '@app/core'
|
import { AuthService, ServerService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||||
import { ServerConfig, UserRegister } from '@peertube/peertube-models'
|
import { ServerConfig, ServerStats, UserRegister } from '@peertube/peertube-models'
|
||||||
import { SignupService } from '../shared/signup.service'
|
import { SignupService } from '../shared/signup.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -45,12 +45,15 @@ export class RegisterComponent implements OnInit {
|
||||||
|
|
||||||
signupDisabled = false
|
signupDisabled = false
|
||||||
|
|
||||||
|
serverStats: ServerStats
|
||||||
|
|
||||||
private serverConfig: ServerConfig
|
private serverConfig: ServerConfig
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private signupService: SignupService,
|
private signupService: SignupService,
|
||||||
|
private server: ServerService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -85,8 +88,10 @@ export class RegisterComponent implements OnInit {
|
||||||
? $localize`:Button on the registration form to finalize the account and channel creation:Signup`
|
? $localize`:Button on the registration form to finalize the account and channel creation:Signup`
|
||||||
: this.defaultNextStepButtonLabel
|
: this.defaultNextStepButtonLabel
|
||||||
|
|
||||||
this.hooks.runAction('action:signup.register.init', 'signup')
|
this.server.getServerStats()
|
||||||
|
.subscribe(stats => this.serverStats = stats)
|
||||||
|
|
||||||
|
this.hooks.runAction('action:signup.register.init', 'signup')
|
||||||
}
|
}
|
||||||
|
|
||||||
hasSameChannelAndAccountNames () {
|
hasSameChannelAndAccountNames () {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<my-instance-banner class="d-block mb-3" rounded="true"></my-instance-banner>
|
<my-instance-banner class="d-block mb-4" rounded="true"></my-instance-banner>
|
||||||
|
|
||||||
<div class="why">
|
<div class="why">
|
||||||
<h3 i18n>Why creating an account?</h3>
|
<h3 i18n>Why creating an account?</h3>
|
||||||
|
@ -18,15 +18,15 @@
|
||||||
|
|
||||||
<p *ngIf="requiresApproval" i18n>
|
<p *ngIf="requiresApproval" i18n>
|
||||||
Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form.
|
Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form.
|
||||||
|
|
||||||
|
<ng-container *ngIf="averageResponseTime">They usually respond within {{ averageResponseTime | myDaysDurationFormatter }}.</ng-container>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 i18n>Do you use Mastodon, ActivityPub or a RSS feed aggregator?</h4>
|
<h4 i18n>Do you use Mastodon, ActivityPub or a RSS feed aggregator?</h4>
|
||||||
|
|
||||||
<p i18n>
|
<p i18n>You can already follow {{ instanceName }} using your favorite tool.</p>
|
||||||
You can already follow {{ instanceName }} using your favorite tool.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="callout callout-orange callout-light">
|
<div class="callout callout-orange callout-light">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
|
import { ServerStats } from '@peertube/peertube-models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-register-step-about',
|
selector: 'my-register-step-about',
|
||||||
|
@ -9,6 +10,7 @@ import { ServerService } from '@app/core'
|
||||||
export class RegisterStepAboutComponent {
|
export class RegisterStepAboutComponent {
|
||||||
@Input() requiresApproval: boolean
|
@Input() requiresApproval: boolean
|
||||||
@Input() videoUploadDisabled: boolean
|
@Input() videoUploadDisabled: boolean
|
||||||
|
@Input() serverStats: ServerStats
|
||||||
|
|
||||||
constructor (private serverService: ServerService) {
|
constructor (private serverService: ServerService) {
|
||||||
|
|
||||||
|
@ -17,4 +19,8 @@ export class RegisterStepAboutComponent {
|
||||||
get instanceName () {
|
get instanceName () {
|
||||||
return this.serverService.getHTMLConfig().instance.name
|
return this.serverService.getHTMLConfig().instance.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get averageResponseTime () {
|
||||||
|
return this.serverStats?.averageRegistrationRequestResponseTimeMs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert pt-alert-primary" i18n *ngIf="isInUpdateForm && getMaxLiveDuration() >= 0">
|
<div class="alert pt-alert-primary" i18n *ngIf="isInUpdateForm && getMaxLiveDuration() >= 0">
|
||||||
Max live duration is {{ getMaxLiveDuration() | myDurationFormatter }}.
|
Max live duration is {{ getMaxLiveDuration() | myTimeDurationFormatter }}.
|
||||||
If your live reaches this limit, it will be automatically terminated.
|
If your live reaches this limit, it will be automatically terminated.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
<div class="attribute attribute-duration" *ngIf="!video.isLive">
|
<div class="attribute attribute-duration" *ngIf="!video.isLive">
|
||||||
<span i18n class="attribute-label">Duration</span>
|
<span i18n class="attribute-label">Duration</span>
|
||||||
<span class="attribute-value">{{ video.duration | myDurationFormatter }}</span>
|
<span class="attribute-value">{{ video.duration | myTimeDurationFormatter }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="attribute attribute-plugin" *ngFor="let metadata of pluginMetadata">
|
<div class="attribute attribute-plugin" *ngFor="let metadata of pluginMetadata">
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div *ngIf="instanceBannerUrl" class="banner" [ngClass]="{ rounded }">
|
<div *ngIf="instanceBannerUrl" class="banner" [ngClass]="{ rounded }">
|
||||||
<img class="rounded" [src]="instanceBannerUrl" alt="Instance banner">
|
<img class="rounded" [src]="instanceBannerUrl" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ServerService } from '@app/core'
|
import { ServerService } from '@app/core'
|
||||||
import { formatICU } from '@app/helpers'
|
import { formatICU } from '@app/helpers'
|
||||||
import { ServerConfig } from '@peertube/peertube-models'
|
import { ServerConfig, ServerStats } from '@peertube/peertube-models'
|
||||||
|
import { DaysDurationFormatterPipe } from '../shared-main'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-instance-features-table',
|
selector: 'my-instance-features-table',
|
||||||
|
@ -11,6 +12,7 @@ import { ServerConfig } from '@peertube/peertube-models'
|
||||||
export class InstanceFeaturesTableComponent implements OnInit {
|
export class InstanceFeaturesTableComponent implements OnInit {
|
||||||
quotaHelpIndication = ''
|
quotaHelpIndication = ''
|
||||||
serverConfig: ServerConfig
|
serverConfig: ServerConfig
|
||||||
|
serverStats: ServerStats
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private serverService: ServerService
|
private serverService: ServerService
|
||||||
|
@ -42,8 +44,12 @@ export class InstanceFeaturesTableComponent implements OnInit {
|
||||||
this.serverService.getConfig()
|
this.serverService.getConfig()
|
||||||
.subscribe(config => {
|
.subscribe(config => {
|
||||||
this.serverConfig = config
|
this.serverConfig = config
|
||||||
|
|
||||||
this.buildQuotaHelpIndication()
|
this.buildQuotaHelpIndication()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.serverService.getServerStats()
|
||||||
|
.subscribe(stats => this.serverStats = stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildNSFWLabel () {
|
buildNSFWLabel () {
|
||||||
|
@ -58,7 +64,17 @@ export class InstanceFeaturesTableComponent implements OnInit {
|
||||||
const config = this.serverConfig.signup
|
const config = this.serverConfig.signup
|
||||||
|
|
||||||
if (config.allowed !== true) return $localize`Disabled`
|
if (config.allowed !== true) return $localize`Disabled`
|
||||||
if (config.requiresApproval === true) return $localize`Requires approval by moderators`
|
|
||||||
|
if (config.requiresApproval === true) {
|
||||||
|
const responseTimeMS = this.serverStats?.averageRegistrationRequestResponseTimeMs
|
||||||
|
|
||||||
|
if (!responseTimeMS) {
|
||||||
|
return $localize`Requires approval by moderators`
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTime = new DaysDurationFormatterPipe().transform(responseTimeMS)
|
||||||
|
return $localize`Requires approval by moderators (~ ${responseTime})`
|
||||||
|
}
|
||||||
|
|
||||||
return $localize`Enabled`
|
return $localize`Enabled`
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'myDaysDurationFormatter'
|
||||||
|
})
|
||||||
|
export class DaysDurationFormatterPipe implements PipeTransform {
|
||||||
|
|
||||||
|
transform (value: number): string {
|
||||||
|
const days = Math.floor(value / (3600 * 24 * 1000))
|
||||||
|
|
||||||
|
if (days <= 1) return $localize`1 day`
|
||||||
|
|
||||||
|
return $localize`${days} days`
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
export * from './auto-colspan.directive'
|
export * from './auto-colspan.directive'
|
||||||
export * from './autofocus.directive'
|
export * from './autofocus.directive'
|
||||||
export * from './bytes.pipe'
|
export * from './bytes.pipe'
|
||||||
|
export * from './days-duration-formatter.pipe'
|
||||||
export * from './defer-loading.directive'
|
export * from './defer-loading.directive'
|
||||||
export * from './duration-formatter.pipe'
|
|
||||||
export * from './from-now.pipe'
|
export * from './from-now.pipe'
|
||||||
export * from './infinite-scroller.directive'
|
export * from './infinite-scroller.directive'
|
||||||
export * from './link.component'
|
export * from './link.component'
|
||||||
export * from './login-link.component'
|
export * from './login-link.component'
|
||||||
export * from './number-formatter.pipe'
|
export * from './number-formatter.pipe'
|
||||||
export * from './peertube-template.directive'
|
export * from './peertube-template.directive'
|
||||||
|
export * from './time-duration-formatter.pipe'
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'myDurationFormatter'
|
name: 'myTimeDurationFormatter'
|
||||||
})
|
})
|
||||||
export class DurationFormatterPipe implements PipeTransform {
|
export class TimeDurationFormatterPipe implements PipeTransform {
|
||||||
|
|
||||||
transform (value: number): string {
|
transform (value: number): string {
|
||||||
const hours = Math.floor(value / 3600)
|
const hours = Math.floor(value / 3600)
|
|
@ -22,7 +22,8 @@ import {
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
DeferLoadingDirective,
|
DeferLoadingDirective,
|
||||||
DurationFormatterPipe,
|
TimeDurationFormatterPipe,
|
||||||
|
DaysDurationFormatterPipe,
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
InfiniteScrollerDirective,
|
InfiniteScrollerDirective,
|
||||||
LinkComponent,
|
LinkComponent,
|
||||||
|
@ -89,7 +90,8 @@ import { VideoChannelService } from './video-channel'
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
DurationFormatterPipe,
|
TimeDurationFormatterPipe,
|
||||||
|
DaysDurationFormatterPipe,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
DeferLoadingDirective,
|
DeferLoadingDirective,
|
||||||
AutoColspanDirective,
|
AutoColspanDirective,
|
||||||
|
@ -152,7 +154,8 @@ import { VideoChannelService } from './video-channel'
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
DurationFormatterPipe,
|
TimeDurationFormatterPipe,
|
||||||
|
DaysDurationFormatterPipe,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
DeferLoadingDirective,
|
DeferLoadingDirective,
|
||||||
AutoColspanDirective,
|
AutoColspanDirective,
|
||||||
|
|
|
@ -447,6 +447,15 @@ thumbnails:
|
||||||
# Minimum value is 2
|
# Minimum value is 2
|
||||||
frames_to_analyze: 50
|
frames_to_analyze: 50
|
||||||
|
|
||||||
|
stats:
|
||||||
|
# Display registration requests stats (average response time, total requests...)
|
||||||
|
registration_requests:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Display abuses stats (average response time, total abuses...)
|
||||||
|
abuses:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
previews:
|
previews:
|
||||||
size: 500 # Max number of previews you want to cache
|
size: 500 # Max number of previews you want to cache
|
||||||
|
|
|
@ -445,6 +445,15 @@ thumbnails:
|
||||||
# Minimum value is 2
|
# Minimum value is 2
|
||||||
frames_to_analyze: 50
|
frames_to_analyze: 50
|
||||||
|
|
||||||
|
stats:
|
||||||
|
# Display registration requests stats (average response time, total requests...)
|
||||||
|
registration_requests:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Display abuses stats (average response time, total abuses...)
|
||||||
|
abuses:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
#
|
#
|
||||||
# From this point, almost all following keys can be overridden by the web interface
|
# From this point, almost all following keys can be overridden by the web interface
|
||||||
|
|
|
@ -36,6 +36,14 @@ export interface ServerStats extends ActivityPubMessagesSuccess, ActivityPubMess
|
||||||
|
|
||||||
activityPubMessagesProcessedPerSecond: number
|
activityPubMessagesProcessedPerSecond: number
|
||||||
totalActivityPubMessagesWaiting: number
|
totalActivityPubMessagesWaiting: number
|
||||||
|
|
||||||
|
averageRegistrationRequestResponseTimeMs: number
|
||||||
|
totalRegistrationRequestsProcessed: number
|
||||||
|
totalRegistrationRequests: number
|
||||||
|
|
||||||
|
averageAbuseResponseTimeMs: number
|
||||||
|
totalAbusesProcessed: number
|
||||||
|
totalAbuses: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideosRedundancyStats {
|
export interface VideosRedundancyStats {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import { ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
import { AbuseState, ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createMultipleServers,
|
createMultipleServers,
|
||||||
|
@ -17,10 +17,8 @@ import {
|
||||||
describe('Test stats (excluding redundancy)', function () {
|
describe('Test stats (excluding redundancy)', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
let channelId
|
let channelId
|
||||||
const user = {
|
const user = { username: 'user1', password: 'super_password' }
|
||||||
username: 'user1',
|
let userAccountId: number
|
||||||
password: 'super_password'
|
|
||||||
}
|
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
@ -33,7 +31,8 @@ describe('Test stats (excluding redundancy)', function () {
|
||||||
|
|
||||||
await doubleFollow(servers[0], servers[1])
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
await servers[0].users.create({ username: user.username, password: user.password })
|
const { account } = await servers[0].users.create({ username: user.username, password: user.password })
|
||||||
|
userAccountId = account.id
|
||||||
|
|
||||||
const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } })
|
const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } })
|
||||||
|
|
||||||
|
@ -48,229 +47,393 @@ describe('Test stats (excluding redundancy)', function () {
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have the correct stats on instance 1', async function () {
|
describe('Total stats', function () {
|
||||||
const data = await servers[0].stats.get()
|
|
||||||
|
|
||||||
expect(data.totalLocalVideoComments).to.equal(1)
|
it('Should have the correct stats on instance 1', async function () {
|
||||||
expect(data.totalLocalVideos).to.equal(1)
|
const data = await servers[0].stats.get()
|
||||||
expect(data.totalLocalVideoViews).to.equal(1)
|
|
||||||
expect(data.totalLocalVideoFilesSize).to.equal(218910)
|
|
||||||
expect(data.totalUsers).to.equal(2)
|
|
||||||
expect(data.totalVideoComments).to.equal(1)
|
|
||||||
expect(data.totalVideos).to.equal(1)
|
|
||||||
expect(data.totalInstanceFollowers).to.equal(2)
|
|
||||||
expect(data.totalInstanceFollowing).to.equal(1)
|
|
||||||
expect(data.totalLocalPlaylists).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct stats on instance 2', async function () {
|
expect(data.totalLocalVideoComments).to.equal(1)
|
||||||
const data = await servers[1].stats.get()
|
expect(data.totalLocalVideos).to.equal(1)
|
||||||
|
expect(data.totalLocalVideoViews).to.equal(1)
|
||||||
expect(data.totalLocalVideoComments).to.equal(0)
|
expect(data.totalLocalVideoFilesSize).to.equal(218910)
|
||||||
expect(data.totalLocalVideos).to.equal(0)
|
expect(data.totalUsers).to.equal(2)
|
||||||
expect(data.totalLocalVideoViews).to.equal(0)
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
expect(data.totalLocalVideoFilesSize).to.equal(0)
|
expect(data.totalVideos).to.equal(1)
|
||||||
expect(data.totalUsers).to.equal(1)
|
expect(data.totalInstanceFollowers).to.equal(2)
|
||||||
expect(data.totalVideoComments).to.equal(1)
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
expect(data.totalVideos).to.equal(1)
|
|
||||||
expect(data.totalInstanceFollowers).to.equal(1)
|
|
||||||
expect(data.totalInstanceFollowing).to.equal(1)
|
|
||||||
expect(data.totalLocalPlaylists).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct stats on instance 3', async function () {
|
|
||||||
const data = await servers[2].stats.get()
|
|
||||||
|
|
||||||
expect(data.totalLocalVideoComments).to.equal(0)
|
|
||||||
expect(data.totalLocalVideos).to.equal(0)
|
|
||||||
expect(data.totalLocalVideoViews).to.equal(0)
|
|
||||||
expect(data.totalUsers).to.equal(1)
|
|
||||||
expect(data.totalVideoComments).to.equal(1)
|
|
||||||
expect(data.totalVideos).to.equal(1)
|
|
||||||
expect(data.totalInstanceFollowing).to.equal(1)
|
|
||||||
expect(data.totalInstanceFollowers).to.equal(0)
|
|
||||||
expect(data.totalLocalPlaylists).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct total videos stats after an unfollow', async function () {
|
|
||||||
this.timeout(15000)
|
|
||||||
|
|
||||||
await servers[2].follows.unfollow({ target: servers[0] })
|
|
||||||
await waitJobs(servers)
|
|
||||||
|
|
||||||
const data = await servers[2].stats.get()
|
|
||||||
|
|
||||||
expect(data.totalVideos).to.equal(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct active user stats', async function () {
|
|
||||||
const server = servers[0]
|
|
||||||
|
|
||||||
{
|
|
||||||
const data = await server.stats.get()
|
|
||||||
|
|
||||||
expect(data.totalDailyActiveUsers).to.equal(1)
|
|
||||||
expect(data.totalWeeklyActiveUsers).to.equal(1)
|
|
||||||
expect(data.totalMonthlyActiveUsers).to.equal(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
await server.login.getAccessToken(user)
|
|
||||||
|
|
||||||
const data = await server.stats.get()
|
|
||||||
|
|
||||||
expect(data.totalDailyActiveUsers).to.equal(2)
|
|
||||||
expect(data.totalWeeklyActiveUsers).to.equal(2)
|
|
||||||
expect(data.totalMonthlyActiveUsers).to.equal(2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct active channel stats', async function () {
|
|
||||||
const server = servers[0]
|
|
||||||
|
|
||||||
{
|
|
||||||
const data = await server.stats.get()
|
|
||||||
|
|
||||||
expect(data.totalLocalVideoChannels).to.equal(2)
|
|
||||||
expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
|
|
||||||
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
|
|
||||||
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const attributes = {
|
|
||||||
name: 'stats_channel',
|
|
||||||
displayName: 'My stats channel'
|
|
||||||
}
|
|
||||||
const created = await server.channels.create({ attributes })
|
|
||||||
channelId = created.id
|
|
||||||
|
|
||||||
const data = await server.stats.get()
|
|
||||||
|
|
||||||
expect(data.totalLocalVideoChannels).to.equal(3)
|
|
||||||
expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
|
|
||||||
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
|
|
||||||
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } })
|
|
||||||
|
|
||||||
const data = await server.stats.get()
|
|
||||||
|
|
||||||
expect(data.totalLocalVideoChannels).to.equal(3)
|
|
||||||
expect(data.totalLocalDailyActiveVideoChannels).to.equal(2)
|
|
||||||
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2)
|
|
||||||
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have the correct playlist stats', async function () {
|
|
||||||
const server = servers[0]
|
|
||||||
|
|
||||||
{
|
|
||||||
const data = await server.stats.get()
|
|
||||||
expect(data.totalLocalPlaylists).to.equal(0)
|
expect(data.totalLocalPlaylists).to.equal(0)
|
||||||
}
|
})
|
||||||
|
|
||||||
{
|
it('Should have the correct stats on instance 2', async function () {
|
||||||
await server.playlists.create({
|
const data = await servers[1].stats.get()
|
||||||
attributes: {
|
|
||||||
displayName: 'playlist for count',
|
|
||||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
|
||||||
videoChannelId: channelId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await server.stats.get()
|
expect(data.totalLocalVideoComments).to.equal(0)
|
||||||
expect(data.totalLocalPlaylists).to.equal(1)
|
expect(data.totalLocalVideos).to.equal(0)
|
||||||
}
|
expect(data.totalLocalVideoViews).to.equal(0)
|
||||||
})
|
expect(data.totalLocalVideoFilesSize).to.equal(0)
|
||||||
|
expect(data.totalUsers).to.equal(1)
|
||||||
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
|
expect(data.totalVideos).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowers).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
|
expect(data.totalLocalPlaylists).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('Should correctly count video file sizes if transcoding is enabled', async function () {
|
it('Should have the correct stats on instance 3', async function () {
|
||||||
this.timeout(120000)
|
const data = await servers[2].stats.get()
|
||||||
|
|
||||||
await servers[0].config.updateCustomSubConfig({
|
expect(data.totalLocalVideoComments).to.equal(0)
|
||||||
newConfig: {
|
expect(data.totalLocalVideos).to.equal(0)
|
||||||
transcoding: {
|
expect(data.totalLocalVideoViews).to.equal(0)
|
||||||
enabled: true,
|
expect(data.totalUsers).to.equal(1)
|
||||||
webVideos: {
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
enabled: true
|
expect(data.totalVideos).to.equal(1)
|
||||||
},
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
hls: {
|
expect(data.totalInstanceFollowers).to.equal(0)
|
||||||
enabled: true
|
expect(data.totalLocalPlaylists).to.equal(0)
|
||||||
},
|
})
|
||||||
resolutions: {
|
|
||||||
'0p': false,
|
it('Should have the correct total videos stats after an unfollow', async function () {
|
||||||
'144p': false,
|
this.timeout(15000)
|
||||||
'240p': false,
|
|
||||||
'360p': false,
|
await servers[2].follows.unfollow({ target: servers[0] })
|
||||||
'480p': false,
|
await waitJobs(servers)
|
||||||
'720p': false,
|
|
||||||
'1080p': false,
|
const data = await servers[2].stats.get()
|
||||||
'1440p': false,
|
|
||||||
'2160p': false
|
expect(data.totalVideos).to.equal(0)
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
it('Should have the correct active user stats', async function () {
|
||||||
|
const server = servers[0]
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await server.stats.get()
|
||||||
|
|
||||||
|
expect(data.totalDailyActiveUsers).to.equal(1)
|
||||||
|
expect(data.totalWeeklyActiveUsers).to.equal(1)
|
||||||
|
expect(data.totalMonthlyActiveUsers).to.equal(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await server.login.getAccessToken(user)
|
||||||
|
|
||||||
|
const data = await server.stats.get()
|
||||||
|
|
||||||
|
expect(data.totalDailyActiveUsers).to.equal(2)
|
||||||
|
expect(data.totalWeeklyActiveUsers).to.equal(2)
|
||||||
|
expect(data.totalMonthlyActiveUsers).to.equal(2)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } })
|
it('Should have the correct active channel stats', async function () {
|
||||||
|
const server = servers[0]
|
||||||
|
|
||||||
await waitJobs(servers)
|
{
|
||||||
|
const data = await server.stats.get()
|
||||||
|
|
||||||
{
|
expect(data.totalLocalVideoChannels).to.equal(2)
|
||||||
const data = await servers[1].stats.get()
|
expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
|
||||||
expect(data.totalLocalVideoFilesSize).to.equal(0)
|
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
|
||||||
}
|
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const data = await servers[0].stats.get()
|
const attributes = {
|
||||||
expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000)
|
name: 'stats_channel',
|
||||||
expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000)
|
displayName: 'My stats channel'
|
||||||
}
|
}
|
||||||
|
const created = await server.channels.create({ attributes })
|
||||||
|
channelId = created.id
|
||||||
|
|
||||||
|
const data = await server.stats.get()
|
||||||
|
|
||||||
|
expect(data.totalLocalVideoChannels).to.equal(3)
|
||||||
|
expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
|
||||||
|
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
|
||||||
|
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } })
|
||||||
|
|
||||||
|
const data = await server.stats.get()
|
||||||
|
|
||||||
|
expect(data.totalLocalVideoChannels).to.equal(3)
|
||||||
|
expect(data.totalLocalDailyActiveVideoChannels).to.equal(2)
|
||||||
|
expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2)
|
||||||
|
expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have the correct playlist stats', async function () {
|
||||||
|
const server = servers[0]
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await server.stats.get()
|
||||||
|
expect(data.totalLocalPlaylists).to.equal(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await server.playlists.create({
|
||||||
|
attributes: {
|
||||||
|
displayName: 'playlist for count',
|
||||||
|
privacy: VideoPlaylistPrivacy.PUBLIC,
|
||||||
|
videoChannelId: channelId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await server.stats.get()
|
||||||
|
expect(data.totalLocalPlaylists).to.equal(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should have the correct AP stats', async function () {
|
describe('File sizes', function () {
|
||||||
this.timeout(240000)
|
|
||||||
|
|
||||||
await servers[0].config.disableTranscoding()
|
it('Should correctly count video file sizes if transcoding is enabled', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
const first = await servers[1].stats.get()
|
await servers[0].config.updateCustomSubConfig({
|
||||||
|
newConfig: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: true,
|
||||||
|
webVideos: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
hls: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
resolutions: {
|
||||||
|
'0p': false,
|
||||||
|
'144p': false,
|
||||||
|
'240p': false,
|
||||||
|
'360p': false,
|
||||||
|
'480p': false,
|
||||||
|
'720p': false,
|
||||||
|
'1080p': false,
|
||||||
|
'1440p': false,
|
||||||
|
'2160p': false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } })
|
||||||
await servers[0].videos.upload({ attributes: { name: 'video' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
await wait(6000)
|
{
|
||||||
|
const data = await servers[1].stats.get()
|
||||||
|
expect(data.totalLocalVideoFilesSize).to.equal(0)
|
||||||
|
}
|
||||||
|
|
||||||
const second = await servers[1].stats.get()
|
{
|
||||||
expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed)
|
const data = await servers[0].stats.get()
|
||||||
|
expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000)
|
||||||
|
expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const apTypes: ActivityType[] = [
|
describe('ActivityPub', function () {
|
||||||
'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag'
|
|
||||||
]
|
|
||||||
|
|
||||||
const processed = apTypes.reduce(
|
it('Should have the correct AP stats', async function () {
|
||||||
(previous, type) => previous + second['totalActivityPub' + type + 'MessagesSuccesses'],
|
this.timeout(240000)
|
||||||
0
|
|
||||||
)
|
|
||||||
expect(second.totalActivityPubMessagesProcessed).to.equal(processed)
|
|
||||||
expect(second.totalActivityPubMessagesSuccesses).to.equal(processed)
|
|
||||||
|
|
||||||
expect(second.totalActivityPubMessagesErrors).to.equal(0)
|
await servers[0].config.disableTranscoding()
|
||||||
|
|
||||||
for (const apType of apTypes) {
|
const first = await servers[1].stats.get()
|
||||||
expect(second['totalActivityPub' + apType + 'MessagesErrors']).to.equal(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(6000)
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await servers[0].videos.upload({ attributes: { name: 'video' } })
|
||||||
|
}
|
||||||
|
|
||||||
const third = await servers[1].stats.get()
|
await waitJobs(servers)
|
||||||
expect(third.totalActivityPubMessagesWaiting).to.equal(0)
|
|
||||||
expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond)
|
await wait(6000)
|
||||||
|
|
||||||
|
const second = await servers[1].stats.get()
|
||||||
|
expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed)
|
||||||
|
|
||||||
|
const apTypes: ActivityType[] = [
|
||||||
|
'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag'
|
||||||
|
]
|
||||||
|
|
||||||
|
const processed = apTypes.reduce(
|
||||||
|
(previous, type) => previous + second['totalActivityPub' + type + 'MessagesSuccesses'],
|
||||||
|
0
|
||||||
|
)
|
||||||
|
expect(second.totalActivityPubMessagesProcessed).to.equal(processed)
|
||||||
|
expect(second.totalActivityPubMessagesSuccesses).to.equal(processed)
|
||||||
|
|
||||||
|
expect(second.totalActivityPubMessagesErrors).to.equal(0)
|
||||||
|
|
||||||
|
for (const apType of apTypes) {
|
||||||
|
expect(second['totalActivityPub' + apType + 'MessagesErrors']).to.equal(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
await wait(6000)
|
||||||
|
|
||||||
|
const third = await servers[1].stats.get()
|
||||||
|
expect(third.totalActivityPubMessagesWaiting).to.equal(0)
|
||||||
|
expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('User registration requests stats', function () {
|
||||||
|
let id2: number
|
||||||
|
let beforeTimestamp: number
|
||||||
|
let lastResponseTime: number
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
await servers[0].config.enableSignup(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not have registration requests stats available', async function () {
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalRegistrationRequests).to.equal(0)
|
||||||
|
expect(data.totalRegistrationRequestsProcessed).to.equal(0)
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should create registration requests, accept one and have correct stats', async function () {
|
||||||
|
beforeTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
const { id: id1 } = await servers[0].registrations.requestRegistration({ username: 'user2', registrationReason: 'reason 1' });
|
||||||
|
({ id: id2 } = await servers[0].registrations.requestRegistration({ username: 'user3', registrationReason: 'reason 2' }))
|
||||||
|
await servers[0].registrations.requestRegistration({ username: 'user4', registrationReason: 'reason 3' })
|
||||||
|
|
||||||
|
await wait(1500)
|
||||||
|
|
||||||
|
await servers[0].registrations.accept({ id: id1, moderationResponse: 'thanks' })
|
||||||
|
|
||||||
|
const middleTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalRegistrationRequests).to.equal(3)
|
||||||
|
expect(data.totalRegistrationRequestsProcessed).to.equal(1)
|
||||||
|
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.greaterThan(1000)
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.below(middleTimestamp - beforeTimestamp)
|
||||||
|
|
||||||
|
lastResponseTime = data.averageRegistrationRequestResponseTimeMs
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should accept another request and update stats', async function () {
|
||||||
|
await wait(1500)
|
||||||
|
|
||||||
|
await servers[0].registrations.accept({ id: id2, moderationResponse: 'thanks' })
|
||||||
|
|
||||||
|
const lastTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalRegistrationRequests).to.equal(3)
|
||||||
|
expect(data.totalRegistrationRequestsProcessed).to.equal(2)
|
||||||
|
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.greaterThan(lastResponseTime)
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.below(lastTimestamp - beforeTimestamp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Abuses stats', function () {
|
||||||
|
let abuse2: number
|
||||||
|
let videoId: number
|
||||||
|
let commentId: number
|
||||||
|
let beforeTimestamp: number
|
||||||
|
let lastResponseTime: number
|
||||||
|
let userToken: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
userToken = await servers[0].users.generateUserAndToken('reporter');
|
||||||
|
|
||||||
|
({ id: videoId } = await servers[0].videos.quickUpload({ name: 'to_report' }));
|
||||||
|
({ id: commentId } = await servers[0].comments.createThread({ videoId, text: 'text' }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not abuses stats available', async function () {
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalAbuses).to.equal(0)
|
||||||
|
expect(data.totalAbusesProcessed).to.equal(0)
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should create abuses, process one and have correct stats', async function () {
|
||||||
|
beforeTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
const { abuse: abuse1 } = await servers[0].abuses.report({ videoId, token: userToken, reason: 'abuse reason' });
|
||||||
|
({ abuse: { id: abuse2 } } = await servers[0].abuses.report({ accountId: userAccountId, token: userToken, reason: 'abuse reason' }))
|
||||||
|
await servers[0].abuses.report({ commentId, token: userToken, reason: 'abuse reason' })
|
||||||
|
|
||||||
|
await wait(1500)
|
||||||
|
|
||||||
|
await servers[0].abuses.update({ abuseId: abuse1.id, body: { state: AbuseState.REJECTED } })
|
||||||
|
|
||||||
|
const middleTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalAbuses).to.equal(3)
|
||||||
|
expect(data.totalAbusesProcessed).to.equal(1)
|
||||||
|
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.greaterThan(1000)
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.below(middleTimestamp - beforeTimestamp)
|
||||||
|
|
||||||
|
lastResponseTime = data.averageAbuseResponseTimeMs
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should accept another request and update stats', async function () {
|
||||||
|
await wait(1500)
|
||||||
|
|
||||||
|
await servers[0].abuses.addMessage({ abuseId: abuse2, message: 'my message' })
|
||||||
|
|
||||||
|
const lastTimestamp = new Date().getTime()
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalAbuses).to.equal(3)
|
||||||
|
expect(data.totalAbusesProcessed).to.equal(2)
|
||||||
|
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.greaterThan(lastResponseTime)
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.below(lastTimestamp - beforeTimestamp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Disabling stats', async function () {
|
||||||
|
|
||||||
|
it('Should disable registration requests and abuses stats', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await servers[0].kill()
|
||||||
|
await servers[0].run({
|
||||||
|
stats: {
|
||||||
|
registration_requests: { enabled: false },
|
||||||
|
abuses: { enabled: false }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await servers[0].stats.get()
|
||||||
|
|
||||||
|
expect(data.totalRegistrationRequests).to.be.null
|
||||||
|
expect(data.totalRegistrationRequestsProcessed).to.be.null
|
||||||
|
expect(data.averageRegistrationRequestResponseTimeMs).to.be.null
|
||||||
|
|
||||||
|
expect(data.totalAbuses).to.be.null
|
||||||
|
expect(data.totalAbusesProcessed).to.be.null
|
||||||
|
expect(data.averageAbuseResponseTimeMs).to.be.null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -131,6 +131,10 @@ async function updateAbuse (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
if (req.body.state !== undefined) {
|
if (req.body.state !== undefined) {
|
||||||
abuse.state = req.body.state
|
abuse.state = req.body.state
|
||||||
|
|
||||||
|
// We consider the abuse has been processed when its state change
|
||||||
|
if (!abuse.processedAt) abuse.processedAt = new Date()
|
||||||
|
|
||||||
stateUpdated = true
|
stateUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,14 +233,21 @@ async function listAbuseMessages (req: express.Request, res: express.Response) {
|
||||||
async function addAbuseMessage (req: express.Request, res: express.Response) {
|
async function addAbuseMessage (req: express.Request, res: express.Response) {
|
||||||
const abuse = res.locals.abuse
|
const abuse = res.locals.abuse
|
||||||
const user = res.locals.oauth.token.user
|
const user = res.locals.oauth.token.user
|
||||||
|
const byModerator = abuse.reporterAccountId !== user.Account.id
|
||||||
|
|
||||||
const abuseMessage = await AbuseMessageModel.create({
|
const abuseMessage = await AbuseMessageModel.create({
|
||||||
message: req.body.message,
|
message: req.body.message,
|
||||||
byModerator: abuse.reporterAccountId !== user.Account.id,
|
byModerator,
|
||||||
accountId: user.Account.id,
|
accountId: user.Account.id,
|
||||||
abuseId: abuse.id
|
abuseId: abuse.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If a moderator created an abuse message, we consider it as processed
|
||||||
|
if (byModerator && !abuse.processedAt) {
|
||||||
|
abuse.processedAt = new Date()
|
||||||
|
await abuse.save()
|
||||||
|
}
|
||||||
|
|
||||||
AbuseModel.loadFull(abuse.id)
|
AbuseModel.loadFull(abuse.id)
|
||||||
.then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage))
|
.then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage))
|
||||||
.catch(err => logger.error('Cannot notify on new abuse message', { err }))
|
.catch(err => logger.error('Cannot notify on new abuse message', { err }))
|
||||||
|
|
|
@ -160,6 +160,8 @@ async function acceptRegistration (req: express.Request, res: express.Response)
|
||||||
registration.state = UserRegistrationState.ACCEPTED
|
registration.state = UserRegistrationState.ACCEPTED
|
||||||
registration.moderationResponse = body.moderationResponse
|
registration.moderationResponse = body.moderationResponse
|
||||||
|
|
||||||
|
if (!registration.processedAt) registration.processedAt = new Date()
|
||||||
|
|
||||||
await registration.save()
|
await registration.save()
|
||||||
|
|
||||||
logger.info('Registration of %s accepted', registration.username)
|
logger.info('Registration of %s accepted', registration.username)
|
||||||
|
@ -178,6 +180,8 @@ async function rejectRegistration (req: express.Request, res: express.Response)
|
||||||
registration.state = UserRegistrationState.REJECTED
|
registration.state = UserRegistrationState.REJECTED
|
||||||
registration.moderationResponse = body.moderationResponse
|
registration.moderationResponse = body.moderationResponse
|
||||||
|
|
||||||
|
if (!registration.processedAt) registration.processedAt = new Date()
|
||||||
|
|
||||||
await registration.save()
|
await registration.save()
|
||||||
|
|
||||||
if (body.preventEmailDelivery !== true) {
|
if (body.preventEmailDelivery !== true) {
|
||||||
|
|
|
@ -363,6 +363,14 @@ const CONFIG = {
|
||||||
FRAMES_TO_ANALYZE: config.get<number>('thumbnails.generation_from_video.frames_to_analyze')
|
FRAMES_TO_ANALYZE: config.get<number>('thumbnails.generation_from_video.frames_to_analyze')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
STATS: {
|
||||||
|
REGISTRATION_REQUESTS: {
|
||||||
|
ENABLED: config.get<boolean>('stats.registration_requests.enabled')
|
||||||
|
},
|
||||||
|
ABUSES: {
|
||||||
|
ENABLED: config.get<boolean>('stats.abuses.enabled')
|
||||||
|
}
|
||||||
|
},
|
||||||
ADMIN: {
|
ADMIN: {
|
||||||
get EMAIL () { return config.get<string>('admin.email') }
|
get EMAIL () { return config.get<string>('admin.email') }
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,7 +45,7 @@ import { cpus } from 'os'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 815
|
const LAST_MIGRATION_VERSION = 820
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('userRegistration', 'processedAt', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('abuse', 'processedAt', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -9,6 +9,9 @@ import { VideoCommentModel } from '@server/models/video/video-comment.js'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||||
import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@peertube/peertube-models'
|
import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@peertube/peertube-models'
|
||||||
|
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||||
|
import { AbuseModel } from '@server/models/abuse/abuse.js'
|
||||||
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
class StatsManager {
|
class StatsManager {
|
||||||
|
|
||||||
|
@ -85,6 +88,9 @@ class StatsManager {
|
||||||
|
|
||||||
videosRedundancy: videosRedundancyStats,
|
videosRedundancy: videosRedundancyStats,
|
||||||
|
|
||||||
|
...await this.buildAbuseStats(),
|
||||||
|
...await this.buildRegistrationRequestsStats(),
|
||||||
|
|
||||||
...this.buildAPStats()
|
...this.buildAPStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +176,34 @@ class StatsManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async buildRegistrationRequestsStats () {
|
||||||
|
if (!CONFIG.STATS.REGISTRATION_REQUESTS.ENABLED) {
|
||||||
|
return {
|
||||||
|
averageRegistrationRequestResponseTimeMs: null,
|
||||||
|
totalRegistrationRequests: null,
|
||||||
|
totalRegistrationRequestsProcessed: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await UserRegistrationModel.getStats()
|
||||||
|
|
||||||
|
return pick(res, [ 'averageRegistrationRequestResponseTimeMs', 'totalRegistrationRequests', 'totalRegistrationRequestsProcessed' ])
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildAbuseStats () {
|
||||||
|
if (!CONFIG.STATS.ABUSES.ENABLED) {
|
||||||
|
return {
|
||||||
|
averageAbuseResponseTimeMs: null,
|
||||||
|
totalAbuses: null,
|
||||||
|
totalAbusesProcessed: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await AbuseModel.getStats()
|
||||||
|
|
||||||
|
return pick(res, [ 'averageAbuseResponseTimeMs', 'totalAbuses', 'totalAbusesProcessed' ])
|
||||||
|
}
|
||||||
|
|
||||||
static get Instance () {
|
static get Instance () {
|
||||||
return this.instance || (this.instance = new this())
|
return this.instance || (this.instance = new this())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { abusePredefinedReasonsMap } from '@peertube/peertube-core-utils'
|
import { abusePredefinedReasonsMap, forceNumber } from '@peertube/peertube-core-utils'
|
||||||
import {
|
import {
|
||||||
AbuseFilter,
|
AbuseFilter,
|
||||||
AbuseObject,
|
AbuseObject,
|
||||||
|
@ -41,7 +41,7 @@ import {
|
||||||
MUserAccountId
|
MUserAccountId
|
||||||
} from '../../types/models/index.js'
|
} from '../../types/models/index.js'
|
||||||
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account.js'
|
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account.js'
|
||||||
import { getSort, throwIfNotValid } from '../shared/index.js'
|
import { getSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
||||||
import { ThumbnailModel } from '../video/thumbnail.js'
|
import { ThumbnailModel } from '../video/thumbnail.js'
|
||||||
import { VideoBlacklistModel } from '../video/video-blacklist.js'
|
import { VideoBlacklistModel } from '../video/video-blacklist.js'
|
||||||
import { SummaryOptions as ChannelSummaryOptions, VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel.js'
|
import { SummaryOptions as ChannelSummaryOptions, VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel.js'
|
||||||
|
@ -220,6 +220,10 @@ export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
|
||||||
@Column(DataType.ARRAY(DataType.INTEGER))
|
@Column(DataType.ARRAY(DataType.INTEGER))
|
||||||
predefinedReasons: AbusePredefinedReasonsType[]
|
predefinedReasons: AbusePredefinedReasonsType[]
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
processedAt: Date
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@ -441,6 +445,35 @@ export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
|
||||||
return { total, data }
|
return { total, data }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static getStats () {
|
||||||
|
const query = `SELECT ` +
|
||||||
|
`AVG(EXTRACT(EPOCH FROM ("processedAt" - "createdAt") * 1000)) ` +
|
||||||
|
`FILTER (WHERE "processedAt" IS NOT NULL AND "createdAt" > CURRENT_DATE - INTERVAL '3 months')` +
|
||||||
|
`AS "avgResponseTime", ` +
|
||||||
|
`COUNT(*) FILTER (WHERE "processedAt" IS NOT NULL) AS "processedAbuses", ` +
|
||||||
|
`COUNT(*) AS "totalAbuses" ` +
|
||||||
|
`FROM "abuse"`
|
||||||
|
|
||||||
|
return AbuseModel.sequelize.query<any>(query, {
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
}).then(([ row ]) => {
|
||||||
|
return {
|
||||||
|
totalAbuses: parseAggregateResult(row.totalAbuses),
|
||||||
|
|
||||||
|
totalAbusesProcessed: parseAggregateResult(row.processedAbuses),
|
||||||
|
|
||||||
|
averageAbuseResponseTimeMs: row?.avgResponseTime
|
||||||
|
? forceNumber(row.avgResponseTime)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) {
|
buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) {
|
||||||
// Associated video comment could have been destroyed if the video has been deleted
|
// Associated video comment could have been destroyed if the video has been deleted
|
||||||
if (!this.VideoCommentAbuse?.VideoComment) return null
|
if (!this.VideoCommentAbuse?.VideoComment) return null
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validator
|
||||||
import { cryptPassword } from '@server/helpers/peertube-crypto.js'
|
import { cryptPassword } from '@server/helpers/peertube-crypto.js'
|
||||||
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
||||||
import { MRegistration, MRegistrationFormattable } from '@server/types/models/index.js'
|
import { MRegistration, MRegistrationFormattable } from '@server/types/models/index.js'
|
||||||
import { FindOptions, Op, WhereOptions } from 'sequelize'
|
import { FindOptions, Op, QueryTypes, WhereOptions } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
|
@ -25,8 +25,9 @@ import {
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users.js'
|
import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users.js'
|
||||||
import { getSort, throwIfNotValid } from '../shared/index.js'
|
import { getSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
||||||
import { UserModel } from './user.js'
|
import { UserModel } from './user.js'
|
||||||
|
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'userRegistration',
|
tableName: 'userRegistration',
|
||||||
|
@ -100,6 +101,10 @@ export class UserRegistrationModel extends Model<Partial<AttributesOnly<UserRegi
|
||||||
@Column
|
@Column
|
||||||
channelDisplayName: string
|
channelDisplayName: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Column
|
||||||
|
processedAt: Date
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@ -227,6 +232,33 @@ export class UserRegistrationModel extends Model<Partial<AttributesOnly<UserRegi
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static getStats () {
|
||||||
|
const query = `SELECT ` +
|
||||||
|
`AVG(EXTRACT(EPOCH FROM ("processedAt" - "createdAt") * 1000)) ` +
|
||||||
|
`FILTER (WHERE "processedAt" IS NOT NULL AND "createdAt" > CURRENT_DATE - INTERVAL '3 months')` +
|
||||||
|
`AS "avgResponseTime", ` +
|
||||||
|
`COUNT(*) FILTER (WHERE "processedAt" IS NOT NULL) AS "processedRequests", ` +
|
||||||
|
`COUNT(*) AS "totalRequests" ` +
|
||||||
|
`FROM "userRegistration"`
|
||||||
|
|
||||||
|
return UserRegistrationModel.sequelize.query<any>(query, {
|
||||||
|
type: QueryTypes.SELECT,
|
||||||
|
raw: true
|
||||||
|
}).then(([ row ]) => {
|
||||||
|
return {
|
||||||
|
totalRegistrationRequests: parseAggregateResult(row.totalRequests),
|
||||||
|
|
||||||
|
totalRegistrationRequestsProcessed: parseAggregateResult(row.processedRequests),
|
||||||
|
|
||||||
|
averageRegistrationRequestResponseTimeMs: row?.avgResponseTime
|
||||||
|
? forceNumber(row.avgResponseTime)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
toFormattedJSON (this: MRegistrationFormattable): UserRegistration {
|
toFormattedJSON (this: MRegistrationFormattable): UserRegistration {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
|
@ -8580,6 +8580,26 @@ components:
|
||||||
totalActivityPubMessagesWaiting:
|
totalActivityPubMessagesWaiting:
|
||||||
type: number
|
type: number
|
||||||
|
|
||||||
|
averageRegistrationRequestResponseTimeMs:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled registration requests stats"
|
||||||
|
totalRegistrationRequestsProcessed:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled registration requests stats"
|
||||||
|
totalRegistrationRequests:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled registration requests stats"
|
||||||
|
|
||||||
|
averageAbuseResponseTimeMs:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled abuses stats"
|
||||||
|
totalAbusesProcessed:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled abuses stats"
|
||||||
|
totalAbuses:
|
||||||
|
type: number
|
||||||
|
description: "**PeerTube >= 6.1** Value is null if the admin disabled abuses stats"
|
||||||
|
|
||||||
ServerConfigAbout:
|
ServerConfigAbout:
|
||||||
properties:
|
properties:
|
||||||
instance:
|
instance:
|
||||||
|
|
Loading…
Reference in New Issue