aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers')
-rw-r--r--server/controllers/activitypub/client.ts86
-rw-r--r--server/controllers/activitypub/inbox.ts6
-rw-r--r--server/controllers/api/accounts.ts18
-rw-r--r--server/controllers/api/config.ts77
-rw-r--r--server/controllers/api/search.ts3
-rw-r--r--server/controllers/api/server/contact.ts28
-rw-r--r--server/controllers/api/server/follows.ts16
-rw-r--r--server/controllers/api/server/index.ts4
-rw-r--r--server/controllers/api/server/server-blocklist.ts132
-rw-r--r--server/controllers/api/server/stats.ts7
-rw-r--r--server/controllers/api/users/index.ts33
-rw-r--r--server/controllers/api/users/me.ts165
-rw-r--r--server/controllers/api/users/my-blocklist.ts125
-rw-r--r--server/controllers/api/users/my-history.ts57
-rw-r--r--server/controllers/api/users/my-notifications.ts108
-rw-r--r--server/controllers/api/users/my-subscriptions.ts170
-rw-r--r--server/controllers/api/video-channel.ts18
-rw-r--r--server/controllers/api/videos/abuse.ts5
-rw-r--r--server/controllers/api/videos/blacklist.ts31
-rw-r--r--server/controllers/api/videos/captions.ts4
-rw-r--r--server/controllers/api/videos/comment.ts15
-rw-r--r--server/controllers/api/videos/import.ts21
-rw-r--r--server/controllers/api/videos/index.ts72
-rw-r--r--server/controllers/api/videos/rate.ts17
-rw-r--r--server/controllers/bots.ts101
-rw-r--r--server/controllers/client.ts21
-rw-r--r--server/controllers/feeds.ts12
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/static.ts24
-rw-r--r--server/controllers/tracker.ts46
30 files changed, 1101 insertions, 322 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 433186179..31c0a5fbd 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -3,7 +3,7 @@ import * as express from 'express'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' 5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
6import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' 6import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send'
7import { audiencify, getAudience } from '../../lib/activitypub/audience' 7import { audiencify, getAudience } from '../../lib/activitypub/audience'
8import { buildCreateActivity } from '../../lib/activitypub/send/send-create' 8import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
9import { 9import {
@@ -11,9 +11,10 @@ import {
11 executeIfActivityPub, 11 executeIfActivityPub,
12 localAccountValidator, 12 localAccountValidator,
13 localVideoChannelValidator, 13 localVideoChannelValidator,
14 videosCustomGetValidator 14 videosCustomGetValidator,
15 videosShareValidator
15} from '../../middlewares' 16} from '../../middlewares'
16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' 17import { getAccountVideoRateValidator, videoCommentGetValidator, videosGetValidator } from '../../middlewares/validators'
17import { AccountModel } from '../../models/account/account' 18import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 19import { ActorModel } from '../../models/activitypub/actor'
19import { ActorFollowModel } from '../../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@@ -25,14 +26,17 @@ import { cacheRoute } from '../../middlewares/cache'
25import { activityPubResponse } from './utils' 26import { activityPubResponse } from './utils'
26import { AccountVideoRateModel } from '../../models/account/account-video-rate' 27import { AccountVideoRateModel } from '../../models/account/account-video-rate'
27import { 28import {
29 getRateUrl,
28 getVideoCommentsActivityPubUrl, 30 getVideoCommentsActivityPubUrl,
29 getVideoDislikesActivityPubUrl, 31 getVideoDislikesActivityPubUrl,
30 getVideoLikesActivityPubUrl, 32 getVideoLikesActivityPubUrl,
31 getVideoSharesActivityPubUrl 33 getVideoSharesActivityPubUrl
32} from '../../lib/activitypub' 34} from '../../lib/activitypub'
33import { VideoCaptionModel } from '../../models/video/video-caption' 35import { VideoCaptionModel } from '../../models/video/video-caption'
34import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' 36import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
35import { getServerActor } from '../../helpers/utils' 37import { getServerActor } from '../../helpers/utils'
38import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
39import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
36 40
37const activityPubClientRouter = express.Router() 41const activityPubClientRouter = express.Router()
38 42
@@ -48,21 +52,29 @@ activityPubClientRouter.get('/accounts?/:name/following',
48 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 52 executeIfActivityPub(asyncMiddleware(localAccountValidator)),
49 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 53 executeIfActivityPub(asyncMiddleware(accountFollowingController))
50) 54)
55activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
56 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
57 executeIfActivityPub(getAccountVideoRate('like'))
58)
59activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
60 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
61 executeIfActivityPub(getAccountVideoRate('dislike'))
62)
51 63
52activityPubClientRouter.get('/videos/watch/:id', 64activityPubClientRouter.get('/videos/watch/:id',
53 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), 65 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
54 executeIfActivityPub(asyncMiddleware(videosGetValidator)), 66 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))),
55 executeIfActivityPub(asyncMiddleware(videoController)) 67 executeIfActivityPub(asyncMiddleware(videoController))
56) 68)
57activityPubClientRouter.get('/videos/watch/:id/activity', 69activityPubClientRouter.get('/videos/watch/:id/activity',
58 executeIfActivityPub(asyncMiddleware(videosGetValidator)), 70 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video-with-rights'))),
59 executeIfActivityPub(asyncMiddleware(videoController)) 71 executeIfActivityPub(asyncMiddleware(videoController))
60) 72)
61activityPubClientRouter.get('/videos/watch/:id/announces', 73activityPubClientRouter.get('/videos/watch/:id/announces',
62 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 74 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
63 executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) 75 executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
64) 76)
65activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', 77activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
66 executeIfActivityPub(asyncMiddleware(videosShareValidator)), 78 executeIfActivityPub(asyncMiddleware(videosShareValidator)),
67 executeIfActivityPub(asyncMiddleware(videoAnnounceController)) 79 executeIfActivityPub(asyncMiddleware(videoAnnounceController))
68) 80)
@@ -101,7 +113,11 @@ activityPubClientRouter.get('/video-channels/:name/following',
101) 113)
102 114
103activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', 115activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?',
104 executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), 116 executeIfActivityPub(asyncMiddleware(videoFileRedundancyGetValidator)),
117 executeIfActivityPub(asyncMiddleware(videoRedundancyController))
118)
119activityPubClientRouter.get('/redundancy/video-playlists/:streamingPlaylistType/:videoId',
120 executeIfActivityPub(asyncMiddleware(videoPlaylistRedundancyGetValidator)),
105 executeIfActivityPub(asyncMiddleware(videoRedundancyController)) 121 executeIfActivityPub(asyncMiddleware(videoRedundancyController))
106) 122)
107 123
@@ -133,8 +149,25 @@ async function accountFollowingController (req: express.Request, res: express.Re
133 return activityPubResponse(activityPubContextify(activityPubResult), res) 149 return activityPubResponse(activityPubContextify(activityPubResult), res)
134} 150}
135 151
136async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 152function getAccountVideoRate (rateType: VideoRateType) {
137 const video: VideoModel = res.locals.video 153 return (req: express.Request, res: express.Response) => {
154 const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
155
156 const byActor = accountVideoRate.Account.Actor
157 const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
158 const APObject = rateType === 'like'
159 ? buildLikeActivity(url, byActor, accountVideoRate.Video)
160 : buildDislikeActivity(url, byActor, accountVideoRate.Video)
161
162 return activityPubResponse(activityPubContextify(APObject), res)
163 }
164}
165
166async function videoController (req: express.Request, res: express.Response) {
167 // We need more attributes
168 const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id)
169
170 if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url)
138 171
139 // We need captions to render AP object 172 // We need captions to render AP object
140 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 173 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
@@ -150,14 +183,17 @@ async function videoController (req: express.Request, res: express.Response, nex
150 return activityPubResponse(activityPubContextify(videoObject), res) 183 return activityPubResponse(activityPubContextify(videoObject), res)
151} 184}
152 185
153async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { 186async function videoAnnounceController (req: express.Request, res: express.Response) {
154 const share = res.locals.videoShare as VideoShareModel 187 const share = res.locals.videoShare as VideoShareModel
188
189 if (share.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(share.url)
190
155 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 191 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
156 192
157 return activityPubResponse(activityPubContextify(activity), res) 193 return activityPubResponse(activityPubContextify(activity), res)
158} 194}
159 195
160async function videoAnnouncesController (req: express.Request, res: express.Response, next: express.NextFunction) { 196async function videoAnnouncesController (req: express.Request, res: express.Response) {
161 const video: VideoModel = res.locals.video 197 const video: VideoModel = res.locals.video
162 198
163 const handler = async (start: number, count: number) => { 199 const handler = async (start: number, count: number) => {
@@ -172,21 +208,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
172 return activityPubResponse(activityPubContextify(json), res) 208 return activityPubResponse(activityPubContextify(json), res)
173} 209}
174 210
175async function videoLikesController (req: express.Request, res: express.Response, next: express.NextFunction) { 211async function videoLikesController (req: express.Request, res: express.Response) {
176 const video: VideoModel = res.locals.video 212 const video: VideoModel = res.locals.video
177 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 213 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
178 214
179 return activityPubResponse(activityPubContextify(json), res) 215 return activityPubResponse(activityPubContextify(json), res)
180} 216}
181 217
182async function videoDislikesController (req: express.Request, res: express.Response, next: express.NextFunction) { 218async function videoDislikesController (req: express.Request, res: express.Response) {
183 const video: VideoModel = res.locals.video 219 const video: VideoModel = res.locals.video
184 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 220 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
185 221
186 return activityPubResponse(activityPubContextify(json), res) 222 return activityPubResponse(activityPubContextify(json), res)
187} 223}
188 224
189async function videoCommentsController (req: express.Request, res: express.Response, next: express.NextFunction) { 225async function videoCommentsController (req: express.Request, res: express.Response) {
190 const video: VideoModel = res.locals.video 226 const video: VideoModel = res.locals.video
191 227
192 const handler = async (start: number, count: number) => { 228 const handler = async (start: number, count: number) => {
@@ -201,29 +237,31 @@ async function videoCommentsController (req: express.Request, res: express.Respo
201 return activityPubResponse(activityPubContextify(json), res) 237 return activityPubResponse(activityPubContextify(json), res)
202} 238}
203 239
204async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { 240async function videoChannelController (req: express.Request, res: express.Response) {
205 const videoChannel: VideoChannelModel = res.locals.videoChannel 241 const videoChannel: VideoChannelModel = res.locals.videoChannel
206 242
207 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) 243 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res)
208} 244}
209 245
210async function videoChannelFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { 246async function videoChannelFollowersController (req: express.Request, res: express.Response) {
211 const videoChannel: VideoChannelModel = res.locals.videoChannel 247 const videoChannel: VideoChannelModel = res.locals.videoChannel
212 const activityPubResult = await actorFollowers(req, videoChannel.Actor) 248 const activityPubResult = await actorFollowers(req, videoChannel.Actor)
213 249
214 return activityPubResponse(activityPubContextify(activityPubResult), res) 250 return activityPubResponse(activityPubContextify(activityPubResult), res)
215} 251}
216 252
217async function videoChannelFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { 253async function videoChannelFollowingController (req: express.Request, res: express.Response) {
218 const videoChannel: VideoChannelModel = res.locals.videoChannel 254 const videoChannel: VideoChannelModel = res.locals.videoChannel
219 const activityPubResult = await actorFollowing(req, videoChannel.Actor) 255 const activityPubResult = await actorFollowing(req, videoChannel.Actor)
220 256
221 return activityPubResponse(activityPubContextify(activityPubResult), res) 257 return activityPubResponse(activityPubContextify(activityPubResult), res)
222} 258}
223 259
224async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { 260async function videoCommentController (req: express.Request, res: express.Response) {
225 const videoComment: VideoCommentModel = res.locals.videoComment 261 const videoComment: VideoCommentModel = res.locals.videoComment
226 262
263 if (videoComment.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoComment.url)
264
227 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 265 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
228 const isPublic = true // Comments are always public 266 const isPublic = true // Comments are always public
229 const audience = getAudience(videoComment.Account.Actor, isPublic) 267 const audience = getAudience(videoComment.Account.Actor, isPublic)
@@ -239,7 +277,9 @@ async function videoCommentController (req: express.Request, res: express.Respon
239} 277}
240 278
241async function videoRedundancyController (req: express.Request, res: express.Response) { 279async function videoRedundancyController (req: express.Request, res: express.Response) {
242 const videoRedundancy = res.locals.videoRedundancy 280 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
281 if (videoRedundancy.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoRedundancy.url)
282
243 const serverActor = await getServerActor() 283 const serverActor = await getServerActor()
244 284
245 const audience = getAudience(serverActor) 285 const audience = getAudience(serverActor)
@@ -260,7 +300,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
260 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) 300 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
261 } 301 }
262 302
263 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 303 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
264} 304}
265 305
266async function actorFollowers (req: express.Request, actor: ActorModel) { 306async function actorFollowers (req: express.Request, actor: ActorModel) {
@@ -268,7 +308,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
268 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) 308 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
269 } 309 }
270 310
271 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) 311 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
272} 312}
273 313
274function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { 314function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
@@ -276,7 +316,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
276 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 316 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
277 return { 317 return {
278 total: result.count, 318 total: result.count,
279 data: result.rows.map(r => r.Account.Actor.url) 319 data: result.rows.map(r => r.url)
280 } 320 }
281 } 321 }
282 return activityPubCollectionPagination(url, handler, req.query.page) 322 return activityPubCollectionPagination(url, handler, req.query.page)
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index 738d155eb..f0e65015b 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -43,11 +43,13 @@ export {
43// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
44 44
45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { 45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
46 processActivities(task.activities, task.signatureActor, task.inboxActor) 46 const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
47
48 processActivities(task.activities, options)
47 .then(() => cb()) 49 .then(() => cb())
48}) 50})
49 51
50function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { 52function inboxController (req: express.Request, res: express.Response) {
51 const rootActivity: RootActivity = req.body 53 const rootActivity: RootActivity = req.body
52 let activities: Activity[] = [] 54 let activities: Activity[] = []
53 55
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index b7691ccba..8c0237203 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,7 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, commonVideosFiltersValidator, 4 asyncMiddleware,
5 commonVideosFiltersValidator,
5 listVideoAccountChannelsValidator, 6 listVideoAccountChannelsValidator,
6 optionalAuthenticate, 7 optionalAuthenticate,
7 paginationValidator, 8 paginationValidator,
@@ -13,6 +14,8 @@ import { AccountModel } from '../../models/account/account'
13import { VideoModel } from '../../models/video/video' 14import { VideoModel } from '../../models/video/video'
14import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 15import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
15import { VideoChannelModel } from '../../models/video/video-channel' 16import { VideoChannelModel } from '../../models/video/video-channel'
17import { JobQueue } from '../../lib/job-queue'
18import { logger } from '../../helpers/logger'
16 19
17const accountsRouter = express.Router() 20const accountsRouter = express.Router()
18 21
@@ -56,6 +59,11 @@ export {
56function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { 59function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) {
57 const account: AccountModel = res.locals.account 60 const account: AccountModel = res.locals.account
58 61
62 if (account.isOutdated()) {
63 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } })
64 .catch(err => logger.error('Cannot create AP refresher job for actor %s.', account.Actor.url, { err }))
65 }
66
59 return res.json(account.toFormattedJSON()) 67 return res.json(account.toFormattedJSON())
60} 68}
61 69
@@ -73,10 +81,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp
73 81
74async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 82async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
75 const account: AccountModel = res.locals.account 83 const account: AccountModel = res.locals.account
76 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 84 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
77 85
78 const resultList = await VideoModel.listForApi({ 86 const resultList = await VideoModel.listForApi({
79 actorId, 87 followerActorId,
80 start: req.query.start, 88 start: req.query.start,
81 count: req.query.count, 89 count: req.query.count,
82 sort: req.query.sort, 90 sort: req.query.sort,
@@ -86,9 +94,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
86 languageOneOf: req.query.languageOneOf, 94 languageOneOf: req.query.languageOneOf,
87 tagsOneOf: req.query.tagsOneOf, 95 tagsOneOf: req.query.tagsOneOf,
88 tagsAllOf: req.query.tagsAllOf, 96 tagsAllOf: req.query.tagsAllOf,
97 filter: req.query.filter,
89 nsfw: buildNSFWFilter(res, req.query.nsfw), 98 nsfw: buildNSFWFilter(res, req.query.nsfw),
90 withFiles: false, 99 withFiles: false,
91 accountId: account.id 100 accountId: account.id,
101 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
92 }) 102 })
93 103
94 return res.json(getFormattedObjects(resultList.data, resultList.total)) 104 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 03c1cec7b..1f3341bc0 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { omit } from 'lodash' 2import { snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 3import { ServerConfig, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 4import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
@@ -10,7 +10,10 @@ import { customConfigUpdateValidator } from '../../middlewares/validators/config
10import { ClientHtml } from '../../lib/client-html' 10import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra' 12import { remove, writeJSON } from 'fs-extra'
13import { getVersion } from '../../helpers/utils' 13import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer'
15import { isNumeric } from 'validator'
16import { objectConverter } from '../../helpers/core-utils'
14 17
15const packageJSON = require('../../../../package.json') 18const packageJSON = require('../../../../package.json')
16const configRouter = express.Router() 19const configRouter = express.Router()
@@ -40,11 +43,11 @@ configRouter.delete('/custom',
40) 43)
41 44
42let serverCommit: string 45let serverCommit: string
43async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 46async function getConfig (req: express.Request, res: express.Response) {
44 const allowed = await isSignupAllowed() 47 const allowed = await isSignupAllowed()
45 const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) 48 const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip)
46 serverCommit = (serverCommit) ? serverCommit : await getVersion() 49
47 if (serverCommit === packageJSON.version) serverCommit = '' 50 if (serverCommit === undefined) serverCommit = await getServerCommit()
48 51
49 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) 52 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
50 .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true) 53 .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
@@ -61,6 +64,12 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
61 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 64 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
62 } 65 }
63 }, 66 },
67 email: {
68 enabled: Emailer.isEnabled()
69 },
70 contactForm: {
71 enabled: CONFIG.CONTACT_FORM.ENABLED
72 },
64 serverVersion: packageJSON.version, 73 serverVersion: packageJSON.version,
65 serverCommit, 74 serverCommit,
66 signup: { 75 signup: {
@@ -69,6 +78,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
69 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION 78 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
70 }, 79 },
71 transcoding: { 80 transcoding: {
81 hls: {
82 enabled: CONFIG.TRANSCODING.HLS.ENABLED
83 },
72 enabledResolutions 84 enabledResolutions
73 }, 85 },
74 import: { 86 import: {
@@ -111,6 +123,11 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
111 user: { 123 user: {
112 videoQuota: CONFIG.USER.VIDEO_QUOTA, 124 videoQuota: CONFIG.USER.VIDEO_QUOTA,
113 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY 125 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
126 },
127 trending: {
128 videos: {
129 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
130 }
114 } 131 }
115 } 132 }
116 133
@@ -150,32 +167,10 @@ async function deleteCustomConfig (req: express.Request, res: express.Response,
150} 167}
151 168
152async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 169async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
153 const toUpdate: CustomConfig = req.body
154 const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) 170 const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
155 171
156 // Force number conversion 172 // camelCase to snake_case key + Force number conversion
157 toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10) 173 const toUpdateJSON = convertCustomConfigBody(req.body)
158 toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10)
159 toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10)
160 toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10)
161 toUpdate.user.videoQuotaDaily = parseInt('' + toUpdate.user.videoQuotaDaily, 10)
162 toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
163
164 // camelCase to snake_case key
165 const toUpdateJSON = omit(
166 toUpdate,
167 'user.videoQuota',
168 'instance.defaultClientRoute',
169 'instance.shortDescription',
170 'cache.videoCaptions',
171 'signup.requiresEmailVerification'
172 )
173 toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
174 toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
175 toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
176 toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
177 toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
178 toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
179 174
180 await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) 175 await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
181 176
@@ -237,12 +232,16 @@ function customConfig (): CustomConfig {
237 admin: { 232 admin: {
238 email: CONFIG.ADMIN.EMAIL 233 email: CONFIG.ADMIN.EMAIL
239 }, 234 },
235 contactForm: {
236 enabled: CONFIG.CONTACT_FORM.ENABLED
237 },
240 user: { 238 user: {
241 videoQuota: CONFIG.USER.VIDEO_QUOTA, 239 videoQuota: CONFIG.USER.VIDEO_QUOTA,
242 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY 240 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
243 }, 241 },
244 transcoding: { 242 transcoding: {
245 enabled: CONFIG.TRANSCODING.ENABLED, 243 enabled: CONFIG.TRANSCODING.ENABLED,
244 allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
246 threads: CONFIG.TRANSCODING.THREADS, 245 threads: CONFIG.TRANSCODING.THREADS,
247 resolutions: { 246 resolutions: {
248 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], 247 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
@@ -250,6 +249,9 @@ function customConfig (): CustomConfig {
250 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], 249 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
251 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], 250 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
252 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] 251 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ]
252 },
253 hls: {
254 enabled: CONFIG.TRANSCODING.HLS.ENABLED
253 } 255 }
254 }, 256 },
255 import: { 257 import: {
@@ -264,3 +266,20 @@ function customConfig (): CustomConfig {
264 } 266 }
265 } 267 }
266} 268}
269
270function convertCustomConfigBody (body: CustomConfig) {
271 function keyConverter (k: string) {
272 // Transcoding resolutions exception
273 if (/^\d{3,4}p$/.exec(k)) return k
274
275 return snakeCase(k)
276 }
277
278 function valueConverter (v: any) {
279 if (isNumeric(v + '')) return parseInt('' + v, 10)
280
281 return v
282 }
283
284 return objectConverter(body, keyConverter, valueConverter)
285}
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 4be2b5ef7..534305ba6 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -118,7 +118,8 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
118 const options = Object.assign(query, { 118 const options = Object.assign(query, {
119 includeLocalVideos: true, 119 includeLocalVideos: true,
120 nsfw: buildNSFWFilter(res, query.nsfw), 120 nsfw: buildNSFWFilter(res, query.nsfw),
121 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 121 filter: query.filter,
122 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
122 }) 123 })
123 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 124 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
124 125
diff --git a/server/controllers/api/server/contact.ts b/server/controllers/api/server/contact.ts
new file mode 100644
index 000000000..b1144c94e
--- /dev/null
+++ b/server/controllers/api/server/contact.ts
@@ -0,0 +1,28 @@
1import * as express from 'express'
2import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
3import { Redis } from '../../../lib/redis'
4import { Emailer } from '../../../lib/emailer'
5import { ContactForm } from '../../../../shared/models/server'
6
7const contactRouter = express.Router()
8
9contactRouter.post('/contact',
10 asyncMiddleware(contactAdministratorValidator),
11 asyncMiddleware(contactAdministrator)
12)
13
14async function contactAdministrator (req: express.Request, res: express.Response) {
15 const data = req.body as ContactForm
16
17 await Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.body)
18
19 await Redis.Instance.setContactFormIp(req.ip)
20
21 return res.status(204).end()
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 contactRouter
28}
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index d62400e42..9fa6c34ba 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -61,14 +61,26 @@ export {
61 61
62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { 62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
63 const serverActor = await getServerActor() 63 const serverActor = await getServerActor()
64 const resultList = await ActorFollowModel.listFollowingForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 64 const resultList = await ActorFollowModel.listFollowingForApi(
65 serverActor.id,
66 req.query.start,
67 req.query.count,
68 req.query.sort,
69 req.query.search
70 )
65 71
66 return res.json(getFormattedObjects(resultList.data, resultList.total)) 72 return res.json(getFormattedObjects(resultList.data, resultList.total))
67} 73}
68 74
69async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { 75async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
70 const serverActor = await getServerActor() 76 const serverActor = await getServerActor()
71 const resultList = await ActorFollowModel.listFollowersForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 77 const resultList = await ActorFollowModel.listFollowersForApi(
78 serverActor.id,
79 req.query.start,
80 req.query.count,
81 req.query.sort,
82 req.query.search
83 )
72 84
73 return res.json(getFormattedObjects(resultList.data, resultList.total)) 85 return res.json(getFormattedObjects(resultList.data, resultList.total))
74} 86}
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts
index 43bca2c10..814248e5f 100644
--- a/server/controllers/api/server/index.ts
+++ b/server/controllers/api/server/index.ts
@@ -2,12 +2,16 @@ import * as express from 'express'
2import { serverFollowsRouter } from './follows' 2import { serverFollowsRouter } from './follows'
3import { statsRouter } from './stats' 3import { statsRouter } from './stats'
4import { serverRedundancyRouter } from './redundancy' 4import { serverRedundancyRouter } from './redundancy'
5import { serverBlocklistRouter } from './server-blocklist'
6import { contactRouter } from './contact'
5 7
6const serverRouter = express.Router() 8const serverRouter = express.Router()
7 9
8serverRouter.use('/', serverFollowsRouter) 10serverRouter.use('/', serverFollowsRouter)
9serverRouter.use('/', serverRedundancyRouter) 11serverRouter.use('/', serverRedundancyRouter)
10serverRouter.use('/', statsRouter) 12serverRouter.use('/', statsRouter)
13serverRouter.use('/', serverBlocklistRouter)
14serverRouter.use('/', contactRouter)
11 15
12// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
13 17
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts
new file mode 100644
index 000000000..3cb3a96e2
--- /dev/null
+++ b/server/controllers/api/server/server-blocklist.ts
@@ -0,0 +1,132 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 ensureUserHasRight,
9 paginationValidator,
10 setDefaultPagination,
11 setDefaultSort
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockAccountByServerValidator,
19 unblockServerByServerValidator
20} from '../../../middlewares/validators'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26import { UserRight } from '../../../../shared/models/users'
27
28const serverBlocklistRouter = express.Router()
29
30serverBlocklistRouter.get('/blocklist/accounts',
31 authenticate,
32 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
33 paginationValidator,
34 accountsBlocklistSortValidator,
35 setDefaultSort,
36 setDefaultPagination,
37 asyncMiddleware(listBlockedAccounts)
38)
39
40serverBlocklistRouter.post('/blocklist/accounts',
41 authenticate,
42 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
43 asyncMiddleware(blockAccountValidator),
44 asyncRetryTransactionMiddleware(blockAccount)
45)
46
47serverBlocklistRouter.delete('/blocklist/accounts/:accountName',
48 authenticate,
49 ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST),
50 asyncMiddleware(unblockAccountByServerValidator),
51 asyncRetryTransactionMiddleware(unblockAccount)
52)
53
54serverBlocklistRouter.get('/blocklist/servers',
55 authenticate,
56 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
57 paginationValidator,
58 serversBlocklistSortValidator,
59 setDefaultSort,
60 setDefaultPagination,
61 asyncMiddleware(listBlockedServers)
62)
63
64serverBlocklistRouter.post('/blocklist/servers',
65 authenticate,
66 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
67 asyncMiddleware(blockServerValidator),
68 asyncRetryTransactionMiddleware(blockServer)
69)
70
71serverBlocklistRouter.delete('/blocklist/servers/:host',
72 authenticate,
73 ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST),
74 asyncMiddleware(unblockServerByServerValidator),
75 asyncRetryTransactionMiddleware(unblockServer)
76)
77
78export {
79 serverBlocklistRouter
80}
81
82// ---------------------------------------------------------------------------
83
84async function listBlockedAccounts (req: express.Request, res: express.Response) {
85 const serverActor = await getServerActor()
86
87 const resultList = await AccountBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort)
88
89 return res.json(getFormattedObjects(resultList.data, resultList.total))
90}
91
92async function blockAccount (req: express.Request, res: express.Response) {
93 const serverActor = await getServerActor()
94 const accountToBlock: AccountModel = res.locals.account
95
96 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
97
98 return res.status(204).end()
99}
100
101async function unblockAccount (req: express.Request, res: express.Response) {
102 const accountBlock: AccountBlocklistModel = res.locals.accountBlock
103
104 await removeAccountFromBlocklist(accountBlock)
105
106 return res.status(204).end()
107}
108
109async function listBlockedServers (req: express.Request, res: express.Response) {
110 const serverActor = await getServerActor()
111
112 const resultList = await ServerBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort)
113
114 return res.json(getFormattedObjects(resultList.data, resultList.total))
115}
116
117async function blockServer (req: express.Request, res: express.Response) {
118 const serverActor = await getServerActor()
119 const serverToBlock: ServerModel = res.locals.server
120
121 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
122
123 return res.status(204).end()
124}
125
126async function unblockServer (req: express.Request, res: express.Response) {
127 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
128
129 await removeServerFromBlocklist(serverBlock)
130
131 return res.status(204).end()
132}
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts
index 85803f69e..89ffd1717 100644
--- a/server/controllers/api/server/stats.ts
+++ b/server/controllers/api/server/stats.ts
@@ -8,6 +8,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 8import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
9import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' 9import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
10import { cacheRoute } from '../../../middlewares/cache' 10import { cacheRoute } from '../../../middlewares/cache'
11import { VideoFileModel } from '../../../models/video/video-file'
11 12
12const statsRouter = express.Router() 13const statsRouter = express.Router()
13 14
@@ -16,11 +17,12 @@ statsRouter.get('/stats',
16 asyncMiddleware(getStats) 17 asyncMiddleware(getStats)
17) 18)
18 19
19async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) { 20async function getStats (req: express.Request, res: express.Response) {
20 const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() 21 const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
21 const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() 22 const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
22 const { totalUsers } = await UserModel.getStats() 23 const { totalUsers } = await UserModel.getStats()
23 const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() 24 const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
25 const { totalLocalVideoFilesSize } = await VideoFileModel.getStats()
24 26
25 const videosRedundancyStats = await Promise.all( 27 const videosRedundancyStats = await Promise.all(
26 CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { 28 CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => {
@@ -32,8 +34,9 @@ async function getStats (req: express.Request, res: express.Response, next: expr
32 const data: ServerStats = { 34 const data: ServerStats = {
33 totalLocalVideos, 35 totalLocalVideos,
34 totalLocalVideoViews, 36 totalLocalVideoViews,
35 totalVideos, 37 totalLocalVideoFilesSize,
36 totalLocalVideoComments, 38 totalLocalVideoComments,
39 totalVideos,
37 totalVideoComments, 40 totalVideoComments,
38 totalUsers, 41 totalUsers,
39 totalInstanceFollowers, 42 totalInstanceFollowers,
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 0b0081520..e3533a7f6 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -37,6 +37,11 @@ import { UserModel } from '../../../models/account/user'
37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
38import { meRouter } from './me' 38import { meRouter } from './me'
39import { deleteUserToken } from '../../../lib/oauth-model' 39import { deleteUserToken } from '../../../lib/oauth-model'
40import { myBlocklistRouter } from './my-blocklist'
41import { myVideosHistoryRouter } from './my-history'
42import { myNotificationsRouter } from './my-notifications'
43import { Notifier } from '../../../lib/notifier'
44import { mySubscriptionsRouter } from './my-subscriptions'
40 45
41const auditLogger = auditLoggerFactory('users') 46const auditLogger = auditLoggerFactory('users')
42 47
@@ -53,6 +58,10 @@ const askSendEmailLimiter = new RateLimit({
53}) 58})
54 59
55const usersRouter = express.Router() 60const usersRouter = express.Router()
61usersRouter.use('/', myNotificationsRouter)
62usersRouter.use('/', mySubscriptionsRouter)
63usersRouter.use('/', myBlocklistRouter)
64usersRouter.use('/', myVideosHistoryRouter)
56usersRouter.use('/', meRouter) 65usersRouter.use('/', meRouter)
57 66
58usersRouter.get('/autocomplete', 67usersRouter.get('/autocomplete',
@@ -207,6 +216,8 @@ async function registerUser (req: express.Request, res: express.Response) {
207 await sendVerifyUserEmail(user) 216 await sendVerifyUserEmail(user)
208 } 217 }
209 218
219 Notifier.Instance.notifyOnNewUserRegistration(user)
220
210 return res.type('json').status(204).end() 221 return res.type('json').status(204).end()
211} 222}
212 223
@@ -218,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
218 return res.status(204).end() 229 return res.status(204).end()
219} 230}
220 231
221async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 232async function blockUser (req: express.Request, res: express.Response) {
222 const user: UserModel = res.locals.user 233 const user: UserModel = res.locals.user
223 const reason = req.body.reason 234 const reason = req.body.reason
224 235
@@ -227,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp
227 return res.status(204).end() 238 return res.status(204).end()
228} 239}
229 240
230function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 241function getUser (req: express.Request, res: express.Response) {
231 return res.json((res.locals.user as UserModel).toFormattedJSON()) 242 return res.json((res.locals.user as UserModel).toFormattedJSON())
232} 243}
233 244
234async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 245async function autocompleteUsers (req: express.Request, res: express.Response) {
235 const resultList = await UserModel.autoComplete(req.query.search as string) 246 const resultList = await UserModel.autoComplete(req.query.search as string)
236 247
237 return res.json(resultList) 248 return res.json(resultList)
238} 249}
239 250
240async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 251async function listUsers (req: express.Request, res: express.Response) {
241 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort) 252 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
242 253
243 return res.json(getFormattedObjects(resultList.data, resultList.total)) 254 return res.json(getFormattedObjects(resultList.data, resultList.total))
244} 255}
245 256
246async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { 257async function removeUser (req: express.Request, res: express.Response) {
247 const user: UserModel = res.locals.user 258 const user: UserModel = res.locals.user
248 259
249 await user.destroy() 260 await user.destroy()
@@ -253,13 +264,15 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
253 return res.sendStatus(204) 264 return res.sendStatus(204)
254} 265}
255 266
256async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 267async function updateUser (req: express.Request, res: express.Response) {
257 const body: UserUpdate = req.body 268 const body: UserUpdate = req.body
258 const userToUpdate = res.locals.user as UserModel 269 const userToUpdate = res.locals.user as UserModel
259 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) 270 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
260 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role 271 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
261 272
273 if (body.password !== undefined) userToUpdate.password = body.password
262 if (body.email !== undefined) userToUpdate.email = body.email 274 if (body.email !== undefined) userToUpdate.email = body.email
275 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
263 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota 276 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
264 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily 277 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
265 if (body.role !== undefined) userToUpdate.role = body.role 278 if (body.role !== undefined) userToUpdate.role = body.role
@@ -267,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
267 const user = await userToUpdate.save() 280 const user = await userToUpdate.save()
268 281
269 // Destroy user token to refresh rights 282 // Destroy user token to refresh rights
270 if (roleChanged) await deleteUserToken(userToUpdate.id) 283 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
271 284
272 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 285 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
273 286
274 // Don't need to send this update to followers, these attributes are not propagated 287 // Don't need to send this update to followers, these attributes are not federated
275 288
276 return res.sendStatus(204) 289 return res.sendStatus(204)
277} 290}
@@ -281,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response
281 294
282 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) 295 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
283 const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString 296 const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
284 await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) 297 await Emailer.Instance.addPasswordResetEmailJob(user.email, url)
285 298
286 return res.status(204).end() 299 return res.status(204).end()
287} 300}
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 591ec6b25..d5e154869 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,47 +2,34 @@ import * as express from 'express'
2import 'multer' 2import 'multer'
3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 3import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { sendUpdateActor } from '../../../lib/activitypub/send' 6import { sendUpdateActor } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
9 asyncRetryTransactionMiddleware, 9 asyncRetryTransactionMiddleware,
10 authenticate, 10 authenticate,
11 commonVideosFiltersValidator,
12 paginationValidator, 11 paginationValidator,
13 setDefaultPagination, 12 setDefaultPagination,
14 setDefaultSort, 13 setDefaultSort,
15 userSubscriptionAddValidator,
16 userSubscriptionGetValidator,
17 usersUpdateMeValidator, 14 usersUpdateMeValidator,
18 usersVideoRatingValidator 15 usersVideoRatingValidator
19} from '../../../middlewares' 16} from '../../../middlewares'
20import { 17import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
21 areSubscriptionsExistValidator,
22 deleteMeValidator,
23 userSubscriptionsSortValidator,
24 videoImportsSortValidator,
25 videosSortValidator
26} from '../../../middlewares/validators'
27import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 18import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
28import { UserModel } from '../../../models/account/user' 19import { UserModel } from '../../../models/account/user'
29import { VideoModel } from '../../../models/video/video' 20import { VideoModel } from '../../../models/video/video'
30import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' 21import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
31import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils' 22import { createReqFiles } from '../../../helpers/express-utils'
32import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 23import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
33import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 24import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
34import { updateActorAvatarFile } from '../../../lib/avatar' 25import { updateActorAvatarFile } from '../../../lib/avatar'
35import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
36import { VideoImportModel } from '../../../models/video/video-import' 27import { VideoImportModel } from '../../../models/video/video-import'
37import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
38import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
39import { JobQueue } from '../../../lib/job-queue'
40import { logger } from '../../../helpers/logger'
41import { AccountModel } from '../../../models/account/account' 28import { AccountModel } from '../../../models/account/account'
42 29
43const auditLogger = auditLoggerFactory('users-me') 30const auditLogger = auditLoggerFactory('users-me')
44 31
45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 32const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
46 33
47const meRouter = express.Router() 34const meRouter = express.Router()
48 35
@@ -98,51 +85,6 @@ meRouter.post('/me/avatar/pick',
98 asyncRetryTransactionMiddleware(updateMyAvatar) 85 asyncRetryTransactionMiddleware(updateMyAvatar)
99) 86)
100 87
101// ##### Subscriptions part #####
102
103meRouter.get('/me/subscriptions/videos',
104 authenticate,
105 paginationValidator,
106 videosSortValidator,
107 setDefaultSort,
108 setDefaultPagination,
109 commonVideosFiltersValidator,
110 asyncMiddleware(getUserSubscriptionVideos)
111)
112
113meRouter.get('/me/subscriptions/exist',
114 authenticate,
115 areSubscriptionsExistValidator,
116 asyncMiddleware(areSubscriptionsExist)
117)
118
119meRouter.get('/me/subscriptions',
120 authenticate,
121 paginationValidator,
122 userSubscriptionsSortValidator,
123 setDefaultSort,
124 setDefaultPagination,
125 asyncMiddleware(getUserSubscriptions)
126)
127
128meRouter.post('/me/subscriptions',
129 authenticate,
130 userSubscriptionAddValidator,
131 asyncMiddleware(addUserSubscription)
132)
133
134meRouter.get('/me/subscriptions/:uri',
135 authenticate,
136 userSubscriptionGetValidator,
137 getUserSubscription
138)
139
140meRouter.delete('/me/subscriptions/:uri',
141 authenticate,
142 userSubscriptionGetValidator,
143 asyncRetryTransactionMiddleware(deleteUserSubscription)
144)
145
146// --------------------------------------------------------------------------- 88// ---------------------------------------------------------------------------
147 89
148export { 90export {
@@ -151,99 +93,6 @@ export {
151 93
152// --------------------------------------------------------------------------- 94// ---------------------------------------------------------------------------
153 95
154async function areSubscriptionsExist (req: express.Request, res: express.Response) {
155 const uris = req.query.uris as string[]
156 const user = res.locals.oauth.token.User as UserModel
157
158 const handles = uris.map(u => {
159 let [ name, host ] = u.split('@')
160 if (host === CONFIG.WEBSERVER.HOST) host = null
161
162 return { name, host, uri: u }
163 })
164
165 const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles)
166
167 const existObject: { [id: string ]: boolean } = {}
168 for (const handle of handles) {
169 const obj = results.find(r => {
170 const server = r.ActorFollowing.Server
171
172 return r.ActorFollowing.preferredUsername === handle.name &&
173 (
174 (!server && !handle.host) ||
175 (server.host === handle.host)
176 )
177 })
178
179 existObject[handle.uri] = obj !== undefined
180 }
181
182 return res.json(existObject)
183}
184
185async function addUserSubscription (req: express.Request, res: express.Response) {
186 const user = res.locals.oauth.token.User as UserModel
187 const [ name, host ] = req.body.uri.split('@')
188
189 const payload = {
190 name,
191 host,
192 followerActorId: user.Account.Actor.id
193 }
194
195 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
196 .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err))
197
198 return res.status(204).end()
199}
200
201function getUserSubscription (req: express.Request, res: express.Response) {
202 const subscription: ActorFollowModel = res.locals.subscription
203
204 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON())
205}
206
207async function deleteUserSubscription (req: express.Request, res: express.Response) {
208 const subscription: ActorFollowModel = res.locals.subscription
209
210 await sequelizeTypescript.transaction(async t => {
211 return subscription.destroy({ transaction: t })
212 })
213
214 return res.type('json').status(204).end()
215}
216
217async function getUserSubscriptions (req: express.Request, res: express.Response) {
218 const user = res.locals.oauth.token.User as UserModel
219 const actorId = user.Account.Actor.id
220
221 const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort)
222
223 return res.json(getFormattedObjects(resultList.data, resultList.total))
224}
225
226async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
227 const user = res.locals.oauth.token.User as UserModel
228 const resultList = await VideoModel.listForApi({
229 start: req.query.start,
230 count: req.query.count,
231 sort: req.query.sort,
232 includeLocalVideos: false,
233 categoryOneOf: req.query.categoryOneOf,
234 licenceOneOf: req.query.licenceOneOf,
235 languageOneOf: req.query.languageOneOf,
236 tagsOneOf: req.query.tagsOneOf,
237 tagsAllOf: req.query.tagsAllOf,
238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter,
240 withFiles: false,
241 actorId: user.Account.Actor.id
242 })
243
244 return res.json(getFormattedObjects(resultList.data, resultList.total))
245}
246
247async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 96async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
248 const user = res.locals.oauth.token.User as UserModel 97 const user = res.locals.oauth.token.User as UserModel
249 const resultList = await VideoModel.listUserVideosForApi( 98 const resultList = await VideoModel.listUserVideosForApi(
@@ -318,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) {
318 return res.sendStatus(204) 167 return res.sendStatus(204)
319} 168}
320 169
321async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { 170async function updateMe (req: express.Request, res: express.Response) {
322 const body: UserUpdateMe = req.body 171 const body: UserUpdateMe = req.body
323 172
324 const user: UserModel = res.locals.oauth.token.user 173 const user: UserModel = res.locals.oauth.token.user
@@ -327,7 +176,9 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
327 if (body.password !== undefined) user.password = body.password 176 if (body.password !== undefined) user.password = body.password
328 if (body.email !== undefined) user.email = body.email 177 if (body.email !== undefined) user.email = body.email
329 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy 178 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
179 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
330 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 180 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
181 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
331 182
332 await sequelizeTypescript.transaction(async t => { 183 await sequelizeTypescript.transaction(async t => {
333 const userAccount = await AccountModel.load(user.Account.id) 184 const userAccount = await AccountModel.load(user.Account.id)
@@ -346,7 +197,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
346 return res.sendStatus(204) 197 return res.sendStatus(204)
347} 198}
348 199
349async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 200async function updateMyAvatar (req: express.Request, res: express.Response) {
350 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 201 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
351 const user: UserModel = res.locals.oauth.token.user 202 const user: UserModel = res.locals.oauth.token.user
352 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 203 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
new file mode 100644
index 000000000..9575eab46
--- /dev/null
+++ b/server/controllers/api/users/my-blocklist.ts
@@ -0,0 +1,125 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils'
4import {
5 asyncMiddleware,
6 asyncRetryTransactionMiddleware,
7 authenticate,
8 paginationValidator,
9 setDefaultPagination,
10 setDefaultSort,
11 unblockAccountByAccountValidator
12} from '../../../middlewares'
13import {
14 accountsBlocklistSortValidator,
15 blockAccountValidator,
16 blockServerValidator,
17 serversBlocklistSortValidator,
18 unblockServerByAccountValidator
19} from '../../../middlewares/validators'
20import { UserModel } from '../../../models/account/user'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26
27const myBlocklistRouter = express.Router()
28
29myBlocklistRouter.get('/me/blocklist/accounts',
30 authenticate,
31 paginationValidator,
32 accountsBlocklistSortValidator,
33 setDefaultSort,
34 setDefaultPagination,
35 asyncMiddleware(listBlockedAccounts)
36)
37
38myBlocklistRouter.post('/me/blocklist/accounts',
39 authenticate,
40 asyncMiddleware(blockAccountValidator),
41 asyncRetryTransactionMiddleware(blockAccount)
42)
43
44myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
45 authenticate,
46 asyncMiddleware(unblockAccountByAccountValidator),
47 asyncRetryTransactionMiddleware(unblockAccount)
48)
49
50myBlocklistRouter.get('/me/blocklist/servers',
51 authenticate,
52 paginationValidator,
53 serversBlocklistSortValidator,
54 setDefaultSort,
55 setDefaultPagination,
56 asyncMiddleware(listBlockedServers)
57)
58
59myBlocklistRouter.post('/me/blocklist/servers',
60 authenticate,
61 asyncMiddleware(blockServerValidator),
62 asyncRetryTransactionMiddleware(blockServer)
63)
64
65myBlocklistRouter.delete('/me/blocklist/servers/:host',
66 authenticate,
67 asyncMiddleware(unblockServerByAccountValidator),
68 asyncRetryTransactionMiddleware(unblockServer)
69)
70
71export {
72 myBlocklistRouter
73}
74
75// ---------------------------------------------------------------------------
76
77async function listBlockedAccounts (req: express.Request, res: express.Response) {
78 const user: UserModel = res.locals.oauth.token.User
79
80 const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
81
82 return res.json(getFormattedObjects(resultList.data, resultList.total))
83}
84
85async function blockAccount (req: express.Request, res: express.Response) {
86 const user: UserModel = res.locals.oauth.token.User
87 const accountToBlock: AccountModel = res.locals.account
88
89 await addAccountInBlocklist(user.Account.id, accountToBlock.id)
90
91 return res.status(204).end()
92}
93
94async function unblockAccount (req: express.Request, res: express.Response) {
95 const accountBlock: AccountBlocklistModel = res.locals.accountBlock
96
97 await removeAccountFromBlocklist(accountBlock)
98
99 return res.status(204).end()
100}
101
102async function listBlockedServers (req: express.Request, res: express.Response) {
103 const user: UserModel = res.locals.oauth.token.User
104
105 const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
106
107 return res.json(getFormattedObjects(resultList.data, resultList.total))
108}
109
110async function blockServer (req: express.Request, res: express.Response) {
111 const user: UserModel = res.locals.oauth.token.User
112 const serverToBlock: ServerModel = res.locals.server
113
114 await addServerInBlocklist(user.Account.id, serverToBlock.id)
115
116 return res.status(204).end()
117}
118
119async function unblockServer (req: express.Request, res: express.Response) {
120 const serverBlock: ServerBlocklistModel = res.locals.serverBlock
121
122 await removeServerFromBlocklist(serverBlock)
123
124 return res.status(204).end()
125}
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
new file mode 100644
index 000000000..6cd782c47
--- /dev/null
+++ b/server/controllers/api/users/my-history.ts
@@ -0,0 +1,57 @@
1import * as express from 'express'
2import {
3 asyncMiddleware,
4 asyncRetryTransactionMiddleware,
5 authenticate,
6 paginationValidator,
7 setDefaultPagination,
8 userHistoryRemoveValidator
9} from '../../../middlewares'
10import { UserModel } from '../../../models/account/user'
11import { getFormattedObjects } from '../../../helpers/utils'
12import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
13import { sequelizeTypescript } from '../../../initializers'
14
15const myVideosHistoryRouter = express.Router()
16
17myVideosHistoryRouter.get('/me/history/videos',
18 authenticate,
19 paginationValidator,
20 setDefaultPagination,
21 asyncMiddleware(listMyVideosHistory)
22)
23
24myVideosHistoryRouter.post('/me/history/videos/remove',
25 authenticate,
26 userHistoryRemoveValidator,
27 asyncRetryTransactionMiddleware(removeUserHistory)
28)
29
30// ---------------------------------------------------------------------------
31
32export {
33 myVideosHistoryRouter
34}
35
36// ---------------------------------------------------------------------------
37
38async function listMyVideosHistory (req: express.Request, res: express.Response) {
39 const user: UserModel = res.locals.oauth.token.User
40
41 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count)
42
43 return res.json(getFormattedObjects(resultList.data, resultList.total))
44}
45
46async function removeUserHistory (req: express.Request, res: express.Response) {
47 const user: UserModel = res.locals.oauth.token.User
48 const beforeDate = req.body.beforeDate || null
49
50 await sequelizeTypescript.transaction(t => {
51 return UserVideoHistoryModel.removeHistoryBefore(user, beforeDate, t)
52 })
53
54 // Do not send the delete to other instances, we delete OUR copy of this video abuse
55
56 return res.type('json').status(204).end()
57}
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
new file mode 100644
index 000000000..76cf97587
--- /dev/null
+++ b/server/controllers/api/users/my-notifications.ts
@@ -0,0 +1,108 @@
1import * as express from 'express'
2import 'multer'
3import {
4 asyncMiddleware,
5 asyncRetryTransactionMiddleware,
6 authenticate,
7 paginationValidator,
8 setDefaultPagination,
9 setDefaultSort,
10 userNotificationsSortValidator
11} from '../../../middlewares'
12import { UserModel } from '../../../models/account/user'
13import { getFormattedObjects } from '../../../helpers/utils'
14import { UserNotificationModel } from '../../../models/account/user-notification'
15import { meRouter } from './me'
16import {
17 listUserNotificationsValidator,
18 markAsReadUserNotificationsValidator,
19 updateNotificationSettingsValidator
20} from '../../../middlewares/validators/user-notifications'
21import { UserNotificationSetting } from '../../../../shared/models/users'
22import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
23
24const myNotificationsRouter = express.Router()
25
26meRouter.put('/me/notification-settings',
27 authenticate,
28 updateNotificationSettingsValidator,
29 asyncRetryTransactionMiddleware(updateNotificationSettings)
30)
31
32myNotificationsRouter.get('/me/notifications',
33 authenticate,
34 paginationValidator,
35 userNotificationsSortValidator,
36 setDefaultSort,
37 setDefaultPagination,
38 listUserNotificationsValidator,
39 asyncMiddleware(listUserNotifications)
40)
41
42myNotificationsRouter.post('/me/notifications/read',
43 authenticate,
44 markAsReadUserNotificationsValidator,
45 asyncMiddleware(markAsReadUserNotifications)
46)
47
48myNotificationsRouter.post('/me/notifications/read-all',
49 authenticate,
50 asyncMiddleware(markAsReadAllUserNotifications)
51)
52
53export {
54 myNotificationsRouter
55}
56
57// ---------------------------------------------------------------------------
58
59async function updateNotificationSettings (req: express.Request, res: express.Response) {
60 const user: UserModel = res.locals.oauth.token.User
61 const body = req.body
62
63 const query = {
64 where: {
65 userId: user.id
66 }
67 }
68
69 const values: UserNotificationSetting = {
70 newVideoFromSubscription: body.newVideoFromSubscription,
71 newCommentOnMyVideo: body.newCommentOnMyVideo,
72 videoAbuseAsModerator: body.videoAbuseAsModerator,
73 blacklistOnMyVideo: body.blacklistOnMyVideo,
74 myVideoPublished: body.myVideoPublished,
75 myVideoImportFinished: body.myVideoImportFinished,
76 newFollow: body.newFollow,
77 newUserRegistration: body.newUserRegistration,
78 commentMention: body.commentMention
79 }
80
81 await UserNotificationSettingModel.update(values, query)
82
83 return res.status(204).end()
84}
85
86async function listUserNotifications (req: express.Request, res: express.Response) {
87 const user: UserModel = res.locals.oauth.token.User
88
89 const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread)
90
91 return res.json(getFormattedObjects(resultList.data, resultList.total))
92}
93
94async function markAsReadUserNotifications (req: express.Request, res: express.Response) {
95 const user: UserModel = res.locals.oauth.token.User
96
97 await UserNotificationModel.markAsRead(user.id, req.body.ids)
98
99 return res.status(204).end()
100}
101
102async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
103 const user: UserModel = res.locals.oauth.token.User
104
105 await UserNotificationModel.markAllAsRead(user.id)
106
107 return res.status(204).end()
108}
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
new file mode 100644
index 000000000..accca6d52
--- /dev/null
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -0,0 +1,170 @@
1import * as express from 'express'
2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils'
4import { CONFIG, sequelizeTypescript } from '../../../initializers'
5import {
6 asyncMiddleware,
7 asyncRetryTransactionMiddleware,
8 authenticate,
9 commonVideosFiltersValidator,
10 paginationValidator,
11 setDefaultPagination,
12 setDefaultSort,
13 userSubscriptionAddValidator,
14 userSubscriptionGetValidator
15} from '../../../middlewares'
16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators'
17import { UserModel } from '../../../models/account/user'
18import { VideoModel } from '../../../models/video/video'
19import { buildNSFWFilter } from '../../../helpers/express-utils'
20import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
21import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
22import { JobQueue } from '../../../lib/job-queue'
23import { logger } from '../../../helpers/logger'
24
25const mySubscriptionsRouter = express.Router()
26
27mySubscriptionsRouter.get('/me/subscriptions/videos',
28 authenticate,
29 paginationValidator,
30 videosSortValidator,
31 setDefaultSort,
32 setDefaultPagination,
33 commonVideosFiltersValidator,
34 asyncMiddleware(getUserSubscriptionVideos)
35)
36
37mySubscriptionsRouter.get('/me/subscriptions/exist',
38 authenticate,
39 areSubscriptionsExistValidator,
40 asyncMiddleware(areSubscriptionsExist)
41)
42
43mySubscriptionsRouter.get('/me/subscriptions',
44 authenticate,
45 paginationValidator,
46 userSubscriptionsSortValidator,
47 setDefaultSort,
48 setDefaultPagination,
49 asyncMiddleware(getUserSubscriptions)
50)
51
52mySubscriptionsRouter.post('/me/subscriptions',
53 authenticate,
54 userSubscriptionAddValidator,
55 asyncMiddleware(addUserSubscription)
56)
57
58mySubscriptionsRouter.get('/me/subscriptions/:uri',
59 authenticate,
60 userSubscriptionGetValidator,
61 getUserSubscription
62)
63
64mySubscriptionsRouter.delete('/me/subscriptions/:uri',
65 authenticate,
66 userSubscriptionGetValidator,
67 asyncRetryTransactionMiddleware(deleteUserSubscription)
68)
69
70// ---------------------------------------------------------------------------
71
72export {
73 mySubscriptionsRouter
74}
75
76// ---------------------------------------------------------------------------
77
78async function areSubscriptionsExist (req: express.Request, res: express.Response) {
79 const uris = req.query.uris as string[]
80 const user = res.locals.oauth.token.User as UserModel
81
82 const handles = uris.map(u => {
83 let [ name, host ] = u.split('@')
84 if (host === CONFIG.WEBSERVER.HOST) host = null
85
86 return { name, host, uri: u }
87 })
88
89 const results = await ActorFollowModel.listSubscribedIn(user.Account.Actor.id, handles)
90
91 const existObject: { [id: string ]: boolean } = {}
92 for (const handle of handles) {
93 const obj = results.find(r => {
94 const server = r.ActorFollowing.Server
95
96 return r.ActorFollowing.preferredUsername === handle.name &&
97 (
98 (!server && !handle.host) ||
99 (server.host === handle.host)
100 )
101 })
102
103 existObject[handle.uri] = obj !== undefined
104 }
105
106 return res.json(existObject)
107}
108
109async function addUserSubscription (req: express.Request, res: express.Response) {
110 const user = res.locals.oauth.token.User as UserModel
111 const [ name, host ] = req.body.uri.split('@')
112
113 const payload = {
114 name,
115 host,
116 followerActorId: user.Account.Actor.id
117 }
118
119 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
120 .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err))
121
122 return res.status(204).end()
123}
124
125function getUserSubscription (req: express.Request, res: express.Response) {
126 const subscription: ActorFollowModel = res.locals.subscription
127
128 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON())
129}
130
131async function deleteUserSubscription (req: express.Request, res: express.Response) {
132 const subscription: ActorFollowModel = res.locals.subscription
133
134 await sequelizeTypescript.transaction(async t => {
135 return subscription.destroy({ transaction: t })
136 })
137
138 return res.type('json').status(204).end()
139}
140
141async function getUserSubscriptions (req: express.Request, res: express.Response) {
142 const user = res.locals.oauth.token.User as UserModel
143 const actorId = user.Account.Actor.id
144
145 const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort)
146
147 return res.json(getFormattedObjects(resultList.data, resultList.total))
148}
149
150async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
151 const user = res.locals.oauth.token.User as UserModel
152 const resultList = await VideoModel.listForApi({
153 start: req.query.start,
154 count: req.query.count,
155 sort: req.query.sort,
156 includeLocalVideos: false,
157 categoryOneOf: req.query.categoryOneOf,
158 licenceOneOf: req.query.licenceOneOf,
159 languageOneOf: req.query.languageOneOf,
160 tagsOneOf: req.query.tagsOneOf,
161 tagsAllOf: req.query.tagsAllOf,
162 nsfw: buildNSFWFilter(res, req.query.nsfw),
163 filter: req.query.filter as VideoFilter,
164 withFiles: false,
165 followerActorId: user.Account.Actor.id,
166 user
167 })
168
169 return res.json(getFormattedObjects(resultList.data, resultList.total))
170}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 1fa842d9c..db7602139 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -22,7 +22,7 @@ import { createVideoChannel } from '../../lib/video-channel'
22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 23import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 24import { AccountModel } from '../../models/account/account'
25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 25import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers'
26import { logger } from '../../helpers/logger' 26import { logger } from '../../helpers/logger'
27import { VideoModel } from '../../models/video/video' 27import { VideoModel } from '../../models/video/video'
28import { updateAvatarValidator } from '../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../middlewares/validators/avatar'
@@ -30,9 +30,10 @@ import { updateActorAvatarFile } from '../../lib/avatar'
30import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 30import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
31import { resetSequelizeInstance } from '../../helpers/database-utils' 31import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user' 32import { UserModel } from '../../models/account/user'
33import { JobQueue } from '../../lib/job-queue'
33 34
34const auditLogger = auditLoggerFactory('channels') 35const auditLogger = auditLoggerFactory('channels')
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 36const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 37
37const videoChannelRouter = express.Router() 38const videoChannelRouter = express.Router()
38 39
@@ -197,15 +198,20 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
197async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { 198async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) {
198 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) 199 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id)
199 200
201 if (videoChannelWithVideos.isOutdated()) {
202 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } })
203 .catch(err => logger.error('Cannot create AP refresher job for actor %s.', videoChannelWithVideos.Actor.url, { err }))
204 }
205
200 return res.json(videoChannelWithVideos.toFormattedJSON()) 206 return res.json(videoChannelWithVideos.toFormattedJSON())
201} 207}
202 208
203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 209async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
204 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 210 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
205 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 211 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
206 212
207 const resultList = await VideoModel.listForApi({ 213 const resultList = await VideoModel.listForApi({
208 actorId, 214 followerActorId,
209 start: req.query.start, 215 start: req.query.start,
210 count: req.query.count, 216 count: req.query.count,
211 sort: req.query.sort, 217 sort: req.query.sort,
@@ -215,9 +221,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
215 languageOneOf: req.query.languageOneOf, 221 languageOneOf: req.query.languageOneOf,
216 tagsOneOf: req.query.tagsOneOf, 222 tagsOneOf: req.query.tagsOneOf,
217 tagsAllOf: req.query.tagsAllOf, 223 tagsAllOf: req.query.tagsAllOf,
224 filter: req.query.filter,
218 nsfw: buildNSFWFilter(res, req.query.nsfw), 225 nsfw: buildNSFWFilter(res, req.query.nsfw),
219 withFiles: false, 226 withFiles: false,
220 videoChannelId: videoChannelInstance.id 227 videoChannelId: videoChannelInstance.id,
228 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
221 }) 229 })
222 230
223 return res.json(getFormattedObjects(resultList.data, resultList.total)) 231 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index d0c81804b..32f9c4793 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -3,7 +3,6 @@ import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
6import { sendVideoAbuse } from '../../../lib/activitypub/send'
7import { 6import {
8 asyncMiddleware, 7 asyncMiddleware,
9 asyncRetryTransactionMiddleware, 8 asyncRetryTransactionMiddleware,
@@ -22,6 +21,8 @@ import { VideoModel } from '../../../models/video/video'
22import { VideoAbuseModel } from '../../../models/video/video-abuse' 21import { VideoAbuseModel } from '../../../models/video/video-abuse'
23import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 22import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
24import { UserModel } from '../../../models/account/user' 23import { UserModel } from '../../../models/account/user'
24import { Notifier } from '../../../lib/notifier'
25import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
25 26
26const auditLogger = auditLoggerFactory('abuse') 27const auditLogger = auditLoggerFactory('abuse')
27const abuseVideoRouter = express.Router() 28const abuseVideoRouter = express.Router()
@@ -117,6 +118,8 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
117 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance) 118 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance)
118 } 119 }
119 120
121 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance)
122
120 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) 123 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON()))
121 124
122 return videoAbuseInstance 125 return videoAbuseInstance
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 7f803c8e9..43b0516e7 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -16,6 +16,10 @@ import {
16} from '../../../middlewares' 16} from '../../../middlewares'
17import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 17import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
18import { sequelizeTypescript } from '../../../initializers' 18import { sequelizeTypescript } from '../../../initializers'
19import { Notifier } from '../../../lib/notifier'
20import { VideoModel } from '../../../models/video/video'
21import { sendCreateVideo, sendDeleteVideo, sendUpdateVideo } from '../../../lib/activitypub/send'
22import { federateVideoIfNeeded } from '../../../lib/activitypub'
19 23
20const blacklistRouter = express.Router() 24const blacklistRouter = express.Router()
21 25
@@ -64,16 +68,26 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
64 68
65 const toCreate = { 69 const toCreate = {
66 videoId: videoInstance.id, 70 videoId: videoInstance.id,
71 unfederated: body.unfederate === true,
67 reason: body.reason 72 reason: body.reason
68 } 73 }
69 74
70 await VideoBlacklistModel.create(toCreate) 75 const blacklist = await VideoBlacklistModel.create(toCreate)
76 blacklist.Video = videoInstance
77
78 if (body.unfederate === true) {
79 await sendDeleteVideo(videoInstance, undefined)
80 }
81
82 Notifier.Instance.notifyOnVideoBlacklist(blacklist)
83
84 logger.info('Video %s blacklisted.', res.locals.video.uuid)
85
71 return res.type('json').status(204).end() 86 return res.type('json').status(204).end()
72} 87}
73 88
74async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 89async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
75 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel 90 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
76 logger.info(videoBlacklist)
77 91
78 if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason 92 if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason
79 93
@@ -92,11 +106,20 @@ async function listBlacklist (req: express.Request, res: express.Response, next:
92 106
93async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 107async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
94 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel 108 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel
109 const video: VideoModel = res.locals.video
95 110
96 await sequelizeTypescript.transaction(t => { 111 await sequelizeTypescript.transaction(async t => {
97 return videoBlacklist.destroy({ transaction: t }) 112 const unfederated = videoBlacklist.unfederated
113 await videoBlacklist.destroy({ transaction: t })
114
115 // Re federate the video
116 if (unfederated === true) {
117 await federateVideoIfNeeded(video, true, t)
118 }
98 }) 119 })
99 120
121 Notifier.Instance.notifyOnVideoUnblacklist(video)
122
100 logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 123 logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
101 124
102 return res.type('json').status(204).end() 125 return res.type('json').status(204).end()
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 3ba918189..9b3661368 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' 2import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' 3import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
4import { createReqFiles } from '../../../helpers/express-utils' 4import { createReqFiles } from '../../../helpers/express-utils'
5import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers' 5import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../../initializers'
6import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
7import { VideoCaptionModel } from '../../../models/video/video-caption' 7import { VideoCaptionModel } from '../../../models/video/video-caption'
8import { VideoModel } from '../../../models/video/video' 8import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
12 12
13const reqVideoCaptionAdd = createReqFiles( 13const reqVideoCaptionAdd = createReqFiles(
14 [ 'captionfile' ], 14 [ 'captionfile' ],
15 VIDEO_CAPTIONS_MIMETYPE_EXT, 15 MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT,
16 { 16 {
17 captionfile: CONFIG.STORAGE.CAPTIONS_DIR 17 captionfile: CONFIG.STORAGE.CAPTIONS_DIR
18 } 18 }
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 4f2b4faee..70c1148ba 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -8,7 +8,7 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
8import { 8import {
9 asyncMiddleware, 9 asyncMiddleware,
10 asyncRetryTransactionMiddleware, 10 asyncRetryTransactionMiddleware,
11 authenticate, 11 authenticate, optionalAuthenticate,
12 paginationValidator, 12 paginationValidator,
13 setDefaultPagination, 13 setDefaultPagination,
14 setDefaultSort 14 setDefaultSort
@@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment'
26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
27import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
28import { UserModel } from '../../../models/account/user' 28import { UserModel } from '../../../models/account/user'
29import { Notifier } from '../../../lib/notifier'
29 30
30const auditLogger = auditLoggerFactory('comments') 31const auditLogger = auditLoggerFactory('comments')
31const videoCommentRouter = express.Router() 32const videoCommentRouter = express.Router()
@@ -36,10 +37,12 @@ videoCommentRouter.get('/:videoId/comment-threads',
36 setDefaultSort, 37 setDefaultSort,
37 setDefaultPagination, 38 setDefaultPagination,
38 asyncMiddleware(listVideoCommentThreadsValidator), 39 asyncMiddleware(listVideoCommentThreadsValidator),
40 optionalAuthenticate,
39 asyncMiddleware(listVideoThreads) 41 asyncMiddleware(listVideoThreads)
40) 42)
41videoCommentRouter.get('/:videoId/comment-threads/:threadId', 43videoCommentRouter.get('/:videoId/comment-threads/:threadId',
42 asyncMiddleware(listVideoThreadCommentsValidator), 44 asyncMiddleware(listVideoThreadCommentsValidator),
45 optionalAuthenticate,
43 asyncMiddleware(listVideoThreadComments) 46 asyncMiddleware(listVideoThreadComments)
44) 47)
45 48
@@ -69,10 +72,12 @@ export {
69 72
70async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { 73async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
71 const video = res.locals.video as VideoModel 74 const video = res.locals.video as VideoModel
75 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
76
72 let resultList: ResultList<VideoCommentModel> 77 let resultList: ResultList<VideoCommentModel>
73 78
74 if (video.commentsEnabled === true) { 79 if (video.commentsEnabled === true) {
75 resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort) 80 resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user)
76 } else { 81 } else {
77 resultList = { 82 resultList = {
78 total: 0, 83 total: 0,
@@ -85,10 +90,12 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne
85 90
86async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { 91async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
87 const video = res.locals.video as VideoModel 92 const video = res.locals.video as VideoModel
93 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
94
88 let resultList: ResultList<VideoCommentModel> 95 let resultList: ResultList<VideoCommentModel>
89 96
90 if (video.commentsEnabled === true) { 97 if (video.commentsEnabled === true) {
91 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id) 98 resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user)
92 } else { 99 } else {
93 resultList = { 100 resultList = {
94 total: 0, 101 total: 0,
@@ -113,6 +120,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
113 }, t) 120 }, t)
114 }) 121 })
115 122
123 Notifier.Instance.notifyOnNewComment(comment)
116 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) 124 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
117 125
118 return res.json({ 126 return res.json({
@@ -134,6 +142,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
134 }, t) 142 }, t)
135 }) 143 })
136 144
145 Notifier.Instance.notifyOnNewComment(comment)
137 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) 146 auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON()))
138 147
139 return res.json({ comment: comment.toFormattedJSON() }).end() 148 return res.json({ comment: comment.toFormattedJSON() }).end()
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 9e51e2000..7053d5253 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -3,14 +3,7 @@ import * as magnetUtil from 'magnet-uri'
3import 'multer' 3import 'multer'
4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
6import { 6import { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers'
7 CONFIG,
8 IMAGE_MIMETYPE_EXT,
9 PREVIEWS_SIZE,
10 sequelizeTypescript,
11 THUMBNAILS_SIZE,
12 TORRENT_MIMETYPE_EXT
13} from '../../../initializers'
14import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' 7import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
15import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
16import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
@@ -28,18 +21,18 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
28import * as Bluebird from 'bluebird' 21import * as Bluebird from 'bluebird'
29import * as parseTorrent from 'parse-torrent' 22import * as parseTorrent from 'parse-torrent'
30import { getSecureTorrentName } from '../../../helpers/utils' 23import { getSecureTorrentName } from '../../../helpers/utils'
31import { readFile, rename } from 'fs-extra' 24import { readFile, move } from 'fs-extra'
32 25
33const auditLogger = auditLoggerFactory('video-imports') 26const auditLogger = auditLoggerFactory('video-imports')
34const videoImportsRouter = express.Router() 27const videoImportsRouter = express.Router()
35 28
36const reqVideoFileImport = createReqFiles( 29const reqVideoFileImport = createReqFiles(
37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ], 30 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 31 Object.assign({}, MIMETYPES.TORRENT.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
39 { 32 {
40 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 33 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
41 previewfile: CONFIG.STORAGE.PREVIEWS_DIR, 34 previewfile: CONFIG.STORAGE.TMP_DIR,
42 torrentfile: CONFIG.STORAGE.TORRENTS_DIR 35 torrentfile: CONFIG.STORAGE.TMP_DIR
43 } 36 }
44) 37)
45 38
@@ -78,7 +71,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
78 71
79 // Rename the torrent to a secured name 72 // Rename the torrent to a secured name
80 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) 73 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
81 await rename(torrentfile.path, newTorrentPath) 74 await move(torrentfile.path, newTorrentPath)
82 torrentfile.path = newTorrentPath 75 torrentfile.path = newTorrentPath
83 76
84 const buf = await readFile(torrentfile.path) 77 const buf = await readFile(torrentfile.path)
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 7d55f06b6..76a318d13 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -8,14 +8,13 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../
8import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 8import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
9import { 9import {
10 CONFIG, 10 CONFIG,
11 IMAGE_MIMETYPE_EXT, 11 MIMETYPES,
12 PREVIEWS_SIZE, 12 PREVIEWS_SIZE,
13 sequelizeTypescript, 13 sequelizeTypescript,
14 THUMBNAILS_SIZE, 14 THUMBNAILS_SIZE,
15 VIDEO_CATEGORIES, 15 VIDEO_CATEGORIES,
16 VIDEO_LANGUAGES, 16 VIDEO_LANGUAGES,
17 VIDEO_LICENCES, 17 VIDEO_LICENCES,
18 VIDEO_MIMETYPE_EXT,
19 VIDEO_PRIVACIES 18 VIDEO_PRIVACIES
20} from '../../../initializers' 19} from '../../../initializers'
21import { 20import {
@@ -24,19 +23,20 @@ import {
24 fetchRemoteVideoDescription, 23 fetchRemoteVideoDescription,
25 getVideoActivityPubUrl 24 getVideoActivityPubUrl
26} from '../../../lib/activitypub' 25} from '../../../lib/activitypub'
27import { sendCreateView } from '../../../lib/activitypub/send'
28import { JobQueue } from '../../../lib/job-queue' 26import { JobQueue } from '../../../lib/job-queue'
29import { Redis } from '../../../lib/redis' 27import { Redis } from '../../../lib/redis'
30import { 28import {
31 asyncMiddleware, 29 asyncMiddleware,
32 asyncRetryTransactionMiddleware, 30 asyncRetryTransactionMiddleware,
33 authenticate, 31 authenticate,
32 checkVideoFollowConstraints,
34 commonVideosFiltersValidator, 33 commonVideosFiltersValidator,
35 optionalAuthenticate, 34 optionalAuthenticate,
36 paginationValidator, 35 paginationValidator,
37 setDefaultPagination, 36 setDefaultPagination,
38 setDefaultSort, 37 setDefaultSort,
39 videosAddValidator, 38 videosAddValidator,
39 videosCustomGetValidator,
40 videosGetValidator, 40 videosGetValidator,
41 videosRemoveValidator, 41 videosRemoveValidator,
42 videosSortValidator, 42 videosSortValidator,
@@ -56,27 +56,29 @@ import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-u
56import { videoCaptionsRouter } from './captions' 56import { videoCaptionsRouter } from './captions'
57import { videoImportsRouter } from './import' 57import { videoImportsRouter } from './import'
58import { resetSequelizeInstance } from '../../../helpers/database-utils' 58import { resetSequelizeInstance } from '../../../helpers/database-utils'
59import { rename } from 'fs-extra' 59import { move } from 'fs-extra'
60import { watchingRouter } from './watching' 60import { watchingRouter } from './watching'
61import { Notifier } from '../../../lib/notifier'
62import { sendView } from '../../../lib/activitypub/send/send-view'
61 63
62const auditLogger = auditLoggerFactory('videos') 64const auditLogger = auditLoggerFactory('videos')
63const videosRouter = express.Router() 65const videosRouter = express.Router()
64 66
65const reqVideoFileAdd = createReqFiles( 67const reqVideoFileAdd = createReqFiles(
66 [ 'videofile', 'thumbnailfile', 'previewfile' ], 68 [ 'videofile', 'thumbnailfile', 'previewfile' ],
67 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 69 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
68 { 70 {
69 videofile: CONFIG.STORAGE.VIDEOS_DIR, 71 videofile: CONFIG.STORAGE.TMP_DIR,
70 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 72 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
71 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 73 previewfile: CONFIG.STORAGE.TMP_DIR
72 } 74 }
73) 75)
74const reqVideoFileUpdate = createReqFiles( 76const reqVideoFileUpdate = createReqFiles(
75 [ 'thumbnailfile', 'previewfile' ], 77 [ 'thumbnailfile', 'previewfile' ],
76 IMAGE_MIMETYPE_EXT, 78 MIMETYPES.IMAGE.MIMETYPE_EXT,
77 { 79 {
78 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 80 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
79 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 81 previewfile: CONFIG.STORAGE.TMP_DIR
80 } 82 }
81) 83)
82 84
@@ -122,8 +124,9 @@ videosRouter.get('/:id/description',
122) 124)
123videosRouter.get('/:id', 125videosRouter.get('/:id',
124 optionalAuthenticate, 126 optionalAuthenticate,
125 asyncMiddleware(videosGetValidator), 127 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
126 getVideo 128 asyncMiddleware(checkVideoFollowConstraints),
129 asyncMiddleware(getVideo)
127) 130)
128videosRouter.post('/:id/views', 131videosRouter.post('/:id/views',
129 asyncMiddleware(videosGetValidator), 132 asyncMiddleware(videosGetValidator),
@@ -207,7 +210,7 @@ async function addVideo (req: express.Request, res: express.Response) {
207 // Move physical file 210 // Move physical file
208 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 211 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
209 const destination = join(videoDir, video.getVideoFilename(videoFile)) 212 const destination = join(videoDir, video.getVideoFilename(videoFile))
210 await rename(videoPhysicalFile.path, destination) 213 await move(videoPhysicalFile.path, destination)
211 // This is important in case if there is another attempt in the retry process 214 // This is important in case if there is another attempt in the retry process
212 videoPhysicalFile.filename = video.getVideoFilename(videoFile) 215 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
213 videoPhysicalFile.path = destination 216 videoPhysicalFile.path = destination
@@ -270,6 +273,8 @@ async function addVideo (req: express.Request, res: express.Response) {
270 return videoCreated 273 return videoCreated
271 }) 274 })
272 275
276 Notifier.Instance.notifyOnNewVideo(videoCreated)
277
273 if (video.state === VideoState.TO_TRANSCODE) { 278 if (video.state === VideoState.TO_TRANSCODE) {
274 // Put uuid because we don't have id auto incremented for now 279 // Put uuid because we don't have id auto incremented for now
275 const dataInput = { 280 const dataInput = {
@@ -294,6 +299,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
294 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) 299 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
295 const videoInfoToUpdate: VideoUpdate = req.body 300 const videoInfoToUpdate: VideoUpdate = req.body
296 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE 301 const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE
302 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
297 303
298 // Process thumbnail or create it from the video 304 // Process thumbnail or create it from the video
299 if (req.files && req.files['thumbnailfile']) { 305 if (req.files && req.files['thumbnailfile']) {
@@ -308,10 +314,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
308 } 314 }
309 315
310 try { 316 try {
311 await sequelizeTypescript.transaction(async t => { 317 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
312 const sequelizeOptions = { 318 const sequelizeOptions = { transaction: t }
313 transaction: t
314 }
315 const oldVideoChannel = videoInstance.VideoChannel 319 const oldVideoChannel = videoInstance.VideoChannel
316 320
317 if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name) 321 if (videoInfoToUpdate.name !== undefined) videoInstance.set('name', videoInfoToUpdate.name)
@@ -363,7 +367,11 @@ async function updateVideo (req: express.Request, res: express.Response) {
363 } 367 }
364 368
365 const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE 369 const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
366 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) 370
371 // Don't send update if the video was unfederated
372 if (!videoInstanceUpdated.VideoBlacklist || videoInstanceUpdated.VideoBlacklist.unfederated === false) {
373 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
374 }
367 375
368 auditLogger.update( 376 auditLogger.update(
369 getAuditIdFromRes(res), 377 getAuditIdFromRes(res),
@@ -371,7 +379,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
371 oldVideoAuditView 379 oldVideoAuditView
372 ) 380 )
373 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 381 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
382
383 return videoInstanceUpdated
374 }) 384 })
385
386 if (wasUnlistedVideo || wasPrivateVideo) {
387 Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated)
388 }
375 } catch (err) { 389 } catch (err) {
376 // Force fields we want to update 390 // Force fields we want to update
377 // If the transaction is retried, sequelize will think the object has not changed 391 // If the transaction is retried, sequelize will think the object has not changed
@@ -384,10 +398,17 @@ async function updateVideo (req: express.Request, res: express.Response) {
384 return res.type('json').status(204).end() 398 return res.type('json').status(204).end()
385} 399}
386 400
387function getVideo (req: express.Request, res: express.Response) { 401async function getVideo (req: express.Request, res: express.Response) {
388 const videoInstance = res.locals.video 402 // We need more attributes
403 const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
404 const video: VideoModel = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId)
389 405
390 return res.json(videoInstance.toFormattedDetailsJSON()) 406 if (video.isOutdated()) {
407 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
408 .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err }))
409 }
410
411 return res.json(video.toFormattedDetailsJSON())
391} 412}
392 413
393async function viewVideo (req: express.Request, res: express.Response) { 414async function viewVideo (req: express.Request, res: express.Response) {
@@ -406,8 +427,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
406 ]) 427 ])
407 428
408 const serverActor = await getServerActor() 429 const serverActor = await getServerActor()
409 430 await sendView(serverActor, videoInstance, undefined)
410 await sendCreateView(serverActor, videoInstance, undefined)
411 431
412 return res.status(204).end() 432 return res.status(204).end()
413} 433}
@@ -425,7 +445,7 @@ async function getVideoDescription (req: express.Request, res: express.Response)
425 return res.json({ description }) 445 return res.json({ description })
426} 446}
427 447
428async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 448async function listVideos (req: express.Request, res: express.Response) {
429 const resultList = await VideoModel.listForApi({ 449 const resultList = await VideoModel.listForApi({
430 start: req.query.start, 450 start: req.query.start,
431 count: req.query.count, 451 count: req.query.count,
@@ -439,7 +459,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
439 nsfw: buildNSFWFilter(res, req.query.nsfw), 459 nsfw: buildNSFWFilter(res, req.query.nsfw),
440 filter: req.query.filter as VideoFilter, 460 filter: req.query.filter as VideoFilter,
441 withFiles: false, 461 withFiles: false,
442 userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined 462 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
443 }) 463 })
444 464
445 return res.json(getFormattedObjects(resultList.data, resultList.total)) 465 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index dc322bb0c..53952a0a2 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -2,8 +2,8 @@ import * as express from 'express'
2import { UserVideoRateUpdate } from '../../../../shared' 2import { UserVideoRateUpdate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' 4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
5import { sendVideoRateChange } from '../../../lib/activitypub' 5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' 6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
7import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
12 12
13rateVideoRouter.put('/:id/rate', 13rateVideoRouter.put('/:id/rate',
14 authenticate, 14 authenticate,
15 asyncMiddleware(videoRateValidator), 15 asyncMiddleware(videoUpdateRateValidator),
16 asyncRetryTransactionMiddleware(rateVideo) 16 asyncRetryTransactionMiddleware(rateVideo)
17) 17)
18 18
@@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
28 const body: UserVideoRateUpdate = req.body 28 const body: UserVideoRateUpdate = req.body
29 const rateType = body.rating 29 const rateType = body.rating
30 const videoInstance: VideoModel = res.locals.video 30 const videoInstance: VideoModel = res.locals.video
31 const userAccount: AccountModel = res.locals.oauth.token.User.Account
31 32
32 await sequelizeTypescript.transaction(async t => { 33 await sequelizeTypescript.transaction(async t => {
33 const sequelizeOptions = { transaction: t } 34 const sequelizeOptions = { transaction: t }
34 35
35 const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 36 const accountInstance = await AccountModel.load(userAccount.id, t)
36 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) 37 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
37 38
38 let likesToIncrement = 0 39 let likesToIncrement = 0
@@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
44 // There was a previous rate, update it 45 // There was a previous rate, update it
45 if (previousRate) { 46 if (previousRate) {
46 // We will remove the previous rate, so we will need to update the video count attribute 47 // We will remove the previous rate, so we will need to update the video count attribute
47 if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- 48 if (previousRate.type === 'like') likesToIncrement--
48 else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- 49 else if (previousRate.type === 'dislike') dislikesToIncrement--
49 50
50 if (rateType === 'none') { // Destroy previous rate 51 if (rateType === 'none') { // Destroy previous rate
51 await previousRate.destroy(sequelizeOptions) 52 await previousRate.destroy(sequelizeOptions)
52 } else { // Update previous rate 53 } else { // Update previous rate
53 previousRate.type = rateType 54 previousRate.type = rateType
55 previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
54 await previousRate.save(sequelizeOptions) 56 await previousRate.save(sequelizeOptions)
55 } 57 }
56 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate 58 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
57 const query = { 59 const query = {
58 accountId: accountInstance.id, 60 accountId: accountInstance.id,
59 videoId: videoInstance.id, 61 videoId: videoInstance.id,
60 type: rateType 62 type: rateType,
63 url: getRateUrl(rateType, userAccount.Actor, videoInstance)
61 } 64 }
62 65
63 await AccountVideoRateModel.create(query, sequelizeOptions) 66 await AccountVideoRateModel.create(query, sequelizeOptions)
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
new file mode 100644
index 000000000..2db86a2d8
--- /dev/null
+++ b/server/controllers/bots.ts
@@ -0,0 +1,101 @@
1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares'
3import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers'
4import * as sitemapModule from 'sitemap'
5import { logger } from '../helpers/logger'
6import { VideoModel } from '../models/video/video'
7import { VideoChannelModel } from '../models/video/video-channel'
8import { AccountModel } from '../models/account/account'
9import { cacheRoute } from '../middlewares/cache'
10import { buildNSFWFilter } from '../helpers/express-utils'
11import { truncate } from 'lodash'
12
13const botsRouter = express.Router()
14
15// Special route that add OpenGraph and oEmbed tags
16// Do not use a template engine for a so little thing
17botsRouter.use('/sitemap.xml',
18 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)),
19 asyncMiddleware(getSitemap)
20)
21
22// ---------------------------------------------------------------------------
23
24export {
25 botsRouter
26}
27
28// ---------------------------------------------------------------------------
29
30async function getSitemap (req: express.Request, res: express.Response) {
31 let urls = getSitemapBasicUrls()
32
33 urls = urls.concat(await getSitemapLocalVideoUrls())
34 urls = urls.concat(await getSitemapVideoChannelUrls())
35 urls = urls.concat(await getSitemapAccountUrls())
36
37 const sitemap = sitemapModule.createSitemap({
38 hostname: CONFIG.WEBSERVER.URL,
39 urls: urls
40 })
41
42 sitemap.toXML((err, xml) => {
43 if (err) {
44 logger.error('Cannot generate sitemap.', { err })
45 return res.sendStatus(500)
46 }
47
48 res.header('Content-Type', 'application/xml')
49 res.send(xml)
50 })
51}
52
53async function getSitemapVideoChannelUrls () {
54 const rows = await VideoChannelModel.listLocalsForSitemap('createdAt')
55
56 return rows.map(channel => ({
57 url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername
58 }))
59}
60
61async function getSitemapAccountUrls () {
62 const rows = await AccountModel.listLocalsForSitemap('createdAt')
63
64 return rows.map(channel => ({
65 url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername
66 }))
67}
68
69async function getSitemapLocalVideoUrls () {
70 const resultList = await VideoModel.listForApi({
71 start: 0,
72 count: undefined,
73 sort: 'createdAt',
74 includeLocalVideos: true,
75 nsfw: buildNSFWFilter(),
76 filter: 'local',
77 withFiles: false
78 })
79
80 return resultList.data.map(v => ({
81 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid,
82 video: [
83 {
84 title: v.name,
85 // Sitemap description should be < 2000 characters
86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
87 player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid,
88 thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath()
89 }
90 ]
91 }))
92}
93
94function getSitemapBasicUrls () {
95 const paths = [
96 '/about/instance',
97 '/videos/local'
98 ]
99
100 return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p }))
101}
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 73b40cf65..f17f2a5d2 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { join } from 'path' 2import { join } from 'path'
3import { root } from '../helpers/core-utils' 3import { root } from '../helpers/core-utils'
4import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers' 4import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers'
5import { asyncMiddleware } from '../middlewares' 5import { asyncMiddleware, embedCSP } from '../middlewares'
6import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n' 6import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n'
7import { ClientHtml } from '../lib/client-html' 7import { ClientHtml } from '../lib/client-html'
8import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
@@ -16,21 +16,20 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
16 16
17// Special route that add OpenGraph and oEmbed tags 17// Special route that add OpenGraph and oEmbed tags
18// Do not use a template engine for a so little thing 18// Do not use a template engine for a so little thing
19clientsRouter.use('/videos/watch/:id', 19clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
20 asyncMiddleware(generateWatchHtmlPage)
21)
22 20
23clientsRouter.use('' + 21clientsRouter.use(
24 '/videos/embed', 22 '/videos/embed',
25 (req: express.Request, res: express.Response, next: express.NextFunction) => { 23 embedCSP,
24 (req: express.Request, res: express.Response) => {
26 res.removeHeader('X-Frame-Options') 25 res.removeHeader('X-Frame-Options')
27 res.sendFile(embedPath) 26 res.sendFile(embedPath)
28 } 27 }
29) 28)
30clientsRouter.use('' + 29clientsRouter.use(
31 '/videos/test-embed', (req: express.Request, res: express.Response, next: express.NextFunction) => { 30 '/videos/test-embed',
32 res.sendFile(testEmbedPath) 31 (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)
33}) 32)
34 33
35// Static HTML/CSS/JS client files 34// Static HTML/CSS/JS client files
36 35
@@ -89,7 +88,7 @@ export {
89// --------------------------------------------------------------------------- 88// ---------------------------------------------------------------------------
90 89
91async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) { 90async function generateHTMLPage (req: express.Request, res: express.Response, paramLang?: string) {
92 const html = await ClientHtml.getIndexHTML(req, res, paramLang) 91 const html = await ClientHtml.getDefaultHTMLPage(req, res, paramLang)
93 92
94 return sendHTML(html, res) 93 return sendHTML(html, res)
95} 94}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index b30ad8e8d..960085af1 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,7 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' 2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants'
3import { THUMBNAILS_SIZE } from '../initializers' 3import { THUMBNAILS_SIZE } from '../initializers'
4import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' 4import {
5 asyncMiddleware,
6 commonVideosFiltersValidator,
7 setDefaultSort,
8 videoCommentsFeedsValidator,
9 videoFeedsValidator,
10 videosSortValidator
11} from '../middlewares'
5import { VideoModel } from '../models/video/video' 12import { VideoModel } from '../models/video/video'
6import * as Feed from 'pfeed' 13import * as Feed from 'pfeed'
7import { AccountModel } from '../models/account/account' 14import { AccountModel } from '../models/account/account'
@@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format',
22 videosSortValidator, 29 videosSortValidator,
23 setDefaultSort, 30 setDefaultSort,
24 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), 31 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
32 commonVideosFiltersValidator,
25 asyncMiddleware(videoFeedsValidator), 33 asyncMiddleware(videoFeedsValidator),
26 asyncMiddleware(generateVideoFeed) 34 asyncMiddleware(generateVideoFeed)
27) 35)
@@ -48,7 +56,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
48 56
49 // Adding video items to the feed, one at a time 57 // Adding video items to the feed, one at a time
50 comments.forEach(comment => { 58 comments.forEach(comment => {
51 const link = CONFIG.WEBSERVER.URL + '/videos/watch/' + comment.Video.uuid + ';threadId=' + comment.getThreadId() 59 const link = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath()
52 60
53 feed.addItem({ 61 feed.addItem({
54 title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, 62 title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`,
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 197fa897a..a88a03c79 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -6,3 +6,4 @@ export * from './services'
6export * from './static' 6export * from './static'
7export * from './webfinger' 7export * from './webfinger'
8export * from './tracker' 8export * from './tracker'
9export * from './bots'
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 75e30353c..b21f9da00 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -1,6 +1,6 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' 3import { CONFIG, HLS_PLAYLIST_DIRECTORY, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
4import { VideosPreviewCache } from '../lib/cache' 4import { VideosPreviewCache } from '../lib/cache'
5import { cacheRoute } from '../middlewares/cache' 5import { cacheRoute } from '../middlewares/cache'
6import { asyncMiddleware, videosGetValidator } from '../middlewares' 6import { asyncMiddleware, videosGetValidator } from '../middlewares'
@@ -34,18 +34,30 @@ staticRouter.use(
34) 34)
35 35
36// Videos path for webseeding 36// Videos path for webseeding
37const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR
38staticRouter.use( 37staticRouter.use(
39 STATIC_PATHS.WEBSEED, 38 STATIC_PATHS.WEBSEED,
40 cors(), 39 cors(),
41 express.static(videosPhysicalPath) 40 express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
42) 41)
43staticRouter.use( 42staticRouter.use(
43 STATIC_PATHS.REDUNDANCY,
44 cors(),
45 express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
46)
47
48staticRouter.use(
44 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 49 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
45 asyncMiddleware(videosGetValidator), 50 asyncMiddleware(videosGetValidator),
46 asyncMiddleware(downloadVideoFile) 51 asyncMiddleware(downloadVideoFile)
47) 52)
48 53
54// HLS
55staticRouter.use(
56 STATIC_PATHS.PLAYLISTS.HLS,
57 cors(),
58 express.static(HLS_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist
59)
60
49// Thumbnails path for express 61// Thumbnails path for express
50const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR 62const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
51staticRouter.use( 63staticRouter.use(
@@ -131,6 +143,12 @@ staticRouter.use('/.well-known/dnt/',
131 } 143 }
132) 144)
133 145
146staticRouter.use('/.well-known/change-password',
147 (_, res: express.Response) => {
148 res.redirect('/my-account/settings')
149 }
150)
151
134// --------------------------------------------------------------------------- 152// ---------------------------------------------------------------------------
135 153
136export { 154export {
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts
index 9bc7586d1..8b77d9de7 100644
--- a/server/controllers/tracker.ts
+++ b/server/controllers/tracker.ts
@@ -6,6 +6,8 @@ import * as proxyAddr from 'proxy-addr'
6import { Server as WebSocketServer } from 'ws' 6import { Server as WebSocketServer } from 'ws'
7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants' 7import { CONFIG, TRACKER_RATE_LIMITS } from '../initializers/constants'
8import { VideoFileModel } from '../models/video/video-file' 8import { VideoFileModel } from '../models/video/video-file'
9import { parse } from 'url'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
9 11
10const TrackerServer = bitTorrentTracker.Server 12const TrackerServer = bitTorrentTracker.Server
11 13
@@ -20,7 +22,7 @@ const trackerServer = new TrackerServer({
20 udp: false, 22 udp: false,
21 ws: false, 23 ws: false,
22 dht: false, 24 dht: false,
23 filter: function (infoHash, params, cb) { 25 filter: async function (infoHash, params, cb) {
24 let ip: string 26 let ip: string
25 27
26 if (params.type === 'ws') { 28 if (params.type === 'ws') {
@@ -31,19 +33,25 @@ const trackerServer = new TrackerServer({
31 33
32 const key = ip + '-' + infoHash 34 const key = ip + '-' + infoHash
33 35
34 peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 36 peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1
35 peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 37 peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1
36 38
37 if (peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { 39 if (peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) {
38 return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) 40 return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`))
39 } 41 }
40 42
41 VideoFileModel.isInfohashExists(infoHash) 43 try {
42 .then(exists => { 44 const videoFileExists = await VideoFileModel.doesInfohashExist(infoHash)
43 if (exists === false) return cb(new Error(`Unknown infoHash ${infoHash}`)) 45 if (videoFileExists === true) return cb()
44 46
45 return cb() 47 const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash)
46 }) 48 if (playlistExists === true) return cb()
49
50 return cb(new Error(`Unknown infoHash ${infoHash}`))
51 } catch (err) {
52 logger.error('Error in tracker filter.', { err })
53 return cb(err)
54 }
47 } 55 }
48}) 56})
49 57
@@ -59,16 +67,26 @@ const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer)
59trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) 67trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' }))
60trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) 68trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' }))
61 69
62function createWebsocketServer (app: express.Application) { 70function createWebsocketTrackerServer (app: express.Application) {
63 const server = http.createServer(app) 71 const server = http.createServer(app)
64 const wss = new WebSocketServer({ server: server, path: '/tracker/socket' }) 72 const wss = new WebSocketServer({ noServer: true })
73
65 wss.on('connection', function (ws, req) { 74 wss.on('connection', function (ws, req) {
66 const ip = proxyAddr(req, CONFIG.TRUST_PROXY) 75 ws['ip'] = proxyAddr(req, CONFIG.TRUST_PROXY)
67 ws['ip'] = ip
68 76
69 trackerServer.onWebSocketConnection(ws) 77 trackerServer.onWebSocketConnection(ws)
70 }) 78 })
71 79
80 server.on('upgrade', (request, socket, head) => {
81 const pathname = parse(request.url).pathname
82
83 if (pathname === '/tracker/socket') {
84 wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request))
85 }
86
87 // Don't destroy socket, we have Socket.IO too
88 })
89
72 return server 90 return server
73} 91}
74 92
@@ -76,7 +94,7 @@ function createWebsocketServer (app: express.Application) {
76 94
77export { 95export {
78 trackerRouter, 96 trackerRouter,
79 createWebsocketServer 97 createWebsocketTrackerServer
80} 98}
81 99
82// --------------------------------------------------------------------------- 100// ---------------------------------------------------------------------------