diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html index 9eeaac74b498..50c94f26c9e1 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html @@ -57,17 +57,17 @@ -
+
- +
{{ reason.label }}
- Reported parts + Reported part {{ startAt }} - {{ endAt }} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts index ae3a63abdac4..8ea9bcec13a7 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts @@ -30,14 +30,6 @@ export class VideoAbuseDetailsComponent { } } - get predefinedReasons () { - if (!this.videoAbuse.predefinedReasons) return [] - return this.videoAbuse.predefinedReasons.map(r => ({ - id: r, - label: this.predefinedReasonsTranslations[r] - })) - } - get startAt () { return durationToString(this.videoAbuse.startAt) } @@ -46,6 +38,14 @@ export class VideoAbuseDetailsComponent { return durationToString(this.videoAbuse.endAt) } + predefinedReasons () { + if (!this.videoAbuse.predefinedReasons) return [] + return this.videoAbuse.predefinedReasons.map(r => ({ + id: r, + label: this.predefinedReasonsTranslations[r] + })) + } + switchToDefaultAvatar ($event: Event) { ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() } diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index 86ad40dee903..d7f5beef3a4b 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts @@ -11,7 +11,7 @@ import { ModerationCommentModalComponent } from './moderation-comment-modal.comp import { Video } from '../../../shared/video/video.model' import { MarkdownService } from '@app/shared/renderer' import { Actor } from '@app/shared/actor/actor.model' -import { buildVideoEmbed } from 'src/assets/player/utils' +import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { DomSanitizer } from '@angular/platform-browser' import { BlocklistService } from '@app/shared/blocklist' import { VideoService } from '@app/shared/video/video.service' @@ -259,7 +259,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV } getVideoEmbed (videoAbuse: VideoAbuse) { - return buildVideoEmbed(`${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`) + return buildVideoEmbed( + buildVideoLink({ + baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`, + title: false, + warningTitle: false, + startTime: videoAbuse.startAt, + stopTime: videoAbuse.endAt + }) + ) } switchToDefaultAvatar ($event: Event) { diff --git a/client/src/app/shared/video-abuse/index.ts b/client/src/app/shared/video-abuse/index.ts index 0c3a48d83b70..92cbfb5f9040 100644 --- a/client/src/app/shared/video-abuse/index.ts +++ b/client/src/app/shared/video-abuse/index.ts @@ -1,2 +1 @@ export * from './video-abuse.service' -export * from './video-abuse-predefined-reasons.model' diff --git a/client/src/app/shared/video-abuse/video-abuse-predefined-reasons.model.ts b/client/src/app/shared/video-abuse/video-abuse-predefined-reasons.model.ts deleted file mode 100644 index 5969f48cea89..000000000000 --- a/client/src/app/shared/video-abuse/video-abuse-predefined-reasons.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum PredefinedReasons { - violentOrRepulsive = 'violentOrRepulsive', - hatefulOrAbusive = 'hatefulOrAbusive', - spamOrMisleading = 'spamOrMisleading', - privacy = 'privacy', - rights = 'rights', - serverRules = 'serverRules', - thumbnails = 'thumbnails', - captions = 'captions' -} - -export type VideoAbusePredefinedReasons = { - [key in PredefinedReasons]?: boolean -} diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index 9af19f014956..19f556c34b8a 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts @@ -3,10 +3,10 @@ import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { SortMeta } from 'primeng/api' import { Observable } from 'rxjs' -import { ResultList, VideoAbuse, VideoAbuseUpdate, VideoAbuseState } from '../../../../../shared' +import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '../../../../../shared' import { environment } from '../../../environments/environment' import { RestExtractor, RestPagination, RestService } from '../rest' -import { VideoAbusePredefinedReasons } from './video-abuse-predefined-reasons.model' +import { omit } from 'lodash-es' @Injectable() export class VideoAbuseService { @@ -59,7 +59,8 @@ export class VideoAbuseService { try { const id = parseInt(v, 10) return id - } catch { + } catch (e) { + console.error('Cannot parse predefinedReasonId in search.', e) return undefined } } @@ -75,11 +76,10 @@ export class VideoAbuseService { ) } - reportVideo (parameters: { id: number, reason: string, predefinedReasons?: VideoAbusePredefinedReasons, timestamp: any }) { + reportVideo (parameters: { id: number } & VideoAbuseCreate) { const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse' - delete parameters.id - const body = { ...parameters } + const body = { ...omit(parameters, [ 'id' ]) } return this.authHttp.post(url, body) .pipe( diff --git a/client/src/app/shared/video/modals/video-report.component.ts b/client/src/app/shared/video/modals/video-report.component.ts index cd9a9bf9d56f..bc21d7caea48 100644 --- a/client/src/app/shared/video/modals/video-report.component.ts +++ b/client/src/app/shared/video/modals/video-report.component.ts @@ -6,10 +6,12 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val import { VideoAbuseValidatorsService } from '@app/shared/forms/form-validators/video-abuse-validators.service' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { VideoAbuseService, VideoAbusePredefinedReasons, PredefinedReasons } from '@app/shared/video-abuse' +import { VideoAbuseService } from '@app/shared/video-abuse' import { Video } from '@app/shared/video/video.model' -import { buildVideoEmbed } from 'src/assets/player/utils' +import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils' import { DomSanitizer, SafeHtml } from '@angular/platform-browser' +import { VideoAbusePredefinedReasonsIn } from '@shared/models/videos/abuse/video-abuse-reason.model' +import { mapValues, pickBy, keys } from 'lodash-es' @Component({ selector: 'my-video-report', @@ -22,7 +24,7 @@ export class VideoReportComponent extends FormReactive implements OnInit { @ViewChild('modal', { static: true }) modal: NgbModal error: string = null - predefinedReasons: { id: PredefinedReasons, label: string, description?: string, help?: string }[] = [] + predefinedReasons: { id: keyof VideoAbusePredefinedReasonsIn, label: string, description?: string, help?: string }[] = [] embedHtml: SafeHtml private openedModal: NgbModalRef @@ -57,25 +59,20 @@ export class VideoReportComponent extends FormReactive implements OnInit { getVideoEmbed () { return this.sanitizer.bypassSecurityTrustHtml( - buildVideoEmbed(this.video.embedUrl) + buildVideoEmbed( + buildVideoLink({ + baseUrl: this.video.embedUrl, + title: false, + warningTitle: false + }) + ) ) } ngOnInit () { - const predefinedReasons: Required = { - violentOrRepulsive: null, - hatefulOrAbusive: null, - spamOrMisleading: null, - privacy: null, - rights: null, - serverRules: null, - captions: null, - thumbnails: null - } - this.buildForm({ reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON, - predefinedReasons: predefinedReasons as {[key: string]: null}, + predefinedReasons: mapValues(VideoAbusePredefinedReasonsIn, r => null), timestamp: { hasStart: null, startAt: null, @@ -86,42 +83,42 @@ export class VideoReportComponent extends FormReactive implements OnInit { this.predefinedReasons = [ { - id: PredefinedReasons.violentOrRepulsive, + id: 'violentOrRepulsive', label: this.i18n('Violent or repulsive'), help: this.i18n('Contains offensive, violent, or coarse language or iconography.') }, { - id: PredefinedReasons.hatefulOrAbusive, + id: 'hatefulOrAbusive', label: this.i18n('Hateful or abusive'), help: this.i18n('Contains abusive, racist or sexist language or iconography.') }, { - id: PredefinedReasons.spamOrMisleading, + id: 'spamOrMisleading', label: this.i18n('Spam, ad or false news'), help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.') }, { - id: PredefinedReasons.privacy, + id: 'privacy', label: this.i18n('Privacy breach or doxxing'), help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).') }, { - id: PredefinedReasons.rights, + id: 'rights', label: this.i18n('Intellectual property violation'), help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.') }, { - id: PredefinedReasons.serverRules, + id: 'serverRules', label: this.i18n('Breaks server rules'), description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.') }, { - id: PredefinedReasons.thumbnails, + id: 'thumbnails', label: this.i18n('Thumbnails'), help: this.i18n('The above can only be seen in thumbnails.') }, { - id: PredefinedReasons.captions, + id: 'captions', label: this.i18n('Captions'), help: this.i18n('The above can only be seen in captions (please describe which).') } @@ -140,15 +137,24 @@ export class VideoReportComponent extends FormReactive implements OnInit { } report () { - this.videoAbuseService.reportVideo({ id: this.video.id, ...this.form.value }) - .subscribe( - () => { - this.notifier.success(this.i18n('Video reported.')) - this.hide() - }, - - err => this.notifier.error(err.message) - ) + const reason = this.form.get('reason').value + const predefinedReasons = pickBy(this.form.get('predefinedReasons').value) + const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value + + this.videoAbuseService.reportVideo({ + id: this.video.id, + reason, + predefinedReasons, + startAt: hasStart && startAt ? startAt : undefined, + endAt: hasEnd && endAt ? endAt : undefined + }).subscribe( + () => { + this.notifier.success(this.i18n('Video reported.')) + this.hide() + }, + + err => this.notifier.error(err.message) + ) } isRemoteVideo () { diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index c761d3d29c6a..3338e00464bd 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts @@ -127,9 +127,6 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) const predefinedReasons = keys(pickBy(body.predefinedReasons)).map(r => VideoAbusePredefinedReasonsIn[r]) - const timestamp = body.timestamp - const startAt = timestamp['hasStart'] && timestamp['startAt'] ? timestamp['startAt'] : undefined - const endAt = timestamp['hasEnd'] && timestamp['endAt'] ? timestamp['endAt'] : undefined const abuseToCreate = { reporterAccountId: reporterAccount.id, @@ -137,8 +134,8 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { videoId: videoInstance.id, state: VideoAbuseState.PENDING, predefinedReasons, - startAt, - endAt + startAt: body.startAt, + endAt: body.endAt } const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts index 672654930542..ffebbca6962c 100644 --- a/server/helpers/custom-validators/video-abuses.ts +++ b/server/helpers/custom-validators/video-abuses.ts @@ -14,10 +14,6 @@ function isVideoAbusePredefinedReasonsValid (value: {}) { return exists(value) } -function isVideoAbuseTimestampValid (value: {}) { - return exists(value) -} - function isVideoAbuseModerationCommentValid (value: string) { return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT) } @@ -38,7 +34,6 @@ function isAbuseVideoIsValid (value: VideoAbuseVideoIs) { export { isVideoAbuseReasonValid, isVideoAbusePredefinedReasonsValid, - isVideoAbuseTimestampValid, isVideoAbuseModerationCommentValid, isVideoAbuseStateValid, isAbuseVideoIsValid diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index fc06cf80a96a..aafb8e914f89 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts @@ -38,9 +38,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t)) - const predefinedReasons = flag.tag?.map(tag => parseInt(tag.name, 10)) || [] + const predefinedReasons = flag.tag?.map(tag => parseInt(tag.name, 10)).filter(v => !isNaN(v)) || [] const startAt = flag.startAt - const endAt = flag.startAt + const endAt = flag.endAt const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { const videoAbuseData = { diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts index 75797c10b91b..c4d540b221f6 100644 --- a/server/middlewares/validators/videos/video-abuses.ts +++ b/server/middlewares/validators/videos/video-abuses.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { body, param, query } from 'express-validator' -import { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' +import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' import { isAbuseVideoIsValid, isVideoAbuseModerationCommentValid, @@ -26,10 +26,14 @@ const videoAbuseReportValidator = [ .optional() .custom(isVideoAbusePredefinedReasonsValid) .withMessage('Should have a valid list of predefined reasons'), - body('timestamp') + body('startAt') .optional() - .custom(isVideoAbuseTimestampValid) - .withMessage('Should have valid timestamp definition'), + .custom(toIntOrNull) + .withMessage('Should have valid starting time value'), + body('endAt') + .optional() + .custom(toIntOrNull) + .withMessage('Should have valid ending time value'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 3c2bc3f35b8b..704979e59400 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts @@ -70,20 +70,19 @@ export type ActivityHtmlUrlObject = { } export interface ActivityHashTagObject { - type: 'Hashtag' | 'Mention' + type: 'Hashtag' href?: string name: string } export interface ActivityMentionObject { - type: 'Hashtag' | 'Mention' + type: 'Mention' href?: string name: string } export interface ActivityFlagReasonObject { - type: 'Hashtag' | 'Mention' - href?: string + type: 'Hashtag' name: string } diff --git a/shared/models/videos/abuse/video-abuse-create.model.ts b/shared/models/videos/abuse/video-abuse-create.model.ts index b2cc2da0b9f1..ba1fee3bda93 100644 --- a/shared/models/videos/abuse/video-abuse-create.model.ts +++ b/shared/models/videos/abuse/video-abuse-create.model.ts @@ -1,5 +1,6 @@ export interface VideoAbuseCreate { reason: string - predefinedReasons: any - timestamp: any + predefinedReasons?: {[key: string]: boolean} // see VideoAbusePredefinedReasonsIn + startAt?: number + endAt?: number } diff --git a/shared/models/videos/abuse/video-abuse-reason.model.ts b/shared/models/videos/abuse/video-abuse-reason.model.ts index 624871711401..ae9bb811cf40 100644 --- a/shared/models/videos/abuse/video-abuse-reason.model.ts +++ b/shared/models/videos/abuse/video-abuse-reason.model.ts @@ -9,7 +9,18 @@ export enum VideoAbusePredefinedReasons { CAPTIONS } -export const VideoAbusePredefinedReasonsIn = { +export interface VideoAbusePredefinedReasonsIn { + violentOrRepulsive: VideoAbusePredefinedReasons + hatefulOrAbusive: VideoAbusePredefinedReasons + spamOrMisleading: VideoAbusePredefinedReasons + privacy: VideoAbusePredefinedReasons + rights: VideoAbusePredefinedReasons + serverRules: VideoAbusePredefinedReasons + thumbnails: VideoAbusePredefinedReasons + captions: VideoAbusePredefinedReasons +} + +export const VideoAbusePredefinedReasonsIn: VideoAbusePredefinedReasonsIn = { violentOrRepulsive: VideoAbusePredefinedReasons.VIOLENT_OR_REPULSIVE, hatefulOrAbusive: VideoAbusePredefinedReasons.HATEFUL_OR_ABUSIVE, spamOrMisleading: VideoAbusePredefinedReasons.SPAM_OR_MISLEADING,