aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
committerChocobozzz <me@florianbigard.com>2019-02-06 12:26:58 +0100
commit73471b1a52f242e86364ffb077ea6cadb3b07ae2 (patch)
tree43dbb7748e281f8d80f15326f489cdea10ec857d /server/lib/activitypub
parentc22419dd265c0c7185bf4197a1cb286eb3d8ebc0 (diff)
parentf5305c04aae14467d6f957b713c5a902275cbb89 (diff)
downloadPeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.gz
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.tar.zst
PeerTube-73471b1a52f242e86364ffb077ea6cadb3b07ae2.zip
Merge branch 'release/v1.2.0'
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/actor.ts137
-rw-r--r--server/lib/activitypub/process/process-accept.ts1
-rw-r--r--server/lib/activitypub/process/process-announce.ts8
-rw-r--r--server/lib/activitypub/process/process-create.ts125
-rw-r--r--server/lib/activitypub/process/process-dislike.ts52
-rw-r--r--server/lib/activitypub/process/process-flag.ts49
-rw-r--r--server/lib/activitypub/process/process-follow.ts14
-rw-r--r--server/lib/activitypub/process/process-like.ts3
-rw-r--r--server/lib/activitypub/process/process-undo.ts8
-rw-r--r--server/lib/activitypub/process/process-update.ts2
-rw-r--r--server/lib/activitypub/process/process-view.ts35
-rw-r--r--server/lib/activitypub/process/process.ts14
-rw-r--r--server/lib/activitypub/share.ts20
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/activitypub/video-rates.ts4
-rw-r--r--server/lib/activitypub/videos.ts54
16 files changed, 322 insertions, 208 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 504263c99..8215840da 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,11 +1,10 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { join } from 'path'
3import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
4import * as url from 'url' 3import * as url from 'url'
5import * as uuidv4 from 'uuid/v4' 4import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' 8import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@@ -13,7 +12,7 @@ import { logger } from '../../helpers/logger'
13import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
14import { doRequest, downloadImage } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
15import { getUrlFromWebfinger } from '../../helpers/webfinger' 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
16import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 15import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
17import { AccountModel } from '../../models/account/account' 16import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../models/activitypub/actor'
19import { AvatarModel } from '../../models/avatar/avatar' 18import { AvatarModel } from '../../models/avatar/avatar'
@@ -43,7 +42,7 @@ async function getOrCreateActorAndServerAndModel (
43 recurseIfNeeded = true, 42 recurseIfNeeded = true,
44 updateCollections = false 43 updateCollections = false
45) { 44) {
46 const actorUrl = getAPUrl(activityActor) 45 const actorUrl = getAPId(activityActor)
47 let created = false 46 let created = false
48 47
49 let actor = await fetchActorByUrl(actorUrl, fetchType) 48 let actor = await fetchActorByUrl(actorUrl, fetchType)
@@ -172,15 +171,13 @@ async function fetchActorTotalItems (url: string) {
172 171
173async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { 172async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
174 if ( 173 if (
175 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && 174 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
176 isActivityPubUrlValid(actorJSON.icon.url) 175 isActivityPubUrlValid(actorJSON.icon.url)
177 ) { 176 ) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] 177 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
179 178
180 const avatarName = uuidv4() + extension 179 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 180 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
182
183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 181
185 return avatarName 182 return avatarName
186 } 183 }
@@ -204,6 +201,69 @@ async function addFetchOutboxJob (actor: ActorModel) {
204 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 201 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
205} 202}
206 203
204async function refreshActorIfNeeded (
205 actorArg: ActorModel,
206 fetchedType: ActorFetchByUrlType
207): Promise<{ actor: ActorModel, refreshed: boolean }> {
208 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
209
210 // We need more attributes
211 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
212
213 try {
214 let actorUrl: string
215 try {
216 actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
217 } catch (err) {
218 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
219 actorUrl = actor.url
220 }
221
222 const { result, statusCode } = await fetchRemoteActor(actorUrl)
223
224 if (statusCode === 404) {
225 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
226 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
227 return { actor: undefined, refreshed: false }
228 }
229
230 if (result === undefined) {
231 logger.warn('Cannot fetch remote actor in refresh actor.')
232 return { actor, refreshed: false }
233 }
234
235 return sequelizeTypescript.transaction(async t => {
236 updateInstanceWithAnother(actor, result.actor)
237
238 if (result.avatarName !== undefined) {
239 await updateActorAvatarInstance(actor, result.avatarName, t)
240 }
241
242 // Force update
243 actor.setDataValue('updatedAt', new Date())
244 await actor.save({ transaction: t })
245
246 if (actor.Account) {
247 actor.Account.set('name', result.name)
248 actor.Account.set('description', result.summary)
249
250 await actor.Account.save({ transaction: t })
251 } else if (actor.VideoChannel) {
252 actor.VideoChannel.set('name', result.name)
253 actor.VideoChannel.set('description', result.summary)
254 actor.VideoChannel.set('support', result.support)
255
256 await actor.VideoChannel.save({ transaction: t })
257 }
258
259 return { refreshed: true, actor }
260 })
261 } catch (err) {
262 logger.warn('Cannot refresh actor.', { err })
263 return { actor, refreshed: false }
264 }
265}
266
207export { 267export {
208 getOrCreateActorAndServerAndModel, 268 getOrCreateActorAndServerAndModel,
209 buildActorInstance, 269 buildActorInstance,
@@ -211,6 +271,7 @@ export {
211 fetchActorTotalItems, 271 fetchActorTotalItems,
212 fetchAvatarIfExists, 272 fetchAvatarIfExists,
213 updateActorInstance, 273 updateActorInstance,
274 refreshActorIfNeeded,
214 updateActorAvatarInstance, 275 updateActorAvatarInstance,
215 addFetchOutboxJob 276 addFetchOutboxJob
216} 277}
@@ -299,7 +360,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
299 360
300 const actorJSON: ActivityPubActor = requestResult.body 361 const actorJSON: ActivityPubActor = requestResult.body
301 if (isActorObjectValid(actorJSON) === false) { 362 if (isActorObjectValid(actorJSON) === false) {
302 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) 363 logger.debug('Remote actor JSON is not valid.', { actorJSON })
303 return { result: undefined, statusCode: requestResult.response.statusCode } 364 return { result: undefined, statusCode: requestResult.response.statusCode }
304 } 365 }
305 366
@@ -375,59 +436,3 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
375 436
376 return videoChannelCreated 437 return videoChannelCreated
377} 438}
378
379async function refreshActorIfNeeded (
380 actorArg: ActorModel,
381 fetchedType: ActorFetchByUrlType
382): Promise<{ actor: ActorModel, refreshed: boolean }> {
383 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
384
385 // We need more attributes
386 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
387
388 try {
389 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
390 const { result, statusCode } = await fetchRemoteActor(actorUrl)
391
392 if (statusCode === 404) {
393 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
394 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
395 return { actor: undefined, refreshed: false }
396 }
397
398 if (result === undefined) {
399 logger.warn('Cannot fetch remote actor in refresh actor.')
400 return { actor, refreshed: false }
401 }
402
403 return sequelizeTypescript.transaction(async t => {
404 updateInstanceWithAnother(actor, result.actor)
405
406 if (result.avatarName !== undefined) {
407 await updateActorAvatarInstance(actor, result.avatarName, t)
408 }
409
410 // Force update
411 actor.setDataValue('updatedAt', new Date())
412 await actor.save({ transaction: t })
413
414 if (actor.Account) {
415 actor.Account.set('name', result.name)
416 actor.Account.set('description', result.summary)
417
418 await actor.Account.save({ transaction: t })
419 } else if (actor.VideoChannel) {
420 actor.VideoChannel.set('name', result.name)
421 actor.VideoChannel.set('description', result.summary)
422 actor.VideoChannel.set('support', result.support)
423
424 await actor.VideoChannel.save({ transaction: t })
425 }
426
427 return { refreshed: true, actor }
428 })
429 } catch (err) {
430 logger.warn('Cannot refresh actor.', { err })
431 return { actor, refreshed: false }
432 }
433}
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts
index 89bda9c32..ebb275e34 100644
--- a/server/lib/activitypub/process/process-accept.ts
+++ b/server/lib/activitypub/process/process-accept.ts
@@ -24,6 +24,7 @@ async function processAccept (actor: ActorModel, targetActor: ActorModel) {
24 if (follow.state !== 'accepted') { 24 if (follow.state !== 'accepted') {
25 follow.set('state', 'accepted') 25 follow.set('state', 'accepted')
26 await follow.save() 26 await follow.save()
27
27 await addFetchOutboxJob(targetActor) 28 await addFetchOutboxJob(targetActor)
28 } 29 }
29} 30}
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index cc88b5423..23310b41e 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -5,6 +5,8 @@ import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoShareModel } from '../../../models/video/video-share' 5import { VideoShareModel } from '../../../models/video/video-share'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { VideoPrivacy } from '../../../../shared/models/videos'
9import { Notifier } from '../../notifier'
8 10
9async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { 11async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) {
10 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) 12 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity)
@@ -21,9 +23,9 @@ export {
21async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { 23async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
22 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id 24 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
23 25
24 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) 26 const { video, created: videoCreated } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri })
25 27
26 return sequelizeTypescript.transaction(async t => { 28 await sequelizeTypescript.transaction(async t => {
27 // Add share entry 29 // Add share entry
28 30
29 const share = { 31 const share = {
@@ -49,4 +51,6 @@ async function processVideoShare (actorAnnouncer: ActorModel, activity: Activity
49 51
50 return undefined 52 return undefined
51 }) 53 })
54
55 if (videoCreated) Notifier.Instance.notifyOnNewVideo(video)
52} 56}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index cd7ea01aa..5f4d793a5 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -1,36 +1,44 @@
1import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' 1import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared'
2import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
3import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
4import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
5import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
6import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
7import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
8import { ActorModel } from '../../../models/activitypub/actor' 6import { ActorModel } from '../../../models/activitypub/actor'
9import { VideoAbuseModel } from '../../../models/video/video-abuse'
10import { addVideoComment, resolveThread } from '../video-comments' 7import { addVideoComment, resolveThread } from '../video-comments'
11import { getOrCreateVideoAndAccountAndChannel } from '../videos' 8import { getOrCreateVideoAndAccountAndChannel } from '../videos'
12import { forwardVideoRelatedActivity } from '../send/utils' 9import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 10import { createOrUpdateCacheFile } from '../cache-file'
15import { getVideoDislikeActivityPubUrl } from '../url' 11import { Notifier } from '../../notifier'
16import { VideoModel } from '../../../models/video/video' 12import { processViewActivity } from './process-view'
13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag'
17 15
18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 16async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
19 const activityObject = activity.object 17 const activityObject = activity.object
20 const activityType = activityObject.type 18 const activityType = activityObject.type
21 19
22 if (activityType === 'View') { 20 if (activityType === 'View') {
23 return processCreateView(byActor, activity) 21 return processViewActivity(activity, byActor)
24 } else if (activityType === 'Dislike') { 22 }
25 return retryTransactionWrapper(processCreateDislike, byActor, activity) 23
26 } else if (activityType === 'Video') { 24 if (activityType === 'Dislike') {
25 return retryTransactionWrapper(processDislikeActivity, activity, byActor)
26 }
27
28 if (activityType === 'Flag') {
29 return retryTransactionWrapper(processFlagActivity, activity, byActor)
30 }
31
32 if (activityType === 'Video') {
27 return processCreateVideo(activity) 33 return processCreateVideo(activity)
28 } else if (activityType === 'Flag') { 34 }
29 return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) 35
30 } else if (activityType === 'Note') { 36 if (activityType === 'Note') {
31 return retryTransactionWrapper(processCreateVideoComment, byActor, activity) 37 return retryTransactionWrapper(processCreateVideoComment, activity, byActor)
32 } else if (activityType === 'CacheFile') { 38 }
33 return retryTransactionWrapper(processCacheFile, byActor, activity) 39
40 if (activityType === 'CacheFile') {
41 return retryTransactionWrapper(processCacheFile, activity, byActor)
34 } 42 }
35 43
36 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 44 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@@ -48,61 +56,14 @@ export {
48async function processCreateVideo (activity: ActivityCreate) { 56async function processCreateVideo (activity: ActivityCreate) {
49 const videoToCreateData = activity.object as VideoTorrentObject 57 const videoToCreateData = activity.object as VideoTorrentObject
50 58
51 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) 59 const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData })
52 60
53 return video 61 if (created) Notifier.Instance.notifyOnNewVideo(video)
54}
55
56async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) {
57 const dislike = activity.object as DislikeObject
58 const byAccount = byActor.Account
59
60 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
61
62 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object })
63 62
64 return sequelizeTypescript.transaction(async t => { 63 return video
65 const rate = {
66 type: 'dislike' as 'dislike',
67 videoId: video.id,
68 accountId: byAccount.id
69 }
70
71 const [ , created ] = await AccountVideoRateModel.findOrCreate({
72 where: rate,
73 defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
74 transaction: t
75 })
76 if (created === true) await video.increment('dislikes', { transaction: t })
77
78 if (video.isOwned() && created === true) {
79 // Don't resend the activity to the sender
80 const exceptions = [ byActor ]
81
82 await forwardVideoRelatedActivity(activity, t, exceptions, video)
83 }
84 })
85}
86
87async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
88 const view = activity.object as ViewObject
89
90 const options = {
91 videoObject: view.object,
92 fetchType: 'only-video' as 'only-video'
93 }
94 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
95
96 await Redis.Instance.addVideoView(video.id)
97
98 if (video.isOwned()) {
99 // Don't resend the activity to the sender
100 const exceptions = [ byActor ]
101 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
102 }
103} 64}
104 65
105async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { 66async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) {
106 const cacheFile = activity.object as CacheFileObject 67 const cacheFile = activity.object as CacheFileObject
107 68
108 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) 69 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
@@ -118,29 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate)
118 } 79 }
119} 80}
120 81
121async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { 82async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) {
122 logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
123
124 const account = byActor.Account
125 if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
126
127 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object })
128
129 return sequelizeTypescript.transaction(async t => {
130 const videoAbuseData = {
131 reporterAccountId: account.id,
132 reason: videoAbuseToCreateData.content,
133 videoId: video.id,
134 state: VideoAbuseState.PENDING
135 }
136
137 await VideoAbuseModel.create(videoAbuseData, { transaction: t })
138
139 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
140 })
141}
142
143async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) {
144 const commentObject = activity.object as VideoCommentObject 83 const commentObject = activity.object as VideoCommentObject
145 const byAccount = byActor.Account 84 const byAccount = byActor.Account
146 85
@@ -148,7 +87,7 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit
148 87
149 const { video } = await resolveThread(commentObject.inReplyTo) 88 const { video } = await resolveThread(commentObject.inReplyTo)
150 89
151 const { created } = await addVideoComment(video, commentObject.id) 90 const { comment, created } = await addVideoComment(video, commentObject.id)
152 91
153 if (video.isOwned() && created === true) { 92 if (video.isOwned() && created === true) {
154 // Don't resend the activity to the sender 93 // Don't resend the activity to the sender
@@ -156,4 +95,6 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit
156 95
157 await forwardVideoRelatedActivity(activity, undefined, exceptions, video) 96 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
158 } 97 }
98
99 if (created === true) Notifier.Instance.notifyOnNewComment(comment)
159} 100}
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
new file mode 100644
index 000000000..bfd69e07a
--- /dev/null
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -0,0 +1,52 @@
1import { ActivityCreate, ActivityDislike } from '../../../../shared'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { ActorModel } from '../../../models/activitypub/actor'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { forwardVideoRelatedActivity } from '../send/utils'
9import { getVideoDislikeActivityPubUrl } from '../url'
10
11async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) {
12 return retryTransactionWrapper(processDislike, activity, byActor)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 processDislikeActivity
19}
20
21// ---------------------------------------------------------------------------
22
23async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) {
24 const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object
25 const byAccount = byActor.Account
26
27 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
28
29 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject })
30
31 return sequelizeTypescript.transaction(async t => {
32 const rate = {
33 type: 'dislike' as 'dislike',
34 videoId: video.id,
35 accountId: byAccount.id
36 }
37
38 const [ , created ] = await AccountVideoRateModel.findOrCreate({
39 where: rate,
40 defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
41 transaction: t
42 })
43 if (created === true) await video.increment('dislikes', { transaction: t })
44
45 if (video.isOwned() && created === true) {
46 // Don't resend the activity to the sender
47 const exceptions = [ byActor ]
48
49 await forwardVideoRelatedActivity(activity, t, exceptions, video)
50 }
51 })
52}
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
new file mode 100644
index 000000000..79ce6fb41
--- /dev/null
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -0,0 +1,49 @@
1import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared'
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers'
6import { ActorModel } from '../../../models/activitypub/actor'
7import { VideoAbuseModel } from '../../../models/video/video-abuse'
8import { getOrCreateVideoAndAccountAndChannel } from '../videos'
9import { Notifier } from '../../notifier'
10import { getAPId } from '../../../helpers/activitypub'
11
12async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) {
13 return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 processFlagActivity
20}
21
22// ---------------------------------------------------------------------------
23
24async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) {
25 const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
26
27 logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object))
28
29 const account = byActor.Account
30 if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
31
32 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object })
33
34 return sequelizeTypescript.transaction(async t => {
35 const videoAbuseData = {
36 reporterAccountId: account.id,
37 reason: flag.content,
38 videoId: video.id,
39 state: VideoAbuseState.PENDING
40 }
41
42 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
43 videoAbuseInstance.Video = video
44
45 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance)
46
47 logger.info('Remote abuse for video uuid %s created', flag.object)
48 })
49}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index 24c9085f7..0cd537187 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -5,9 +5,11 @@ import { sequelizeTypescript } from '../../../initializers'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
7import { sendAccept } from '../send' 7import { sendAccept } from '../send'
8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub'
8 10
9async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { 11async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
10 const activityObject = activity.object 12 const activityObject = getAPId(activity.object)
11 13
12 return retryTransactionWrapper(processFollow, byActor, activityObject) 14 return retryTransactionWrapper(processFollow, byActor, activityObject)
13} 15}
@@ -21,13 +23,13 @@ export {
21// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
22 24
23async function processFollow (actor: ActorModel, targetActorURL: string) { 25async function processFollow (actor: ActorModel, targetActorURL: string) {
24 await sequelizeTypescript.transaction(async t => { 26 const { actorFollow, created } = await sequelizeTypescript.transaction(async t => {
25 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) 27 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
26 28
27 if (!targetActor) throw new Error('Unknown actor') 29 if (!targetActor) throw new Error('Unknown actor')
28 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') 30 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
29 31
30 const [ actorFollow ] = await ActorFollowModel.findOrCreate({ 32 const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({
31 where: { 33 where: {
32 actorId: actor.id, 34 actorId: actor.id,
33 targetActorId: targetActor.id 35 targetActorId: targetActor.id
@@ -52,8 +54,12 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
52 actorFollow.ActorFollowing = targetActor 54 actorFollow.ActorFollowing = targetActor
53 55
54 // Target sends to actor he accepted the follow request 56 // Target sends to actor he accepted the follow request
55 return sendAccept(actorFollow) 57 await sendAccept(actorFollow)
58
59 return { actorFollow, created }
56 }) 60 })
57 61
62 if (created) Notifier.Instance.notifyOfNewFollow(actorFollow)
63
58 logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url) 64 logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url)
59} 65}
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index e8e97eece..2a04167d7 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -6,6 +6,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { getVideoLikeActivityPubUrl } from '../url' 8import { getVideoLikeActivityPubUrl } from '../url'
9import { getAPId } from '../../../helpers/activitypub'
9 10
10async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 11async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
11 return retryTransactionWrapper(processLikeVideo, byActor, activity) 12 return retryTransactionWrapper(processLikeVideo, byActor, activity)
@@ -20,7 +21,7 @@ export {
20// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
21 22
22async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { 23async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
23 const videoUrl = activity.object 24 const videoUrl = getAPId(activity.object)
24 25
25 const byAccount = byActor.Account 26 const byAccount = byActor.Account
26 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) 27 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 438a013b6..ed0177a67 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -26,6 +26,10 @@ async function processUndoActivity (activity: ActivityUndo, byActor: ActorModel)
26 } 26 }
27 } 27 }
28 28
29 if (activityToUndo.type === 'Dislike') {
30 return retryTransactionWrapper(processUndoDislike, byActor, activity)
31 }
32
29 if (activityToUndo.type === 'Follow') { 33 if (activityToUndo.type === 'Follow') {
30 return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) 34 return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo)
31 } 35 }
@@ -72,7 +76,9 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
72} 76}
73 77
74async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { 78async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) {
75 const dislike = activity.object.object as DislikeObject 79 const dislike = activity.object.type === 'Dislike'
80 ? activity.object
81 : activity.object.object as DislikeObject
76 82
77 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) 83 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object })
78 84
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 03831a00e..c6b42d846 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
51 return undefined 51 return undefined
52 } 52 }
53 53
54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) 54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false })
55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
56 56
57 const updateOptions = { 57 const updateOptions = {
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
new file mode 100644
index 000000000..8f66d3630
--- /dev/null
+++ b/server/lib/activitypub/process/process-view.ts
@@ -0,0 +1,35 @@
1import { ActorModel } from '../../../models/activitypub/actor'
2import { getOrCreateVideoAndAccountAndChannel } from '../videos'
3import { forwardVideoRelatedActivity } from '../send/utils'
4import { Redis } from '../../redis'
5import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub'
6
7async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) {
8 return processCreateView(activity, byActor)
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 processViewActivity
15}
16
17// ---------------------------------------------------------------------------
18
19async function processCreateView (activity: ActivityView | ActivityCreate, byActor: ActorModel) {
20 const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object
21
22 const options = {
23 videoObject: videoObject,
24 fetchType: 'only-video' as 'only-video'
25 }
26 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
27
28 await Redis.Instance.addVideoView(video.id)
29
30 if (video.isOwned()) {
31 // Don't resend the activity to the sender
32 const exceptions = [ byActor ]
33 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
34 }
35}
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
index bcc5cac7a..9dd241402 100644
--- a/server/lib/activitypub/process/process.ts
+++ b/server/lib/activitypub/process/process.ts
@@ -1,5 +1,5 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { checkUrlsSameHost, getAPUrl } from '../../../helpers/activitypub' 2import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { ActorModel } from '../../../models/activitypub/actor'
5import { processAcceptActivity } from './process-accept' 5import { processAcceptActivity } from './process-accept'
@@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject'
12import { processUndoActivity } from './process-undo' 12import { processUndoActivity } from './process-undo'
13import { processUpdateActivity } from './process-update' 13import { processUpdateActivity } from './process-update'
14import { getOrCreateActorAndServerAndModel } from '../actor' 14import { getOrCreateActorAndServerAndModel } from '../actor'
15import { processDislikeActivity } from './process-dislike'
16import { processFlagActivity } from './process-flag'
17import { processViewActivity } from './process-view'
15 18
16const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { 19const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = {
17 Create: processCreateActivity, 20 Create: processCreateActivity,
@@ -22,7 +25,10 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
22 Reject: processRejectActivity, 25 Reject: processRejectActivity,
23 Announce: processAnnounceActivity, 26 Announce: processAnnounceActivity,
24 Undo: processUndoActivity, 27 Undo: processUndoActivity,
25 Like: processLikeActivity 28 Like: processLikeActivity,
29 Dislike: processDislikeActivity,
30 Flag: processFlagActivity,
31 View: processViewActivity
26} 32}
27 33
28async function processActivities ( 34async function processActivities (
@@ -35,12 +41,12 @@ async function processActivities (
35 const actorsCache: { [ url: string ]: ActorModel } = {} 41 const actorsCache: { [ url: string ]: ActorModel } = {}
36 42
37 for (const activity of activities) { 43 for (const activity of activities) {
38 if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { 44 if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) {
39 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) 45 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
40 continue 46 continue
41 } 47 }
42 48
43 const actorUrl = getAPUrl(activity.actor) 49 const actorUrl = getAPId(activity.actor)
44 50
45 // When we fetch remote data, we don't have signature 51 // When we fetch remote data, we don't have signature
46 if (options.signatureActor && actorUrl !== options.signatureActor.url) { 52 if (options.signatureActor && actorUrl !== options.signatureActor.url) {
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 5dcba778c..1767df0ae 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -11,7 +11,7 @@ import { doRequest } from '../../helpers/requests'
11import { getOrCreateActorAndServerAndModel } from './actor' 11import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
14import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 14import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
15 15
16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -41,7 +41,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
41 }) 41 })
42 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 42 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
43 43
44 const actorUrl = getAPUrl(body.actor) 44 const actorUrl = getAPId(body.actor)
45 if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { 45 if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
46 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) 46 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
47 } 47 }
@@ -78,7 +78,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
78 const serverActor = await getServerActor() 78 const serverActor = await getServerActor()
79 79
80 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) 80 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
81 return VideoShareModel.findOrCreate({ 81 const [ serverShare ] = await VideoShareModel.findOrCreate({
82 defaults: { 82 defaults: {
83 actorId: serverActor.id, 83 actorId: serverActor.id,
84 videoId: video.id, 84 videoId: video.id,
@@ -88,16 +88,14 @@ async function shareByServer (video: VideoModel, t: Transaction) {
88 url: serverShareUrl 88 url: serverShareUrl
89 }, 89 },
90 transaction: t 90 transaction: t
91 }).then(([ serverShare, created ]) => {
92 if (created) return sendVideoAnnounce(serverActor, serverShare, video, t)
93
94 return undefined
95 }) 91 })
92
93 return sendVideoAnnounce(serverActor, serverShare, video, t)
96} 94}
97 95
98async function shareByVideoChannel (video: VideoModel, t: Transaction) { 96async function shareByVideoChannel (video: VideoModel, t: Transaction) {
99 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) 97 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
100 return VideoShareModel.findOrCreate({ 98 const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
101 defaults: { 99 defaults: {
102 actorId: video.VideoChannel.actorId, 100 actorId: video.VideoChannel.actorId,
103 videoId: video.id, 101 videoId: video.id,
@@ -107,11 +105,9 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) {
107 url: videoChannelShareUrl 105 url: videoChannelShareUrl
108 }, 106 },
109 transaction: t 107 transaction: t
110 }).then(([ videoChannelShare, created ]) => {
111 if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
112
113 return undefined
114 }) 108 })
109
110 return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
115} 111}
116 112
117async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { 113async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 5868e7297..e87301fe7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -70,7 +70,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) 70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
71 } 71 }
72 72
73 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 73 const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
75 if (!entry) return { created: false } 75 if (!entry) return { created: false }
76 76
@@ -80,6 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
80 }, 80 },
81 defaults: entry 81 defaults: entry
82 }) 82 })
83 comment.Account = actor.Account
84 comment.Video = videoInstance
83 85
84 return { comment, created } 86 return { comment, created }
85} 87}
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 2cce67f0c..45a2b22ea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -9,7 +9,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
11import { doRequest } from '../../helpers/requests' 11import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 12import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
13import { ActorModel } from '../../models/activitypub/actor' 13import { ActorModel } from '../../models/activitypub/actor'
14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' 14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
15 15
@@ -26,7 +26,7 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
26 }) 26 })
27 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 27 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
28 28
29 const actorUrl = getAPUrl(body.actor) 29 const actorUrl = getAPId(body.actor)
30 if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { 30 if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
31 throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) 31 throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
32 } 32 }
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 998f90330..e1e523499 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,7 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize' 2import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { join } from 'path'
5import * as request from 'request' 4import * as request from 'request'
6import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index'
7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
@@ -11,7 +10,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 10import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
13import { doRequest, downloadImage } from '../../helpers/requests' 12import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers' 13import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 14import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 15import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
@@ -29,7 +28,8 @@ import { createRates } from './video-rates'
29import { addVideoShares, shareVideoByServerAndChannel } from './share' 28import { addVideoShares, shareVideoByServerAndChannel } from './share'
30import { AccountModel } from '../../models/account/account' 29import { AccountModel } from '../../models/account/account'
31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
32import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 31import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
32import { Notifier } from '../notifier'
33 33
34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
35 // If the video is not private and published, we federate it 35 // If the video is not private and published, we federate it
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu
95 95
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 98
100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) 99 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
101} 100}
102 101
103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -156,29 +155,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
156} 155}
157 156
158async function getOrCreateVideoAndAccountAndChannel (options: { 157async function getOrCreateVideoAndAccountAndChannel (options: {
159 videoObject: VideoTorrentObject | string, 158 videoObject: { id: string } | string,
160 syncParam?: SyncParam, 159 syncParam?: SyncParam,
161 fetchType?: VideoFetchByUrlType 160 fetchType?: VideoFetchByUrlType,
161 allowRefresh?: boolean // true by default
162}) { 162}) {
163 // Default params 163 // Default params
164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
165 const fetchType = options.fetchType || 'all' 165 const fetchType = options.fetchType || 'all'
166 const allowRefresh = options.allowRefresh !== false
166 167
167 // Get video url 168 // Get video url
168 const videoUrl = getAPUrl(options.videoObject) 169 const videoUrl = getAPId(options.videoObject)
169 170
170 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
171 if (videoFromDatabase) { 172 if (videoFromDatabase) {
172 const refreshOptions = {
173 video: videoFromDatabase,
174 fetchedType: fetchType,
175 syncParam
176 }
177 173
178 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 174 if (allowRefresh === true) {
179 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) 175 const refreshOptions = {
176 video: videoFromDatabase,
177 fetchedType: fetchType,
178 syncParam
179 }
180
181 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
183 }
180 184
181 return { video: videoFromDatabase } 185 return { video: videoFromDatabase, created: false }
182 } 186 }
183 187
184 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) 188 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
@@ -189,7 +193,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
189 193
190 await syncVideoExternalAttributes(video, fetchedVideo, syncParam) 194 await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
191 195
192 return { video } 196 return { video, created: true }
193} 197}
194 198
195async function updateVideoFromAP (options: { 199async function updateVideoFromAP (options: {
@@ -200,13 +204,14 @@ async function updateVideoFromAP (options: {
200 overrideTo?: string[] 204 overrideTo?: string[]
201}) { 205}) {
202 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 206 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
207
203 let videoFieldsSave: any 208 let videoFieldsSave: any
209 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
210 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
204 211
205 try { 212 try {
206 await sequelizeTypescript.transaction(async t => { 213 await sequelizeTypescript.transaction(async t => {
207 const sequelizeOptions = { 214 const sequelizeOptions = { transaction: t }
208 transaction: t
209 }
210 215
211 videoFieldsSave = options.video.toJSON() 216 videoFieldsSave = options.video.toJSON()
212 217
@@ -276,6 +281,11 @@ async function updateVideoFromAP (options: {
276 } 281 }
277 }) 282 })
278 283
284 // Notify our users?
285 if (wasPrivateVideo || wasUnlistedVideo) {
286 Notifier.Instance.notifyOnNewVideo(options.video)
287 }
288
279 logger.info('Remote video with uuid %s updated', options.videoObject.uuid) 289 logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
280 } catch (err) { 290 } catch (err) {
281 if (options.video !== undefined && videoFieldsSave !== undefined) { 291 if (options.video !== undefined && videoFieldsSave !== undefined) {
@@ -358,7 +368,7 @@ export {
358// --------------------------------------------------------------------------- 368// ---------------------------------------------------------------------------
359 369
360function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 370function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
361 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) 371 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
362 372
363 const urlMediaType = url.mediaType || url.mimeType 373 const urlMediaType = url.mediaType || url.mimeType
364 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 374 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
@@ -486,7 +496,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
486 496
487 const mediaType = fileUrl.mediaType || fileUrl.mimeType 497 const mediaType = fileUrl.mediaType || fileUrl.mimeType
488 const attribute = { 498 const attribute = {
489 extname: VIDEO_MIMETYPE_EXT[ mediaType ], 499 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
490 infoHash: parsed.infoHash, 500 infoHash: parsed.infoHash,
491 resolution: fileUrl.height, 501 resolution: fileUrl.height,
492 size: fileUrl.size, 502 size: fileUrl.size,