aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--package.json2
-rw-r--r--server/controllers/activitypub/client.ts27
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/users/index.ts2
-rw-r--r--server/controllers/api/videos/index.ts4
-rw-r--r--server/helpers/activitypub.ts197
-rw-r--r--server/helpers/middlewares/videos.ts17
-rw-r--r--server/helpers/video.ts27
-rw-r--r--server/lib/activitypub/audience.ts8
-rw-r--r--server/lib/activitypub/process/process-view.ts3
-rw-r--r--server/lib/activitypub/send/send-create.ts5
-rw-r--r--server/lib/activitypub/send/send-update.ts2
-rw-r--r--server/lib/activitypub/send/send-view.ts4
-rw-r--r--server/lib/activitypub/send/utils.ts12
-rw-r--r--server/lib/activitypub/videos.ts46
-rw-r--r--server/middlewares/validators/videos/videos.ts8
-rw-r--r--server/models/account/account.ts67
-rw-r--r--server/models/activitypub/actor.ts108
-rw-r--r--server/models/model-cache.ts91
-rw-r--r--server/models/video/video.ts50
-rw-r--r--server/typings/express.ts3
-rw-r--r--server/typings/models/video/video.ts1
-rw-r--r--yarn.lock8
24 files changed, 464 insertions, 234 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c573b12ed..cd9f2036d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
4 4
5### IMPORTANT NOTES 5### IMPORTANT NOTES
6 6
7 * **/!\ VERY IMPORTANT /!\\** You need to execute manually a script (can be executed after your upgrade, while your PeerTube instance is running) to create HLS video torrents: 7 * **/!\ VERY IMPORTANT /!\\** You need to execute manually a script (must be executed after the upgrade and a PeerTube restart, while your instance is running) to create HLS video torrents:
8 * `cd /var/www/peertube/peertube-latest && sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production node dist/scripts/migrations/peertube-2.1.js` 8 * `cd /var/www/peertube/peertube-latest && sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production node dist/scripts/migrations/peertube-2.1.js`
9 * **/!\ VERY IMPORTANT /!\\** In the next PeerTube release (v2.2.0), we'll add a unique index on actors usernames to fix some federation bugs. 9 * **/!\ VERY IMPORTANT /!\\** In the next PeerTube release (v2.2.0), we'll add a unique index on actors usernames to fix some federation bugs.
10 Please check now if you have conflicts using: 10 Please check now if you have conflicts using:
diff --git a/package.json b/package.json
index 0a5484d2a..cec311a18 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
95 "deep-object-diff": "^1.1.0", 95 "deep-object-diff": "^1.1.0",
96 "express": "^4.12.4", 96 "express": "^4.12.4",
97 "express-oauth-server": "^2.0.0", 97 "express-oauth-server": "^2.0.0",
98 "express-rate-limit": "^4.0.4", 98 "express-rate-limit": "^5.0.0",
99 "express-validator": "^6.4.0", 99 "express-validator": "^6.4.0",
100 "flat": "^5.0.0", 100 "flat": "^5.0.0",
101 "fluent-ffmpeg": "^2.1.0", 101 "fluent-ffmpeg": "^2.1.0",
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 2812bfe1e..84828e7e0 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -1,4 +1,3 @@
1// Intercept ActivityPub client requests
2import * as express from 'express' 1import * as express from 'express'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 2import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 3import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
@@ -37,10 +36,12 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
37import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' 36import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
38import { VideoPlaylistModel } from '../../models/video/video-playlist' 37import { VideoPlaylistModel } from '../../models/video/video-playlist'
39import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 38import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
40import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models' 39import { MAccountId, MActorId, MVideoAPWithoutCaption, MVideoId } from '@server/typings/models'
41 40
42const activityPubClientRouter = express.Router() 41const activityPubClientRouter = express.Router()
43 42
43// Intercept ActivityPub client requests
44
44activityPubClientRouter.get('/accounts?/:name', 45activityPubClientRouter.get('/accounts?/:name',
45 executeIfActivityPub, 46 executeIfActivityPub,
46 asyncMiddleware(localAccountValidator), 47 asyncMiddleware(localAccountValidator),
@@ -85,7 +86,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
85) 86)
86activityPubClientRouter.get('/videos/watch/:id/announces', 87activityPubClientRouter.get('/videos/watch/:id/announces',
87 executeIfActivityPub, 88 executeIfActivityPub,
88 asyncMiddleware(videosCustomGetValidator('only-video')), 89 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
89 asyncMiddleware(videoAnnouncesController) 90 asyncMiddleware(videoAnnouncesController)
90) 91)
91activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', 92activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
@@ -95,17 +96,17 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
95) 96)
96activityPubClientRouter.get('/videos/watch/:id/likes', 97activityPubClientRouter.get('/videos/watch/:id/likes',
97 executeIfActivityPub, 98 executeIfActivityPub,
98 asyncMiddleware(videosCustomGetValidator('only-video')), 99 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
99 asyncMiddleware(videoLikesController) 100 asyncMiddleware(videoLikesController)
100) 101)
101activityPubClientRouter.get('/videos/watch/:id/dislikes', 102activityPubClientRouter.get('/videos/watch/:id/dislikes',
102 executeIfActivityPub, 103 executeIfActivityPub,
103 asyncMiddleware(videosCustomGetValidator('only-video')), 104 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
104 asyncMiddleware(videoDislikesController) 105 asyncMiddleware(videoDislikesController)
105) 106)
106activityPubClientRouter.get('/videos/watch/:id/comments', 107activityPubClientRouter.get('/videos/watch/:id/comments',
107 executeIfActivityPub, 108 executeIfActivityPub,
108 asyncMiddleware(videosCustomGetValidator('only-video')), 109 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
109 asyncMiddleware(videoCommentsController) 110 asyncMiddleware(videoCommentsController)
110) 111)
111activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', 112activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
@@ -238,7 +239,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
238} 239}
239 240
240async function videoAnnouncesController (req: express.Request, res: express.Response) { 241async function videoAnnouncesController (req: express.Request, res: express.Response) {
241 const video = res.locals.onlyVideo 242 const video = res.locals.onlyImmutableVideo
242 243
243 const handler = async (start: number, count: number) => { 244 const handler = async (start: number, count: number) => {
244 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) 245 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@@ -253,21 +254,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
253} 254}
254 255
255async function videoLikesController (req: express.Request, res: express.Response) { 256async function videoLikesController (req: express.Request, res: express.Response) {
256 const video = res.locals.onlyVideo 257 const video = res.locals.onlyImmutableVideo
257 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 258 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
258 259
259 return activityPubResponse(activityPubContextify(json), res) 260 return activityPubResponse(activityPubContextify(json), res)
260} 261}
261 262
262async function videoDislikesController (req: express.Request, res: express.Response) { 263async function videoDislikesController (req: express.Request, res: express.Response) {
263 const video = res.locals.onlyVideo 264 const video = res.locals.onlyImmutableVideo
264 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 265 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
265 266
266 return activityPubResponse(activityPubContextify(json), res) 267 return activityPubResponse(activityPubContextify(json), res)
267} 268}
268 269
269async function videoCommentsController (req: express.Request, res: express.Response) { 270async function videoCommentsController (req: express.Request, res: express.Response) {
270 const video = res.locals.onlyVideo 271 const video = res.locals.onlyImmutableVideo
271 272
272 const handler = async (start: number, count: number) => { 273 const handler = async (start: number, count: number) => {
273 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) 274 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@@ -334,10 +335,10 @@ async function videoRedundancyController (req: express.Request, res: express.Res
334 335
335 if (req.path.endsWith('/activity')) { 336 if (req.path.endsWith('/activity')) {
336 const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience) 337 const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience)
337 return activityPubResponse(activityPubContextify(data), res) 338 return activityPubResponse(activityPubContextify(data, 'CacheFile'), res)
338 } 339 }
339 340
340 return activityPubResponse(activityPubContextify(object), res) 341 return activityPubResponse(activityPubContextify(object, 'CacheFile'), res)
341} 342}
342 343
343async function videoPlaylistController (req: express.Request, res: express.Response) { 344async function videoPlaylistController (req: express.Request, res: express.Response) {
@@ -386,7 +387,7 @@ async function actorPlaylists (req: express.Request, account: MAccountId) {
386 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) 387 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
387} 388}
388 389
389function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) { 390function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
390 const handler = async (start: number, count: number) => { 391 const handler = async (start: number, count: number) => {
391 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 392 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
392 return { 393 return {
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 6138a32de..7bec6c527 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,5 +1,4 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit'
3import { configRouter } from './config' 2import { configRouter } from './config'
4import { jobsRouter } from './jobs' 3import { jobsRouter } from './jobs'
5import { oauthClientsRouter } from './oauth-clients' 4import { oauthClientsRouter } from './oauth-clients'
@@ -15,6 +14,7 @@ import { overviewsRouter } from './overviews'
15import { videoPlaylistRouter } from './video-playlist' 14import { videoPlaylistRouter } from './video-playlist'
16import { CONFIG } from '../../initializers/config' 15import { CONFIG } from '../../initializers/config'
17import { pluginRouter } from './plugins' 16import { pluginRouter } from './plugins'
17import * as RateLimit from 'express-rate-limit'
18 18
19const apiRouter = express.Router() 19const apiRouter = express.Router()
20 20
@@ -24,8 +24,6 @@ apiRouter.use(cors({
24 credentials: true 24 credentials: true
25})) 25}))
26 26
27// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
28// @ts-ignore
29const apiRateLimiter = RateLimit({ 27const apiRateLimiter = RateLimit({
30 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, 28 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
31 max: CONFIG.RATES_LIMIT.API.MAX 29 max: CONFIG.RATES_LIMIT.API.MAX
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index b960e80c1..0b7012537 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -53,8 +53,6 @@ import { Hooks } from '@server/lib/plugins/hooks'
53 53
54const auditLogger = auditLoggerFactory('users') 54const auditLogger = auditLoggerFactory('users')
55 55
56// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
57// @ts-ignore
58const loginRateLimiter = RateLimit({ 56const loginRateLimiter = RateLimit({
59 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, 57 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
60 max: CONFIG.RATES_LIMIT.LOGIN.MAX 58 max: CONFIG.RATES_LIMIT.LOGIN.MAX
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 1d61f8427..eb46ea01f 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -135,7 +135,7 @@ videosRouter.get('/:id',
135 asyncMiddleware(getVideo) 135 asyncMiddleware(getVideo)
136) 136)
137videosRouter.post('/:id/views', 137videosRouter.post('/:id/views',
138 asyncMiddleware(videosGetValidator), 138 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
139 asyncMiddleware(viewVideo) 139 asyncMiddleware(viewVideo)
140) 140)
141 141
@@ -458,7 +458,7 @@ async function getVideo (req: express.Request, res: express.Response) {
458} 458}
459 459
460async function viewVideo (req: express.Request, res: express.Response) { 460async function viewVideo (req: express.Request, res: express.Response) {
461 const videoInstance = res.locals.videoAll 461 const videoInstance = res.locals.onlyImmutableVideo
462 462
463 const ip = req.ip 463 const ip = req.ip
464 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid) 464 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid)
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 326785b68..2d49e6869 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -8,102 +8,117 @@ import { pageToStartAndCount } from './core-utils'
8import { URL } from 'url' 8import { URL } from 'url'
9import { MActor, MVideoAccountLight } from '../typings/models' 9import { MActor, MVideoAccountLight } from '../typings/models'
10 10
11export type ContextType = 'All' | 'View' | 'Announce' 11export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile'
12
13function getContextData (type: ContextType) {
14 const context: any[] = [
15 'https://www.w3.org/ns/activitystreams',
16 'https://w3id.org/security/v1',
17 {
18 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017'
19 }
20 ]
12 21
13function activityPubContextify <T> (data: T, type: ContextType = 'All') { 22 if (type !== 'View' && type !== 'Announce') {
14 const base = { 23 const additional = {
15 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' 24 pt: 'https://joinpeertube.org/ns#',
25 sc: 'http://schema.org#'
26 }
27
28 if (type === 'CacheFile') {
29 Object.assign(additional, {
30 expires: 'sc:expires',
31 CacheFile: 'pt:CacheFile'
32 })
33 } else {
34 Object.assign(additional, {
35 Hashtag: 'as:Hashtag',
36 uuid: 'sc:identifier',
37 category: 'sc:category',
38 licence: 'sc:license',
39 subtitleLanguage: 'sc:subtitleLanguage',
40 sensitive: 'as:sensitive',
41 language: 'sc:inLanguage',
42
43 Infohash: 'pt:Infohash',
44 originallyPublishedAt: 'sc:datePublished',
45 views: {
46 '@type': 'sc:Number',
47 '@id': 'pt:views'
48 },
49 state: {
50 '@type': 'sc:Number',
51 '@id': 'pt:state'
52 },
53 size: {
54 '@type': 'sc:Number',
55 '@id': 'pt:size'
56 },
57 fps: {
58 '@type': 'sc:Number',
59 '@id': 'pt:fps'
60 },
61 startTimestamp: {
62 '@type': 'sc:Number',
63 '@id': 'pt:startTimestamp'
64 },
65 stopTimestamp: {
66 '@type': 'sc:Number',
67 '@id': 'pt:stopTimestamp'
68 },
69 position: {
70 '@type': 'sc:Number',
71 '@id': 'pt:position'
72 },
73 commentsEnabled: {
74 '@type': 'sc:Boolean',
75 '@id': 'pt:commentsEnabled'
76 },
77 downloadEnabled: {
78 '@type': 'sc:Boolean',
79 '@id': 'pt:downloadEnabled'
80 },
81 waitTranscoding: {
82 '@type': 'sc:Boolean',
83 '@id': 'pt:waitTranscoding'
84 },
85 support: {
86 '@type': 'sc:Text',
87 '@id': 'pt:support'
88 },
89 likes: {
90 '@id': 'as:likes',
91 '@type': '@id'
92 },
93 dislikes: {
94 '@id': 'as:dislikes',
95 '@type': '@id'
96 },
97 playlists: {
98 '@id': 'pt:playlists',
99 '@type': '@id'
100 },
101 shares: {
102 '@id': 'as:shares',
103 '@type': '@id'
104 },
105 comments: {
106 '@id': 'as:comments',
107 '@type': '@id'
108 }
109 })
110 }
111
112 context.push(additional)
16 } 113 }
17 114
18 if (type === 'All') { 115 return {
19 Object.assign(base, { 116 '@context': context
20 pt: 'https://joinpeertube.org/ns#',
21 sc: 'http://schema.org#',
22 Hashtag: 'as:Hashtag',
23 uuid: 'sc:identifier',
24 category: 'sc:category',
25 licence: 'sc:license',
26 subtitleLanguage: 'sc:subtitleLanguage',
27 sensitive: 'as:sensitive',
28 language: 'sc:inLanguage',
29 expires: 'sc:expires',
30 CacheFile: 'pt:CacheFile',
31 Infohash: 'pt:Infohash',
32 originallyPublishedAt: 'sc:datePublished',
33 views: {
34 '@type': 'sc:Number',
35 '@id': 'pt:views'
36 },
37 state: {
38 '@type': 'sc:Number',
39 '@id': 'pt:state'
40 },
41 size: {
42 '@type': 'sc:Number',
43 '@id': 'pt:size'
44 },
45 fps: {
46 '@type': 'sc:Number',
47 '@id': 'pt:fps'
48 },
49 startTimestamp: {
50 '@type': 'sc:Number',
51 '@id': 'pt:startTimestamp'
52 },
53 stopTimestamp: {
54 '@type': 'sc:Number',
55 '@id': 'pt:stopTimestamp'
56 },
57 position: {
58 '@type': 'sc:Number',
59 '@id': 'pt:position'
60 },
61 commentsEnabled: {
62 '@type': 'sc:Boolean',
63 '@id': 'pt:commentsEnabled'
64 },
65 downloadEnabled: {
66 '@type': 'sc:Boolean',
67 '@id': 'pt:downloadEnabled'
68 },
69 waitTranscoding: {
70 '@type': 'sc:Boolean',
71 '@id': 'pt:waitTranscoding'
72 },
73 support: {
74 '@type': 'sc:Text',
75 '@id': 'pt:support'
76 },
77 likes: {
78 '@id': 'as:likes',
79 '@type': '@id'
80 },
81 dislikes: {
82 '@id': 'as:dislikes',
83 '@type': '@id'
84 },
85 playlists: {
86 '@id': 'pt:playlists',
87 '@type': '@id'
88 },
89 shares: {
90 '@id': 'as:shares',
91 '@type': '@id'
92 },
93 comments: {
94 '@id': 'as:comments',
95 '@type': '@id'
96 }
97 })
98 } 117 }
118}
99 119
100 return Object.assign({}, data, { 120function activityPubContextify <T> (data: T, type: ContextType = 'All') {
101 '@context': [ 121 return Object.assign({}, data, getContextData(type))
102 'https://www.w3.org/ns/activitystreams',
103 'https://w3id.org/security/v1',
104 base
105 ]
106 })
107} 122}
108 123
109type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> 124type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index 74f529804..409f78650 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -2,7 +2,16 @@ import { Response } from 'express'
2import { fetchVideo, VideoFetchType } from '../video' 2import { fetchVideo, VideoFetchType } from '../video'
3import { UserRight } from '../../../shared/models/users' 3import { UserRight } from '../../../shared/models/users'
4import { VideoChannelModel } from '../../models/video/video-channel' 4import { VideoChannelModel } from '../../models/video/video-channel'
5import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models' 5import {
6 MUser,
7 MUserAccountId,
8 MVideoAccountLight,
9 MVideoFullLight,
10 MVideoIdThumbnail,
11 MVideoImmutable,
12 MVideoThumbnail,
13 MVideoWithRights
14} from '@server/typings/models'
6 15
7async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') { 16async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
8 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 17 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
@@ -22,8 +31,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
22 res.locals.videoAll = video as MVideoFullLight 31 res.locals.videoAll = video as MVideoFullLight
23 break 32 break
24 33
34 case 'only-immutable-attributes':
35 res.locals.onlyImmutableVideo = video as MVideoImmutable
36 break
37
25 case 'id': 38 case 'id':
26 res.locals.videoId = video 39 res.locals.videoId = video as MVideoIdThumbnail
27 break 40 break
28 41
29 case 'only-video': 42 case 'only-video':
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index 5b9c026b1..4fe2a60f0 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -5,13 +5,15 @@ import {
5 MVideoFullLight, 5 MVideoFullLight,
6 MVideoIdThumbnail, 6 MVideoIdThumbnail,
7 MVideoThumbnail, 7 MVideoThumbnail,
8 MVideoWithRights 8 MVideoWithRights,
9 MVideoImmutable
9} from '@server/typings/models' 10} from '@server/typings/models'
10import { Response } from 'express' 11import { Response } from 'express'
11 12
12type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' 13type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes'
13 14
14function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight> 15function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
16function fetchVideo (id: number | string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
15function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail> 17function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
16function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights> 18function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
17function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail> 19function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
@@ -19,14 +21,16 @@ function fetchVideo (
19 id: number | string, 21 id: number | string,
20 fetchType: VideoFetchType, 22 fetchType: VideoFetchType,
21 userId?: number 23 userId?: number
22): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> 24): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
23function fetchVideo ( 25function fetchVideo (
24 id: number | string, 26 id: number | string,
25 fetchType: VideoFetchType, 27 fetchType: VideoFetchType,
26 userId?: number 28 userId?: number
27): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> { 29): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
28 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 30 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
29 31
32 if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
33
30 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) 34 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
31 35
32 if (fetchType === 'only-video') return VideoModel.load(id) 36 if (fetchType === 'only-video') return VideoModel.load(id)
@@ -34,14 +38,23 @@ function fetchVideo (
34 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) 38 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
35} 39}
36 40
37type VideoFetchByUrlType = 'all' | 'only-video' 41type VideoFetchByUrlType = 'all' | 'only-video' | 'only-immutable-attributes'
38 42
39function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles> 43function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles>
44function fetchVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
40function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail> 45function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail>
41function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> 46function fetchVideoByUrl (
42function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> { 47 url: string,
48 fetchType: VideoFetchByUrlType
49): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable>
50function fetchVideoByUrl (
51 url: string,
52 fetchType: VideoFetchByUrlType
53): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
43 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url) 54 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url)
44 55
56 if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
57
45 if (fetchType === 'only-video') return VideoModel.loadByUrl(url) 58 if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
46} 59}
47 60
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index f2ab54cf7..9933ae2b5 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -4,11 +4,11 @@ import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/activitypub/actor' 4import { ActorModel } from '../../models/activitypub/actor'
5import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
6import { VideoShareModel } from '../../models/video/video-share' 6import { VideoShareModel } from '../../models/video/video-share'
7import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models' 7import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../typings/models'
8 8
9function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { 9function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
10 return { 10 return {
11 to: [ video.VideoChannel.Account.Actor.url ], 11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl) 12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 } 13 }
14} 14}
@@ -48,7 +48,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[
48 } 48 }
49} 49}
50 50
51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { 51async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) 52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
53 53
54 const videoAll = video as VideoModel 54 const videoAll = video as VideoModel
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index df29ee968..b3b6c933d 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -23,7 +23,8 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct
23 23
24 const options = { 24 const options = {
25 videoObject, 25 videoObject,
26 fetchType: 'only-video' as 'only-video' 26 fetchType: 'only-immutable-attributes' as 'only-immutable-attributes',
27 allowRefresh: false as false
27 } 28 }
28 const { video } = await getOrCreateVideoAndAccountAndChannel(options) 29 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
29 30
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 3585d704a..8bdcf6417 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -16,6 +16,7 @@ import {
16 MVideoRedundancyFileVideo, 16 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo 17 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models' 18} from '../../../typings/models'
19import { ContextType } from '@server/helpers/activitypub'
19 20
20async function sendCreateVideo (video: MVideoAP, t: Transaction) { 21async function sendCreateVideo (video: MVideoAP, t: Transaction) {
21 if (!video.hasPrivacyForFederation()) return undefined 22 if (!video.hasPrivacyForFederation()) return undefined
@@ -42,7 +43,8 @@ async function sendCreateCacheFile (
42 byActor, 43 byActor,
43 video, 44 video,
44 url: fileRedundancy.url, 45 url: fileRedundancy.url,
45 object: fileRedundancy.toActivityPubObject() 46 object: fileRedundancy.toActivityPubObject(),
47 contextType: 'CacheFile'
46 }) 48 })
47} 49}
48 50
@@ -135,6 +137,7 @@ async function sendVideoRelatedCreateActivity (options: {
135 url: string 137 url: string
136 object: any 138 object: any
137 transaction?: Transaction 139 transaction?: Transaction
140 contextType?: ContextType
138}) { 141}) {
139 const activityBuilder = (audience: ActivityAudience) => { 142 const activityBuilder = (audience: ActivityAudience) => {
140 return buildCreateActivity(options.url, options.byActor, options.object, audience) 143 return buildCreateActivity(options.url, options.byActor, options.object, audience)
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index cb500bd34..2b01ca5e7 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -84,7 +84,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
84 return buildUpdateActivity(url, byActor, redundancyObject, audience) 84 return buildUpdateActivity(url, byActor, redundancyObject, audience)
85 } 85 }
86 86
87 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 87 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
88} 88}
89 89
90async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { 90async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index 47482b9a9..1f864ea52 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -5,9 +5,9 @@ import { getVideoLikeActivityPubUrl } from '../url'
5import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models' 8import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/typings/models'
9 9
10async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) { 10async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
11 logger.info('Creating job to send view of %s.', video.url) 11 logger.info('Creating job to send view of %s.', video.url)
12 12
13 const activityBuilder = (audience: ActivityAudience) => { 13 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 0d67bb3d6..b57bae8fd 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -7,12 +7,12 @@ import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { getServerActor } from '../../../helpers/utils' 8import { getServerActor } from '../../../helpers/utils'
9import { afterCommitIfTransaction } from '../../../helpers/database-utils' 9import { afterCommitIfTransaction } from '../../../helpers/database-utils'
10import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' 10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
11import { ContextType } from '@server/helpers/activitypub' 11import { ContextType } from '@server/helpers/activitypub'
12 12
13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
14 byActor: MActorLight 14 byActor: MActorLight
15 video: MVideoAccountLight 15 video: MVideoImmutable | MVideoAccountLight
16 transaction?: Transaction 16 transaction?: Transaction
17 contextType?: ContextType 17 contextType?: ContextType
18}) { 18}) {
@@ -22,11 +22,13 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
22 22
23 // Send to origin 23 // Send to origin
24 if (video.isOwned() === false) { 24 if (video.isOwned() === false) {
25 const audience = getRemoteVideoAudience(video, actorsInvolvedInVideo) 25 const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id)
26
27 const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
26 const activity = activityBuilder(audience) 28 const activity = activityBuilder(audience)
27 29
28 return afterCommitIfTransaction(transaction, () => { 30 return afterCommitIfTransaction(transaction, () => {
29 return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox(), contextType) 31 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
30 }) 32 })
31 } 33 }
32 34
@@ -43,7 +45,7 @@ async function forwardVideoRelatedActivity (
43 activity: Activity, 45 activity: Activity,
44 t: Transaction, 46 t: Transaction,
45 followersException: MActorWithInboxes[] = [], 47 followersException: MActorWithInboxes[] = [],
46 video: MVideo 48 video: MVideoId
47) { 49) {
48 // Mastodon does not add our announces in audience, so we forward to them manually 50 // Mastodon does not add our announces in audience, so we forward to them manually
49 const additionalActors = await getActorsInvolvedInVideo(video, t) 51 const additionalActors = await getActorsInvolvedInVideo(video, t)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 9e43caa20..7d8296e45 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -68,7 +68,7 @@ import {
68 MVideoAPWithoutCaption, 68 MVideoAPWithoutCaption,
69 MVideoFile, 69 MVideoFile,
70 MVideoFullLight, 70 MVideoFullLight,
71 MVideoId, 71 MVideoId, MVideoImmutable,
72 MVideoThumbnail 72 MVideoThumbnail
73} from '../../typings/models' 73} from '../../typings/models'
74import { MThumbnail } from '../../typings/models/video/thumbnail' 74import { MThumbnail } from '../../typings/models/video/thumbnail'
@@ -200,24 +200,41 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
200 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })) 200 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }))
201} 201}
202 202
203function getOrCreateVideoAndAccountAndChannel (options: { 203type GetVideoResult <T> = Promise<{
204 video: T
205 created: boolean
206 autoBlacklisted?: boolean
207}>
208
209type GetVideoParamAll = {
204 videoObject: { id: string } | string 210 videoObject: { id: string } | string
205 syncParam?: SyncParam 211 syncParam?: SyncParam
206 fetchType?: 'all' 212 fetchType?: 'all'
207 allowRefresh?: boolean 213 allowRefresh?: boolean
208}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> 214}
209function getOrCreateVideoAndAccountAndChannel (options: { 215
216type GetVideoParamImmutable = {
210 videoObject: { id: string } | string 217 videoObject: { id: string } | string
211 syncParam?: SyncParam 218 syncParam?: SyncParam
212 fetchType?: VideoFetchByUrlType 219 fetchType: 'only-immutable-attributes'
213 allowRefresh?: boolean 220 allowRefresh: false
214}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> 221}
215async function getOrCreateVideoAndAccountAndChannel (options: { 222
223type GetVideoParamOther = {
216 videoObject: { id: string } | string 224 videoObject: { id: string } | string
217 syncParam?: SyncParam 225 syncParam?: SyncParam
218 fetchType?: VideoFetchByUrlType 226 fetchType?: 'all' | 'only-video'
219 allowRefresh?: boolean // true by default 227 allowRefresh?: boolean
220}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { 228}
229
230function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
231function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
232function getOrCreateVideoAndAccountAndChannel (
233 options: GetVideoParamOther
234): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
235async function getOrCreateVideoAndAccountAndChannel (
236 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
237): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
221 // Default params 238 // Default params
222 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 239 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
223 const fetchType = options.fetchType || 'all' 240 const fetchType = options.fetchType || 'all'
@@ -225,12 +242,13 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
225 242
226 // Get video url 243 // Get video url
227 const videoUrl = getAPId(options.videoObject) 244 const videoUrl = getAPId(options.videoObject)
228
229 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 245 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
246
230 if (videoFromDatabase) { 247 if (videoFromDatabase) {
231 if (videoFromDatabase.isOutdated() && allowRefresh === true) { 248 // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
249 if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
232 const refreshOptions = { 250 const refreshOptions = {
233 video: videoFromDatabase, 251 video: videoFromDatabase as MVideoThumbnail,
234 fetchedType: fetchType, 252 fetchedType: fetchType,
235 syncParam 253 syncParam
236 } 254 }
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 11dd02706..a027c4840 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -147,7 +147,10 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
147 }) 147 })
148} 148}
149 149
150const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights', authenticateInQuery = false) => { 150const videosCustomGetValidator = (
151 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
152 authenticateInQuery = false
153) => {
151 return [ 154 return [
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 155 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153 156
@@ -157,6 +160,9 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-
157 if (areValidationErrors(req, res)) return 160 if (areValidationErrors(req, res)) return
158 if (!await doesVideoExist(req.params.id, res, fetchType)) return 161 if (!await doesVideoExist(req.params.id, res, fetchType)) return
159 162
163 // Controllers does not need to check video rights
164 if (fetchType === 'only-immutable-attributes') return next()
165
160 const video = getVideoWithAttributes(res) 166 const video = getVideoWithAttributes(res)
161 const videoAll = video as MVideoFullLight 167 const videoAll = video as MVideoFullLight
162 168
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 0905a0fb2..a0081f259 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -32,8 +32,9 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
32import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
33import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models' 35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
37import { ModelCache } from '@server/models/model-cache'
37 38
38export enum ScopeNames { 39export enum ScopeNames {
39 SUMMARY = 'SUMMARY' 40 SUMMARY = 'SUMMARY'
@@ -218,8 +219,6 @@ export class AccountModel extends Model<AccountModel> {
218 }) 219 })
219 BlockedAccounts: AccountBlocklistModel[] 220 BlockedAccounts: AccountBlocklistModel[]
220 221
221 private static cache: { [ id: string ]: any } = {}
222
223 @BeforeDestroy 222 @BeforeDestroy
224 static async sendDeleteIfOwned (instance: AccountModel, options) { 223 static async sendDeleteIfOwned (instance: AccountModel, options) {
225 if (!instance.Actor) { 224 if (!instance.Actor) {
@@ -247,45 +246,43 @@ export class AccountModel extends Model<AccountModel> {
247 } 246 }
248 247
249 static loadLocalByName (name: string): Bluebird<MAccountDefault> { 248 static loadLocalByName (name: string): Bluebird<MAccountDefault> {
250 // The server actor never change, so we can easily cache it 249 const fun = () => {
251 if (name === SERVER_ACTOR_NAME && AccountModel.cache[name]) { 250 const query = {
252 return Bluebird.resolve(AccountModel.cache[name]) 251 where: {
253 } 252 [Op.or]: [
254 253 {
255 const query = { 254 userId: {
256 where: { 255 [Op.ne]: null
257 [Op.or]: [ 256 }
258 { 257 },
259 userId: { 258 {
260 [Op.ne]: null 259 applicationId: {
260 [Op.ne]: null
261 }
261 } 262 }
262 }, 263 ]
264 },
265 include: [
263 { 266 {
264 applicationId: { 267 model: ActorModel,
265 [Op.ne]: null 268 required: true,
269 where: {
270 preferredUsername: name
266 } 271 }
267 } 272 }
268 ] 273 ]
269 }, 274 }
270 include: [
271 {
272 model: ActorModel,
273 required: true,
274 where: {
275 preferredUsername: name
276 }
277 }
278 ]
279 }
280 275
281 return AccountModel.findOne(query) 276 return AccountModel.findOne(query)
282 .then(account => { 277 }
283 if (name === SERVER_ACTOR_NAME) {
284 AccountModel.cache[name] = account
285 }
286 278
287 return account 279 return ModelCache.Instance.doCache({
288 }) 280 cacheType: 'local-account-name',
281 key: name,
282 fun,
283 // The server actor never change, so we can easily cache it
284 whitelist: () => name === SERVER_ACTOR_NAME
285 })
289 } 286 }
290 287
291 static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> { 288 static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> {
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 00e8dc954..e547d2c0c 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -48,6 +48,7 @@ import {
48} from '../../typings/models' 48} from '../../typings/models'
49import * as Bluebird from 'bluebird' 49import * as Bluebird from 'bluebird'
50import { Op, Transaction, literal } from 'sequelize' 50import { Op, Transaction, literal } from 'sequelize'
51import { ModelCache } from '@server/models/model-cache'
51 52
52enum ScopeNames { 53enum ScopeNames {
53 FULL = 'FULL' 54 FULL = 'FULL'
@@ -276,9 +277,6 @@ export class ActorModel extends Model<ActorModel> {
276 }) 277 })
277 VideoChannel: VideoChannelModel 278 VideoChannel: VideoChannelModel
278 279
279 private static localNameCache: { [ id: string ]: any } = {}
280 private static localUrlCache: { [ id: string ]: any } = {}
281
282 static load (id: number): Bluebird<MActor> { 280 static load (id: number): Bluebird<MActor> {
283 return ActorModel.unscoped().findByPk(id) 281 return ActorModel.unscoped().findByPk(id)
284 } 282 }
@@ -345,54 +343,50 @@ export class ActorModel extends Model<ActorModel> {
345 } 343 }
346 344
347 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { 345 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> {
348 // The server actor never change, so we can easily cache it 346 const fun = () => {
349 if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localNameCache[preferredUsername]) { 347 const query = {
350 return Bluebird.resolve(ActorModel.localNameCache[preferredUsername]) 348 where: {
351 } 349 preferredUsername,
350 serverId: null
351 },
352 transaction
353 }
352 354
353 const query = { 355 return ActorModel.scope(ScopeNames.FULL)
354 where: { 356 .findOne(query)
355 preferredUsername,
356 serverId: null
357 },
358 transaction
359 } 357 }
360 358
361 return ActorModel.scope(ScopeNames.FULL) 359 return ModelCache.Instance.doCache({
362 .findOne(query) 360 cacheType: 'local-actor-name',
363 .then(actor => { 361 key: preferredUsername,
364 if (preferredUsername === SERVER_ACTOR_NAME) { 362 // The server actor never change, so we can easily cache it
365 ActorModel.localNameCache[preferredUsername] = actor 363 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
366 } 364 fun
367 365 })
368 return actor
369 })
370 } 366 }
371 367
372 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> { 368 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> {
373 // The server actor never change, so we can easily cache it 369 const fun = () => {
374 if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.localUrlCache[preferredUsername]) { 370 const query = {
375 return Bluebird.resolve(ActorModel.localUrlCache[preferredUsername]) 371 attributes: [ 'url' ],
376 } 372 where: {
373 preferredUsername,
374 serverId: null
375 },
376 transaction
377 }
377 378
378 const query = { 379 return ActorModel.unscoped()
379 attributes: [ 'url' ], 380 .findOne(query)
380 where: {
381 preferredUsername,
382 serverId: null
383 },
384 transaction
385 } 381 }
386 382
387 return ActorModel.unscoped() 383 return ModelCache.Instance.doCache({
388 .findOne(query) 384 cacheType: 'local-actor-name',
389 .then(actor => { 385 key: preferredUsername,
390 if (preferredUsername === SERVER_ACTOR_NAME) { 386 // The server actor never change, so we can easily cache it
391 ActorModel.localUrlCache[preferredUsername] = actor 387 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
392 } 388 fun
393 389 })
394 return actor
395 })
396 } 390 }
397 391
398 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> { 392 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
@@ -468,6 +462,36 @@ export class ActorModel extends Model<ActorModel> {
468 }, { where, transaction }) 462 }, { where, transaction })
469 } 463 }
470 464
465 static loadAccountActorByVideoId (videoId: number): Bluebird<MActor> {
466 const query = {
467 include: [
468 {
469 attributes: [ 'id' ],
470 model: AccountModel.unscoped(),
471 required: true,
472 include: [
473 {
474 attributes: [ 'id', 'accountId' ],
475 model: VideoChannelModel.unscoped(),
476 required: true,
477 include: [
478 {
479 attributes: [ 'id', 'channelId' ],
480 model: VideoModel.unscoped(),
481 where: {
482 id: videoId
483 }
484 }
485 ]
486 }
487 ]
488 }
489 ]
490 }
491
492 return ActorModel.unscoped().findOne(query)
493 }
494
471 getSharedInbox (this: MActorWithInboxes) { 495 getSharedInbox (this: MActorWithInboxes) {
472 return this.sharedInboxUrl || this.inboxUrl 496 return this.sharedInboxUrl || this.inboxUrl
473 } 497 }
diff --git a/server/models/model-cache.ts b/server/models/model-cache.ts
new file mode 100644
index 000000000..a87f99aa2
--- /dev/null
+++ b/server/models/model-cache.ts
@@ -0,0 +1,91 @@
1import { Model } from 'sequelize-typescript'
2import * as Bluebird from 'bluebird'
3import { logger } from '@server/helpers/logger'
4
5type ModelCacheType =
6 'local-account-name'
7 | 'local-actor-name'
8 | 'local-actor-url'
9 | 'load-video-immutable-id'
10 | 'load-video-immutable-url'
11
12type DeleteKey =
13 'video'
14
15class ModelCache {
16
17 private static instance: ModelCache
18
19 private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
20 'local-account-name': new Map(),
21 'local-actor-name': new Map(),
22 'local-actor-url': new Map(),
23 'load-video-immutable-id': new Map(),
24 'load-video-immutable-url': new Map()
25 }
26
27 private readonly deleteIds: {
28 [deleteKey in DeleteKey]: Map<number, { cacheType: ModelCacheType, key: string }[]>
29 } = {
30 video: new Map()
31 }
32
33 private constructor () {
34 }
35
36 static get Instance () {
37 return this.instance || (this.instance = new this())
38 }
39
40 doCache<T extends Model> (options: {
41 cacheType: ModelCacheType
42 key: string
43 fun: () => Bluebird<T>
44 whitelist?: () => boolean
45 deleteKey?: DeleteKey
46 }) {
47 const { cacheType, key, fun, whitelist, deleteKey } = options
48
49 if (whitelist && whitelist() !== true) return fun()
50
51 const cache = this.localCache[cacheType]
52
53 if (cache.has(key)) {
54 logger.debug('Model cache hit for %s -> %s.', cacheType, key)
55 return Bluebird.resolve<T>(cache.get(key))
56 }
57
58 return fun().then(m => {
59 if (!m) return m
60
61 if (!whitelist || whitelist()) cache.set(key, m)
62
63 if (deleteKey) {
64 const map = this.deleteIds[deleteKey]
65 if (!map.has(m.id)) map.set(m.id, [])
66
67 const a = map.get(m.id)
68 a.push({ cacheType, key })
69 }
70
71 return m
72 })
73 }
74
75 invalidateCache (deleteKey: DeleteKey, modelId: number) {
76 const map = this.deleteIds[deleteKey]
77
78 if (!map.has(modelId)) return
79
80 for (const toDelete of map.get(modelId)) {
81 logger.debug('Removing %s -> %d of model cache %s -> %s.', deleteKey, modelId, toDelete.cacheType, toDelete.key)
82 this.localCache[toDelete.cacheType].delete(toDelete.key)
83 }
84
85 map.delete(modelId)
86 }
87}
88
89export {
90 ModelCache
91}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 1ec8d717e..5964526a9 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -120,7 +120,7 @@ import {
120 MVideoFormattableDetails, 120 MVideoFormattableDetails,
121 MVideoForUser, 121 MVideoForUser,
122 MVideoFullLight, 122 MVideoFullLight,
123 MVideoIdThumbnail, 123 MVideoIdThumbnail, MVideoImmutable,
124 MVideoThumbnail, 124 MVideoThumbnail,
125 MVideoThumbnailBlacklist, 125 MVideoThumbnailBlacklist,
126 MVideoWithAllFiles, 126 MVideoWithAllFiles,
@@ -132,6 +132,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail'
132import { VideoFile } from '@shared/models/videos/video-file.model' 132import { VideoFile } from '@shared/models/videos/video-file.model'
133import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 133import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
134import validator from 'validator' 134import validator from 'validator'
135import { ModelCache } from '@server/models/model-cache'
135 136
136export enum ScopeNames { 137export enum ScopeNames {
137 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 138 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -144,6 +145,7 @@ export enum ScopeNames {
144 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 145 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
145 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 146 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
146 WITH_USER_ID = 'WITH_USER_ID', 147 WITH_USER_ID = 'WITH_USER_ID',
148 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
147 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 149 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
148} 150}
149 151
@@ -187,6 +189,9 @@ export type AvailableForListIDsOptions = {
187} 189}
188 190
189@Scopes(() => ({ 191@Scopes(() => ({
192 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
193 attributes: [ 'id', 'url', 'uuid', 'remote' ]
194 },
190 [ScopeNames.FOR_API]: (options: ForAPIOptions) => { 195 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
191 const query: FindOptions = { 196 const query: FindOptions = {
192 include: [ 197 include: [
@@ -1074,6 +1079,11 @@ export class VideoModel extends Model<VideoModel> {
1074 return undefined 1079 return undefined
1075 } 1080 }
1076 1081
1082 @BeforeDestroy
1083 static invalidateCache (instance: VideoModel) {
1084 ModelCache.Instance.invalidateCache('video', instance.id)
1085 }
1086
1077 static listLocal (): Bluebird<MVideoWithAllFiles[]> { 1087 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1078 const query = { 1088 const query = {
1079 where: { 1089 where: {
@@ -1468,6 +1478,24 @@ export class VideoModel extends Model<VideoModel> {
1468 ]).findOne(options) 1478 ]).findOne(options)
1469 } 1479 }
1470 1480
1481 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1482 const fun = () => {
1483 const query = {
1484 where: buildWhereIdOrUUID(id),
1485 transaction: t
1486 }
1487
1488 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1489 }
1490
1491 return ModelCache.Instance.doCache({
1492 cacheType: 'load-video-immutable-id',
1493 key: '' + id,
1494 deleteKey: 'video',
1495 fun
1496 })
1497 }
1498
1471 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { 1499 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1472 const where = buildWhereIdOrUUID(id) 1500 const where = buildWhereIdOrUUID(id)
1473 const options = { 1501 const options = {
@@ -1531,6 +1559,26 @@ export class VideoModel extends Model<VideoModel> {
1531 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1559 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1532 } 1560 }
1533 1561
1562 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1563 const fun = () => {
1564 const query: FindOptions = {
1565 where: {
1566 url
1567 },
1568 transaction
1569 }
1570
1571 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1572 }
1573
1574 return ModelCache.Instance.doCache({
1575 cacheType: 'load-video-immutable-url',
1576 key: url,
1577 deleteKey: 'video',
1578 fun
1579 })
1580 }
1581
1534 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { 1582 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1535 const query: FindOptions = { 1583 const query: FindOptions = {
1536 where: { 1584 where: {
diff --git a/server/typings/express.ts b/server/typings/express.ts
index 43a9b2c99..f4188bf3d 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -21,7 +21,7 @@ import {
21} from './models' 21} from './models'
22import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist' 22import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
23import { MVideoImportDefault } from '@server/typings/models/video/video-import' 23import { MVideoImportDefault } from '@server/typings/models/video/video-import'
24import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile } from '@server/typings/models' 24import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile, MVideoImmutable } from '@server/typings/models'
25import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element' 25import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
26import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate' 26import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
27import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership' 27import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
@@ -35,6 +35,7 @@ declare module 'express' {
35 35
36 locals: { 36 locals: {
37 videoAll?: MVideoFullLight 37 videoAll?: MVideoFullLight
38 onlyImmutableVideo?: MVideoImmutable
38 onlyVideo?: MVideoThumbnail 39 onlyVideo?: MVideoThumbnail
39 onlyVideoWithRights?: MVideoWithRights 40 onlyVideoWithRights?: MVideoWithRights
40 videoId?: MVideoIdThumbnail 41 videoId?: MVideoIdThumbnail
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts
index 7eff0a913..022a9566d 100644
--- a/server/typings/models/video/video.ts
+++ b/server/typings/models/video/video.ts
@@ -37,6 +37,7 @@ export type MVideoId = Pick<MVideo, 'id'>
37export type MVideoUrl = Pick<MVideo, 'url'> 37export type MVideoUrl = Pick<MVideo, 'url'>
38export type MVideoUUID = Pick<MVideo, 'uuid'> 38export type MVideoUUID = Pick<MVideo, 'uuid'>
39 39
40export type MVideoImmutable = Pick<MVideo, 'id' | 'url' | 'uuid' | 'remote' | 'isOwned'>
40export type MVideoIdUrl = MVideoId & MVideoUrl 41export type MVideoIdUrl = MVideoId & MVideoUrl
41export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> 42export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
42 43
diff --git a/yarn.lock b/yarn.lock
index 6e75dab9a..68f281e45 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2282,10 +2282,10 @@ express-oauth-server@^2.0.0:
2282 express "^4.13.3" 2282 express "^4.13.3"
2283 oauth2-server "3.0.0" 2283 oauth2-server "3.0.0"
2284 2284
2285express-rate-limit@^4.0.4: 2285express-rate-limit@^5.0.0:
2286 version "4.0.4" 2286 version "5.0.0"
2287 resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-4.0.4.tgz#a495338ae9e58c856b66d1346ec0d86f43ba2e43" 2287 resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.0.0.tgz#9a6f4cacc388c1a1da7ba2f65db69f7395e9b04e"
2288 integrity sha512-DLRj2vMO7Xgai8qWKU9O6ZztF2bdDmfFNFi9k3G9BPzJ+7MG7eWaaBikbe0eBpNGSxU8JziwW0PQKG78aNWa6g== 2288 integrity sha512-dhT57wqxfqmkOi4HM7NuT4Gd7gbUgSK2ocG27Y6lwm8lbOAw9XQfeANawGq8wLDtlGPO1ZgDj0HmKsykTxfFAg==
2289 2289
2290express-validator@^6.4.0: 2290express-validator@^6.4.0:
2291 version "6.4.0" 2291 version "6.4.0"