diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
commit | 88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch) | |
tree | b242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /server/lib/activitypub | |
parent | 53a94c7cfa8368da4cd248d65df8346905938f0c (diff) | |
parent | 9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff) | |
download | PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip |
Merge branch 'develop' into pr/1217
Diffstat (limited to 'server/lib/activitypub')
27 files changed, 743 insertions, 398 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 45dd4443d..a3f379b76 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,19 +1,18 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { join } from 'path' | ||
3 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
4 | import * as url from 'url' | 3 | import * as url from 'url' |
5 | import * as uuidv4 from 'uuid/v4' | 4 | import * as uuidv4 from 'uuid/v4' |
6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 5 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
8 | import { getActorUrl } from '../../helpers/activitypub' | 7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
9 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
11 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
13 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
14 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 13 | import { doRequest, downloadImage } from '../../helpers/requests' |
15 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
16 | import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' | 15 | import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers' |
17 | import { AccountModel } from '../../models/account/account' | 16 | import { AccountModel } from '../../models/account/account' |
18 | import { ActorModel } from '../../models/activitypub/actor' | 17 | import { ActorModel } from '../../models/activitypub/actor' |
19 | import { AvatarModel } from '../../models/avatar/avatar' | 18 | import { 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 = getActorUrl(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) |
@@ -65,8 +64,12 @@ async function getOrCreateActorAndServerAndModel ( | |||
65 | const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') | 64 | const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') |
66 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) | 65 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) |
67 | 66 | ||
67 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
68 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
69 | } | ||
70 | |||
68 | try { | 71 | try { |
69 | // Assert we don't recurse another time | 72 | // Don't recurse another time |
70 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) | 73 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) |
71 | } catch (err) { | 74 | } catch (err) { |
72 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) | 75 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) |
@@ -168,18 +171,13 @@ async function fetchActorTotalItems (url: string) { | |||
168 | 171 | ||
169 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { | 172 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { |
170 | if ( | 173 | if ( |
171 | 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 && |
172 | isActivityPubUrlValid(actorJSON.icon.url) | 175 | isActivityPubUrlValid(actorJSON.icon.url) |
173 | ) { | 176 | ) { |
174 | const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] | 177 | const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] |
175 | 178 | ||
176 | const avatarName = uuidv4() + extension | 179 | const avatarName = uuidv4() + extension |
177 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | 180 | await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE) |
178 | |||
179 | await doRequestAndSaveToFile({ | ||
180 | method: 'GET', | ||
181 | uri: actorJSON.icon.url | ||
182 | }, destPath) | ||
183 | 181 | ||
184 | return avatarName | 182 | return avatarName |
185 | } | 183 | } |
@@ -203,6 +201,69 @@ async function addFetchOutboxJob (actor: ActorModel) { | |||
203 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 201 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) |
204 | } | 202 | } |
205 | 203 | ||
204 | async 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 | |||
206 | export { | 267 | export { |
207 | getOrCreateActorAndServerAndModel, | 268 | getOrCreateActorAndServerAndModel, |
208 | buildActorInstance, | 269 | buildActorInstance, |
@@ -210,6 +271,7 @@ export { | |||
210 | fetchActorTotalItems, | 271 | fetchActorTotalItems, |
211 | fetchAvatarIfExists, | 272 | fetchAvatarIfExists, |
212 | updateActorInstance, | 273 | updateActorInstance, |
274 | refreshActorIfNeeded, | ||
213 | updateActorAvatarInstance, | 275 | updateActorAvatarInstance, |
214 | addFetchOutboxJob | 276 | addFetchOutboxJob |
215 | } | 277 | } |
@@ -293,16 +355,19 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
293 | 355 | ||
294 | logger.info('Fetching remote actor %s.', actorUrl) | 356 | logger.info('Fetching remote actor %s.', actorUrl) |
295 | 357 | ||
296 | const requestResult = await doRequest(options) | 358 | const requestResult = await doRequest<ActivityPubActor>(options) |
297 | normalizeActor(requestResult.body) | 359 | normalizeActor(requestResult.body) |
298 | 360 | ||
299 | const actorJSON: ActivityPubActor = requestResult.body | 361 | const actorJSON = requestResult.body |
300 | |||
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 | ||
367 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { | ||
368 | throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id) | ||
369 | } | ||
370 | |||
306 | const followersCount = await fetchActorTotalItems(actorJSON.followers) | 371 | const followersCount = await fetchActorTotalItems(actorJSON.followers) |
307 | const followingCount = await fetchActorTotalItems(actorJSON.following) | 372 | const followingCount = await fetchActorTotalItems(actorJSON.following) |
308 | 373 | ||
@@ -371,59 +436,3 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu | |||
371 | 436 | ||
372 | return videoChannelCreated | 437 | return videoChannelCreated |
373 | } | 438 | } |
374 | |||
375 | async function refreshActorIfNeeded ( | ||
376 | actorArg: ActorModel, | ||
377 | fetchedType: ActorFetchByUrlType | ||
378 | ): Promise<{ actor: ActorModel, refreshed: boolean }> { | ||
379 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
380 | |||
381 | // We need more attributes | ||
382 | const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
383 | |||
384 | try { | ||
385 | const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
386 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | ||
387 | |||
388 | if (statusCode === 404) { | ||
389 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
390 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | ||
391 | return { actor: undefined, refreshed: false } | ||
392 | } | ||
393 | |||
394 | if (result === undefined) { | ||
395 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
396 | return { actor, refreshed: false } | ||
397 | } | ||
398 | |||
399 | return sequelizeTypescript.transaction(async t => { | ||
400 | updateInstanceWithAnother(actor, result.actor) | ||
401 | |||
402 | if (result.avatarName !== undefined) { | ||
403 | await updateActorAvatarInstance(actor, result.avatarName, t) | ||
404 | } | ||
405 | |||
406 | // Force update | ||
407 | actor.setDataValue('updatedAt', new Date()) | ||
408 | await actor.save({ transaction: t }) | ||
409 | |||
410 | if (actor.Account) { | ||
411 | actor.Account.set('name', result.name) | ||
412 | actor.Account.set('description', result.summary) | ||
413 | |||
414 | await actor.Account.save({ transaction: t }) | ||
415 | } else if (actor.VideoChannel) { | ||
416 | actor.VideoChannel.set('name', result.name) | ||
417 | actor.VideoChannel.set('description', result.summary) | ||
418 | actor.VideoChannel.set('support', result.support) | ||
419 | |||
420 | await actor.VideoChannel.save({ transaction: t }) | ||
421 | } | ||
422 | |||
423 | return { refreshed: true, actor } | ||
424 | }) | ||
425 | } catch (err) { | ||
426 | logger.warn('Cannot refresh actor.', { err }) | ||
427 | return { actor, refreshed: false } | ||
428 | } | ||
429 | } | ||
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index f6f068b45..9a40414bb 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,11 +1,28 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | 1 | import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' |
2 | import { VideoModel } from '../../models/video/video' | 2 | import { VideoModel } from '../../models/video/video' |
3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
4 | import { Transaction } from 'sequelize' | 4 | import { Transaction } from 'sequelize' |
5 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | 6 | ||
6 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { | 7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { |
7 | const url = cacheFileObject.url | ||
8 | 8 | ||
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | |||
15 | return { | ||
16 | expiresOn: new Date(cacheFileObject.expires), | ||
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | ||
24 | |||
25 | const url = cacheFileObject.url | ||
9 | const videoFile = video.VideoFiles.find(f => { | 26 | const videoFile = video.VideoFiles.find(f => { |
10 | return f.resolution === url.height && f.fps === url.fps | 27 | return f.resolution === url.height && f.fps === url.fps |
11 | }) | 28 | }) |
@@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
15 | return { | 32 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: new Date(cacheFileObject.expires), |
17 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
18 | fileUrl: cacheFileObject.url.href, | 35 | fileUrl: url.href, |
19 | strategy: null, | 36 | strategy: null, |
20 | videoFileId: videoFile.id, | 37 | videoFileId: videoFile.id, |
21 | actorId: byActor.id | 38 | actorId: byActor.id |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 55912341c..1b9b14c2e 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' | 1 | import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' |
2 | import { doRequest } from '../../helpers/requests' | 2 | import { doRequest } from '../../helpers/requests' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import Bluebird = require('bluebird') | 4 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
5 | 6 | ||
6 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 7 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { |
7 | logger.info('Crawling ActivityPub data on %s.', uri) | 8 | logger.info('Crawling ActivityPub data on %s.', uri) |
@@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr | |||
14 | timeout: JOB_REQUEST_TIMEOUT | 15 | timeout: JOB_REQUEST_TIMEOUT |
15 | } | 16 | } |
16 | 17 | ||
17 | const response = await doRequest(options) | 18 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) |
18 | const firstBody = response.body | 19 | const firstBody = response.body |
19 | 20 | ||
20 | let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT | 21 | let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT |
@@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr | |||
23 | while (nextLink && i < limit) { | 24 | while (nextLink && i < limit) { |
24 | options.uri = nextLink | 25 | options.uri = nextLink |
25 | 26 | ||
26 | const { body } = await doRequest(options) | 27 | const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options) |
27 | nextLink = body.next | 28 | nextLink = body.next |
28 | i++ | 29 | i++ |
29 | 30 | ||
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts index db4980a72..5466739c1 100644 --- a/server/lib/activitypub/process/index.ts +++ b/server/lib/activitypub/process/index.ts | |||
@@ -1,9 +1 @@ | |||
1 | export * from './process' | export * from './process' | |
2 | export * from './process-accept' | ||
3 | export * from './process-announce' | ||
4 | export * from './process-create' | ||
5 | export * from './process-delete' | ||
6 | export * from './process-follow' | ||
7 | export * from './process-like' | ||
8 | export * from './process-undo' | ||
9 | export * from './process-update' | ||
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' | |||
5 | import { VideoShareModel } from '../../../models/video/video-share' | 5 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
9 | import { Notifier } from '../../notifier' | ||
8 | 10 | ||
9 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { | 11 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { |
10 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) | 12 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) |
@@ -21,9 +23,9 @@ export { | |||
21 | async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { | 23 | async 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 cefe89db0..5f4d793a5 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,34 +1,44 @@ | |||
1 | import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' | 1 | import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' |
2 | import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers' |
7 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
8 | import { ActorModel } from '../../../models/activitypub/actor' | 6 | import { ActorModel } from '../../../models/activitypub/actor' |
9 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
10 | import { addVideoComment, resolveThread } from '../video-comments' | 7 | import { addVideoComment, resolveThread } from '../video-comments' |
11 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 9 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | import { Redis } from '../../redis' | ||
14 | import { createOrUpdateCacheFile } from '../cache-file' | 10 | import { createOrUpdateCacheFile } from '../cache-file' |
11 | import { Notifier } from '../../notifier' | ||
12 | import { processViewActivity } from './process-view' | ||
13 | import { processDislikeActivity } from './process-dislike' | ||
14 | import { processFlagActivity } from './process-flag' | ||
15 | 15 | ||
16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
17 | const activityObject = activity.object | 17 | const activityObject = activity.object |
18 | const activityType = activityObject.type | 18 | const activityType = activityObject.type |
19 | 19 | ||
20 | if (activityType === 'View') { | 20 | if (activityType === 'View') { |
21 | return processCreateView(byActor, activity) | 21 | return processViewActivity(activity, byActor) |
22 | } else if (activityType === 'Dislike') { | 22 | } |
23 | return retryTransactionWrapper(processCreateDislike, byActor, activity) | 23 | |
24 | } 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') { | ||
25 | return processCreateVideo(activity) | 33 | return processCreateVideo(activity) |
26 | } else if (activityType === 'Flag') { | 34 | } |
27 | return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) | 35 | |
28 | } else if (activityType === 'Note') { | 36 | if (activityType === 'Note') { |
29 | return retryTransactionWrapper(processCreateVideoComment, byActor, activity) | 37 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor) |
30 | } else if (activityType === 'CacheFile') { | 38 | } |
31 | return retryTransactionWrapper(processCacheFile, byActor, activity) | 39 | |
40 | if (activityType === 'CacheFile') { | ||
41 | return retryTransactionWrapper(processCacheFile, activity, byActor) | ||
32 | } | 42 | } |
33 | 43 | ||
34 | 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 }) |
@@ -46,60 +56,14 @@ export { | |||
46 | async function processCreateVideo (activity: ActivityCreate) { | 56 | async function processCreateVideo (activity: ActivityCreate) { |
47 | const videoToCreateData = activity.object as VideoTorrentObject | 57 | const videoToCreateData = activity.object as VideoTorrentObject |
48 | 58 | ||
49 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) | 59 | const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) |
50 | |||
51 | return video | ||
52 | } | ||
53 | |||
54 | async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) { | ||
55 | const dislike = activity.object as DislikeObject | ||
56 | const byAccount = byActor.Account | ||
57 | |||
58 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
59 | |||
60 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | ||
61 | |||
62 | return sequelizeTypescript.transaction(async t => { | ||
63 | const rate = { | ||
64 | type: 'dislike' as 'dislike', | ||
65 | videoId: video.id, | ||
66 | accountId: byAccount.id | ||
67 | } | ||
68 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | ||
69 | where: rate, | ||
70 | defaults: rate, | ||
71 | transaction: t | ||
72 | }) | ||
73 | if (created === true) await video.increment('dislikes', { transaction: t }) | ||
74 | |||
75 | if (video.isOwned() && created === true) { | ||
76 | // Don't resend the activity to the sender | ||
77 | const exceptions = [ byActor ] | ||
78 | |||
79 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
80 | } | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { | ||
85 | const view = activity.object as ViewObject | ||
86 | |||
87 | const options = { | ||
88 | videoObject: view.object, | ||
89 | fetchType: 'only-video' as 'only-video' | ||
90 | } | ||
91 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
92 | 60 | ||
93 | await Redis.Instance.addVideoView(video.id) | 61 | if (created) Notifier.Instance.notifyOnNewVideo(video) |
94 | 62 | ||
95 | if (video.isOwned()) { | 63 | return video |
96 | // Don't resend the activity to the sender | ||
97 | const exceptions = [ byActor ] | ||
98 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
99 | } | ||
100 | } | 64 | } |
101 | 65 | ||
102 | async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { | 66 | async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { |
103 | const cacheFile = activity.object as CacheFileObject | 67 | const cacheFile = activity.object as CacheFileObject |
104 | 68 | ||
105 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 69 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
@@ -115,29 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) | |||
115 | } | 79 | } |
116 | } | 80 | } |
117 | 81 | ||
118 | async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { | 82 | async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) { |
119 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) | ||
120 | |||
121 | const account = byActor.Account | ||
122 | if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
123 | |||
124 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object }) | ||
125 | |||
126 | return sequelizeTypescript.transaction(async t => { | ||
127 | const videoAbuseData = { | ||
128 | reporterAccountId: account.id, | ||
129 | reason: videoAbuseToCreateData.content, | ||
130 | videoId: video.id, | ||
131 | state: VideoAbuseState.PENDING | ||
132 | } | ||
133 | |||
134 | await VideoAbuseModel.create(videoAbuseData, { transaction: t }) | ||
135 | |||
136 | logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) | ||
137 | }) | ||
138 | } | ||
139 | |||
140 | async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { | ||
141 | const commentObject = activity.object as VideoCommentObject | 83 | const commentObject = activity.object as VideoCommentObject |
142 | const byAccount = byActor.Account | 84 | const byAccount = byActor.Account |
143 | 85 | ||
@@ -145,7 +87,7 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
145 | 87 | ||
146 | const { video } = await resolveThread(commentObject.inReplyTo) | 88 | const { video } = await resolveThread(commentObject.inReplyTo) |
147 | 89 | ||
148 | const { created } = await addVideoComment(video, commentObject.id) | 90 | const { comment, created } = await addVideoComment(video, commentObject.id) |
149 | 91 | ||
150 | if (video.isOwned() && created === true) { | 92 | if (video.isOwned() && created === true) { |
151 | // Don't resend the activity to the sender | 93 | // Don't resend the activity to the sender |
@@ -153,4 +95,6 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit | |||
153 | 95 | ||
154 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | 96 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) |
155 | } | 97 | } |
98 | |||
99 | if (created === true) Notifier.Instance.notifyOnNewComment(comment) | ||
156 | } | 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 @@ | |||
1 | import { ActivityCreate, ActivityDislike } from '../../../../shared' | ||
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { sequelizeTypescript } from '../../../initializers' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
8 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
9 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
10 | |||
11 | async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { | ||
12 | return retryTransactionWrapper(processDislike, activity, byActor) | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | processDislikeActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async 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 @@ | |||
1 | import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' | ||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { sequelizeTypescript } from '../../../initializers' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
8 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
9 | import { Notifier } from '../../notifier' | ||
10 | import { getAPId } from '../../../helpers/activitypub' | ||
11 | |||
12 | async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { | ||
13 | return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | processFlagActivity | ||
20 | } | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | async 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' | |||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
7 | import { sendAccept } from '../send' | 7 | import { sendAccept } from '../send' |
8 | import { Notifier } from '../../notifier' | ||
9 | import { getAPId } from '../../../helpers/activitypub' | ||
8 | 10 | ||
9 | async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { | 11 | async 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 | ||
23 | async function processFollow (actor: ActorModel, targetActorURL: string) { | 25 | async 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 f7200db61..2a04167d7 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { getVideoLikeActivityPubUrl } from '../url' | ||
9 | import { getAPId } from '../../../helpers/activitypub' | ||
8 | 10 | ||
9 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { | 11 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { |
10 | return retryTransactionWrapper(processLikeVideo, byActor, activity) | 12 | return retryTransactionWrapper(processLikeVideo, byActor, activity) |
@@ -19,7 +21,7 @@ export { | |||
19 | // --------------------------------------------------------------------------- | 21 | // --------------------------------------------------------------------------- |
20 | 22 | ||
21 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | 23 | async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { |
22 | const videoUrl = activity.object | 24 | const videoUrl = getAPId(activity.object) |
23 | 25 | ||
24 | const byAccount = byActor.Account | 26 | const byAccount = byActor.Account |
25 | 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) |
@@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { | |||
34 | } | 36 | } |
35 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ | 37 | const [ , created ] = await AccountVideoRateModel.findOrCreate({ |
36 | where: rate, | 38 | where: rate, |
37 | defaults: rate, | 39 | defaults: Object.assign({}, rate, { url: getVideoLikeActivityPubUrl(byActor, video) }), |
38 | transaction: t | 40 | transaction: t |
39 | }) | 41 | }) |
40 | if (created === true) await video.increment('likes', { transaction: t }) | 42 | if (created === true) await video.increment('likes', { transaction: t }) |
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index ff019cd8c..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 | } |
@@ -55,7 +59,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
55 | return sequelizeTypescript.transaction(async t => { | 59 | return sequelizeTypescript.transaction(async t => { |
56 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 60 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
57 | 61 | ||
58 | const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | 62 | let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t) |
63 | if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | ||
59 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) | 64 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) |
60 | 65 | ||
61 | await rate.destroy({ transaction: t }) | 66 | await rate.destroy({ transaction: t }) |
@@ -71,14 +76,17 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { | |||
71 | } | 76 | } |
72 | 77 | ||
73 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { | 78 | async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { |
74 | const dislike = activity.object.object as DislikeObject | 79 | const dislike = activity.object.type === 'Dislike' |
80 | ? activity.object | ||
81 | : activity.object.object as DislikeObject | ||
75 | 82 | ||
76 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | 83 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) |
77 | 84 | ||
78 | return sequelizeTypescript.transaction(async t => { | 85 | return sequelizeTypescript.transaction(async t => { |
79 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 86 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
80 | 87 | ||
81 | const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | 88 | let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t) |
89 | if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) | ||
82 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) | 90 | if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) |
83 | 91 | ||
84 | await rate.destroy({ transaction: t }) | 92 | await rate.destroy({ transaction: t }) |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index bd4013555..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 = { |
@@ -59,7 +59,6 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) | |||
59 | videoObject, | 59 | videoObject, |
60 | account: actor.Account, | 60 | account: actor.Account, |
61 | channel: channelActor.VideoChannel, | 61 | channel: channelActor.VideoChannel, |
62 | updateViews: true, | ||
63 | overrideTo: activity.to | 62 | overrideTo: activity.to |
64 | } | 63 | } |
65 | return updateVideoFromAP(updateOptions) | 64 | return updateVideoFromAP(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 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | ||
3 | import { forwardVideoRelatedActivity } from '../send/utils' | ||
4 | import { Redis } from '../../redis' | ||
5 | import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' | ||
6 | |||
7 | async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) { | ||
8 | return processCreateView(activity, byActor) | ||
9 | } | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | export { | ||
14 | processViewActivity | ||
15 | } | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | async 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 b263f1ea2..9dd241402 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
2 | import { getActorUrl } from '../../../helpers/activitypub' | 2 | import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { processAcceptActivity } from './process-accept' | 5 | import { processAcceptActivity } from './process-accept' |
@@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject' | |||
12 | import { processUndoActivity } from './process-undo' | 12 | import { processUndoActivity } from './process-undo' |
13 | import { processUpdateActivity } from './process-update' | 13 | import { processUpdateActivity } from './process-update' |
14 | import { getOrCreateActorAndServerAndModel } from '../actor' | 14 | import { getOrCreateActorAndServerAndModel } from '../actor' |
15 | import { processDislikeActivity } from './process-dislike' | ||
16 | import { processFlagActivity } from './process-flag' | ||
17 | import { processViewActivity } from './process-view' | ||
15 | 18 | ||
16 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { | 19 | const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise<any> } = { |
17 | Create: processCreateActivity, | 20 | Create: processCreateActivity, |
@@ -22,27 +25,41 @@ 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 | ||
28 | async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { | 34 | async function processActivities ( |
35 | activities: Activity[], | ||
36 | options: { | ||
37 | signatureActor?: ActorModel | ||
38 | inboxActor?: ActorModel | ||
39 | outboxUrl?: string | ||
40 | } = {}) { | ||
29 | const actorsCache: { [ url: string ]: ActorModel } = {} | 41 | const actorsCache: { [ url: string ]: ActorModel } = {} |
30 | 42 | ||
31 | for (const activity of activities) { | 43 | for (const activity of activities) { |
32 | if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { | 44 | if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { |
33 | 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) |
34 | continue | 46 | continue |
35 | } | 47 | } |
36 | 48 | ||
37 | const actorUrl = getActorUrl(activity.actor) | 49 | const actorUrl = getAPId(activity.actor) |
38 | 50 | ||
39 | // When we fetch remote data, we don't have signature | 51 | // When we fetch remote data, we don't have signature |
40 | if (signatureActor && actorUrl !== signatureActor.url) { | 52 | if (options.signatureActor && actorUrl !== options.signatureActor.url) { |
41 | logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) | 53 | logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url) |
42 | continue | 54 | continue |
43 | } | 55 | } |
44 | 56 | ||
45 | const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) | 57 | if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) { |
58 | logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl) | ||
59 | continue | ||
60 | } | ||
61 | |||
62 | const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) | ||
46 | actorsCache[actorUrl] = byActor | 63 | actorsCache[actorUrl] = byActor |
47 | 64 | ||
48 | const activityProcessor = processActivity[activity.type] | 65 | const activityProcessor = processActivity[activity.type] |
@@ -52,7 +69,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor | |||
52 | } | 69 | } |
53 | 70 | ||
54 | try { | 71 | try { |
55 | await activityProcessor(activity, byActor, inboxActor) | 72 | await activityProcessor(activity, byActor, options.inboxActor) |
56 | } catch (err) { | 73 | } catch (err) { |
57 | logger.warn('Cannot process activity %s.', activity.type, { err }) | 74 | logger.warn('Cannot process activity %s.', activity.type, { err }) |
58 | } | 75 | } |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 285edba3b..ef20e404c 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -3,9 +3,7 @@ import { ActivityAudience, ActivityCreate } from '../../../../shared/models/acti | |||
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../../shared/models/videos' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
6 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' | ||
9 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
10 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 8 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
11 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
@@ -25,31 +23,14 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) { | |||
25 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) | 23 | return broadcastToFollowers(createActivity, byActor, [ byActor ], t) |
26 | } | 24 | } |
27 | 25 | ||
28 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | 26 | async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { |
29 | if (!video.VideoChannel.Account.Actor.serverId) return // Local | ||
30 | |||
31 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
32 | |||
33 | logger.info('Creating job to send video abuse %s.', url) | ||
34 | |||
35 | // Custom audience, we only send the abuse to the origin instance | ||
36 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
37 | const createActivity = buildCreateActivity(url, byActor, videoAbuse.toActivityPubObject(), audience) | ||
38 | |||
39 | return unicastTo(createActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
40 | } | ||
41 | |||
42 | async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) { | ||
43 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 27 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) |
44 | 28 | ||
45 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id) | ||
46 | const redundancyObject = fileRedundancy.toActivityPubObject() | ||
47 | |||
48 | return sendVideoRelatedCreateActivity({ | 29 | return sendVideoRelatedCreateActivity({ |
49 | byActor, | 30 | byActor, |
50 | video, | 31 | video, |
51 | url: fileRedundancy.url, | 32 | url: fileRedundancy.url, |
52 | object: redundancyObject | 33 | object: fileRedundancy.toActivityPubObject() |
53 | }) | 34 | }) |
54 | } | 35 | } |
55 | 36 | ||
@@ -91,37 +72,6 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio | |||
91 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) | 72 | return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) |
92 | } | 73 | } |
93 | 74 | ||
94 | async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
95 | logger.info('Creating job to send view of %s.', video.url) | ||
96 | |||
97 | const url = getVideoViewActivityPubUrl(byActor, video) | ||
98 | const viewActivity = buildViewActivity(byActor, video) | ||
99 | |||
100 | return sendVideoRelatedCreateActivity({ | ||
101 | // Use the server actor to send the view | ||
102 | byActor, | ||
103 | video, | ||
104 | url, | ||
105 | object: viewActivity, | ||
106 | transaction: t | ||
107 | }) | ||
108 | } | ||
109 | |||
110 | async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
111 | logger.info('Creating job to dislike %s.', video.url) | ||
112 | |||
113 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
114 | const dislikeActivity = buildDislikeActivity(byActor, video) | ||
115 | |||
116 | return sendVideoRelatedCreateActivity({ | ||
117 | byActor, | ||
118 | video, | ||
119 | url, | ||
120 | object: dislikeActivity, | ||
121 | transaction: t | ||
122 | }) | ||
123 | } | ||
124 | |||
125 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { | 75 | function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { |
126 | if (!audience) audience = getAudience(byActor) | 76 | if (!audience) audience = getAudience(byActor) |
127 | 77 | ||
@@ -136,31 +86,11 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud | |||
136 | ) | 86 | ) |
137 | } | 87 | } |
138 | 88 | ||
139 | function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { | ||
140 | return { | ||
141 | type: 'Dislike', | ||
142 | actor: byActor.url, | ||
143 | object: video.url | ||
144 | } | ||
145 | } | ||
146 | |||
147 | function buildViewActivity (byActor: ActorModel, video: VideoModel) { | ||
148 | return { | ||
149 | type: 'View', | ||
150 | actor: byActor.url, | ||
151 | object: video.url | ||
152 | } | ||
153 | } | ||
154 | |||
155 | // --------------------------------------------------------------------------- | 89 | // --------------------------------------------------------------------------- |
156 | 90 | ||
157 | export { | 91 | export { |
158 | sendCreateVideo, | 92 | sendCreateVideo, |
159 | sendVideoAbuse, | ||
160 | buildCreateActivity, | 93 | buildCreateActivity, |
161 | sendCreateView, | ||
162 | sendCreateDislike, | ||
163 | buildDislikeActivity, | ||
164 | sendCreateVideoComment, | 94 | sendCreateVideoComment, |
165 | sendCreateCacheFile | 95 | sendCreateCacheFile |
166 | } | 96 | } |
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts new file mode 100644 index 000000000..a88436f2c --- /dev/null +++ b/server/lib/activitypub/send/send-dislike.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActorModel } from '../../../models/activitypub/actor' | ||
3 | import { VideoModel } from '../../../models/video/video' | ||
4 | import { getVideoDislikeActivityPubUrl } from '../url' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub' | ||
7 | import { sendVideoRelatedActivity } from './utils' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to dislike %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoDislikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildDislikeActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'Dislike' as 'Dislike', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendDislike, | ||
40 | buildDislikeActivity | ||
41 | } | ||
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts new file mode 100644 index 000000000..96a7311b9 --- /dev/null +++ b/server/lib/activitypub/send/send-flag.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | ||
2 | import { VideoModel } from '../../../models/video/video' | ||
3 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | ||
4 | import { getVideoAbuseActivityPubUrl } from '../url' | ||
5 | import { unicastTo } from './utils' | ||
6 | import { logger } from '../../../helpers/logger' | ||
7 | import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' | ||
8 | import { audiencify, getAudience } from '../audience' | ||
9 | |||
10 | async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { | ||
11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user | ||
12 | |||
13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | ||
14 | |||
15 | logger.info('Creating job to send video abuse %s.', url) | ||
16 | |||
17 | // Custom audience, we only send the abuse to the origin instance | ||
18 | const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } | ||
19 | const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience) | ||
20 | |||
21 | return unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
22 | } | ||
23 | |||
24 | function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag { | ||
25 | if (!audience) audience = getAudience(byActor) | ||
26 | |||
27 | const activity = Object.assign( | ||
28 | { id: url, actor: byActor.url }, | ||
29 | videoAbuse.toActivityPubObject() | ||
30 | ) | ||
31 | |||
32 | return audiencify(activity, audience) | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | sendVideoAbuse | ||
39 | } | ||
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 89307acc6..35227887a 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts | |||
@@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel, | |||
24 | 24 | ||
25 | return audiencify( | 25 | return audiencify( |
26 | { | 26 | { |
27 | type: 'Like' as 'Like', | ||
28 | id: url, | 27 | id: url, |
28 | type: 'Like' as 'Like', | ||
29 | actor: byActor.url, | 29 | actor: byActor.url, |
30 | object: video.url | 30 | object: video.url |
31 | }, | 31 | }, |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 5236d2cb3..ecbf605d6 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { | 2 | import { |
3 | ActivityAnnounce, | 3 | ActivityAnnounce, |
4 | ActivityAudience, | 4 | ActivityAudience, |
5 | ActivityCreate, | 5 | ActivityCreate, ActivityDislike, |
6 | ActivityFollow, | 6 | ActivityFollow, |
7 | ActivityLike, | 7 | ActivityLike, |
8 | ActivityUndo | 8 | ActivityUndo |
@@ -13,13 +13,14 @@ import { VideoModel } from '../../../models/video/video' | |||
13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' | 13 | import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' |
14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 14 | import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
15 | import { audiencify, getAudience } from '../audience' | 15 | import { audiencify, getAudience } from '../audience' |
16 | import { buildCreateActivity, buildDislikeActivity } from './send-create' | 16 | import { buildCreateActivity } from './send-create' |
17 | import { buildFollowActivity } from './send-follow' | 17 | import { buildFollowActivity } from './send-follow' |
18 | import { buildLikeActivity } from './send-like' | 18 | import { buildLikeActivity } from './send-like' |
19 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
20 | import { buildAnnounceWithVideoAudience } from './send-announce' | 20 | import { buildAnnounceWithVideoAudience } from './send-announce' |
21 | import { logger } from '../../../helpers/logger' | 21 | import { logger } from '../../../helpers/logger' |
22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
23 | import { buildDislikeActivity } from './send-dislike' | ||
23 | 24 | ||
24 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { | 25 | async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { |
25 | const me = actorFollow.ActorFollower | 26 | const me = actorFollow.ActorFollower |
@@ -64,16 +65,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans | |||
64 | logger.info('Creating job to undo a dislike of video %s.', video.url) | 65 | logger.info('Creating job to undo a dislike of video %s.', video.url) |
65 | 66 | ||
66 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) | 67 | const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) |
67 | const dislikeActivity = buildDislikeActivity(byActor, video) | 68 | const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) |
68 | const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) | ||
69 | 69 | ||
70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) | 70 | return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) |
71 | } | 71 | } |
72 | 72 | ||
73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { | 73 | async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { |
74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) | 74 | logger.info('Creating job to undo cache file %s.', redundancyModel.url) |
75 | 75 | ||
76 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 76 | const videoId = redundancyModel.getVideo().id |
77 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
77 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) | 78 | const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) |
78 | 79 | ||
79 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) | 80 | return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) |
@@ -94,7 +95,7 @@ export { | |||
94 | function undoActivityData ( | 95 | function undoActivityData ( |
95 | url: string, | 96 | url: string, |
96 | byActor: ActorModel, | 97 | byActor: ActorModel, |
97 | object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 98 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
98 | audience?: ActivityAudience | 99 | audience?: ActivityAudience |
99 | ): ActivityUndo { | 100 | ): ActivityUndo { |
100 | if (!audience) audience = getAudience(byActor) | 101 | if (!audience) audience = getAudience(byActor) |
@@ -114,7 +115,7 @@ async function sendUndoVideoRelatedActivity (options: { | |||
114 | byActor: ActorModel, | 115 | byActor: ActorModel, |
115 | video: VideoModel, | 116 | video: VideoModel, |
116 | url: string, | 117 | url: string, |
117 | activity: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, | 118 | activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, |
118 | transaction: Transaction | 119 | transaction: Transaction |
119 | }) { | 120 | }) { |
120 | const activityBuilder = (audience: ActivityAudience) => { | 121 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index a68f03edf..839f66470 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -61,7 +61,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod | |||
61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { | 61 | async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { |
62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) | 62 | logger.info('Creating job to update cache file %s.', redundancyModel.url) |
63 | 63 | ||
64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) | 64 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) |
65 | 65 | ||
66 | const activityBuilder = (audience: ActivityAudience) => { | 66 | const activityBuilder = (audience: ActivityAudience) => { |
67 | const redundancyObject = redundancyModel.toActivityPubObject() | 67 | const redundancyObject = redundancyModel.toActivityPubObject() |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts new file mode 100644 index 000000000..8ad126be0 --- /dev/null +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' | ||
3 | import { ActorModel } from '../../../models/activitypub/actor' | ||
4 | import { VideoModel } from '../../../models/video/video' | ||
5 | import { getVideoLikeActivityPubUrl } from '../url' | ||
6 | import { sendVideoRelatedActivity } from './utils' | ||
7 | import { audiencify, getAudience } from '../audience' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | |||
10 | async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
11 | logger.info('Creating job to send view of %s.', video.url) | ||
12 | |||
13 | const activityBuilder = (audience: ActivityAudience) => { | ||
14 | const url = getVideoLikeActivityPubUrl(byActor, video) | ||
15 | |||
16 | return buildViewActivity(url, byActor, video, audience) | ||
17 | } | ||
18 | |||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | ||
20 | } | ||
21 | |||
22 | function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView { | ||
23 | if (!audience) audience = getAudience(byActor) | ||
24 | |||
25 | return audiencify( | ||
26 | { | ||
27 | id: url, | ||
28 | type: 'View' as 'View', | ||
29 | actor: byActor.url, | ||
30 | object: video.url | ||
31 | }, | ||
32 | audience | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | // --------------------------------------------------------------------------- | ||
37 | |||
38 | export { | ||
39 | sendView | ||
40 | } | ||
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 3ff60a97c..1767df0ae 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils' | |||
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { VideoShareModel } from '../../models/video/video-share' | 5 | import { VideoShareModel } from '../../models/video/video-share' |
6 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 6 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
7 | import { getAnnounceActivityPubUrl } from './url' | 7 | import { getVideoAnnounceActivityPubUrl } from './url' |
8 | import { VideoChannelModel } from '../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../models/video/video-channel' |
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { doRequest } from '../../helpers/requests' | 10 | import { doRequest } from '../../helpers/requests' |
11 | import { getOrCreateActorAndServerAndModel } from './actor' | 11 | import { getOrCreateActorAndServerAndModel } from './actor' |
12 | import { logger } from '../../helpers/logger' | 12 | import { logger } from '../../helpers/logger' |
13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 13 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
14 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
14 | 15 | ||
15 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { |
16 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 17 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
@@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) { | |||
38 | json: true, | 39 | json: true, |
39 | activityPub: true | 40 | activityPub: true |
40 | }) | 41 | }) |
41 | if (!body || !body.actor) throw new Error('Body of body actor is invalid') | 42 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
43 | |||
44 | const actorUrl = getAPId(body.actor) | ||
45 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
46 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
47 | } | ||
42 | 48 | ||
43 | const actorUrl = body.actor | ||
44 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 49 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
45 | 50 | ||
46 | const entry = { | 51 | const entry = { |
@@ -72,8 +77,8 @@ export { | |||
72 | async function shareByServer (video: VideoModel, t: Transaction) { | 77 | async function shareByServer (video: VideoModel, t: Transaction) { |
73 | const serverActor = await getServerActor() | 78 | const serverActor = await getServerActor() |
74 | 79 | ||
75 | const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) | 80 | const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) |
76 | return VideoShareModel.findOrCreate({ | 81 | const [ serverShare ] = await VideoShareModel.findOrCreate({ |
77 | defaults: { | 82 | defaults: { |
78 | actorId: serverActor.id, | 83 | actorId: serverActor.id, |
79 | videoId: video.id, | 84 | videoId: video.id, |
@@ -83,16 +88,14 @@ async function shareByServer (video: VideoModel, t: Transaction) { | |||
83 | url: serverShareUrl | 88 | url: serverShareUrl |
84 | }, | 89 | }, |
85 | transaction: t | 90 | transaction: t |
86 | }).then(([ serverShare, created ]) => { | ||
87 | if (created) return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
88 | |||
89 | return undefined | ||
90 | }) | 91 | }) |
92 | |||
93 | return sendVideoAnnounce(serverActor, serverShare, video, t) | ||
91 | } | 94 | } |
92 | 95 | ||
93 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { | 96 | async function shareByVideoChannel (video: VideoModel, t: Transaction) { |
94 | const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) | 97 | const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) |
95 | return VideoShareModel.findOrCreate({ | 98 | const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ |
96 | defaults: { | 99 | defaults: { |
97 | actorId: video.VideoChannel.actorId, | 100 | actorId: video.VideoChannel.actorId, |
98 | videoId: video.id, | 101 | videoId: video.id, |
@@ -102,11 +105,9 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) { | |||
102 | url: videoChannelShareUrl | 105 | url: videoChannelShareUrl |
103 | }, | 106 | }, |
104 | transaction: t | 107 | transaction: t |
105 | }).then(([ videoChannelShare, created ]) => { | ||
106 | if (created) return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
107 | |||
108 | return undefined | ||
109 | }) | 108 | }) |
109 | |||
110 | return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) | ||
110 | } | 111 | } |
111 | 112 | ||
112 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { | 113 | async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index e792be698..4229fe094 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -5,6 +5,8 @@ import { VideoModel } from '../../models/video/video' | |||
5 | import { VideoAbuseModel } from '../../models/video/video-abuse' | 5 | import { VideoAbuseModel } from '../../models/video/video-abuse' |
6 | import { VideoCommentModel } from '../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../models/video/video-comment' |
7 | import { VideoFileModel } from '../../models/video/video-file' | 7 | import { VideoFileModel } from '../../models/video/video-file' |
8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | ||
9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
8 | 10 | ||
9 | function getVideoActivityPubUrl (video: VideoModel) { | 11 | function getVideoActivityPubUrl (video: VideoModel) { |
10 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 12 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
@@ -16,6 +18,10 @@ function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { | |||
16 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` | 18 | return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` |
17 | } | 19 | } |
18 | 20 | ||
21 | function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { | ||
22 | return `${CONFIG.WEBSERVER.URL}/redundancy/video-playlists/${playlist.getStringType()}/${video.uuid}` | ||
23 | } | ||
24 | |||
19 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { | 25 | function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { |
20 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id | 26 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id |
21 | } | 27 | } |
@@ -33,14 +39,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { | |||
33 | } | 39 | } |
34 | 40 | ||
35 | function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 41 | function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { |
36 | return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() | 42 | return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString() |
37 | } | 43 | } |
38 | 44 | ||
39 | function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 45 | function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) { |
40 | return byActor.url + '/likes/' + video.id | 46 | return byActor.url + '/likes/' + video.id |
41 | } | 47 | } |
42 | 48 | ||
43 | function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { | 49 | function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) { |
44 | return byActor.url + '/dislikes/' + video.id | 50 | return byActor.url + '/dislikes/' + video.id |
45 | } | 51 | } |
46 | 52 | ||
@@ -74,8 +80,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) { | |||
74 | return follower.url + '/accepts/follows/' + me.id | 80 | return follower.url + '/accepts/follows/' + me.id |
75 | } | 81 | } |
76 | 82 | ||
77 | function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { | 83 | function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) { |
78 | return originalUrl + '/announces/' + byActor.id | 84 | return video.url + '/announces/' + byActor.id |
79 | } | 85 | } |
80 | 86 | ||
81 | function getDeleteActivityPubUrl (originalUrl: string) { | 87 | function getDeleteActivityPubUrl (originalUrl: string) { |
@@ -92,12 +98,13 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
92 | 98 | ||
93 | export { | 99 | export { |
94 | getVideoActivityPubUrl, | 100 | getVideoActivityPubUrl, |
101 | getVideoCacheStreamingPlaylistActivityPubUrl, | ||
95 | getVideoChannelActivityPubUrl, | 102 | getVideoChannelActivityPubUrl, |
96 | getAccountActivityPubUrl, | 103 | getAccountActivityPubUrl, |
97 | getVideoAbuseActivityPubUrl, | 104 | getVideoAbuseActivityPubUrl, |
98 | getActorFollowActivityPubUrl, | 105 | getActorFollowActivityPubUrl, |
99 | getActorFollowAcceptActivityPubUrl, | 106 | getActorFollowAcceptActivityPubUrl, |
100 | getAnnounceActivityPubUrl, | 107 | getVideoAnnounceActivityPubUrl, |
101 | getUpdateActivityPubUrl, | 108 | getUpdateActivityPubUrl, |
102 | getUndoActivityPubUrl, | 109 | getUndoActivityPubUrl, |
103 | getVideoViewActivityPubUrl, | 110 | getVideoViewActivityPubUrl, |
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index c8c17f4c4..e87301fe7 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment' | |||
9 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateActorAndServerAndModel } from './actor' |
10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' |
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
12 | 13 | ||
13 | async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { | 14 | async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { |
14 | let originCommentId: number = null | 15 | let originCommentId: number = null |
@@ -61,7 +62,15 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
61 | const actorUrl = body.attributedTo | 62 | const actorUrl = body.attributedTo |
62 | if (!actorUrl) return { created: false } | 63 | if (!actorUrl) return { created: false } |
63 | 64 | ||
64 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 65 | if (checkUrlsSameHost(commentUrl, actorUrl) !== true) { |
66 | throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`) | ||
67 | } | ||
68 | |||
69 | if (checkUrlsSameHost(body.id, commentUrl) !== true) { | ||
70 | throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) | ||
71 | } | ||
72 | |||
73 | const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') | ||
65 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) | 74 | const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) |
66 | if (!entry) return { created: false } | 75 | if (!entry) return { created: false } |
67 | 76 | ||
@@ -71,6 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) { | |||
71 | }, | 80 | }, |
72 | defaults: entry | 81 | defaults: entry |
73 | }) | 82 | }) |
83 | comment.Account = actor.Account | ||
84 | comment.Video = videoInstance | ||
74 | 85 | ||
75 | return { comment, created } | 86 | return { comment, created } |
76 | } | 87 | } |
@@ -134,6 +145,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) { | |||
134 | const actorUrl = body.attributedTo | 145 | const actorUrl = body.attributedTo |
135 | if (!actorUrl) throw new Error('Miss attributed to in comment') | 146 | if (!actorUrl) throw new Error('Miss attributed to in comment') |
136 | 147 | ||
148 | if (checkUrlsSameHost(url, actorUrl) !== true) { | ||
149 | throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`) | ||
150 | } | ||
151 | |||
152 | if (checkUrlsSameHost(body.id, url) !== true) { | ||
153 | throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`) | ||
154 | } | ||
155 | |||
137 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 156 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
138 | const comment = new VideoCommentModel({ | 157 | const comment = new VideoCommentModel({ |
139 | url: body.id, | 158 | url: body.id, |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 1619251c3..7aac79118 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -1,20 +1,43 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { AccountModel } from '../../models/account/account' | 2 | import { AccountModel } from '../../models/account/account' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send' | 4 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' |
5 | import { VideoRateType } from '../../../shared/models/videos' | 5 | import { VideoRateType } from '../../../shared/models/videos' |
6 | import * as Bluebird from 'bluebird' | 6 | import * as Bluebird from 'bluebird' |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | 7 | import { getOrCreateActorAndServerAndModel } from './actor' |
8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
9 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' | 10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' |
11 | import { doRequest } from '../../helpers/requests' | ||
12 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
13 | import { ActorModel } from '../../models/activitypub/actor' | ||
14 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' | ||
15 | import { sendDislike } from './send/send-dislike' | ||
11 | 16 | ||
12 | async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { | 17 | async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { |
13 | let rateCounts = 0 | 18 | let rateCounts = 0 |
14 | 19 | ||
15 | await Bluebird.map(actorUrls, async actorUrl => { | 20 | await Bluebird.map(ratesUrl, async rateUrl => { |
16 | try { | 21 | try { |
22 | // Fetch url | ||
23 | const { body } = await doRequest({ | ||
24 | uri: rateUrl, | ||
25 | json: true, | ||
26 | activityPub: true | ||
27 | }) | ||
28 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
29 | |||
30 | const actorUrl = getAPId(body.actor) | ||
31 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
32 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
33 | } | ||
34 | |||
35 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
36 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
37 | } | ||
38 | |||
17 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 39 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) |
40 | |||
18 | const [ , created ] = await AccountVideoRateModel | 41 | const [ , created ] = await AccountVideoRateModel |
19 | .findOrCreate({ | 42 | .findOrCreate({ |
20 | where: { | 43 | where: { |
@@ -24,13 +47,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR | |||
24 | defaults: { | 47 | defaults: { |
25 | videoId: video.id, | 48 | videoId: video.id, |
26 | accountId: actor.Account.id, | 49 | accountId: actor.Account.id, |
27 | type: rate | 50 | type: rate, |
51 | url: body.id | ||
28 | } | 52 | } |
29 | }) | 53 | }) |
30 | 54 | ||
31 | if (created) rateCounts += 1 | 55 | if (created) rateCounts += 1 |
32 | } catch (err) { | 56 | } catch (err) { |
33 | logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) | 57 | logger.warn('Cannot add rate %s.', rateUrl, { err }) |
34 | } | 58 | } |
35 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 59 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
36 | 60 | ||
@@ -59,10 +83,15 @@ async function sendVideoRateChange (account: AccountModel, | |||
59 | // Like | 83 | // Like |
60 | if (likes > 0) await sendLike(actor, video, t) | 84 | if (likes > 0) await sendLike(actor, video, t) |
61 | // Dislike | 85 | // Dislike |
62 | if (dislikes > 0) await sendCreateDislike(actor, video, t) | 86 | if (dislikes > 0) await sendDislike(actor, video, t) |
87 | } | ||
88 | |||
89 | function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { | ||
90 | return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video) | ||
63 | } | 91 | } |
64 | 92 | ||
65 | export { | 93 | export { |
94 | getRateUrl, | ||
66 | createRates, | 95 | createRates, |
67 | sendVideoRateChange | 96 | sendVideoRateChange |
68 | } | 97 | } |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 8521572a1..710929aac 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,17 +1,23 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as sequelize from 'sequelize' | 2 | import * as sequelize from 'sequelize' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { join } from 'path' | ||
5 | import * as request from 'request' | 4 | import * as request from 'request' |
6 | import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | ||
8 | ActivityPlaylistUrlObject, | ||
9 | ActivityUrlObject, | ||
10 | ActivityVideoUrlObject, | ||
11 | VideoState | ||
12 | } from '../../../shared/index' | ||
7 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
8 | import { VideoPrivacy } from '../../../shared/models/videos' | 14 | import { VideoPrivacy } from '../../../shared/models/videos' |
9 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 15 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
10 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 16 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
11 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
12 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 19 | import { doRequest, downloadImage } from '../../helpers/requests' |
14 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' | 20 | import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' |
15 | import { ActorModel } from '../../models/activitypub/actor' | 21 | import { ActorModel } from '../../models/activitypub/actor' |
16 | import { TagModel } from '../../models/video/tag' | 22 | import { TagModel } from '../../models/video/tag' |
17 | import { VideoModel } from '../../models/video/video' | 23 | import { VideoModel } from '../../models/video/video' |
@@ -29,6 +35,11 @@ import { createRates } from './video-rates' | |||
29 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 35 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
30 | import { AccountModel } from '../../models/account/account' | 36 | import { AccountModel } from '../../models/account/account' |
31 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 37 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
38 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
39 | import { Notifier } from '../notifier' | ||
40 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
41 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
42 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
32 | 43 | ||
33 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 44 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
34 | // If the video is not private and published, we federate it | 45 | // If the video is not private and published, we federate it |
@@ -63,7 +74,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request. | |||
63 | 74 | ||
64 | const { response, body } = await doRequest(options) | 75 | const { response, body } = await doRequest(options) |
65 | 76 | ||
66 | if (sanitizeAndCheckVideoTorrentObject(body) === false) { | 77 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
67 | logger.debug('Remote video JSON is not valid.', { body }) | 78 | logger.debug('Remote video JSON is not valid.', { body }) |
68 | return { response, videoObject: undefined } | 79 | return { response, videoObject: undefined } |
69 | } | 80 | } |
@@ -94,19 +105,18 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu | |||
94 | 105 | ||
95 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 106 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
96 | const thumbnailName = video.getThumbnailName() | 107 | const thumbnailName = video.getThumbnailName() |
97 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
98 | 108 | ||
99 | const options = { | 109 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) |
100 | method: 'GET', | ||
101 | uri: icon.url | ||
102 | } | ||
103 | return doRequestAndSaveToFile(options, thumbnailPath) | ||
104 | } | 110 | } |
105 | 111 | ||
106 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 112 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
107 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | 113 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') |
108 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | 114 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) |
109 | 115 | ||
116 | if (checkUrlsSameHost(channel.id, videoObject.id) !== true) { | ||
117 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`) | ||
118 | } | ||
119 | |||
110 | return getOrCreateActorAndServerAndModel(channel.id, 'all') | 120 | return getOrCreateActorAndServerAndModel(channel.id, 'all') |
111 | } | 121 | } |
112 | 122 | ||
@@ -116,7 +126,7 @@ type SyncParam = { | |||
116 | shares: boolean | 126 | shares: boolean |
117 | comments: boolean | 127 | comments: boolean |
118 | thumbnail: boolean | 128 | thumbnail: boolean |
119 | refreshVideo: boolean | 129 | refreshVideo?: boolean |
120 | } | 130 | } |
121 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { | 131 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { |
122 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) | 132 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) |
@@ -155,31 +165,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
155 | } | 165 | } |
156 | 166 | ||
157 | async function getOrCreateVideoAndAccountAndChannel (options: { | 167 | async function getOrCreateVideoAndAccountAndChannel (options: { |
158 | videoObject: VideoTorrentObject | string, | 168 | videoObject: { id: string } | string, |
159 | syncParam?: SyncParam, | 169 | syncParam?: SyncParam, |
160 | fetchType?: VideoFetchByUrlType, | 170 | fetchType?: VideoFetchByUrlType, |
161 | refreshViews?: boolean | 171 | allowRefresh?: boolean // true by default |
162 | }) { | 172 | }) { |
163 | // Default params | 173 | // Default params |
164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 174 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
165 | const fetchType = options.fetchType || 'all' | 175 | const fetchType = options.fetchType || 'all' |
166 | const refreshViews = options.refreshViews || false | 176 | const allowRefresh = options.allowRefresh !== false |
167 | 177 | ||
168 | // Get video url | 178 | // Get video url |
169 | const videoUrl = typeof options.videoObject === 'string' ? options.videoObject : options.videoObject.id | 179 | const videoUrl = getAPId(options.videoObject) |
170 | 180 | ||
171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 181 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
172 | if (videoFromDatabase) { | 182 | if (videoFromDatabase) { |
173 | const refreshOptions = { | 183 | |
174 | video: videoFromDatabase, | 184 | if (allowRefresh === true) { |
175 | fetchedType: fetchType, | 185 | const refreshOptions = { |
176 | syncParam, | 186 | video: videoFromDatabase, |
177 | refreshViews | 187 | fetchedType: fetchType, |
188 | syncParam | ||
189 | } | ||
190 | |||
191 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
192 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) | ||
178 | } | 193 | } |
179 | const p = refreshVideoIfNeeded(refreshOptions) | ||
180 | if (syncParam.refreshVideo === true) videoFromDatabase = await p | ||
181 | 194 | ||
182 | return { video: videoFromDatabase } | 195 | return { video: videoFromDatabase, created: false } |
183 | } | 196 | } |
184 | 197 | ||
185 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) | 198 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) |
@@ -190,7 +203,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
190 | 203 | ||
191 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) | 204 | await syncVideoExternalAttributes(video, fetchedVideo, syncParam) |
192 | 205 | ||
193 | return { video } | 206 | return { video, created: true } |
194 | } | 207 | } |
195 | 208 | ||
196 | async function updateVideoFromAP (options: { | 209 | async function updateVideoFromAP (options: { |
@@ -198,17 +211,17 @@ async function updateVideoFromAP (options: { | |||
198 | videoObject: VideoTorrentObject, | 211 | videoObject: VideoTorrentObject, |
199 | account: AccountModel, | 212 | account: AccountModel, |
200 | channel: VideoChannelModel, | 213 | channel: VideoChannelModel, |
201 | updateViews: boolean, | ||
202 | overrideTo?: string[] | 214 | overrideTo?: string[] |
203 | }) { | 215 | }) { |
204 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) | 216 | logger.debug('Updating remote video "%s".', options.videoObject.uuid) |
217 | |||
205 | let videoFieldsSave: any | 218 | let videoFieldsSave: any |
219 | const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE | ||
220 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED | ||
206 | 221 | ||
207 | try { | 222 | try { |
208 | await sequelizeTypescript.transaction(async t => { | 223 | await sequelizeTypescript.transaction(async t => { |
209 | const sequelizeOptions = { | 224 | const sequelizeOptions = { transaction: t } |
210 | transaction: t | ||
211 | } | ||
212 | 225 | ||
213 | videoFieldsSave = options.video.toJSON() | 226 | videoFieldsSave = options.video.toJSON() |
214 | 227 | ||
@@ -238,14 +251,10 @@ async function updateVideoFromAP (options: { | |||
238 | options.video.set('publishedAt', videoData.publishedAt) | 251 | options.video.set('publishedAt', videoData.publishedAt) |
239 | options.video.set('privacy', videoData.privacy) | 252 | options.video.set('privacy', videoData.privacy) |
240 | options.video.set('channelId', videoData.channelId) | 253 | options.video.set('channelId', videoData.channelId) |
254 | options.video.set('views', videoData.views) | ||
241 | 255 | ||
242 | if (options.updateViews === true) options.video.set('views', videoData.views) | ||
243 | await options.video.save(sequelizeOptions) | 256 | await options.video.save(sequelizeOptions) |
244 | 257 | ||
245 | // Don't block on request | ||
246 | generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
247 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })) | ||
248 | |||
249 | { | 258 | { |
250 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) | 259 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) |
251 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 260 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
@@ -266,6 +275,25 @@ async function updateVideoFromAP (options: { | |||
266 | } | 275 | } |
267 | 276 | ||
268 | { | 277 | { |
278 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject) | ||
279 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
280 | |||
281 | // Remove video files that do not exist anymore | ||
282 | const destroyTasks = options.video.VideoStreamingPlaylists | ||
283 | .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) | ||
284 | .map(f => f.destroy(sequelizeOptions)) | ||
285 | await Promise.all(destroyTasks) | ||
286 | |||
287 | // Update or add other one | ||
288 | const upsertTasks = streamingPlaylistAttributes.map(a => { | ||
289 | return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) | ||
290 | .then(([ streamingPlaylist ]) => streamingPlaylist) | ||
291 | }) | ||
292 | |||
293 | options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks) | ||
294 | } | ||
295 | |||
296 | { | ||
269 | // Update Tags | 297 | // Update Tags |
270 | const tags = options.videoObject.tag.map(tag => tag.name) | 298 | const tags = options.videoObject.tag.map(tag => tag.name) |
271 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 299 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
@@ -283,6 +311,11 @@ async function updateVideoFromAP (options: { | |||
283 | } | 311 | } |
284 | }) | 312 | }) |
285 | 313 | ||
314 | // Notify our users? | ||
315 | if (wasPrivateVideo || wasUnlistedVideo) { | ||
316 | Notifier.Instance.notifyOnNewVideo(options.video) | ||
317 | } | ||
318 | |||
286 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) | 319 | logger.info('Remote video with uuid %s updated', options.videoObject.uuid) |
287 | } catch (err) { | 320 | } catch (err) { |
288 | if (options.video !== undefined && videoFieldsSave !== undefined) { | 321 | if (options.video !== undefined && videoFieldsSave !== undefined) { |
@@ -293,10 +326,66 @@ async function updateVideoFromAP (options: { | |||
293 | logger.debug('Cannot update the remote video.', { err }) | 326 | logger.debug('Cannot update the remote video.', { err }) |
294 | throw err | 327 | throw err |
295 | } | 328 | } |
329 | |||
330 | try { | ||
331 | await generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
332 | } catch (err) { | ||
333 | logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) | ||
334 | } | ||
335 | } | ||
336 | |||
337 | async function refreshVideoIfNeeded (options: { | ||
338 | video: VideoModel, | ||
339 | fetchedType: VideoFetchByUrlType, | ||
340 | syncParam: SyncParam | ||
341 | }): Promise<VideoModel> { | ||
342 | if (!options.video.isOutdated()) return options.video | ||
343 | |||
344 | // We need more attributes if the argument video was fetched with not enough joints | ||
345 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
346 | |||
347 | try { | ||
348 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
349 | if (response.statusCode === 404) { | ||
350 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
351 | |||
352 | // Video does not exist anymore | ||
353 | await video.destroy() | ||
354 | return undefined | ||
355 | } | ||
356 | |||
357 | if (videoObject === undefined) { | ||
358 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
359 | |||
360 | await video.setAsRefreshed() | ||
361 | return video | ||
362 | } | ||
363 | |||
364 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
365 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
366 | |||
367 | const updateOptions = { | ||
368 | video, | ||
369 | videoObject, | ||
370 | account, | ||
371 | channel: channelActor.VideoChannel | ||
372 | } | ||
373 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
374 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
375 | |||
376 | return video | ||
377 | } catch (err) { | ||
378 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
379 | |||
380 | // Don't refresh in loop | ||
381 | await video.setAsRefreshed() | ||
382 | return video | ||
383 | } | ||
296 | } | 384 | } |
297 | 385 | ||
298 | export { | 386 | export { |
299 | updateVideoFromAP, | 387 | updateVideoFromAP, |
388 | refreshVideoIfNeeded, | ||
300 | federateVideoIfNeeded, | 389 | federateVideoIfNeeded, |
301 | fetchRemoteVideo, | 390 | fetchRemoteVideo, |
302 | getOrCreateVideoAndAccountAndChannel, | 391 | getOrCreateVideoAndAccountAndChannel, |
@@ -308,10 +397,23 @@ export { | |||
308 | 397 | ||
309 | // --------------------------------------------------------------------------- | 398 | // --------------------------------------------------------------------------- |
310 | 399 | ||
311 | function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 400 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { |
312 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | 401 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
313 | 402 | ||
314 | return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') | 403 | const urlMediaType = url.mediaType || url.mimeType |
404 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | ||
405 | } | ||
406 | |||
407 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | ||
408 | const urlMediaType = url.mediaType || url.mimeType | ||
409 | |||
410 | return urlMediaType === 'application/x-mpegURL' | ||
411 | } | ||
412 | |||
413 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
414 | const urlMediaType = tag.mediaType || tag.mimeType | ||
415 | |||
416 | return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' | ||
315 | } | 417 | } |
316 | 418 | ||
317 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 419 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
@@ -334,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
334 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 436 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
335 | await Promise.all(videoFilePromises) | 437 | await Promise.all(videoFilePromises) |
336 | 438 | ||
439 | const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject) | ||
440 | const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) | ||
441 | await Promise.all(playlistPromises) | ||
442 | |||
337 | // Process tags | 443 | // Process tags |
338 | const tags = videoObject.tag.map(t => t.name) | 444 | const tags = videoObject.tag |
445 | .filter(t => t.type === 'Hashtag') | ||
446 | .map(t => t.name) | ||
339 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 447 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
340 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 448 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
341 | 449 | ||
@@ -359,52 +467,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
359 | return videoCreated | 467 | return videoCreated |
360 | } | 468 | } |
361 | 469 | ||
362 | async function refreshVideoIfNeeded (options: { | ||
363 | video: VideoModel, | ||
364 | fetchedType: VideoFetchByUrlType, | ||
365 | syncParam: SyncParam, | ||
366 | refreshViews: boolean | ||
367 | }): Promise<VideoModel> { | ||
368 | if (!options.video.isOutdated()) return options.video | ||
369 | |||
370 | // We need more attributes if the argument video was fetched with not enough joints | ||
371 | const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
372 | |||
373 | try { | ||
374 | const { response, videoObject } = await fetchRemoteVideo(video.url) | ||
375 | if (response.statusCode === 404) { | ||
376 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
377 | |||
378 | // Video does not exist anymore | ||
379 | await video.destroy() | ||
380 | return undefined | ||
381 | } | ||
382 | |||
383 | if (videoObject === undefined) { | ||
384 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
385 | return video | ||
386 | } | ||
387 | |||
388 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
389 | const account = await AccountModel.load(channelActor.VideoChannel.accountId) | ||
390 | |||
391 | const updateOptions = { | ||
392 | video, | ||
393 | videoObject, | ||
394 | account, | ||
395 | channel: channelActor.VideoChannel, | ||
396 | updateViews: options.refreshViews | ||
397 | } | ||
398 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | ||
399 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
400 | |||
401 | return video | ||
402 | } catch (err) { | ||
403 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
404 | return video | ||
405 | } | ||
406 | } | ||
407 | |||
408 | async function videoActivityObjectToDBAttributes ( | 470 | async function videoActivityObjectToDBAttributes ( |
409 | videoChannel: VideoChannelModel, | 471 | videoChannel: VideoChannelModel, |
410 | videoObject: VideoTorrentObject, | 472 | videoObject: VideoTorrentObject, |
@@ -460,17 +522,18 @@ async function videoActivityObjectToDBAttributes ( | |||
460 | } | 522 | } |
461 | 523 | ||
462 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | 524 | function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { |
463 | const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] | 525 | const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] |
464 | 526 | ||
465 | if (fileUrls.length === 0) { | 527 | if (fileUrls.length === 0) { |
466 | throw new Error('Cannot find video files for ' + video.url) | 528 | throw new Error('Cannot find video files for ' + video.url) |
467 | } | 529 | } |
468 | 530 | ||
469 | const attributes: VideoFileModel[] = [] | 531 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] |
470 | for (const fileUrl of fileUrls) { | 532 | for (const fileUrl of fileUrls) { |
471 | // Fetch associated magnet uri | 533 | // Fetch associated magnet uri |
472 | const magnet = videoObject.url.find(u => { | 534 | const magnet = videoObject.url.find(u => { |
473 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height | 535 | const mediaType = u.mediaType || u.mimeType |
536 | return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height | ||
474 | }) | 537 | }) |
475 | 538 | ||
476 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | 539 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) |
@@ -480,14 +543,53 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid | |||
480 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 543 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
481 | } | 544 | } |
482 | 545 | ||
546 | const mediaType = fileUrl.mediaType || fileUrl.mimeType | ||
483 | const attribute = { | 547 | const attribute = { |
484 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | 548 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], |
485 | infoHash: parsed.infoHash, | 549 | infoHash: parsed.infoHash, |
486 | resolution: fileUrl.height, | 550 | resolution: fileUrl.height, |
487 | size: fileUrl.size, | 551 | size: fileUrl.size, |
488 | videoId: video.id, | 552 | videoId: video.id, |
489 | fps: fileUrl.fps || -1 | 553 | fps: fileUrl.fps || -1 |
490 | } as VideoFileModel | 554 | } |
555 | |||
556 | attributes.push(attribute) | ||
557 | } | ||
558 | |||
559 | return attributes | ||
560 | } | ||
561 | |||
562 | function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { | ||
563 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
564 | if (playlistUrls.length === 0) return [] | ||
565 | |||
566 | const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] | ||
567 | for (const playlistUrlObject of playlistUrls) { | ||
568 | const p2pMediaLoaderInfohashes = playlistUrlObject.tag | ||
569 | .filter(t => t.type === 'Infohash') | ||
570 | .map(t => t.name) | ||
571 | if (p2pMediaLoaderInfohashes.length === 0) { | ||
572 | logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
573 | continue | ||
574 | } | ||
575 | |||
576 | const segmentsSha256UrlObject = playlistUrlObject.tag | ||
577 | .find(t => { | ||
578 | return isAPPlaylistSegmentHashesUrlObject(t) | ||
579 | }) as ActivityPlaylistSegmentHashesObject | ||
580 | if (!segmentsSha256UrlObject) { | ||
581 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
582 | continue | ||
583 | } | ||
584 | |||
585 | const attribute = { | ||
586 | type: VideoStreamingPlaylistType.HLS, | ||
587 | playlistUrl: playlistUrlObject.href, | ||
588 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
589 | p2pMediaLoaderInfohashes, | ||
590 | videoId: video.id | ||
591 | } | ||
592 | |||
491 | attributes.push(attribute) | 593 | attributes.push(attribute) |
492 | } | 594 | } |
493 | 595 | ||