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