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