@@ -22,11 +22,11 @@ export class VideoAttributesComponent implements OnInit { | |||
constructor (private hooks: HooksService) { } | |||
async ngOnInit () { | |||
this.pluginMetadata = await this.hooks.wrapFunResult( | |||
this.buildPluginMetadata.bind(this), | |||
{ video: this.video }, | |||
this.pluginMetadata = await this.hooks.wrapObject( | |||
this.pluginMetadata, | |||
'video-watch', | |||
'filter:video-watch.video-plugin-metadata.result' | |||
'filter:video-watch.video-plugin-metadata.result', | |||
{ video: this.video } | |||
) | |||
} | |||
@@ -39,11 +39,4 @@ export class VideoAttributesComponent implements OnInit { | |||
return this.video.tags | |||
} | |||
// Used for plugin hooks | |||
private buildPluginMetadata (_options: { | |||
video: VideoDetails | |||
}): PluginMetadata[] { | |||
return [] | |||
} | |||
} |
@@ -48,15 +48,6 @@ export class HooksService { | |||
return this.pluginService.runHook(hookResultName, result, params) | |||
} | |||
async wrapFunResult <P, R, H extends ClientFilterHookName> | |||
(fun: RawFunction<P, R>, params: P, scope: PluginClientScope, hookResultName: H) { | |||
await this.pluginService.ensurePluginsAreLoaded(scope) | |||
const result = fun(params) | |||
return this.pluginService.runHook(hookResultName, result, params) | |||
} | |||
runAction<T, U extends ClientActionHookName> (hookName: U, scope: PluginClientScope, params?: T) { | |||
// Use setTimeout to give priority to Angular change detector | |||
setTimeout(() => { | |||
@@ -66,13 +57,13 @@ export class HooksService { | |||
}) | |||
} | |||
async wrapObject<T, U extends ClientFilterHookName> (result: T, scope: PluginClientScope, hookName: U) { | |||
async wrapObject<T, U extends ClientFilterHookName> (result: T, scope: PluginClientScope, hookName: U, context?: any) { | |||
await this.pluginService.ensurePluginsAreLoaded(scope) | |||
return this.wrapObjectWithoutScopeLoad(result, hookName) | |||
return this.wrapObjectWithoutScopeLoad(result, hookName, context) | |||
} | |||
private wrapObjectWithoutScopeLoad<T, U extends ClientFilterHookName> (result: T, hookName: U) { | |||
return this.pluginService.runHook(hookName, result) | |||
private wrapObjectWithoutScopeLoad<T, U extends ClientFilterHookName> (result: T, hookName: U, context?: any) { | |||
return this.pluginService.runHook(hookName, result, context) | |||
} | |||
} |
@@ -48,7 +48,7 @@ activityPubClientRouter.get( | |||
[ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ], | |||
executeIfActivityPub, | |||
asyncMiddleware(localAccountValidator), | |||
accountController | |||
asyncMiddleware(accountController) | |||
) | |||
activityPubClientRouter.get('/accounts?/:name/followers', | |||
executeIfActivityPub, | |||
@@ -69,13 +69,13 @@ activityPubClientRouter.get('/accounts?/:name/likes/:videoId', | |||
executeIfActivityPub, | |||
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), | |||
asyncMiddleware(getAccountVideoRateValidatorFactory('like')), | |||
getAccountVideoRateFactory('like') | |||
asyncMiddleware(getAccountVideoRateFactory('like')) | |||
) | |||
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', | |||
executeIfActivityPub, | |||
cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), | |||
asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')), | |||
getAccountVideoRateFactory('dislike') | |||
asyncMiddleware(getAccountVideoRateFactory('dislike')) | |||
) | |||
activityPubClientRouter.get( | |||
@@ -131,7 +131,7 @@ activityPubClientRouter.get( | |||
executeIfActivityPub, | |||
asyncMiddleware(videoChannelsNameWithHostValidator), | |||
ensureIsLocalChannel, | |||
videoChannelController | |||
asyncMiddleware(videoChannelController) | |||
) | |||
activityPubClientRouter.get('/video-channels/:nameWithHost/followers', | |||
executeIfActivityPub, | |||
@@ -172,13 +172,13 @@ activityPubClientRouter.get( | |||
activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId', | |||
executeIfActivityPub, | |||
asyncMiddleware(videoPlaylistElementAPGetValidator), | |||
videoPlaylistElementController | |||
asyncMiddleware(videoPlaylistElementController) | |||
) | |||
activityPubClientRouter.get('/videos/local-viewer/:localViewerId', | |||
executeIfActivityPub, | |||
asyncMiddleware(getVideoLocalViewerValidator), | |||
getVideoLocalViewerController | |||
asyncMiddleware(getVideoLocalViewerController) | |||
) | |||
// --------------------------------------------------------------------------- | |||
@@ -189,10 +189,10 @@ export { | |||
// --------------------------------------------------------------------------- | |||
function accountController (req: express.Request, res: express.Response) { | |||
async function accountController (req: express.Request, res: express.Response) { | |||
const account = res.locals.account | |||
return activityPubResponse(activityPubContextify(account.toActivityPubObject(), 'Actor'), res) | |||
return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor'), res) | |||
} | |||
async function accountFollowersController (req: express.Request, res: express.Response) { | |||
@@ -246,7 +246,7 @@ async function videoController (req: express.Request, res: express.Response) { | |||
const videoWithCaptions = Object.assign(video, { VideoCaptions: captions }) | |||
const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC) | |||
const videoObject = audiencify(videoWithCaptions.toActivityPubObject(), audience) | |||
const videoObject = audiencify(await videoWithCaptions.toActivityPubObject(), audience) | |||
if (req.path.endsWith('/activity')) { | |||
const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience) | |||
@@ -321,10 +321,10 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
return activityPubResponse(activityPubContextify(json, 'Collection'), res) | |||
} | |||
function videoChannelController (req: express.Request, res: express.Response) { | |||
async function videoChannelController (req: express.Request, res: express.Response) { | |||
const videoChannel = res.locals.videoChannel | |||
return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject(), 'Actor'), res) | |||
return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor'), res) | |||
} | |||
async function videoChannelFollowersController (req: express.Request, res: express.Response) { | |||
@@ -63,7 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number | |||
activities.push(announceActivity) | |||
} else { | |||
const videoObject = video.toActivityPubObject() | |||
const videoObject = await video.toActivityPubObject() | |||
const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) | |||
activities.push(createActivity) | |||
@@ -1,6 +1,8 @@ | |||
import express from 'express' | |||
function activityPubResponse (data: any, res: express.Response) { | |||
async function activityPubResponse (promise: Promise<any>, res: express.Response) { | |||
const data = await promise | |||
return res.type('application/activity+json; charset=utf-8') | |||
.json(data) | |||
} | |||
@@ -1,7 +1,8 @@ | |||
import { ContextType } from '@shared/models' | |||
import { Hooks } from '../plugins/hooks' | |||
function activityPubContextify <T> (data: T, type: ContextType) { | |||
return { ...getContextData(type), ...data } | |||
async function activityPubContextify <T> (data: T, type: ContextType) { | |||
return { ...await getContextData(type), ...data } | |||
} | |||
// --------------------------------------------------------------------------- | |||
@@ -165,10 +166,13 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string | |||
Rate: buildContext() | |||
} | |||
function getContextData (type: ContextType) { | |||
return { | |||
'@context': contextStore[type] | |||
} | |||
async function getContextData (type: ContextType) { | |||
const contextData = await Hooks.wrapObject( | |||
contextStore[type], | |||
'filter:activity-pub.activity.context.build.result' | |||
) | |||
return { '@context': contextData } | |||
} | |||
function buildContext (contextValue?: ContextValue) { | |||
@@ -52,9 +52,9 @@ function buildGlobalHeaders (body: any) { | |||
} | |||
} | |||
function signAndContextify <T> (byActor: MActor, data: T, contextType: ContextType | null) { | |||
async function signAndContextify <T> (byActor: MActor, data: T, contextType: ContextType | null) { | |||
const activity = contextType | |||
? activityPubContextify(data, contextType) | |||
? await activityPubContextify(data, contextType) | |||
: data | |||
return signJsonLDObject(byActor, activity) | |||
@@ -33,7 +33,7 @@ async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { | |||
logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) | |||
const byActor = video.VideoChannel.Account.Actor | |||
const videoObject = video.toActivityPubObject() | |||
const videoObject = await video.toActivityPubObject() | |||
const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) | |||
const createActivity = buildCreateActivity(video.url, byActor, videoObject, audience) | |||
@@ -36,7 +36,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: T | |||
video.VideoCaptions = await video.$get('VideoCaptions', { transaction }) | |||
} | |||
const videoObject = video.toActivityPubObject() | |||
const videoObject = await video.toActivityPubObject() | |||
const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) | |||
const updateActivity = buildUpdateActivity(url, byActor, videoObject, audience) | |||
@@ -447,8 +447,8 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
} | |||
} | |||
toActivityPubObject (this: MAccountAP) { | |||
const obj = this.Actor.toActivityPubObject(this.name) | |||
async toActivityPubObject (this: MAccountAP) { | |||
const obj = await this.Actor.toActivityPubObject(this.name) | |||
return Object.assign(obj, { | |||
summary: this.description | |||
@@ -809,8 +809,8 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
return Object.assign(actor, videoChannel) | |||
} | |||
toActivityPubObject (this: MChannelAP): ActivityPubActor { | |||
const obj = this.Actor.toActivityPubObject(this.name) | |||
async toActivityPubObject (this: MChannelAP): Promise<ActivityPubActor> { | |||
const obj = await this.Actor.toActivityPubObject(this.name) | |||
return Object.assign(obj, { | |||
summary: this.description, | |||
@@ -137,6 +137,7 @@ import { VideoShareModel } from './video-share' | |||
import { VideoSourceModel } from './video-source' | |||
import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | |||
import { VideoTagModel } from './video-tag' | |||
import { Hooks } from '@server/lib/plugins/hooks' | |||
export enum ScopeNames { | |||
FOR_API = 'FOR_API', | |||
@@ -1713,8 +1714,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
return files | |||
} | |||
toActivityPubObject (this: MVideoAP): VideoObject { | |||
return videoModelToActivityPubObject(this) | |||
toActivityPubObject (this: MVideoAP): Promise<VideoObject> { | |||
return Hooks.wrapObject( | |||
videoModelToActivityPubObject(this), | |||
'filter:activity-pub.video.jsonld.build.result', | |||
{ video: this } | |||
) | |||
} | |||
getTruncatedDescription () { | |||
@@ -2,10 +2,10 @@ | |||
import { expect } from 'chai' | |||
import { buildDigest } from '@server/helpers/peertube-crypto' | |||
import { HTTP_SIGNATURE } from '@server/initializers/constants' | |||
import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants' | |||
import { activityPubContextify } from '@server/lib/activitypub/context' | |||
import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send' | |||
import { makeFollowRequest, makePOSTAPRequest } from '@server/tests/shared' | |||
import { makePOSTAPRequest } from '@server/tests/shared' | |||
import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' | |||
import { HttpStatusCode } from '@shared/models' | |||
import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands' | |||
@@ -43,6 +43,32 @@ function getAnnounceWithoutContext (server: PeerTubeServer) { | |||
return result | |||
} | |||
async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { | |||
const follow = { | |||
type: 'Follow', | |||
id: by.url + '/' + new Date().getTime(), | |||
actor: by.url, | |||
object: to.url | |||
} | |||
const body = await activityPubContextify(follow, 'Follow') | |||
const httpSignature = { | |||
algorithm: HTTP_SIGNATURE.ALGORITHM, | |||
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | |||
keyId: by.url, | |||
key: by.privateKey, | |||
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN | |||
} | |||
const headers = { | |||
'digest': buildDigest(body), | |||
'content-type': 'application/activity+json', | |||
'accept': ACTIVITY_PUB.ACCEPT_HEADER | |||
} | |||
return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers) | |||
} | |||
describe('Test ActivityPub security', function () { | |||
let servers: PeerTubeServer[] | |||
let url: string | |||
@@ -77,7 +103,7 @@ describe('Test ActivityPub security', function () { | |||
describe('When checking HTTP signature', function () { | |||
it('Should fail with an invalid digest', async function () { | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = { | |||
Digest: buildDigest({ hello: 'coucou' }) | |||
} | |||
@@ -91,7 +117,7 @@ describe('Test ActivityPub security', function () { | |||
}) | |||
it('Should fail with an invalid date', async function () { | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' | |||
@@ -107,7 +133,7 @@ describe('Test ActivityPub security', function () { | |||
await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) | |||
await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
try { | |||
@@ -122,7 +148,7 @@ describe('Test ActivityPub security', function () { | |||
await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) | |||
await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey) | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
const signatureOptions = baseHttpSignature() | |||
@@ -145,7 +171,7 @@ describe('Test ActivityPub security', function () { | |||
}) | |||
it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () { | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
const signatureOptions = baseHttpSignature() | |||
@@ -156,7 +182,7 @@ describe('Test ActivityPub security', function () { | |||
}) | |||
it('Should succeed with a valid HTTP signature', async function () { | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | |||
@@ -175,7 +201,7 @@ describe('Test ActivityPub security', function () { | |||
await killallServers([ servers[1] ]) | |||
await servers[1].run() | |||
const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | |||
const headers = buildGlobalHeaders(body) | |||
try { | |||
@@ -1,7 +1,4 @@ | |||
import { buildDigest } from '@server/helpers/peertube-crypto' | |||
import { doRequest } from '@server/helpers/requests' | |||
import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants' | |||
import { activityPubContextify } from '@server/lib/activitypub/context' | |||
export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { | |||
const options = { | |||
@@ -13,29 +10,3 @@ export function makePOSTAPRequest (url: string, body: any, httpSignature: any, h | |||
return doRequest(url, options) | |||
} | |||
export async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { | |||
const follow = { | |||
type: 'Follow', | |||
id: by.url + '/' + new Date().getTime(), | |||
actor: by.url, | |||
object: to.url | |||
} | |||
const body = activityPubContextify(follow, 'Follow') | |||
const httpSignature = { | |||
algorithm: HTTP_SIGNATURE.ALGORITHM, | |||
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | |||
keyId: by.url, | |||
key: by.privateKey, | |||
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN | |||
} | |||
const headers = { | |||
'digest': buildDigest(body), | |||
'content-type': 'application/activity+json', | |||
'accept': ACTIVITY_PUB.ACCEPT_HEADER | |||
} | |||
return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers) | |||
} |
@@ -75,8 +75,7 @@ const I18N_LOCALE_ALIAS = { | |||
'zh': 'zh-Hans-CN' | |||
} | |||
export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) | |||
.concat(Object.keys(I18N_LOCALE_ALIAS)) | |||
export const POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).concat(Object.keys(I18N_LOCALE_ALIAS)) | |||
export function getDefaultLocale () { | |||
return 'en-US' | |||
@@ -113,7 +113,13 @@ export const serverFilterHookObject = { | |||
'filter:transcoding.manual.resolutions-to-transcode.result': true, | |||
'filter:transcoding.auto.resolutions-to-transcode.result': true, | |||
'filter:activity-pub.remote-video-comment.create.accept.result': true | |||
'filter:activity-pub.remote-video-comment.create.accept.result': true, | |||
'filter:activity-pub.activity.context.build.result': true, | |||
// Filter the result of video JSON LD builder | |||
// You may also need to use filter:activity-pub.activity.context.build.result to also update JSON LD context | |||
'filter:activity-pub.video.jsonld.build.result': true | |||
} | |||
export type ServerFilterHookName = keyof typeof serverFilterHookObject | |||