]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/job-queue/handlers/activitypub-cleaner.ts
Move to bullmq
[github/Chocobozzz/PeerTube.git] / server / lib / job-queue / handlers / activitypub-cleaner.ts
CommitLineData
41fb13c3 1import { map } from 'bluebird'
5a921e7b 2import { Job } from 'bullmq'
67f87b66
C
3import {
4 isAnnounceActivityValid,
5 isDislikeActivityValid,
6 isLikeActivityValid
7} from '@server/helpers/custom-validators/activitypub/activity'
74d249bc 8import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
b5c36108 9import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
f1569117 10import { AP_CLEANER } from '@server/initializers/constants'
7e98a7df 11import { checkUrlsSameHost } from '@server/lib/activitypub/url'
f1569117 12import { Redis } from '@server/lib/redis'
74d249bc
C
13import { VideoModel } from '@server/models/video/video'
14import { VideoCommentModel } from '@server/models/video/video-comment'
15import { VideoShareModel } from '@server/models/video/video-share'
c0e8b12e 16import { HttpStatusCode } from '@shared/models'
10a72a7e 17import { logger, loggerTagsFactory } from '../../../helpers/logger'
74d249bc
C
18import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
19
10a72a7e
C
20const lTags = loggerTagsFactory('ap-cleaner')
21
74d249bc
C
22// Job to clean remote interactions off local videos
23
41fb13c3 24async function processActivityPubCleaner (_job: Job) {
10a72a7e 25 logger.info('Processing ActivityPub cleaner.', lTags())
74d249bc
C
26
27 {
28 const rateUrls = await AccountVideoRateModel.listRemoteRateUrlsOfLocalVideos()
29 const { bodyValidator, deleter, updater } = rateOptionsFactory()
30
41fb13c3 31 await map(rateUrls, async rateUrl => {
69d48ee3
C
32 // TODO: remove when https://github.com/mastodon/mastodon/issues/13571 is fixed
33 if (rateUrl.includes('#')) return
34
10a72a7e 35 const result = await updateObjectIfNeeded({ url: rateUrl, bodyValidator, updater, deleter })
74d249bc 36
10a72a7e
C
37 if (result?.status === 'deleted') {
38 const { videoId, type } = result.data
74d249bc 39
57e4e1c1 40 await VideoModel.syncLocalRates(videoId, type, undefined)
74d249bc 41 }
f1569117 42 }, { concurrency: AP_CLEANER.CONCURRENCY })
74d249bc
C
43 }
44
45 {
46 const shareUrls = await VideoShareModel.listRemoteShareUrlsOfLocalVideos()
47 const { bodyValidator, deleter, updater } = shareOptionsFactory()
48
41fb13c3 49 await map(shareUrls, async shareUrl => {
10a72a7e 50 await updateObjectIfNeeded({ url: shareUrl, bodyValidator, updater, deleter })
f1569117 51 }, { concurrency: AP_CLEANER.CONCURRENCY })
74d249bc
C
52 }
53
54 {
55 const commentUrls = await VideoCommentModel.listRemoteCommentUrlsOfLocalVideos()
56 const { bodyValidator, deleter, updater } = commentOptionsFactory()
57
41fb13c3 58 await map(commentUrls, async commentUrl => {
10a72a7e 59 await updateObjectIfNeeded({ url: commentUrl, bodyValidator, updater, deleter })
f1569117 60 }, { concurrency: AP_CLEANER.CONCURRENCY })
74d249bc
C
61 }
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 processActivityPubCleaner
68}
69
70// ---------------------------------------------------------------------------
71
f1569117
C
72async function updateObjectIfNeeded <T> (options: {
73 url: string
74 bodyValidator: (body: any) => boolean
75 updater: (url: string, newUrl: string) => Promise<T>
76 deleter: (url: string) => Promise<T> }
74d249bc 77): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
f1569117
C
78 const { url, bodyValidator, updater, deleter } = options
79
b5c36108 80 const on404OrTombstone = async () => {
10a72a7e 81 logger.info('Removing remote AP object %s.', url, lTags(url))
74d249bc
C
82 const data = await deleter(url)
83
b5c36108 84 return { status: 'deleted' as 'deleted', data }
74d249bc
C
85 }
86
b5c36108
C
87 try {
88 const { body } = await doJSONRequest<any>(url, { activityPub: true })
74d249bc 89
b5c36108
C
90 // If not same id, check same host and update
91 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
74d249bc 92
b5c36108
C
93 if (body.type === 'Tombstone') {
94 return on404OrTombstone()
95 }
74d249bc 96
b5c36108
C
97 const newUrl = body.id
98 if (newUrl !== url) {
99 if (checkUrlsSameHost(newUrl, url) !== true) {
100 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
101 }
102
10a72a7e 103 logger.info('Updating remote AP object %s.', url, lTags(url))
b5c36108
C
104 const data = await updater(url, newUrl)
105
106 return { status: 'updated', data }
74d249bc
C
107 }
108
b5c36108
C
109 return null
110 } catch (err) {
111 // Does not exist anymore, remove entry
112 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
113 return on404OrTombstone()
114 }
74d249bc 115
10a72a7e 116 logger.debug('Remote AP object %s is unavailable.', url, lTags(url))
f1569117
C
117
118 const unavailability = await Redis.Instance.addAPUnavailability(url)
119 if (unavailability >= AP_CLEANER.UNAVAILABLE_TRESHOLD) {
10a72a7e 120 logger.info('Removing unavailable AP resource %s.', url, lTags(url))
f1569117
C
121 return on404OrTombstone()
122 }
123
124 return null
74d249bc 125 }
74d249bc
C
126}
127
128function rateOptionsFactory () {
129 return {
130 bodyValidator: (body: any) => isLikeActivityValid(body) || isDislikeActivityValid(body),
131
132 updater: async (url: string, newUrl: string) => {
133 const rate = await AccountVideoRateModel.loadByUrl(url, undefined)
134 rate.url = newUrl
135
136 const videoId = rate.videoId
137 const type = rate.type
138
139 await rate.save()
140
141 return { videoId, type }
142 },
143
144 deleter: async (url) => {
145 const rate = await AccountVideoRateModel.loadByUrl(url, undefined)
146
147 const videoId = rate.videoId
148 const type = rate.type
149
150 await rate.destroy()
151
152 return { videoId, type }
153 }
154 }
155}
156
157function shareOptionsFactory () {
158 return {
67f87b66 159 bodyValidator: (body: any) => isAnnounceActivityValid(body),
74d249bc
C
160
161 updater: async (url: string, newUrl: string) => {
162 const share = await VideoShareModel.loadByUrl(url, undefined)
163 share.url = newUrl
164
165 await share.save()
166
167 return undefined
168 },
169
170 deleter: async (url) => {
171 const share = await VideoShareModel.loadByUrl(url, undefined)
172
173 await share.destroy()
174
175 return undefined
176 }
177 }
178}
179
180function commentOptionsFactory () {
181 return {
182 bodyValidator: (body: any) => sanitizeAndCheckVideoCommentObject(body),
183
184 updater: async (url: string, newUrl: string) => {
185 const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(url)
186 comment.url = newUrl
187
188 await comment.save()
189
190 return undefined
191 },
192
193 deleter: async (url) => {
194 const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(url)
195
196 await comment.destroy()
197
198 return undefined
199 }
200 }
201}