Init commit
This commit is contained in:
603
server/core/controllers/activitypub/client.ts
Normal file
603
server/core/controllers/activitypub/client.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
import { HttpStatusCode, VideoChaptersObject, VideoCommentObject, VideoRateType } from '@peertube/peertube-models'
|
||||
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
|
||||
import { getContextFilter } from '@server/lib/activitypub/context.js'
|
||||
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
||||
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models/index.js'
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { activityPubContextify } from '../../helpers/activity-pub-utils.js'
|
||||
import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
|
||||
import { audiencify, getCommentAudience, getPlaylistAudience, getPublicAudience, getVideoAudience } from '../../lib/activitypub/audience.js'
|
||||
import { buildAnnounceWithVideoAudience, buildApprovalActivity, buildLikeActivity } from '../../lib/activitypub/send/index.js'
|
||||
import { buildCreateActivity } from '../../lib/activitypub/send/send-create.js'
|
||||
import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike.js'
|
||||
import {
|
||||
getLocalVideoChaptersActivityPubUrl,
|
||||
getLocalVideoCommentsActivityPubUrl,
|
||||
getLocalVideoDislikesActivityPubUrl,
|
||||
getLocalVideoLikesActivityPubUrl,
|
||||
getLocalVideoSharesActivityPubUrl
|
||||
} from '../../lib/activitypub/url.js'
|
||||
import {
|
||||
apVideoChaptersSetCacheKey,
|
||||
buildAPVideoChaptersGroupsCache,
|
||||
cacheRoute,
|
||||
cacheRouteFactory
|
||||
} from '../../middlewares/cache/cache.js'
|
||||
import {
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware,
|
||||
executeIfActivityPub,
|
||||
videoChannelsHandleValidatorFactory,
|
||||
videosCustomGetValidator,
|
||||
videosShareValidator
|
||||
} from '../../middlewares/index.js'
|
||||
import {
|
||||
accountHandleGetValidatorFactory,
|
||||
getAccountVideoRateValidatorFactory,
|
||||
getVideoLocalViewerValidator,
|
||||
videoCommentGetValidator
|
||||
} from '../../middlewares/validators/index.js'
|
||||
import { videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy.js'
|
||||
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists.js'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
|
||||
import { AccountModel } from '../../models/account/account.js'
|
||||
import { ActorFollowModel } from '../../models/actor/actor-follow.js'
|
||||
import { VideoCommentModel } from '../../models/video/video-comment.js'
|
||||
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
|
||||
import { VideoShareModel } from '../../models/video/video-share.js'
|
||||
import { activityPubResponse } from './utils.js'
|
||||
|
||||
const activityPubClientRouter = express.Router()
|
||||
activityPubClientRouter.use(cors())
|
||||
|
||||
// Intercept ActivityPub client requests
|
||||
|
||||
activityPubClientRouter.get(
|
||||
[ '/accounts?/:handle', '/accounts?/:handle/video-channels', '/a/:handle', '/a/:handle/video-channels' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/followers',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountFollowersController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/following',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountFollowingController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/playlists',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountPlaylistsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:accountName/likes/:videoId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
|
||||
asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
|
||||
asyncMiddleware(getAccountVideoRateFactory('like'))
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:accountName/dislikes/:videoId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
|
||||
asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
|
||||
asyncMiddleware(getAccountVideoRateFactory('dislike'))
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/comments',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoCommentsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:videoId/comments/:commentId/approve-reply',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoCommentGetValidator),
|
||||
asyncMiddleware(videoCommentApprovedController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:videoId/comments/:commentId/activity',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoCommentGetValidator),
|
||||
asyncMiddleware(videoCommentController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
[ '/videos/watch/:videoId/comments/:commentId', '/w/:videoId([^;]+);threadId=:commentId([0-9]+)' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoCommentGetValidator),
|
||||
asyncMiddleware(videoCommentController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
activityPubClientRouter.get(
|
||||
[ '/videos/watch/:id', '/w/:id' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
|
||||
asyncMiddleware(videosCustomGetValidator('all')),
|
||||
asyncMiddleware(videoController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/activity',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('all')),
|
||||
asyncMiddleware(videoController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/announces',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoAnnouncesController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/announces/:actorId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosShareValidator),
|
||||
asyncMiddleware(videoAnnounceController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/likes',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoLikesController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/dislikes',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoDislikesController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/player-settings',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoPlayerSettingsController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { middleware: chaptersCacheRouteMiddleware, instance: chaptersApiCache } = cacheRouteFactory()
|
||||
|
||||
InternalEventEmitter.Instance.on('chapters-updated', ({ video }) => {
|
||||
if (video.remote) return
|
||||
|
||||
chaptersApiCache.clearGroupSafe(buildAPVideoChaptersGroupsCache({ videoId: video.uuid }))
|
||||
})
|
||||
|
||||
activityPubClientRouter.get(
|
||||
'/videos/watch/:id/chapters',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
apVideoChaptersSetCacheKey,
|
||||
chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoChaptersController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
activityPubClientRouter.get(
|
||||
[ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/followers',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelFollowersController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/following',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelFollowingController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/playlists',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelPlaylistsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/player-settings',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(channelPlayerSettingsController)
|
||||
)
|
||||
|
||||
activityPubClientRouter.get(
|
||||
'/redundancy/streaming-playlists/:streamingPlaylistType/:videoId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoPlaylistRedundancyGetValidator),
|
||||
asyncMiddleware(videoRedundancyController)
|
||||
)
|
||||
|
||||
activityPubClientRouter.get(
|
||||
[ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoPlaylistsGetValidator('all')),
|
||||
asyncMiddleware(videoPlaylistController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-playlists/:playlistId/videos/:playlistElementId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoPlaylistElementAPGetValidator),
|
||||
asyncMiddleware(videoPlaylistElementController)
|
||||
)
|
||||
|
||||
activityPubClientRouter.get(
|
||||
'/videos/local-viewer/:localViewerId',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(getVideoLocalViewerValidator),
|
||||
asyncMiddleware(getVideoLocalViewerController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
activityPubClientRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function accountController (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
|
||||
return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function accountFollowersController (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
const activityPubResult = await actorFollowers(req, account.Actor)
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function accountFollowingController (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
const activityPubResult = await actorFollowing(req, account.Actor)
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function accountPlaylistsController (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
const activityPubResult = await actorPlaylists(req, { account })
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoChannelPlaylistsController (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
const activityPubResult = await actorPlaylists(req, { channel })
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
function getAccountVideoRateFactory (rateType: VideoRateType) {
|
||||
return (req: express.Request, res: express.Response) => {
|
||||
const accountVideoRate = res.locals.accountVideoRate
|
||||
|
||||
const byActor = accountVideoRate.Account.Actor
|
||||
const APObject = rateType === 'like'
|
||||
? buildLikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
|
||||
: buildDislikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video)
|
||||
|
||||
return activityPubResponse(activityPubContextify(APObject, 'Rate', getContextFilter()), res)
|
||||
}
|
||||
}
|
||||
|
||||
async function videoController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
// We need captions to render AP object
|
||||
const videoAP = await video.lightAPToFullAP(undefined)
|
||||
|
||||
const audience = getVideoAudience({ account: videoAP.VideoChannel.Account, channel: videoAP.VideoChannel, privacy: videoAP.privacy })
|
||||
const videoObject = audiencify(await videoAP.toActivityPubObject(), audience)
|
||||
|
||||
if (req.path.endsWith('/activity')) {
|
||||
const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience)
|
||||
return activityPubResponse(activityPubContextify(data, 'Video', getContextFilter()), res)
|
||||
}
|
||||
|
||||
return activityPubResponse(activityPubContextify(videoObject, 'Video', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoAnnounceController (req: express.Request, res: express.Response) {
|
||||
const share = res.locals.videoShare
|
||||
|
||||
if (redirectIfNotOwned(share.url, res)) return
|
||||
|
||||
const activity = buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll)
|
||||
|
||||
return activityPubResponse(activityPubContextify(activity, 'Announce', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoAnnouncesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const handler = async (start: number, count: number) => {
|
||||
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
|
||||
return {
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page)
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoLikesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const json = await videoRates(req, 'like', video, getLocalVideoLikesActivityPubUrl(video))
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoDislikesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const json = await videoRates(req, 'dislike', video, getLocalVideoDislikesActivityPubUrl(video))
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoCommentsController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const handler = async (start: number, count: number) => {
|
||||
const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count })
|
||||
|
||||
return {
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page)
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function videoPlayerSettingsController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const settings = await PlayerSettingModel.loadByVideoId(video.id)
|
||||
const json = PlayerSettingModel.formatAPPlayerSetting({ channel: undefined, video, settings })
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'PlayerSettings', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function channelPlayerSettingsController (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const settings = await PlayerSettingModel.loadByChannelId(channel.id)
|
||||
const json = PlayerSettingModel.formatAPPlayerSetting({ channel, video: undefined, settings })
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'PlayerSettings', getContextFilter()), res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function videoChannelController (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoChannelFollowersController (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const activityPubResult = await actorFollowers(req, videoChannel.Actor)
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoChannelFollowingController (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const activityPubResult = await actorFollowing(req, videoChannel.Actor)
|
||||
|
||||
return activityPubResponse(activityPubContextify(activityPubResult, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoCommentController (req: express.Request, res: express.Response) {
|
||||
const videoComment = res.locals.videoCommentFull
|
||||
|
||||
if (redirectIfNotOwned(videoComment.url, res)) return
|
||||
if (videoComment.Video.isLocal() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment })
|
||||
|
||||
let videoCommentObject = videoComment.toActivityPubObject(threadParentComments)
|
||||
|
||||
if (videoComment.Account) {
|
||||
const video = await VideoModel.loadByUrlAndPopulateAccount(videoComment.Video.url)
|
||||
const audience = getCommentAudience({ comment: videoComment, video, threadParentComments })
|
||||
videoCommentObject = audiencify(videoCommentObject, audience)
|
||||
|
||||
if (req.path.endsWith('/activity')) {
|
||||
const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience)
|
||||
return activityPubResponse(activityPubContextify(data, 'Comment', getContextFilter()), res)
|
||||
}
|
||||
}
|
||||
|
||||
return activityPubResponse(activityPubContextify(videoCommentObject, 'Comment', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoCommentApprovedController (req: express.Request, res: express.Response) {
|
||||
const comment = res.locals.videoCommentFull
|
||||
|
||||
if (!comment.Video.isLocal() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const activity = buildApprovalActivity({ comment, type: 'ApproveReply' })
|
||||
|
||||
return activityPubResponse(activityPubContextify(activity, 'ApproveReply', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoChaptersController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
const chapters = await VideoChapterModel.listChaptersOfVideo(video.id)
|
||||
|
||||
const chaptersObject: VideoChaptersObject = {
|
||||
id: getLocalVideoChaptersActivityPubUrl(video),
|
||||
hasPart: buildChaptersAPHasPart(video, chapters)
|
||||
}
|
||||
|
||||
return activityPubResponse(activityPubContextify(chaptersObject, 'Chapters', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoRedundancyController (req: express.Request, res: express.Response) {
|
||||
const videoRedundancy = res.locals.videoRedundancy
|
||||
|
||||
if (redirectIfNotOwned(videoRedundancy.url, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const audience = getPublicAudience(serverActor)
|
||||
const object = audiencify(videoRedundancy.toActivityPubObject(), audience)
|
||||
|
||||
if (req.path.endsWith('/activity')) {
|
||||
const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience)
|
||||
return activityPubResponse(activityPubContextify(data, 'CacheFile', getContextFilter()), res)
|
||||
}
|
||||
|
||||
return activityPubResponse(activityPubContextify(object, 'CacheFile', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function videoPlaylistController (req: express.Request, res: express.Response) {
|
||||
const playlist = res.locals.videoPlaylistFull
|
||||
|
||||
if (redirectIfNotOwned(playlist.url, res)) return
|
||||
|
||||
// We need more attributes
|
||||
playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
|
||||
|
||||
const json = await playlist.toActivityPubObject(req.query.page, null)
|
||||
const audience = getPlaylistAudience(playlist.OwnerAccount.Actor, playlist.privacy)
|
||||
const object = audiencify(json, audience)
|
||||
|
||||
return activityPubResponse(activityPubContextify(object, 'Playlist', getContextFilter()), res)
|
||||
}
|
||||
|
||||
function videoPlaylistElementController (req: express.Request, res: express.Response) {
|
||||
const videoPlaylistElement = res.locals.videoPlaylistElementAP
|
||||
|
||||
if (redirectIfNotOwned(videoPlaylistElement.url, res)) return
|
||||
|
||||
const json = videoPlaylistElement.toActivityPubObject()
|
||||
return activityPubResponse(activityPubContextify(json, 'Playlist', getContextFilter()), res)
|
||||
}
|
||||
|
||||
function getVideoLocalViewerController (req: express.Request, res: express.Response) {
|
||||
const localViewer = res.locals.localViewerFull
|
||||
|
||||
return activityPubResponse(activityPubContextify(localViewer.toActivityPubObject(), 'WatchAction', getContextFilter()), res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function actorFollowing (req: express.Request, actor: MActorId) {
|
||||
const handler = (start: number, count: number) => {
|
||||
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
|
||||
}
|
||||
|
||||
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
|
||||
}
|
||||
|
||||
function actorFollowers (req: express.Request, actor: MActorId) {
|
||||
const handler = (start: number, count: number) => {
|
||||
return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
|
||||
}
|
||||
|
||||
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
|
||||
}
|
||||
|
||||
function actorPlaylists (req: express.Request, options: { account: MAccountId } | { channel: MChannelId }) {
|
||||
const handler = (start: number, count: number) => {
|
||||
return VideoPlaylistModel.listPublicUrlsOfForAP(options, start, count)
|
||||
}
|
||||
|
||||
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
|
||||
}
|
||||
|
||||
function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
|
||||
const handler = async (start: number, count: number) => {
|
||||
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
|
||||
return {
|
||||
total: result.total,
|
||||
data: result.data.map(r => r.url)
|
||||
}
|
||||
}
|
||||
return activityPubCollectionPagination(url, handler, req.query.page)
|
||||
}
|
||||
|
||||
function redirectIfNotOwned (url: string, res: express.Response) {
|
||||
if (url.startsWith(WEBSERVER.URL) === false) {
|
||||
res.redirect(url)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
85
server/core/controllers/activitypub/inbox.ts
Normal file
85
server/core/controllers/activitypub/inbox.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, HttpStatusCode, RootActivity } from '@peertube/peertube-models'
|
||||
import { InboxManager } from '@server/lib/activitypub/inbox-manager.js'
|
||||
import express from 'express'
|
||||
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import {
|
||||
accountHandleGetValidatorFactory,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware,
|
||||
checkSignature,
|
||||
signatureValidator,
|
||||
videoChannelsHandleValidatorFactory
|
||||
} from '../../middlewares/index.js'
|
||||
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity.js'
|
||||
|
||||
const inboxRouter = express.Router()
|
||||
|
||||
inboxRouter.post(
|
||||
'/inbox',
|
||||
activityPubRateLimiter,
|
||||
signatureValidator,
|
||||
asyncMiddleware(checkSignature),
|
||||
asyncMiddleware(activityPubValidator),
|
||||
inboxController
|
||||
)
|
||||
|
||||
inboxRouter.post(
|
||||
'/accounts/:handle/inbox',
|
||||
activityPubRateLimiter,
|
||||
signatureValidator,
|
||||
asyncMiddleware(checkSignature),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(activityPubValidator),
|
||||
inboxController
|
||||
)
|
||||
|
||||
inboxRouter.post(
|
||||
'/video-channels/:handle/inbox',
|
||||
activityPubRateLimiter,
|
||||
signatureValidator,
|
||||
asyncMiddleware(checkSignature),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(activityPubValidator),
|
||||
inboxController
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
inboxRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function inboxController (req: express.Request, res: express.Response) {
|
||||
const rootActivity: RootActivity = req.body
|
||||
let activities: Activity[]
|
||||
|
||||
if ([ 'Collection', 'CollectionPage' ].includes(rootActivity.type)) {
|
||||
activities = (rootActivity as ActivityPubCollection).items
|
||||
} else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].includes(rootActivity.type)) {
|
||||
activities = (rootActivity as ActivityPubOrderedCollection<Activity>).orderedItems
|
||||
} else {
|
||||
activities = [ rootActivity as Activity ]
|
||||
}
|
||||
|
||||
// Only keep activities we are able to process
|
||||
logger.debug('Filtering %d activities...', activities.length, { activities })
|
||||
activities = activities.filter(a => isActivityValid(a))
|
||||
logger.debug('We keep %d activities.', activities.length, { activities })
|
||||
|
||||
const accountOrChannel = res.locals.account || res.locals.videoChannel
|
||||
|
||||
logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url)
|
||||
|
||||
InboxManager.Instance.addInboxMessage({
|
||||
activities,
|
||||
signatureActor: res.locals.signature.actor,
|
||||
inboxActor: accountOrChannel
|
||||
? accountOrChannel.Actor
|
||||
: undefined
|
||||
})
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
16
server/core/controllers/activitypub/index.ts
Normal file
16
server/core/controllers/activitypub/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { activityPubClientRouter } from './client.js'
|
||||
import { inboxRouter } from './inbox.js'
|
||||
import { outboxRouter } from './outbox.js'
|
||||
|
||||
const activityPubRouter = express.Router()
|
||||
|
||||
activityPubRouter.use('/', inboxRouter)
|
||||
activityPubRouter.use('/', outboxRouter)
|
||||
activityPubRouter.use('/', activityPubClientRouter)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
activityPubRouter
|
||||
}
|
||||
88
server/core/controllers/activitypub/outbox.ts
Normal file
88
server/core/controllers/activitypub/outbox.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Activity } from '@peertube/peertube-models'
|
||||
import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
|
||||
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
|
||||
import { getContextFilter } from '@server/lib/activitypub/context.js'
|
||||
import { MActorLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { getVideoAudience } from '../../lib/activitypub/audience.js'
|
||||
import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send/index.js'
|
||||
import {
|
||||
accountHandleGetValidatorFactory,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware,
|
||||
videoChannelsHandleValidatorFactory
|
||||
} from '../../middlewares/index.js'
|
||||
import { apPaginationValidator } from '../../middlewares/validators/activitypub/index.js'
|
||||
import { VideoModel } from '../../models/video/video.js'
|
||||
import { activityPubResponse } from './utils.js'
|
||||
|
||||
const outboxRouter = express.Router()
|
||||
|
||||
outboxRouter.get(
|
||||
'/accounts/:handle/outbox',
|
||||
activityPubRateLimiter,
|
||||
apPaginationValidator,
|
||||
accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false }),
|
||||
asyncMiddleware(outboxController)
|
||||
)
|
||||
|
||||
outboxRouter.get(
|
||||
'/video-channels/:handle/outbox',
|
||||
activityPubRateLimiter,
|
||||
apPaginationValidator,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(outboxController)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
outboxRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function outboxController (req: express.Request, res: express.Response) {
|
||||
const accountOrVideoChannel = res.locals.account || res.locals.videoChannel
|
||||
const actor = accountOrVideoChannel.Actor
|
||||
const actorOutboxUrl = actor.url + '/outbox'
|
||||
|
||||
logger.info('Receiving outbox request for %s.', actorOutboxUrl)
|
||||
|
||||
const handler = (start: number, count: number) => buildActivities(actor, start, count)
|
||||
const json = await activityPubCollectionPagination(actorOutboxUrl, handler, req.query.page, req.query.size)
|
||||
|
||||
return activityPubResponse(activityPubContextify(json, 'Collection', getContextFilter()), res)
|
||||
}
|
||||
|
||||
async function buildActivities (actor: MActorLight, start: number, count: number) {
|
||||
const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count)
|
||||
const activities: Activity[] = []
|
||||
|
||||
for (const video of data.data) {
|
||||
const byActor = video.VideoChannel.Account.Actor
|
||||
const createActivityAudience = getVideoAudience({
|
||||
account: video.VideoChannel.Account,
|
||||
channel: video.VideoChannel,
|
||||
privacy: video.privacy
|
||||
})
|
||||
|
||||
// This is a shared video
|
||||
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
|
||||
const videoShare = video.VideoShares[0]
|
||||
const announceActivity = buildAnnounceActivity(videoShare.url, actor, video.url, createActivityAudience)
|
||||
|
||||
activities.push(announceActivity)
|
||||
} else {
|
||||
const createActivity = buildCreateActivity(video.url, byActor, video.url, createActivityAudience)
|
||||
|
||||
activities.push(createActivity)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: activities,
|
||||
total: data.total
|
||||
}
|
||||
}
|
||||
11
server/core/controllers/activitypub/utils.ts
Normal file
11
server/core/controllers/activitypub/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import express from 'express'
|
||||
|
||||
export async function activityPubResponse (promise: Promise<any>, res: express.Response) {
|
||||
const data = await promise
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.type('application/activity+json; charset=utf-8')
|
||||
}
|
||||
|
||||
return res.json(data)
|
||||
}
|
||||
Reference in New Issue
Block a user