aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/search.ts38
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/lib/activitypub/process/process-announce.ts4
-rw-r--r--server/lib/activitypub/process/process-create.ts10
-rw-r--r--server/lib/activitypub/process/process-like.ts4
-rw-r--r--server/lib/activitypub/process/process-undo.ts6
-rw-r--r--server/lib/activitypub/process/process-update.ts99
-rw-r--r--server/lib/activitypub/share.ts38
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/activitypub/video-rates.ts40
-rw-r--r--server/lib/activitypub/videos.ts241
-rw-r--r--server/models/video/video.ts27
-rw-r--r--server/tests/api/index-1.ts2
-rw-r--r--server/tests/api/index-2.ts2
-rw-r--r--server/tests/api/index-3.ts1
-rw-r--r--server/tests/api/index-fast.ts18
-rw-r--r--server/tests/api/index-slow.ts12
-rw-r--r--server/tests/api/index.ts5
-rw-r--r--server/tests/api/search/index.ts2
-rw-r--r--server/tests/api/search/search-activitypub-videos.ts161
-rw-r--r--server/tests/api/server/index.ts8
-rw-r--r--server/tests/api/users/index.ts3
-rw-r--r--server/tests/api/videos/index.ts15
-rw-r--r--server/tests/feeds/index.ts1
-rw-r--r--server/tests/index.ts3
25 files changed, 490 insertions, 264 deletions
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 9c2c7d6c1..d95e7cac9 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -13,8 +13,10 @@ import {
13 videosSearchSortValidator 13 videosSearchSortValidator
14} from '../../middlewares' 14} from '../../middlewares'
15import { VideosSearchQuery } from '../../../shared/models/search' 15import { VideosSearchQuery } from '../../../shared/models/search'
16import { getOrCreateAccountAndVideoAndChannel } from '../../lib/activitypub' 16import { getOrCreateVideoAndAccountAndChannel } from '../../lib/activitypub'
17import { logger } from '../../helpers/logger' 17import { logger } from '../../helpers/logger'
18import { User } from '../../../shared/models/users'
19import { CONFIG } from '../../initializers/constants'
18 20
19const searchRouter = express.Router() 21const searchRouter = express.Router()
20 22
@@ -56,20 +58,30 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
56 58
57async function searchVideoUrl (url: string, res: express.Response) { 59async function searchVideoUrl (url: string, res: express.Response) {
58 let video: VideoModel 60 let video: VideoModel
61 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
59 62
60 try { 63 // Check if we can fetch a remote video with the URL
61 const syncParam = { 64 if (
62 likes: false, 65 CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
63 dislikes: false, 66 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
64 shares: false, 67 ) {
65 comments: false, 68 try {
66 thumbnail: true 69 const syncParam = {
67 } 70 likes: false,
71 dislikes: false,
72 shares: false,
73 comments: false,
74 thumbnail: true,
75 refreshVideo: false
76 }
68 77
69 const res = await getOrCreateAccountAndVideoAndChannel(url, syncParam) 78 const res = await getOrCreateVideoAndAccountAndChannel(url, syncParam)
70 video = res ? res.video : undefined 79 video = res ? res.video : undefined
71 } catch (err) { 80 } catch (err) {
72 logger.info('Cannot search remote video %s.', url) 81 logger.info('Cannot search remote video %s.', url)
82 }
83 } else {
84 video = await VideoModel.loadByUrlAndPopulateAccount(url)
73 } 85 }
74 86
75 return res.json({ 87 return res.json({
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 99b10a7fc..cd709cd3f 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -181,6 +181,12 @@ const CONFIG = {
181 LOG: { 181 LOG: {
182 LEVEL: config.get<string>('log.level') 182 LEVEL: config.get<string>('log.level')
183 }, 183 },
184 SEARCH: {
185 REMOTE_URI: {
186 USERS: config.get<boolean>('search.remote_uri.users'),
187 ANONYMOUS: config.get<boolean>('search.remote_uri.anonymous')
188 }
189 },
184 ADMIN: { 190 ADMIN: {
185 get EMAIL () { return config.get<string>('admin.email') } 191 get EMAIL () { return config.get<string>('admin.email') }
186 }, 192 },
@@ -462,7 +468,8 @@ const ACTIVITY_PUB = {
462 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] 468 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
463 }, 469 },
464 MAX_RECURSION_COMMENTS: 100, 470 MAX_RECURSION_COMMENTS: 100,
465 ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 // 1 day 471 ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000, // 1 day
472 VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 // 1 day
466} 473}
467 474
468const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = { 475const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
@@ -574,6 +581,7 @@ if (isTestInstance() === true) {
574 581
575 ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2 582 ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
576 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 583 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
584 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
577 585
578 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB 586 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
579 587
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index b08156aa1..814556817 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -6,7 +6,7 @@ import { VideoModel } from '../../../models/video/video'
6import { VideoShareModel } from '../../../models/video/video-share' 6import { VideoShareModel } from '../../../models/video/video-share'
7import { getOrCreateActorAndServerAndModel } from '../actor' 7import { getOrCreateActorAndServerAndModel } from '../actor'
8import { forwardVideoRelatedActivity } from '../send/utils' 8import { forwardVideoRelatedActivity } from '../send/utils'
9import { getOrCreateAccountAndVideoAndChannel } from '../videos' 9import { getOrCreateVideoAndAccountAndChannel } from '../videos'
10 10
11async function processAnnounceActivity (activity: ActivityAnnounce) { 11async function processAnnounceActivity (activity: ActivityAnnounce) {
12 const actorAnnouncer = await getOrCreateActorAndServerAndModel(activity.actor) 12 const actorAnnouncer = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -25,7 +25,7 @@ export {
25async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { 25async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
26 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id 26 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
27 27
28 const { video } = await getOrCreateAccountAndVideoAndChannel(objectUri) 28 const { video } = await getOrCreateVideoAndAccountAndChannel(objectUri)
29 29
30 return sequelizeTypescript.transaction(async t => { 30 return sequelizeTypescript.transaction(async t => {
31 // Add share entry 31 // Add share entry
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 9655d015f..e8f5ade06 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -10,7 +10,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
10import { VideoCommentModel } from '../../../models/video/video-comment' 10import { VideoCommentModel } from '../../../models/video/video-comment'
11import { getOrCreateActorAndServerAndModel } from '../actor' 11import { getOrCreateActorAndServerAndModel } from '../actor'
12import { resolveThread } from '../video-comments' 12import { resolveThread } from '../video-comments'
13import { getOrCreateAccountAndVideoAndChannel } from '../videos' 13import { getOrCreateVideoAndAccountAndChannel } from '../videos'
14import { forwardActivity, forwardVideoRelatedActivity } from '../send/utils' 14import { forwardActivity, forwardVideoRelatedActivity } from '../send/utils'
15 15
16async function processCreateActivity (activity: ActivityCreate) { 16async function processCreateActivity (activity: ActivityCreate) {
@@ -45,7 +45,7 @@ export {
45async function processCreateVideo (activity: ActivityCreate) { 45async function processCreateVideo (activity: ActivityCreate) {
46 const videoToCreateData = activity.object as VideoTorrentObject 46 const videoToCreateData = activity.object as VideoTorrentObject
47 47
48 const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData) 48 const { video } = await getOrCreateVideoAndAccountAndChannel(videoToCreateData)
49 49
50 return video 50 return video
51} 51}
@@ -56,7 +56,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
56 56
57 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) 57 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
58 58
59 const { video } = await getOrCreateAccountAndVideoAndChannel(dislike.object) 59 const { video } = await getOrCreateVideoAndAccountAndChannel(dislike.object)
60 60
61 return sequelizeTypescript.transaction(async t => { 61 return sequelizeTypescript.transaction(async t => {
62 const rate = { 62 const rate = {
@@ -83,7 +83,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
83async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { 83async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
84 const view = activity.object as ViewObject 84 const view = activity.object as ViewObject
85 85
86 const { video } = await getOrCreateAccountAndVideoAndChannel(view.object) 86 const { video } = await getOrCreateVideoAndAccountAndChannel(view.object)
87 87
88 const actor = await ActorModel.loadByUrl(view.actor) 88 const actor = await ActorModel.loadByUrl(view.actor)
89 if (!actor) throw new Error('Unknown actor ' + view.actor) 89 if (!actor) throw new Error('Unknown actor ' + view.actor)
@@ -103,7 +103,7 @@ async function processCreateVideoAbuse (actor: ActorModel, videoAbuseToCreateDat
103 const account = actor.Account 103 const account = actor.Account
104 if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url) 104 if (!account) throw new Error('Cannot create dislike with the non account actor ' + actor.url)
105 105
106 const { video } = await getOrCreateAccountAndVideoAndChannel(videoAbuseToCreateData.object) 106 const { video } = await getOrCreateVideoAndAccountAndChannel(videoAbuseToCreateData.object)
107 107
108 return sequelizeTypescript.transaction(async t => { 108 return sequelizeTypescript.transaction(async t => {
109 const videoAbuseData = { 109 const videoAbuseData = {
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index d0865b78c..9e1664fd8 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -5,7 +5,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { getOrCreateActorAndServerAndModel } from '../actor' 6import { getOrCreateActorAndServerAndModel } from '../actor'
7import { forwardVideoRelatedActivity } from '../send/utils' 7import { forwardVideoRelatedActivity } from '../send/utils'
8import { getOrCreateAccountAndVideoAndChannel } from '../videos' 8import { getOrCreateVideoAndAccountAndChannel } from '../videos'
9 9
10async function processLikeActivity (activity: ActivityLike) { 10async function processLikeActivity (activity: ActivityLike) {
11 const actor = await getOrCreateActorAndServerAndModel(activity.actor) 11 const actor = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -27,7 +27,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
27 const byAccount = byActor.Account 27 const byAccount = byActor.Account
28 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) 28 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
29 29
30 const { video } = await getOrCreateAccountAndVideoAndChannel(videoUrl) 30 const { video } = await getOrCreateVideoAndAccountAndChannel(videoUrl)
31 31
32 return sequelizeTypescript.transaction(async t => { 32 return sequelizeTypescript.transaction(async t => {
33 const rate = { 33 const rate = {
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index b6de107ad..eab9e3d61 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -9,7 +9,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
9import { ActorModel } from '../../../models/activitypub/actor' 9import { ActorModel } from '../../../models/activitypub/actor'
10import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 10import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
11import { forwardVideoRelatedActivity } from '../send/utils' 11import { forwardVideoRelatedActivity } from '../send/utils'
12import { getOrCreateAccountAndVideoAndChannel } from '../videos' 12import { getOrCreateVideoAndAccountAndChannel } from '../videos'
13import { VideoShareModel } from '../../../models/video/video-share' 13import { VideoShareModel } from '../../../models/video/video-share'
14 14
15async function processUndoActivity (activity: ActivityUndo) { 15async function processUndoActivity (activity: ActivityUndo) {
@@ -43,7 +43,7 @@ export {
43async function processUndoLike (actorUrl: string, activity: ActivityUndo) { 43async function processUndoLike (actorUrl: string, activity: ActivityUndo) {
44 const likeActivity = activity.object as ActivityLike 44 const likeActivity = activity.object as ActivityLike
45 45
46 const { video } = await getOrCreateAccountAndVideoAndChannel(likeActivity.object) 46 const { video } = await getOrCreateVideoAndAccountAndChannel(likeActivity.object)
47 47
48 return sequelizeTypescript.transaction(async t => { 48 return sequelizeTypescript.transaction(async t => {
49 const byAccount = await AccountModel.loadByUrl(actorUrl, t) 49 const byAccount = await AccountModel.loadByUrl(actorUrl, t)
@@ -67,7 +67,7 @@ async function processUndoLike (actorUrl: string, activity: ActivityUndo) {
67async function processUndoDislike (actorUrl: string, activity: ActivityUndo) { 67async function processUndoDislike (actorUrl: string, activity: ActivityUndo) {
68 const dislike = activity.object.object as DislikeObject 68 const dislike = activity.object.object as DislikeObject
69 69
70 const { video } = await getOrCreateAccountAndVideoAndChannel(dislike.object) 70 const { video } = await getOrCreateVideoAndAccountAndChannel(dislike.object)
71 71
72 return sequelizeTypescript.transaction(async t => { 72 return sequelizeTypescript.transaction(async t => {
73 const byAccount = await AccountModel.loadByUrl(actorUrl, t) 73 const byAccount = await AccountModel.loadByUrl(actorUrl, t)
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 11226e275..07a5ff92f 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -1,4 +1,3 @@
1import * as Bluebird from 'bluebird'
2import { ActivityUpdate, VideoTorrentObject } from '../../../../shared/models/activitypub' 1import { ActivityUpdate, VideoTorrentObject } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 2import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 3import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -6,19 +5,10 @@ import { logger } from '../../../helpers/logger'
6import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
7import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
8import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
9import { TagModel } from '../../../models/video/tag'
10import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
11import { VideoFileModel } from '../../../models/video/video-file'
12import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
13import { 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannel, updateVideoFromAP } from '../videos'
14 generateThumbnailFromUrl,
15 getOrCreateAccountAndVideoAndChannel,
16 getOrCreateVideoChannel,
17 videoActivityObjectToDBAttributes,
18 videoFileActivityUrlToDBAttributes
19} from '../videos'
20import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
21import { VideoCaptionModel } from '../../../models/video/video-caption'
22 12
23async function processUpdateActivity (activity: ActivityUpdate) { 13async function processUpdateActivity (activity: ActivityUpdate) {
24 const actor = await getOrCreateActorAndServerAndModel(activity.actor) 14 const actor = await getOrCreateActorAndServerAndModel(activity.actor)
@@ -49,91 +39,10 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
49 return undefined 39 return undefined
50 } 40 }
51 41
52 const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id) 42 const { video } = await getOrCreateVideoAndAccountAndChannel(videoObject.id)
43 const channelActor = await getOrCreateVideoChannel(videoObject)
53 44
54 // Fetch video channel outside the transaction 45 return updateVideoFromAP(video, videoObject, actor, channelActor, activity.to)
55 const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
56 const newVideoChannel = newVideoChannelActor.VideoChannel
57
58 logger.debug('Updating remote video "%s".', videoObject.uuid)
59 let videoInstance = res.video
60 let videoFieldsSave: any
61
62 try {
63 await sequelizeTypescript.transaction(async t => {
64 const sequelizeOptions = {
65 transaction: t
66 }
67
68 videoFieldsSave = videoInstance.toJSON()
69
70 // Check actor has the right to update the video
71 const videoChannel = videoInstance.VideoChannel
72 if (videoChannel.Account.Actor.id !== actor.id) {
73 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
74 }
75
76 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
77 videoInstance.set('name', videoData.name)
78 videoInstance.set('uuid', videoData.uuid)
79 videoInstance.set('url', videoData.url)
80 videoInstance.set('category', videoData.category)
81 videoInstance.set('licence', videoData.licence)
82 videoInstance.set('language', videoData.language)
83 videoInstance.set('description', videoData.description)
84 videoInstance.set('support', videoData.support)
85 videoInstance.set('nsfw', videoData.nsfw)
86 videoInstance.set('commentsEnabled', videoData.commentsEnabled)
87 videoInstance.set('waitTranscoding', videoData.waitTranscoding)
88 videoInstance.set('state', videoData.state)
89 videoInstance.set('duration', videoData.duration)
90 videoInstance.set('createdAt', videoData.createdAt)
91 videoInstance.set('updatedAt', videoData.updatedAt)
92 videoInstance.set('views', videoData.views)
93 videoInstance.set('privacy', videoData.privacy)
94 videoInstance.set('channelId', videoData.channelId)
95
96 await videoInstance.save(sequelizeOptions)
97
98 // Don't block on request
99 generateThumbnailFromUrl(videoInstance, videoObject.icon)
100 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
101
102 // Remove old video files
103 const videoFileDestroyTasks: Bluebird<void>[] = []
104 for (const videoFile of videoInstance.VideoFiles) {
105 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
106 }
107 await Promise.all(videoFileDestroyTasks)
108
109 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
110 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, sequelizeOptions))
111 await Promise.all(tasks)
112
113 // Update Tags
114 const tags = videoObject.tag.map(tag => tag.name)
115 const tagInstances = await TagModel.findOrCreateTags(tags, t)
116 await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
117
118 // Update captions
119 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoInstance.id, t)
120
121 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
122 return VideoCaptionModel.insertOrReplaceLanguage(videoInstance.id, c.identifier, t)
123 })
124 await Promise.all(videoCaptionsPromises)
125 })
126
127 logger.info('Remote video with uuid %s updated', videoObject.uuid)
128 } catch (err) {
129 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
130 resetSequelizeInstance(videoInstance, videoFieldsSave)
131 }
132
133 // This is just a debug because we will retry the insert
134 logger.debug('Cannot update the remote video.', { err })
135 throw err
136 }
137} 46}
138 47
139async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) { 48async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) {
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 698414867..fe3d73e9b 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -6,6 +6,11 @@ import { VideoShareModel } from '../../models/video/video-share'
6import { sendUndoAnnounce, sendVideoAnnounce } from './send' 6import { sendUndoAnnounce, sendVideoAnnounce } from './send'
7import { getAnnounceActivityPubUrl } from './url' 7import { getAnnounceActivityPubUrl } from './url'
8import { VideoChannelModel } from '../../models/video/video-channel' 8import { VideoChannelModel } from '../../models/video/video-channel'
9import * as Bluebird from 'bluebird'
10import { doRequest } from '../../helpers/requests'
11import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
9 14
10async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 15async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
11 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -22,8 +27,41 @@ async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: Vide
22 await shareByVideoChannel(video, t) 27 await shareByVideoChannel(video, t)
23} 28}
24 29
30async function addVideoShares (shareUrls: string[], instance: VideoModel) {
31 await Bluebird.map(shareUrls, async shareUrl => {
32 try {
33 // Fetch url
34 const { body } = await doRequest({
35 uri: shareUrl,
36 json: true,
37 activityPub: true
38 })
39 if (!body || !body.actor) throw new Error('Body of body actor is invalid')
40
41 const actorUrl = body.actor
42 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
43
44 const entry = {
45 actorId: actor.id,
46 videoId: instance.id,
47 url: shareUrl
48 }
49
50 await VideoShareModel.findOrCreate({
51 where: {
52 url: shareUrl
53 },
54 defaults: entry
55 })
56 } catch (err) {
57 logger.warn('Cannot add share %s.', shareUrl, { err })
58 }
59 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
60}
61
25export { 62export {
26 changeVideoChannelShare, 63 changeVideoChannelShare,
64 addVideoShares,
27 shareVideoByServerAndChannel 65 shareVideoByServerAndChannel
28} 66}
29 67
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 14c7fde69..beff557bc 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -7,7 +7,7 @@ import { ActorModel } from '../../models/activitypub/actor'
7import { VideoModel } from '../../models/video/video' 7import { VideoModel } from '../../models/video/video'
8import { VideoCommentModel } from '../../models/video/video-comment' 8import { VideoCommentModel } from '../../models/video/video-comment'
9import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
10import { getOrCreateAccountAndVideoAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
11import * as Bluebird from 'bluebird' 11import * as Bluebird from 'bluebird'
12 12
13async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { 13async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
@@ -91,7 +91,7 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
91 91
92 try { 92 try {
93 // Maybe it's a reply to a video? 93 // Maybe it's a reply to a video?
94 const { video } = await getOrCreateAccountAndVideoAndChannel(url) 94 const { video } = await getOrCreateVideoAndAccountAndChannel(url)
95 95
96 if (comments.length !== 0) { 96 if (comments.length !== 0) {
97 const firstReply = comments[ comments.length - 1 ] 97 const firstReply = comments[ comments.length - 1 ]
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 19011b4ab..1619251c3 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -2,6 +2,45 @@ import { Transaction } from 'sequelize'
2import { AccountModel } from '../../models/account/account' 2import { AccountModel } from '../../models/account/account'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send' 4import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send'
5import { VideoRateType } from '../../../shared/models/videos'
6import * as Bluebird from 'bluebird'
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
11
12async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
13 let rateCounts = 0
14
15 await Bluebird.map(actorUrls, async actorUrl => {
16 try {
17 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
18 const [ , created ] = await AccountVideoRateModel
19 .findOrCreate({
20 where: {
21 videoId: video.id,
22 accountId: actor.Account.id
23 },
24 defaults: {
25 videoId: video.id,
26 accountId: actor.Account.id,
27 type: rate
28 }
29 })
30
31 if (created) rateCounts += 1
32 } catch (err) {
33 logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err })
34 }
35 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
36
37 logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
38
39 // This is "likes" and "dislikes"
40 if (rateCounts !== 0) await video.increment(rate + 's', { by: rateCounts })
41
42 return
43}
5 44
6async function sendVideoRateChange (account: AccountModel, 45async function sendVideoRateChange (account: AccountModel,
7 video: VideoModel, 46 video: VideoModel,
@@ -24,5 +63,6 @@ async function sendVideoRateChange (account: AccountModel,
24} 63}
25 64
26export { 65export {
66 createRates,
27 sendVideoRateChange 67 sendVideoRateChange
28} 68}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index fac1d3fc7..388c31fe5 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -5,29 +5,30 @@ import { join } from 'path'
5import * as request from 'request' 5import * as request from 'request'
6import { ActivityIconObject, VideoState } from '../../../shared/index' 6import { ActivityIconObject, VideoState } from '../../../shared/index'
7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
8import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 8import { VideoPrivacy } from '../../../shared/models/videos'
9import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 9import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
11import { retryTransactionWrapper } from '../../helpers/database-utils' 11import { resetSequelizeInstance, retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' 14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
15import { AccountVideoRateModel } from '../../models/account/account-video-rate'
16import { ActorModel } from '../../models/activitypub/actor' 15import { ActorModel } from '../../models/activitypub/actor'
17import { TagModel } from '../../models/video/tag' 16import { TagModel } from '../../models/video/tag'
18import { VideoModel } from '../../models/video/video' 17import { VideoModel } from '../../models/video/video'
19import { VideoChannelModel } from '../../models/video/video-channel' 18import { VideoChannelModel } from '../../models/video/video-channel'
20import { VideoFileModel } from '../../models/video/video-file' 19import { VideoFileModel } from '../../models/video/video-file'
21import { VideoShareModel } from '../../models/video/video-share' 20import { getOrCreateActorAndServerAndModel, updateActorAvatarInstance } from './actor'
22import { getOrCreateActorAndServerAndModel } from './actor'
23import { addVideoComments } from './video-comments' 21import { addVideoComments } from './video-comments'
24import { crawlCollectionPage } from './crawl' 22import { crawlCollectionPage } from './crawl'
25import { sendCreateVideo, sendUpdateVideo } from './send' 23import { sendCreateVideo, sendUpdateVideo } from './send'
26import { shareVideoByServerAndChannel } from './index'
27import { isArray } from '../../helpers/custom-validators/misc' 24import { isArray } from '../../helpers/custom-validators/misc'
28import { VideoCaptionModel } from '../../models/video/video-caption' 25import { VideoCaptionModel } from '../../models/video/video-caption'
29import { JobQueue } from '../job-queue' 26import { JobQueue } from '../job-queue'
30import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher' 27import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
28import { getUrlFromWebfinger } from '../../helpers/webfinger'
29import { createRates } from './video-rates'
30import { addVideoShares, shareVideoByServerAndChannel } from './share'
31import { AccountModel } from '../../models/account/account'
31 32
32async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 33async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
33 // If the video is not private and published, we federate it 34 // If the video is not private and published, we federate it
@@ -180,15 +181,11 @@ function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
180 return getOrCreateActorAndServerAndModel(channel.id) 181 return getOrCreateActorAndServerAndModel(channel.id)
181} 182}
182 183
183async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 184async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
184 logger.debug('Adding remote video %s.', videoObject.id) 185 logger.debug('Adding remote video %s.', videoObject.id)
185 186
186 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => { 187 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
187 const sequelizeOptions = { 188 const sequelizeOptions = { transaction: t }
188 transaction: t
189 }
190 const videoFromDatabase = await VideoModel.loadByUUIDOrURLAndPopulateAccount(videoObject.uuid, videoObject.id, t)
191 if (videoFromDatabase) return videoFromDatabase
192 189
193 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) 190 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
194 const video = VideoModel.build(videoData) 191 const video = VideoModel.build(videoData)
@@ -230,26 +227,32 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
230} 227}
231 228
232type SyncParam = { 229type SyncParam = {
233 likes: boolean, 230 likes: boolean
234 dislikes: boolean, 231 dislikes: boolean
235 shares: boolean, 232 shares: boolean
236 comments: boolean, 233 comments: boolean
237 thumbnail: boolean 234 thumbnail: boolean
235 refreshVideo: boolean
238} 236}
239async function getOrCreateAccountAndVideoAndChannel ( 237async function getOrCreateVideoAndAccountAndChannel (
240 videoObject: VideoTorrentObject | string, 238 videoObject: VideoTorrentObject | string,
241 syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } 239 syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
242) { 240) {
243 const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id 241 const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
244 242
245 const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl) 243 let videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
246 if (videoFromDatabase) return { video: videoFromDatabase } 244 if (videoFromDatabase) {
245 const p = retryTransactionWrapper(refreshVideoIfNeeded, videoFromDatabase)
246 if (syncParam.refreshVideo === true) videoFromDatabase = await p
247
248 return { video: videoFromDatabase }
249 }
247 250
248 const fetchedVideo = await fetchRemoteVideo(videoUrl) 251 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
249 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl) 252 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
250 253
251 const channelActor = await getOrCreateVideoChannel(fetchedVideo) 254 const channelActor = await getOrCreateVideoChannel(fetchedVideo)
252 const video = await retryTransactionWrapper(getOrCreateVideo, fetchedVideo, channelActor, syncParam.thumbnail) 255 const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
253 256
254 // Process outside the transaction because we could fetch remote data 257 // Process outside the transaction because we could fetch remote data
255 258
@@ -290,101 +293,153 @@ async function getOrCreateAccountAndVideoAndChannel (
290 return { video } 293 return { video }
291} 294}
292 295
293async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { 296async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoTorrentObject }> {
294 let rateCounts = 0 297 const options = {
295 298 uri: videoUrl,
296 await Bluebird.map(actorUrls, async actorUrl => { 299 method: 'GET',
297 try { 300 json: true,
298 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 301 activityPub: true
299 const [ , created ] = await AccountVideoRateModel 302 }
300 .findOrCreate({ 303
301 where: { 304 logger.info('Fetching remote video %s.', videoUrl)
302 videoId: video.id, 305
303 accountId: actor.Account.id 306 const { response, body } = await doRequest(options)
304 }, 307
305 defaults: { 308 if (sanitizeAndCheckVideoTorrentObject(body) === false) {
306 videoId: video.id, 309 logger.debug('Remote video JSON is not valid.', { body })
307 accountId: actor.Account.id, 310 return { response, videoObject: undefined }
308 type: rate 311 }
309 } 312
310 }) 313 return { response, videoObject: body }
311 314}
312 if (created) rateCounts += 1 315
313 } catch (err) { 316async function refreshVideoIfNeeded (video: VideoModel): Promise<VideoModel> {
314 logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) 317 if (!video.isOutdated()) return video
318
319 try {
320 const { response, videoObject } = await fetchRemoteVideo(video.url)
321 if (response.statusCode === 404) {
322 // Video does not exist anymore
323 await video.destroy()
324 return undefined
315 } 325 }
316 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
317 326
318 logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid) 327 if (videoObject === undefined) {
328 logger.warn('Cannot refresh remote video: invalid body.')
329 return video
330 }
319 331
320 // This is "likes" and "dislikes" 332 const channelActor = await getOrCreateVideoChannel(videoObject)
321 if (rateCounts !== 0) await video.increment(rate + 's', { by: rateCounts }) 333 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
334 return updateVideoFromAP(video, videoObject, account.Actor, channelActor)
322 335
323 return 336 } catch (err) {
337 logger.warn('Cannot refresh video.', { err })
338 return video
339 }
324} 340}
325 341
326async function addVideoShares (shareUrls: string[], instance: VideoModel) { 342async function updateVideoFromAP (
327 await Bluebird.map(shareUrls, async shareUrl => { 343 video: VideoModel,
328 try { 344 videoObject: VideoTorrentObject,
329 // Fetch url 345 accountActor: ActorModel,
330 const { body } = await doRequest({ 346 channelActor: ActorModel,
331 uri: shareUrl, 347 overrideTo?: string[]
332 json: true, 348) {
333 activityPub: true 349 logger.debug('Updating remote video "%s".', videoObject.uuid)
334 }) 350 let videoFieldsSave: any
335 if (!body || !body.actor) throw new Error('Body of body actor is invalid') 351
352 try {
353 const updatedVideo: VideoModel = await sequelizeTypescript.transaction(async t => {
354 const sequelizeOptions = {
355 transaction: t
356 }
336 357
337 const actorUrl = body.actor 358 videoFieldsSave = video.toJSON()
338 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
339 359
340 const entry = { 360 // Check actor has the right to update the video
341 actorId: actor.id, 361 const videoChannel = video.VideoChannel
342 videoId: instance.id, 362 if (videoChannel.Account.Actor.id !== accountActor.id) {
343 url: shareUrl 363 throw new Error('Account ' + accountActor.url + ' does not own video channel ' + videoChannel.Actor.url)
344 } 364 }
345 365
346 await VideoShareModel.findOrCreate({ 366 const to = overrideTo ? overrideTo : videoObject.to
347 where: { 367 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, to)
348 url: shareUrl 368 video.set('name', videoData.name)
349 }, 369 video.set('uuid', videoData.uuid)
350 defaults: entry 370 video.set('url', videoData.url)
351 }) 371 video.set('category', videoData.category)
352 } catch (err) { 372 video.set('licence', videoData.licence)
353 logger.warn('Cannot add share %s.', shareUrl, { err }) 373 video.set('language', videoData.language)
354 } 374 video.set('description', videoData.description)
355 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 375 video.set('support', videoData.support)
356} 376 video.set('nsfw', videoData.nsfw)
377 video.set('commentsEnabled', videoData.commentsEnabled)
378 video.set('waitTranscoding', videoData.waitTranscoding)
379 video.set('state', videoData.state)
380 video.set('duration', videoData.duration)
381 video.set('createdAt', videoData.createdAt)
382 video.set('publishedAt', videoData.publishedAt)
383 video.set('views', videoData.views)
384 video.set('privacy', videoData.privacy)
385 video.set('channelId', videoData.channelId)
386
387 await video.save(sequelizeOptions)
388
389 // Don't block on request
390 generateThumbnailFromUrl(video, videoObject.icon)
391 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
392
393 // Remove old video files
394 const videoFileDestroyTasks: Bluebird<void>[] = []
395 for (const videoFile of video.VideoFiles) {
396 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
397 }
398 await Promise.all(videoFileDestroyTasks)
357 399
358async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { 400 const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject)
359 const options = { 401 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, sequelizeOptions))
360 uri: videoUrl, 402 await Promise.all(tasks)
361 method: 'GET',
362 json: true,
363 activityPub: true
364 }
365 403
366 logger.info('Fetching remote video %s.', videoUrl) 404 // Update Tags
405 const tags = videoObject.tag.map(tag => tag.name)
406 const tagInstances = await TagModel.findOrCreateTags(tags, t)
407 await video.$set('Tags', tagInstances, sequelizeOptions)
367 408
368 const { body } = await doRequest(options) 409 // Update captions
410 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
369 411
370 if (sanitizeAndCheckVideoTorrentObject(body) === false) { 412 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
371 logger.debug('Remote video JSON is not valid.', { body }) 413 return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
372 return undefined 414 })
373 } 415 await Promise.all(videoCaptionsPromises)
416 })
417
418 logger.info('Remote video with uuid %s updated', videoObject.uuid)
374 419
375 return body 420 return updatedVideo
421 } catch (err) {
422 if (video !== undefined && videoFieldsSave !== undefined) {
423 resetSequelizeInstance(video, videoFieldsSave)
424 }
425
426 // This is just a debug because we will retry the insert
427 logger.debug('Cannot update the remote video.', { err })
428 throw err
429 }
376} 430}
377 431
378export { 432export {
433 updateVideoFromAP,
379 federateVideoIfNeeded, 434 federateVideoIfNeeded,
380 fetchRemoteVideo, 435 fetchRemoteVideo,
381 getOrCreateAccountAndVideoAndChannel, 436 getOrCreateVideoAndAccountAndChannel,
382 fetchRemoteVideoStaticFile, 437 fetchRemoteVideoStaticFile,
383 fetchRemoteVideoDescription, 438 fetchRemoteVideoDescription,
384 generateThumbnailFromUrl, 439 generateThumbnailFromUrl,
385 videoActivityObjectToDBAttributes, 440 videoActivityObjectToDBAttributes,
386 videoFileActivityUrlToDBAttributes, 441 videoFileActivityUrlToDBAttributes,
387 getOrCreateVideo, 442 createVideo,
388 getOrCreateVideoChannel, 443 getOrCreateVideoChannel,
389 addVideoShares, 444 addVideoShares,
390 createRates 445 createRates
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 25a1cd177..7acbc60f7 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -56,6 +56,7 @@ import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, tr
56import { logger } from '../../helpers/logger' 56import { logger } from '../../helpers/logger'
57import { getServerActor } from '../../helpers/utils' 57import { getServerActor } from '../../helpers/utils'
58import { 58import {
59 ACTIVITY_PUB,
59 API_VERSION, 60 API_VERSION,
60 CONFIG, 61 CONFIG,
61 CONSTRAINTS_FIELDS, 62 CONSTRAINTS_FIELDS,
@@ -1004,21 +1005,6 @@ export class VideoModel extends Model<VideoModel> {
1004 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1005 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1005 } 1006 }
1006 1007
1007 static loadByUUIDOrURLAndPopulateAccount (uuid: string, url: string, t?: Sequelize.Transaction) {
1008 const query: IFindOptions<VideoModel> = {
1009 where: {
1010 [Sequelize.Op.or]: [
1011 { uuid },
1012 { url }
1013 ]
1014 }
1015 }
1016
1017 if (t !== undefined) query.transaction = t
1018
1019 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1020 }
1021
1022 static loadAndPopulateAccountAndServerAndTags (id: number) { 1008 static loadAndPopulateAccountAndServerAndTags (id: number) {
1023 const options = { 1009 const options = {
1024 order: [ [ 'Tags', 'name', 'ASC' ] ] 1010 order: [ [ 'Tags', 'name', 'ASC' ] ]
@@ -1646,6 +1632,17 @@ export class VideoModel extends Model<VideoModel> {
1646 return 'PT' + this.duration + 'S' 1632 return 'PT' + this.duration + 'S'
1647 } 1633 }
1648 1634
1635 isOutdated () {
1636 if (this.isOwned()) return false
1637
1638 const now = Date.now()
1639 const createdAtTime = this.createdAt.getTime()
1640 const updatedAtTime = this.updatedAt.getTime()
1641
1642 return (now - createdAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL &&
1643 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
1644 }
1645
1649 private getBaseUrls () { 1646 private getBaseUrls () {
1650 let baseUrlHttp 1647 let baseUrlHttp
1651 let baseUrlWs 1648 let baseUrlWs
diff --git a/server/tests/api/index-1.ts b/server/tests/api/index-1.ts
new file mode 100644
index 000000000..80d752f42
--- /dev/null
+++ b/server/tests/api/index-1.ts
@@ -0,0 +1,2 @@
1import './check-params'
2import './search'
diff --git a/server/tests/api/index-2.ts b/server/tests/api/index-2.ts
new file mode 100644
index 000000000..ed93faa91
--- /dev/null
+++ b/server/tests/api/index-2.ts
@@ -0,0 +1,2 @@
1import './server'
2import './users'
diff --git a/server/tests/api/index-3.ts b/server/tests/api/index-3.ts
new file mode 100644
index 000000000..39823b82c
--- /dev/null
+++ b/server/tests/api/index-3.ts
@@ -0,0 +1 @@
import './videos'
diff --git a/server/tests/api/index-fast.ts b/server/tests/api/index-fast.ts
deleted file mode 100644
index 02ffdd4f1..000000000
--- a/server/tests/api/index-fast.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1// Order of the tests we want to execute
2import './server/stats'
3import './check-params'
4import './users/users'
5import './videos/single-server'
6import './videos/video-abuse'
7import './videos/video-captions'
8import './videos/video-blacklist'
9import './videos/video-blacklist-management'
10import './videos/video-description'
11import './videos/video-nsfw'
12import './videos/video-privacy'
13import './videos/services'
14import './server/email'
15import './server/config'
16import './server/reverse-proxy'
17import './search/search-videos'
18import './server/tracker'
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts
deleted file mode 100644
index e24a7b664..000000000
--- a/server/tests/api/index-slow.ts
+++ /dev/null
@@ -1,12 +0,0 @@
1// Order of the tests we want to execute
2import './videos/video-channels'
3import './videos/video-transcoder'
4import './videos/multiple-servers'
5import './server/follows'
6import './server/jobs'
7import './videos/video-comments'
8import './users/users-multiple-servers'
9import './users/user-subscriptions'
10import './server/handle-down'
11import './videos/video-schedule-update'
12import './videos/video-imports'
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index 258502d26..2d996dbf9 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -1,3 +1,4 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './index-fast' 2import './index-1'
3import './index-slow' 3import './index-2'
4import './index-3'
diff --git a/server/tests/api/search/index.ts b/server/tests/api/search/index.ts
new file mode 100644
index 000000000..64b3d0910
--- /dev/null
+++ b/server/tests/api/search/index.ts
@@ -0,0 +1,2 @@
1import './search-activitypub-videos'
2import './search-videos'
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts
new file mode 100644
index 000000000..6dc792696
--- /dev/null
+++ b/server/tests/api/search/search-activitypub-videos.ts
@@ -0,0 +1,161 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 addVideoChannel,
7 flushAndRunMultipleServers,
8 flushTests,
9 getVideosList,
10 killallServers,
11 removeVideo,
12 searchVideoWithToken,
13 ServerInfo,
14 setAccessTokensToServers,
15 updateVideo,
16 uploadVideo,
17 wait,
18 searchVideo
19} from '../../utils'
20import { waitJobs } from '../../utils/server/jobs'
21import { Video, VideoPrivacy } from '../../../../shared/models/videos'
22
23const expect = chai.expect
24
25describe('Test a ActivityPub videos search', function () {
26 let servers: ServerInfo[]
27 let videoServer1UUID: string
28 let videoServer2UUID: string
29
30 before(async function () {
31 this.timeout(120000)
32
33 await flushTests()
34
35 servers = await flushAndRunMultipleServers(2)
36
37 await setAccessTokensToServers(servers)
38
39 {
40 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1 on server 1' })
41 videoServer1UUID = res.body.video.uuid
42 }
43
44 {
45 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 on server 2' })
46 videoServer2UUID = res.body.video.uuid
47 }
48
49 await waitJobs(servers)
50 })
51
52 it('Should not find a remote video', async function () {
53 {
54 const res = await searchVideoWithToken(servers[ 0 ].url, 'http://localhost:9002/videos/watch/43', servers[ 0 ].accessToken)
55
56 expect(res.body.total).to.equal(0)
57 expect(res.body.data).to.be.an('array')
58 expect(res.body.data).to.have.lengthOf(0)
59 }
60
61 {
62 const res = await searchVideo(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID)
63
64 expect(res.body.total).to.equal(0)
65 expect(res.body.data).to.be.an('array')
66 expect(res.body.data).to.have.lengthOf(0)
67 }
68 })
69
70 it('Should search a local video', async function () {
71 const res = await searchVideo(servers[0].url, 'http://localhost:9001/videos/watch/' + videoServer1UUID)
72
73 expect(res.body.total).to.equal(1)
74 expect(res.body.data).to.be.an('array')
75 expect(res.body.data).to.have.lengthOf(1)
76 expect(res.body.data[0].name).to.equal('video 1 on server 1')
77 })
78
79 it('Should search a remote video', async function () {
80 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
81
82 expect(res.body.total).to.equal(1)
83 expect(res.body.data).to.be.an('array')
84 expect(res.body.data).to.have.lengthOf(1)
85 expect(res.body.data[0].name).to.equal('video 1 on server 2')
86 })
87
88 it('Should not list this remote video', async function () {
89 const res = await getVideosList(servers[0].url)
90 expect(res.body.total).to.equal(1)
91 expect(res.body.data).to.have.lengthOf(1)
92 expect(res.body.data[0].name).to.equal('video 1 on server 1')
93 })
94
95 it('Should update video of server 2, and refresh it on server 1', async function () {
96 this.timeout(60000)
97
98 const channelAttributes = {
99 name: 'super_channel',
100 displayName: 'super channel'
101 }
102 const resChannel = await addVideoChannel(servers[1].url, servers[1].accessToken, channelAttributes)
103 const videoChannelId = resChannel.body.videoChannel.id
104
105 const attributes = {
106 name: 'updated',
107 tag: [ 'tag1', 'tag2' ],
108 privacy: VideoPrivacy.UNLISTED,
109 channelId: videoChannelId
110 }
111 await updateVideo(servers[1].url, servers[1].accessToken, videoServer2UUID, attributes)
112
113 await waitJobs(servers)
114 // Expire video
115 await wait(10000)
116
117 // Will run refresh async
118 await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
119
120 // Wait refresh
121 await wait(5000)
122
123 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
124 expect(res.body.total).to.equal(1)
125 expect(res.body.data).to.have.lengthOf(1)
126
127 const video: Video = res.body.data[0]
128 expect(video.name).to.equal('updated')
129 expect(video.channel.name).to.equal('super_channel')
130 expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
131 })
132
133 it('Should delete video of server 2, and delete it on server 1', async function () {
134 this.timeout(60000)
135
136 await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID)
137
138 await waitJobs(servers)
139 // Expire video
140 await wait(10000)
141
142 // Will run refresh async
143 await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
144
145 // Wait refresh
146 await wait(5000)
147
148 const res = await searchVideoWithToken(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID, servers[0].accessToken)
149 expect(res.body.total).to.equal(0)
150 expect(res.body.data).to.have.lengthOf(0)
151 })
152
153 after(async function () {
154 killallServers(servers)
155
156 // Keep the logs if the test failed
157 if (this['ok']) {
158 await flushTests()
159 }
160 })
161})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
new file mode 100644
index 000000000..eeb8b7a28
--- /dev/null
+++ b/server/tests/api/server/index.ts
@@ -0,0 +1,8 @@
1import './config'
2import './email'
3import './follows'
4import './handle-down'
5import './jobs'
6import './reverse-proxy'
7import './stats'
8import './tracker'
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
new file mode 100644
index 000000000..4ce87fb91
--- /dev/null
+++ b/server/tests/api/users/index.ts
@@ -0,0 +1,3 @@
1import './user-subscriptions'
2import './users'
3import './users-multiple-servers'
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
new file mode 100644
index 000000000..9f1230767
--- /dev/null
+++ b/server/tests/api/videos/index.ts
@@ -0,0 +1,15 @@
1import './multiple-servers'
2import './services'
3import './single-server'
4import './video-abuse'
5import './video-blacklist'
6import './video-blacklist-management'
7import './video-captions'
8import './video-channels'
9import './video-comme'
10import './video-description'
11import './video-impo'
12import './video-nsfw'
13import './video-privacy'
14import './video-schedule-update'
15import './video-transcoder'
diff --git a/server/tests/feeds/index.ts b/server/tests/feeds/index.ts
new file mode 100644
index 000000000..aa6236a91
--- /dev/null
+++ b/server/tests/feeds/index.ts
@@ -0,0 +1 @@
import './feeds'
diff --git a/server/tests/index.ts b/server/tests/index.ts
index 755fb2604..e659fd3df 100644
--- a/server/tests/index.ts
+++ b/server/tests/index.ts
@@ -1,5 +1,6 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './client' 2import './client'
3import './activitypub' 3import './activitypub'
4import './api/' 4import './feeds/'
5import './cli/' 5import './cli/'
6import './api/'