1 import * as Bluebird from 'bluebird'
2 import * as Bull from 'bull'
3 import { checkUrlsSameHost } from '@server/helpers/activitypub'
4 import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate'
5 import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share'
6 import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
7 import { doRequest } from '@server/helpers/requests'
8 import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
9 import { VideoModel } from '@server/models/video/video'
10 import { VideoCommentModel } from '@server/models/video/video-comment'
11 import { VideoShareModel } from '@server/models/video/video-share'
12 import { HttpStatusCode } from '@shared/core-utils'
13 import { logger } from '../../../helpers/logger'
14 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
16 // Job to clean remote interactions off local videos
18 async function processActivityPubCleaner (_job: Bull.Job) {
19 logger.info('Processing ActivityPub cleaner.')
22 const rateUrls = await AccountVideoRateModel.listRemoteRateUrlsOfLocalVideos()
23 const { bodyValidator, deleter, updater } = rateOptionsFactory()
25 await Bluebird.map(rateUrls, async rateUrl => {
27 const result = await updateObjectIfNeeded(rateUrl, bodyValidator, updater, deleter)
29 if (result?.status === 'deleted') {
30 const { videoId, type } = result.data
32 await VideoModel.updateRatesOf(videoId, type, undefined)
35 logger.warn('Cannot update/delete remote AP rate %s.', rateUrl, { err })
37 }, { concurrency: AP_CLEANER_CONCURRENCY })
41 const shareUrls = await VideoShareModel.listRemoteShareUrlsOfLocalVideos()
42 const { bodyValidator, deleter, updater } = shareOptionsFactory()
44 await Bluebird.map(shareUrls, async shareUrl => {
46 await updateObjectIfNeeded(shareUrl, bodyValidator, updater, deleter)
48 logger.warn('Cannot update/delete remote AP share %s.', shareUrl, { err })
50 }, { concurrency: AP_CLEANER_CONCURRENCY })
54 const commentUrls = await VideoCommentModel.listRemoteCommentUrlsOfLocalVideos()
55 const { bodyValidator, deleter, updater } = commentOptionsFactory()
57 await Bluebird.map(commentUrls, async commentUrl => {
59 await updateObjectIfNeeded(commentUrl, bodyValidator, updater, deleter)
61 logger.warn('Cannot update/delete remote AP comment %s.', commentUrl, { err })
63 }, { concurrency: AP_CLEANER_CONCURRENCY })
67 // ---------------------------------------------------------------------------
70 processActivityPubCleaner
73 // ---------------------------------------------------------------------------
75 async function updateObjectIfNeeded <T> (
77 bodyValidator: (body: any) => boolean,
78 updater: (url: string, newUrl: string) => Promise<T>,
79 deleter: (url: string) => Promise<T>
80 ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
82 const { response, body } = await doRequest<any>({
88 // Does not exist anymore, remove entry
89 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
90 logger.info('Removing remote AP object %s.', url)
91 const data = await deleter(url)
93 return { status: 'deleted', data }
96 // If not same id, check same host and update
97 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
99 if (body.type === 'Tombstone') {
100 logger.info('Removing remote AP object %s.', url)
101 const data = await deleter(url)
103 return { status: 'deleted', data }
106 const newUrl = body.id
107 if (newUrl !== url) {
108 if (checkUrlsSameHost(newUrl, url) !== true) {
109 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
112 logger.info('Updating remote AP object %s.', url)
113 const data = await updater(url, newUrl)
115 return { status: 'updated', data }
121 function rateOptionsFactory () {
123 bodyValidator: (body: any) => isLikeActivityValid(body) || isDislikeActivityValid(body),
125 updater: async (url: string, newUrl: string) => {
126 const rate = await AccountVideoRateModel.loadByUrl(url, undefined)
129 const videoId = rate.videoId
130 const type = rate.type
134 return { videoId, type }
137 deleter: async (url) => {
138 const rate = await AccountVideoRateModel.loadByUrl(url, undefined)
140 const videoId = rate.videoId
141 const type = rate.type
145 return { videoId, type }
150 function shareOptionsFactory () {
152 bodyValidator: (body: any) => isShareActivityValid(body),
154 updater: async (url: string, newUrl: string) => {
155 const share = await VideoShareModel.loadByUrl(url, undefined)
163 deleter: async (url) => {
164 const share = await VideoShareModel.loadByUrl(url, undefined)
166 await share.destroy()
173 function commentOptionsFactory () {
175 bodyValidator: (body: any) => sanitizeAndCheckVideoCommentObject(body),
177 updater: async (url: string, newUrl: string) => {
178 const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(url)
186 deleter: async (url) => {
187 const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(url)
189 await comment.destroy()