aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/server/controllers/activitypub/client.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/server/controllers/activitypub/client.ts')
-rw-r--r--server/server/controllers/activitypub/client.ts485
1 files changed, 485 insertions, 0 deletions
diff --git a/server/server/controllers/activitypub/client.ts b/server/server/controllers/activitypub/client.ts
new file mode 100644
index 000000000..5d5e43bf5
--- /dev/null
+++ b/server/server/controllers/activitypub/client.ts
@@ -0,0 +1,485 @@
1import cors from 'cors'
2import express from 'express'
3import { VideoCommentObject, VideoPlaylistPrivacy, VideoPrivacy, VideoRateType } from '@peertube/peertube-models'
4import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
5import { getContextFilter } from '@server/lib/activitypub/context.js'
6import { getServerActor } from '@server/models/application/application.js'
7import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models/index.js'
8import { activityPubContextify } from '../../helpers/activity-pub-utils.js'
9import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
10import { audiencify, getAudience } from '../../lib/activitypub/audience.js'
11import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send/index.js'
12import { buildCreateActivity } from '../../lib/activitypub/send/send-create.js'
13import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike.js'
14import {
15 getLocalVideoCommentsActivityPubUrl,
16 getLocalVideoDislikesActivityPubUrl,
17 getLocalVideoLikesActivityPubUrl,
18 getLocalVideoSharesActivityPubUrl
19} from '../../lib/activitypub/url.js'
20import { cacheRoute } from '../../middlewares/cache/cache.js'
21import {
22 activityPubRateLimiter,
23 asyncMiddleware,
24 ensureIsLocalChannel,
25 executeIfActivityPub,
26 localAccountValidator,
27 videoChannelsNameWithHostValidator,
28 videosCustomGetValidator,
29 videosShareValidator
30} from '../../middlewares/index.js'
31import {
32 getAccountVideoRateValidatorFactory,
33 getVideoLocalViewerValidator,
34 videoCommentGetValidator
35} from '../../middlewares/validators/index.js'
36import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy.js'
37import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists.js'
38import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
39import { AccountModel } from '../../models/account/account.js'
40import { ActorFollowModel } from '../../models/actor/actor-follow.js'
41import { VideoCommentModel } from '../../models/video/video-comment.js'
42import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
43import { VideoShareModel } from '../../models/video/video-share.js'
44import { activityPubResponse } from './utils.js'
45
46const activityPubClientRouter = express.Router()
47activityPubClientRouter.use(cors())
48
49// Intercept ActivityPub client requests
50
51activityPubClientRouter.get(
52 [ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ],
53 executeIfActivityPub,
54 activityPubRateLimiter,
55 asyncMiddleware(localAccountValidator),
56 asyncMiddleware(accountController)
57)
58activityPubClientRouter.get('/accounts?/:name/followers',
59 executeIfActivityPub,
60 activityPubRateLimiter,
61 asyncMiddleware(localAccountValidator),
62 asyncMiddleware(accountFollowersController)
63)
64activityPubClientRouter.get('/accounts?/:name/following',
65 executeIfActivityPub,
66 activityPubRateLimiter,
67 asyncMiddleware(localAccountValidator),
68 asyncMiddleware(accountFollowingController)
69)
70activityPubClientRouter.get('/accounts?/:name/playlists',
71 executeIfActivityPub,
72 activityPubRateLimiter,
73 asyncMiddleware(localAccountValidator),
74 asyncMiddleware(accountPlaylistsController)
75)
76activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
77 executeIfActivityPub,
78 activityPubRateLimiter,
79 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
80 asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
81 asyncMiddleware(getAccountVideoRateFactory('like'))
82)
83activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
84 executeIfActivityPub,
85 activityPubRateLimiter,
86 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
87 asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
88 asyncMiddleware(getAccountVideoRateFactory('dislike'))
89)
90
91activityPubClientRouter.get(
92 [ '/videos/watch/:id', '/w/:id' ],
93 executeIfActivityPub,
94 activityPubRateLimiter,
95 cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
96 asyncMiddleware(videosCustomGetValidator('all')),
97 asyncMiddleware(videoController)
98)
99activityPubClientRouter.get('/videos/watch/:id/activity',
100 executeIfActivityPub,
101 activityPubRateLimiter,
102 asyncMiddleware(videosCustomGetValidator('all')),
103 asyncMiddleware(videoController)
104)
105activityPubClientRouter.get('/videos/watch/:id/announces',
106 executeIfActivityPub,
107 activityPubRateLimiter,
108 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
109 asyncMiddleware(videoAnnouncesController)
110)
111activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
112 executeIfActivityPub,
113 activityPubRateLimiter,
114 asyncMiddleware(videosShareValidator),
115 asyncMiddleware(videoAnnounceController)
116)
117activityPubClientRouter.get('/videos/watch/:id/likes',
118 executeIfActivityPub,
119 activityPubRateLimiter,
120 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
121 asyncMiddleware(videoLikesController)
122)
123activityPubClientRouter.get('/videos/watch/:id/dislikes',
124 executeIfActivityPub,
125 activityPubRateLimiter,
126 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
127 asyncMiddleware(videoDislikesController)
128)
129activityPubClientRouter.get('/videos/watch/:id/comments',
130 executeIfActivityPub,
131 activityPubRateLimiter,
132 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
133 asyncMiddleware(videoCommentsController)
134)
135activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
136 executeIfActivityPub,
137 activityPubRateLimiter,
138 asyncMiddleware(videoCommentGetValidator),
139 asyncMiddleware(videoCommentController)
140)
141activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity',
142 executeIfActivityPub,
143 activityPubRateLimiter,
144 asyncMiddleware(videoCommentGetValidator),
145 asyncMiddleware(videoCommentController)
146)
147
148activityPubClientRouter.get(
149 [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ],
150 executeIfActivityPub,
151 activityPubRateLimiter,
152 asyncMiddleware(videoChannelsNameWithHostValidator),
153 ensureIsLocalChannel,
154 asyncMiddleware(videoChannelController)
155)
156activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
157 executeIfActivityPub,
158 activityPubRateLimiter,
159 asyncMiddleware(videoChannelsNameWithHostValidator),
160 ensureIsLocalChannel,
161 asyncMiddleware(videoChannelFollowersController)
162)
163activityPubClientRouter.get('/video-channels/:nameWithHost/following',
164 executeIfActivityPub,
165 activityPubRateLimiter,
166 asyncMiddleware(videoChannelsNameWithHostValidator),
167 ensureIsLocalChannel,
168 asyncMiddleware(videoChannelFollowingController)
169)
170activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
171 executeIfActivityPub,
172 activityPubRateLimiter,
173 asyncMiddleware(videoChannelsNameWithHostValidator),
174 ensureIsLocalChannel,
175 asyncMiddleware(videoChannelPlaylistsController)
176)
177
178activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?',
179 executeIfActivityPub,
180 activityPubRateLimiter,
181 asyncMiddleware(videoFileRedundancyGetValidator),
182 asyncMiddleware(videoRedundancyController)
183)
184activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistType/:videoId',
185 executeIfActivityPub,
186 activityPubRateLimiter,
187 asyncMiddleware(videoPlaylistRedundancyGetValidator),
188 asyncMiddleware(videoRedundancyController)
189)
190
191activityPubClientRouter.get(
192 [ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ],
193 executeIfActivityPub,
194 activityPubRateLimiter,
195 asyncMiddleware(videoPlaylistsGetValidator('all')),
196 asyncMiddleware(videoPlaylistController)
197)
198activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId',
199 executeIfActivityPub,
200 activityPubRateLimiter,
201 asyncMiddleware(videoPlaylistElementAPGetValidator),
202 asyncMiddleware(videoPlaylistElementController)
203)
204
205activityPubClientRouter.get('/videos/local-viewer/:localViewerId',
206 executeIfActivityPub,
207 activityPubRateLimiter,
208 asyncMiddleware(getVideoLocalViewerValidator),
209 asyncMiddleware(getVideoLocalViewerController)
210)
211
212// ---------------------------------------------------------------------------
213
214export {
215 activityPubClientRouter
216}
217
218// ---------------------------------------------------------------------------
219
220async function accountController (req: express.Request, res: express.Response) {
221 const account = res.locals.account
222
223 return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor', getContextFilter()), res)
224}
225
226async function accountFollowersController (req: express.Request, res: express.Response) {
227 const account = res.locals.account
228 const activityPubResult = await actorFollowers(req, account.Actor)
229
230 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
231}
232
233async function accountFollowingController (req: express.Request, res: express.Response) {
234 const account = res.locals.account
235 const activityPubResult = await actorFollowing(req, account.Actor)
236
237 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
238}
239
240async function accountPlaylistsController (req: express.Request, res: express.Response) {
241 const account = res.locals.account
242 const activityPubResult = await actorPlaylists(req, { account })
243
244 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
245}
246
247async function videoChannelPlaylistsController (req: express.Request, res: express.Response) {
248 const channel = res.locals.videoChannel
249 const activityPubResult = await actorPlaylists(req, { channel })
250
251 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
252}
253
254function getAccountVideoRateFactory (rateType: VideoRateType) {
255 return (req: express.Request, res: express.Response) => {
256 const accountVideoRate = res.locals.accountVideoRate
257
258 const byActor = accountVideoRate.Account.Actor
259 const APObject = rateType === 'like'
260 ? buildLikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
261 : buildDislikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
262
263 return activityPubResponse(activityPubContextify(APObject, 'Rate', getContextFilter()), res)
264 }
265}
266
267async function videoController (req: express.Request, res: express.Response) {
268 const video = res.locals.videoAll
269
270 if (redirectIfNotOwned(video.url, res)) return
271
272 // We need captions to render AP object
273 const videoAP = await video.lightAPToFullAP(undefined)
274
275 const audience = getAudience(videoAP.VideoChannel.Account.Actor, videoAP.privacy === VideoPrivacy.PUBLIC)
276 const videoObject = audiencify(await videoAP.toActivityPubObject(), audience)
277
278 if (req.path.endsWith('/activity')) {
279 const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience)
280 return activityPubResponse(activityPubContextify(data, 'Video', getContextFilter()), res)
281 }
282
283 return activityPubResponse(activityPubContextify(videoObject, 'Video', getContextFilter()), res)
284}
285
286async function videoAnnounceController (req: express.Request, res: express.Response) {
287 const share = res.locals.videoShare
288
289 if (redirectIfNotOwned(share.url, res)) return
290
291 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined)
292
293 return activityPubResponse(activityPubContextify(activity, 'Announce', getContextFilter()), res)
294}
295
296async function videoAnnouncesController (req: express.Request, res: express.Response) {
297 const video = res.locals.onlyImmutableVideo
298
299 if (redirectIfNotOwned(video.url, res)) return
300
301 const handler = async (start: number, count: number) => {
302 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
303 return {
304 total: result.total,
305 data: result.data.map(r => r.url)
306 }
307 }
308 const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page)
309
310 return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
311}
312
313async function videoLikesController (req: express.Request, res: express.Response) {
314 const video = res.locals.onlyImmutableVideo
315
316 if (redirectIfNotOwned(video.url, res)) return
317
318 const json = await videoRates(req, 'like', video, getLocalVideoLikesActivityPubUrl(video))
319
320 return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
321}
322
323async function videoDislikesController (req: express.Request, res: express.Response) {
324 const video = res.locals.onlyImmutableVideo
325
326 if (redirectIfNotOwned(video.url, res)) return
327
328 const json = await videoRates(req, 'dislike', video, getLocalVideoDislikesActivityPubUrl(video))
329
330 return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
331}
332
333async function videoCommentsController (req: express.Request, res: express.Response) {
334 const video = res.locals.onlyImmutableVideo
335
336 if (redirectIfNotOwned(video.url, res)) return
337
338 const handler = async (start: number, count: number) => {
339 const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count })
340
341 return {
342 total: result.total,
343 data: result.data.map(r => r.url)
344 }
345 }
346 const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page)
347
348 return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
349}
350
351async function videoChannelController (req: express.Request, res: express.Response) {
352 const videoChannel = res.locals.videoChannel
353
354 return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor', getContextFilter()), res)
355}
356
357async function videoChannelFollowersController (req: express.Request, res: express.Response) {
358 const videoChannel = res.locals.videoChannel
359 const activityPubResult = await actorFollowers(req, videoChannel.Actor)
360
361 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
362}
363
364async function videoChannelFollowingController (req: express.Request, res: express.Response) {
365 const videoChannel = res.locals.videoChannel
366 const activityPubResult = await actorFollowing(req, videoChannel.Actor)
367
368 return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
369}
370
371async function videoCommentController (req: express.Request, res: express.Response) {
372 const videoComment = res.locals.videoCommentFull
373
374 if (redirectIfNotOwned(videoComment.url, res)) return
375
376 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
377 const isPublic = true // Comments are always public
378 let videoCommentObject = videoComment.toActivityPubObject(threadParentComments)
379
380 if (videoComment.Account) {
381 const audience = getAudience(videoComment.Account.Actor, isPublic)
382 videoCommentObject = audiencify(videoCommentObject, audience)
383
384 if (req.path.endsWith('/activity')) {
385 const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience)
386 return activityPubResponse(activityPubContextify(data, 'Comment', getContextFilter()), res)
387 }
388 }
389
390 return activityPubResponse(activityPubContextify(videoCommentObject, 'Comment', getContextFilter()), res)
391}
392
393async function videoRedundancyController (req: express.Request, res: express.Response) {
394 const videoRedundancy = res.locals.videoRedundancy
395
396 if (redirectIfNotOwned(videoRedundancy.url, res)) return
397
398 const serverActor = await getServerActor()
399
400 const audience = getAudience(serverActor)
401 const object = audiencify(videoRedundancy.toActivityPubObject(), audience)
402
403 if (req.path.endsWith('/activity')) {
404 const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience)
405 return activityPubResponse(activityPubContextify(data, 'CacheFile', getContextFilter()), res)
406 }
407
408 return activityPubResponse(activityPubContextify(object, 'CacheFile', getContextFilter()), res)
409}
410
411async function videoPlaylistController (req: express.Request, res: express.Response) {
412 const playlist = res.locals.videoPlaylistFull
413
414 if (redirectIfNotOwned(playlist.url, res)) return
415
416 // We need more attributes
417 playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
418
419 const json = await playlist.toActivityPubObject(req.query.page, null)
420 const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
421 const object = audiencify(json, audience)
422
423 return activityPubResponse(activityPubContextify(object, 'Playlist', getContextFilter()), res)
424}
425
426function videoPlaylistElementController (req: express.Request, res: express.Response) {
427 const videoPlaylistElement = res.locals.videoPlaylistElementAP
428
429 if (redirectIfNotOwned(videoPlaylistElement.url, res)) return
430
431 const json = videoPlaylistElement.toActivityPubObject()
432 return activityPubResponse(activityPubContextify(json, 'Playlist', getContextFilter()), res)
433}
434
435function getVideoLocalViewerController (req: express.Request, res: express.Response) {
436 const localViewer = res.locals.localViewerFull
437
438 return activityPubResponse(activityPubContextify(localViewer.toActivityPubObject(), 'WatchAction', getContextFilter()), res)
439}
440
441// ---------------------------------------------------------------------------
442
443function actorFollowing (req: express.Request, actor: MActorId) {
444 const handler = (start: number, count: number) => {
445 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
446 }
447
448 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
449}
450
451function actorFollowers (req: express.Request, actor: MActorId) {
452 const handler = (start: number, count: number) => {
453 return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
454 }
455
456 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
457}
458
459function actorPlaylists (req: express.Request, options: { account: MAccountId } | { channel: MChannelId }) {
460 const handler = (start: number, count: number) => {
461 return VideoPlaylistModel.listPublicUrlsOfForAP(options, start, count)
462 }
463
464 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
465}
466
467function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
468 const handler = async (start: number, count: number) => {
469 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
470 return {
471 total: result.total,
472 data: result.data.map(r => r.url)
473 }
474 }
475 return activityPubCollectionPagination(url, handler, req.query.page)
476}
477
478function redirectIfNotOwned (url: string, res: express.Response) {
479 if (url.startsWith(WEBSERVER.URL) === false) {
480 res.redirect(url)
481 return true
482 }
483
484 return false
485}