]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/process/process-create.ts
Send comment to followers and parents
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / process / process-create.ts
CommitLineData
50d6de9c
C
1import * as Bluebird from 'bluebird'
2import { ActivityCreate, VideoTorrentObject } from '../../../../shared'
3fd3ab2d 3import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
6d852470 4import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
50d6de9c 5import { VideoRateType } from '../../../../shared/models/videos'
da854ddd
C
6import { retryTransactionWrapper } from '../../../helpers/database-utils'
7import { logger } from '../../../helpers/logger'
3fd3ab2d 8import { sequelizeTypescript } from '../../../initializers'
3fd3ab2d 9import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
50d6de9c
C
10import { ActorModel } from '../../../models/activitypub/actor'
11import { TagModel } from '../../../models/video/tag'
3fd3ab2d
C
12import { VideoModel } from '../../../models/video/video'
13import { VideoAbuseModel } from '../../../models/video/video-abuse'
6d852470 14import { VideoCommentModel } from '../../../models/video/video-comment'
50d6de9c 15import { VideoFileModel } from '../../../models/video/video-file'
93ef8a9d 16import { VideoShareModel } from '../../../models/video/video-share'
50d6de9c 17import { getOrCreateActorAndServerAndModel } from '../actor'
93ef8a9d 18import { forwardActivity, getActorsInvolvedInVideo } from '../send/misc'
50d6de9c 19import { generateThumbnailFromUrl } from '../videos'
da854ddd 20import { addVideoComments, addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
e4f97bab 21
0d0e8dd0 22async function processCreateActivity (activity: ActivityCreate) {
e4f97bab
C
23 const activityObject = activity.object
24 const activityType = activityObject.type
50d6de9c 25 const actor = await getOrCreateActorAndServerAndModel(activity.actor)
e4f97bab 26
40ff5707 27 if (activityType === 'View') {
50d6de9c 28 return processCreateView(actor, activity)
0032ebe9 29 } else if (activityType === 'Dislike') {
50d6de9c
C
30 return processCreateDislike(actor, activity)
31 } else if (activityType === 'Video') {
32 return processCreateVideo(actor, activity)
8e13fa7d 33 } else if (activityType === 'Flag') {
50d6de9c 34 return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject)
6d852470
C
35 } else if (activityType === 'Note') {
36 return processCreateVideoComment(actor, activity)
e4f97bab
C
37 }
38
39 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
0d0e8dd0 40 return Promise.resolve(undefined)
e4f97bab
C
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 processCreateActivity
47}
48
49// ---------------------------------------------------------------------------
50
50d6de9c
C
51async function processCreateVideo (
52 actor: ActorModel,
53 activity: ActivityCreate
54) {
55 const videoToCreateData = activity.object as VideoTorrentObject
56
57 const channel = videoToCreateData.attributedTo.find(a => a.type === 'Group')
58 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoToCreateData.url)
59
60 const channelActor = await getOrCreateActorAndServerAndModel(channel.id)
61
62 const options = {
63 arguments: [ actor, activity, videoToCreateData, channelActor ],
64 errorMessage: 'Cannot insert the remote video with many retries.'
65 }
66
67 const video = await retryTransactionWrapper(createRemoteVideo, options)
68
69 // Process outside the transaction because we could fetch remote data
70 if (videoToCreateData.likes && Array.isArray(videoToCreateData.likes.orderedItems)) {
da854ddd 71 logger.info('Adding likes of video %s.', video.uuid)
50d6de9c
C
72 await createRates(videoToCreateData.likes.orderedItems, video, 'like')
73 }
74
75 if (videoToCreateData.dislikes && Array.isArray(videoToCreateData.dislikes.orderedItems)) {
da854ddd 76 logger.info('Adding dislikes of video %s.', video.uuid)
50d6de9c
C
77 await createRates(videoToCreateData.dislikes.orderedItems, video, 'dislike')
78 }
79
80 if (videoToCreateData.shares && Array.isArray(videoToCreateData.shares.orderedItems)) {
da854ddd 81 logger.info('Adding shares of video %s.', video.uuid)
50d6de9c
C
82 await addVideoShares(video, videoToCreateData.shares.orderedItems)
83 }
84
da854ddd
C
85 if (videoToCreateData.comments && Array.isArray(videoToCreateData.comments.orderedItems)) {
86 logger.info('Adding comments of video %s.', video.uuid)
87 await addVideoComments(video, videoToCreateData.comments.orderedItems)
88 }
89
50d6de9c
C
90 return video
91}
92
93function createRemoteVideo (
94 account: ActorModel,
95 activity: ActivityCreate,
96 videoToCreateData: VideoTorrentObject,
97 channelActor: ActorModel
98) {
99 logger.debug('Adding remote video %s.', videoToCreateData.id)
100
101 return sequelizeTypescript.transaction(async t => {
102 const sequelizeOptions = {
103 transaction: t
104 }
105 const videoFromDatabase = await VideoModel.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t)
106 if (videoFromDatabase) return videoFromDatabase
107
108 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoToCreateData, activity.to, activity.cc)
109 const video = VideoModel.build(videoData)
110
111 // Don't block on request
112 generateThumbnailFromUrl(video, videoToCreateData.icon)
113 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
114
115 const videoCreated = await video.save(sequelizeOptions)
116
117 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
118 if (videoFileAttributes.length === 0) {
119 throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
120 }
121
122 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
123 await Promise.all(tasks)
124
125 const tags = videoToCreateData.tag.map(t => t.name)
126 const tagInstances = await TagModel.findOrCreateTags(tags, t)
127 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
128
129 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
130
131 return videoCreated
132 })
133}
134
135async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
136 let rateCounts = 0
137 const tasks: Bluebird<any>[] = []
138
139 for (const actorUrl of actorUrls) {
140 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
141 const p = AccountVideoRateModel
142 .create({
143 videoId: video.id,
144 accountId: actor.Account.id,
145 type: rate
146 })
147 .then(() => rateCounts += 1)
148
149 tasks.push(p)
150 }
151
152 await Promise.all(tasks)
153
154 logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
155
156 // This is "likes" and "dislikes"
157 await video.increment(rate + 's', { by: rateCounts })
158
159 return
160}
161
162async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) {
0032ebe9 163 const options = {
50d6de9c 164 arguments: [ byActor, activity ],
0032ebe9
C
165 errorMessage: 'Cannot dislike the video with many retries.'
166 }
167
168 return retryTransactionWrapper(createVideoDislike, options)
169}
170
50d6de9c 171function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) {
63c93323 172 const dislike = activity.object as DislikeObject
50d6de9c
C
173 const byAccount = byActor.Account
174
175 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
0032ebe9 176
3fd3ab2d
C
177 return sequelizeTypescript.transaction(async t => {
178 const video = await VideoModel.loadByUrlAndPopulateAccount(dislike.object, t)
0032ebe9
C
179 if (!video) throw new Error('Unknown video ' + dislike.object)
180
181 const rate = {
182 type: 'dislike' as 'dislike',
183 videoId: video.id,
184 accountId: byAccount.id
185 }
3fd3ab2d 186 const [ , created ] = await AccountVideoRateModel.findOrCreate({
0032ebe9 187 where: rate,
63c93323
C
188 defaults: rate,
189 transaction: t
0032ebe9 190 })
f00984c0 191 if (created === true) await video.increment('dislikes', { transaction: t })
0032ebe9 192
63c93323
C
193 if (video.isOwned() && created === true) {
194 // Don't resend the activity to the sender
50d6de9c 195 const exceptions = [ byActor ]
63c93323
C
196 await forwardActivity(activity, t, exceptions)
197 }
0032ebe9
C
198 })
199}
200
6d852470 201async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
63c93323
C
202 const view = activity.object as ViewObject
203
3fd3ab2d 204 const video = await VideoModel.loadByUrlAndPopulateAccount(view.object)
40ff5707
C
205
206 if (!video) throw new Error('Unknown video ' + view.object)
207
50d6de9c 208 const account = await ActorModel.loadByUrl(view.actor)
40ff5707
C
209 if (!account) throw new Error('Unknown account ' + view.actor)
210
211 await video.increment('views')
212
63c93323
C
213 if (video.isOwned()) {
214 // Don't resend the activity to the sender
6d852470 215 const exceptions = [ byActor ]
63c93323
C
216 await forwardActivity(activity, undefined, exceptions)
217 }
40ff5707
C
218}
219
50d6de9c 220function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) {
8e13fa7d 221 const options = {
50d6de9c 222 arguments: [ actor, videoAbuseToCreateData ],
8e13fa7d
C
223 errorMessage: 'Cannot insert the remote video abuse with many retries.'
224 }
225
226 return retryTransactionWrapper(addRemoteVideoAbuse, options)
227}
228
50d6de9c 229function addRemoteVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) {
8e13fa7d
C
230 logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
231
50d6de9c
C
232 const account = actor.Account
233 if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url)
234
3fd3ab2d
C
235 return sequelizeTypescript.transaction(async t => {
236 const video = await VideoModel.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
8e13fa7d
C
237 if (!video) {
238 logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object)
79d5caf9 239 return undefined
8e13fa7d
C
240 }
241
242 const videoAbuseData = {
243 reporterAccountId: account.id,
244 reason: videoAbuseToCreateData.content,
245 videoId: video.id
246 }
247
3fd3ab2d 248 await VideoAbuseModel.create(videoAbuseData)
8e13fa7d
C
249
250 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
251 })
252}
6d852470
C
253
254function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) {
255 const options = {
256 arguments: [ byActor, activity ],
257 errorMessage: 'Cannot create video comment with many retries.'
258 }
259
260 return retryTransactionWrapper(createVideoComment, options)
261}
262
263function createVideoComment (byActor: ActorModel, activity: ActivityCreate) {
264 const comment = activity.object as VideoCommentObject
265 const byAccount = byActor.Account
266
267 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
268
269 return sequelizeTypescript.transaction(async t => {
93ef8a9d
C
270 let video = await VideoModel.loadByUrlAndPopulateAccount(comment.inReplyTo, t)
271 let objectToCreate
6d852470
C
272
273 // This is a new thread
274 if (video) {
93ef8a9d 275 objectToCreate = {
6d852470
C
276 url: comment.id,
277 text: comment.content,
278 originCommentId: null,
279 inReplyToComment: null,
280 videoId: video.id,
d3ea8975 281 accountId: byAccount.id
93ef8a9d 282 }
ea44f375
C
283 } else {
284 const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t)
285 if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo)
6d852470 286
ea44f375 287 video = await VideoModel.load(inReplyToComment.videoId)
6d852470 288
ea44f375 289 const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id
93ef8a9d 290 objectToCreate = {
ea44f375
C
291 url: comment.id,
292 text: comment.content,
293 originCommentId,
294 inReplyToCommentId: inReplyToComment.id,
295 videoId: video.id,
296 accountId: byAccount.id
93ef8a9d 297 }
ea44f375
C
298 }
299
93ef8a9d
C
300 const options = {
301 where: {
302 url: objectToCreate.url
303 },
304 defaults: objectToCreate,
305 transaction: t
306 }
307 const [ ,created ] = await VideoCommentModel.findOrCreate(options)
308
309 if (video.isOwned() && created === true) {
ea44f375
C
310 // Don't resend the activity to the sender
311 const exceptions = [ byActor ]
93ef8a9d
C
312
313 // Mastodon does not add our announces in audience, so we forward to them manually
314 const additionalActors = await getActorsInvolvedInVideo(video, t)
315 const additionalFollowerUrls = additionalActors.map(a => a.followersUrl)
316
317 await forwardActivity(activity, t, exceptions, additionalFollowerUrls)
ea44f375 318 }
6d852470
C
319 })
320}