Check activities host

This commit is contained in:
Chocobozzz 2018-11-14 15:01:28 +01:00
parent df66d81583
commit 5c6d985fae
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
37 changed files with 403 additions and 127 deletions

View File

@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
import { ResultList } from '../../../../../shared/models/result-list.model'
import {
UserVideoRate,
UserVideoRateType,
UserVideoRateUpdate,
VideoConstant,
VideoFilter,
VideoPrivacy,
VideoRateType,
VideoUpdate
} from '../../../../../shared/models/videos'
import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
return privacies
}
private setVideoRate (id: number, rateType: VideoRateType) {
private setVideoRate (id: number, rateType: UserVideoRateType) {
const url = VideoService.BASE_VIDEO_URL + id + '/rate'
const body: UserVideoRateUpdate = {
rating: rateType

View File

@ -450,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.checkUserRating()
}
private setRating (nextRating: VideoRateType) {
private setRating (nextRating: UserVideoRateType) {
let method
switch (nextRating) {
case 'like':
@ -476,7 +476,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
}
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
let likesToIncrement = 0
let dislikesToIncrement = 0

View File

@ -4,7 +4,7 @@ import { VideoModel } from '../server/models/video/video'
import { ActorModel } from '../server/models/activitypub/actor'
import {
getAccountActivityPubUrl,
getAnnounceActivityPubUrl,
getVideoAnnounceActivityPubUrl,
getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
getVideoCommentActivityPubUrl
} from '../server/lib/activitypub'
@ -78,7 +78,7 @@ async function run () {
console.log('Updating video share ' + videoShare.url)
videoShare.url = getAnnounceActivityPubUrl(videoShare.Video.url, videoShare.Actor)
videoShare.url = getVideoAnnounceActivityPubUrl(videoShare.Actor, videoShare.Video)
await videoShare.save()
}

View File

@ -3,17 +3,22 @@ import * as express from 'express'
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send'
import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send'
import { audiencify, getAudience } from '../../lib/activitypub/audience'
import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
import {
asyncMiddleware,
videosShareValidator,
executeIfActivityPub,
localAccountValidator,
localVideoChannelValidator,
videosCustomGetValidator
} from '../../middlewares'
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
import {
getAccountVideoRateValidator,
videoCommentGetValidator,
videosGetValidator
} from '../../middlewares/validators'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
import { activityPubResponse } from './utils'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import {
getRateUrl,
getVideoCommentsActivityPubUrl,
getVideoDislikesActivityPubUrl,
getVideoLikesActivityPubUrl,
@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(asyncMiddleware(accountFollowingController))
)
activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
executeIfActivityPub(getAccountVideoRate('like'))
)
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
executeIfActivityPub(getAccountVideoRate('dislike'))
)
activityPubClientRouter.get('/videos/watch/:id',
executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
@ -62,7 +76,7 @@ activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
)
activityPubClientRouter.get('/videos/watch/:id/announces/:accountId',
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
executeIfActivityPub(asyncMiddleware(videosShareValidator)),
executeIfActivityPub(asyncMiddleware(videoAnnounceController))
)
@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
return activityPubResponse(activityPubContextify(activityPubResult), res)
}
function getAccountVideoRate (rateType: VideoRateType) {
return (req: express.Request, res: express.Response) => {
const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
const byActor = accountVideoRate.Account.Actor
const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
const APObject = rateType === 'like'
? buildLikeActivity(url, byActor, accountVideoRate.Video)
: buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video))
return activityPubResponse(activityPubContextify(APObject), res)
}
}
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video
@ -276,7 +304,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
return {
total: result.count,
data: result.rows.map(r => r.Account.Actor.url)
data: result.rows.map(r => r.url)
}
}
return activityPubCollectionPagination(url, handler, req.query.page)

View File

@ -43,11 +43,13 @@ export {
// ---------------------------------------------------------------------------
const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
processActivities(task.activities, task.signatureActor, task.inboxActor)
const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
processActivities(task.activities, options)
.then(() => cb())
})
function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
function inboxController (req: express.Request, res: express.Response) {
const rootActivity: RootActivity = req.body
let activities: Activity[] = []

View File

@ -2,8 +2,8 @@ import * as express from 'express'
import { UserVideoRateUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
import { sendVideoRateChange } from '../../../lib/activitypub'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoModel } from '../../../models/video/video'
@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
rateVideoRouter.put('/:id/rate',
authenticate,
asyncMiddleware(videoRateValidator),
asyncMiddleware(videoUpdateRateValidator),
asyncRetryTransactionMiddleware(rateVideo)
)
@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
const body: UserVideoRateUpdate = req.body
const rateType = body.rating
const videoInstance: VideoModel = res.locals.video
const userAccount: AccountModel = res.locals.oauth.token.User.Account
await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t }
const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
const accountInstance = await AccountModel.load(userAccount.id, t)
const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
let likesToIncrement = 0
@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
// There was a previous rate, update it
if (previousRate) {
// We will remove the previous rate, so we will need to update the video count attribute
if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement--
else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
if (previousRate.type === 'like') likesToIncrement--
else if (previousRate.type === 'dislike') dislikesToIncrement--
if (rateType === 'none') { // Destroy previous rate
await previousRate.destroy(sequelizeOptions)
} else { // Update previous rate
previousRate.type = rateType
previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
await previousRate.save(sequelizeOptions)
}
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
const query = {
accountId: accountInstance.id,
videoId: videoInstance.id,
type: rateType
type: rateType,
url: getRateUrl(rateType, userAccount.Actor, videoInstance)
}
await AccountVideoRateModel.create(query, sequelizeOptions)

View File

@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
import { ActorModel } from '../models/activitypub/actor'
import { signJsonLDObject } from './peertube-crypto'
import { pageToStartAndCount } from './core-utils'
import { parse } from 'url'
function activityPubContextify <T> (data: T) {
return Object.assign(data, {
@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
return activityActor.id
}
function checkUrlsSameHost (url1: string, url2: string) {
const idHost = parse(url1).host
const actorHost = parse(url2).host
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
}
// ---------------------------------------------------------------------------
export {
checkUrlsSameHost,
getActorUrl,
activityPubContextify,
activityPubCollectionPagination,

View File

@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers'
function doRequest (
function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
): Bluebird<{ response: request.RequestResponse, body: any }> {
if (requestOptions.activityPub === true) {
@ -11,7 +11,7 @@ function doRequest (
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
}
return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => {
return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
})
}

View File

@ -16,7 +16,7 @@ let config: IConfig = require('config')
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 285
const LAST_MIGRATION_VERSION = 290
// ---------------------------------------------------------------------------
@ -336,6 +336,9 @@ const CONSTRAINTS_FIELDS = {
VIDEOS_REDUNDANCY: {
URL: { min: 3, max: 2000 } // Length
},
VIDEO_RATES: {
URL: { min: 3, max: 2000 } // Length
},
VIDEOS: {
NAME: { min: 3, max: 120 }, // Length
LANGUAGE: { min: 1, max: 10 }, // Length

View File

@ -0,0 +1,46 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize,
db: any
}): Promise<void> {
{
const data = {
type: Sequelize.STRING(2000),
allowNull: true
}
await utils.queryInterface.addColumn('accountVideoRate', 'url', data)
}
{
const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` +
'FROM "accountVideoRate" ' +
'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
'INNER JOIN actor ON actor.id = account."actorId" ' +
'WHERE "base".id = "accountVideoRate".id'
const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL'
await utils.sequelize.query(query)
}
{
const data = {
type: Sequelize.STRING(2000),
allowNull: false,
defaultValue: null
}
await utils.queryInterface.changeColumn('accountVideoRate', 'url', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -5,7 +5,7 @@ import * as url from 'url'
import * as uuidv4 from 'uuid/v4'
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
import { getActorUrl } from '../../helpers/activitypub'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
}
try {
// Assert we don't recurse another time
// Don't recurse another time
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
} catch (err) {
logger.error('Cannot get or create account attributed to video channel ' + actor.url)
@ -297,12 +301,15 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
normalizeActor(requestResult.body)
const actorJSON: ActivityPubActor = requestResult.body
if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
return { result: undefined, statusCode: requestResult.response.statusCode }
}
if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
}
const followersCount = await fetchActorTotalItems(actorJSON.followers)
const followingCount = await fetchActorTotalItems(actorJSON.following)

View File

@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
import { doRequest } from '../../helpers/requests'
import { logger } from '../../helpers/logger'
import * as Bluebird from 'bluebird'
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
logger.info('Crawling ActivityPub data on %s.', uri)
@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
timeout: JOB_REQUEST_TIMEOUT
}
const response = await doRequest(options)
const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
const firstBody = response.body
let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
while (nextLink && i < limit) {
options.uri = nextLink
const { body } = await doRequest(options)
const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
nextLink = body.next
i++

View File

@ -1,9 +1 @@
export * from './process'
export * from './process-accept'
export * from './process-announce'
export * from './process-create'
export * from './process-delete'
export * from './process-follow'
export * from './process-like'
export * from './process-undo'
export * from './process-update'

View File

@ -12,6 +12,8 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { forwardVideoRelatedActivity } from '../send/utils'
import { Redis } from '../../redis'
import { createOrUpdateCacheFile } from '../cache-file'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
const activityObject = activity.object
@ -65,9 +67,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
videoId: video.id,
accountId: byAccount.id
}
const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate,
defaults: rate,
defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
transaction: t
})
if (created === true) await video.increment('dislikes', { transaction: t })

View File

@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
import { ActorModel } from '../../../models/activitypub/actor'
import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
return retryTransactionWrapper(processLikeVideo, byActor, activity)
@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
}
const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate,
defaults: rate,
defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
transaction: t
})
if (created === true) await video.increment('likes', { transaction: t })

View File

@ -55,7 +55,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })
@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })

View File

@ -1,5 +1,5 @@
import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { getActorUrl } from '../../../helpers/activitypub'
import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/activitypub/actor'
import { processAcceptActivity } from './process-accept'
@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
Like: processLikeActivity
}
async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) {
async function processActivities (
activities: Activity[],
options: {
signatureActor?: ActorModel
inboxActor?: ActorModel
outboxUrl?: string
} = {}) {
const actorsCache: { [ url: string ]: ActorModel } = {}
for (const activity of activities) {
if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
continue
}
@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
const actorUrl = getActorUrl(activity.actor)
// When we fetch remote data, we don't have signature
if (signatureActor && actorUrl !== signatureActor.url) {
logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url)
if (options.signatureActor && actorUrl !== options.signatureActor.url) {
logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
continue
}
const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) {
logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl)
continue
}
const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
actorsCache[actorUrl] = byActor
const activityProcessor = processActivity[activity.type]
@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
}
try {
await activityProcessor(activity, byActor, inboxActor)
await activityProcessor(activity, byActor, options.inboxActor)
} catch (err) {
logger.warn('Cannot process activity %s.', activity.type, { err })
}

View File

@ -95,7 +95,7 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
logger.info('Creating job to send view of %s.', video.url)
const url = getVideoViewActivityPubUrl(byActor, video)
const viewActivity = buildViewActivity(byActor, video)
const viewActivity = buildViewActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({
// Use the server actor to send the view
@ -111,7 +111,7 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
logger.info('Creating job to dislike %s.', video.url)
const url = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video)
const dislikeActivity = buildDislikeActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({
byActor,
@ -136,16 +136,18 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
)
}
function buildDislikeActivity (byActor: ActorModel, video: VideoModel) {
function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
return {
id: url,
type: 'Dislike',
actor: byActor.url,
object: video.url
}
}
function buildViewActivity (byActor: ActorModel, video: VideoModel) {
function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
return {
id: url,
type: 'View',
actor: byActor.url,
object: video.url

View File

@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
return audiencify(
{
type: 'Like' as 'Like',
id: url,
type: 'Like' as 'Like',
actor: byActor.url,
object: video.url
},

View File

@ -64,7 +64,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
logger.info('Creating job to undo a dislike of video %s.', video.url)
const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video)
const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t })

View File

@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils'
import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share'
import { sendUndoAnnounce, sendVideoAnnounce } from './send'
import { getAnnounceActivityPubUrl } from './url'
import { getVideoAnnounceActivityPubUrl } from './url'
import { VideoChannelModel } from '../../models/video/video-channel'
import * as Bluebird from 'bluebird'
import { doRequest } from '../../helpers/requests'
import { getOrCreateActorAndServerAndModel } from './actor'
import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
json: true,
activityPub: true
})
if (!body || !body.actor) throw new Error('Body of body actor is invalid')
if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
}
const actorUrl = body.actor
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = {
@ -72,7 +77,7 @@ export {
async function shareByServer (video: VideoModel, t: Transaction) {
const serverActor = await getServerActor()
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor)
const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
return VideoShareModel.findOrCreate({
defaults: {
actorId: serverActor.id,
@ -91,7 +96,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
}
async function shareByVideoChannel (video: VideoModel, t: Transaction) {
const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor)
const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
return VideoShareModel.findOrCreate({
defaults: {
actorId: video.VideoChannel.actorId,

View File

@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
}
function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString()
return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
}
function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/likes/' + video.id
}
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/dislikes/' + video.id
}
@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
return follower.url + '/accepts/follows/' + me.id
}
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) {
return originalUrl + '/announces/' + byActor.id
function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/announces/' + byActor.id
}
function getDeleteActivityPubUrl (originalUrl: string) {
@ -97,7 +97,7 @@ export {
getVideoAbuseActivityPubUrl,
getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl,
getAnnounceActivityPubUrl,
getVideoAnnounceActivityPubUrl,
getUpdateActivityPubUrl,
getUndoActivityPubUrl,
getVideoViewActivityPubUrl,

View File

@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
import { getOrCreateActorAndServerAndModel } from './actor'
import { getOrCreateVideoAndAccountAndChannel } from './videos'
import * as Bluebird from 'bluebird'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
let originCommentId: number = null
@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
const actorUrl = body.attributedTo
if (!actorUrl) return { created: false }
if (checkUrlsSameHost(commentUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`)
}
if (checkUrlsSameHost(body.id, commentUrl) !== true) {
throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
if (!entry) return { created: false }
@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
const actorUrl = body.attributedTo
if (!actorUrl) throw new Error('Miss attributed to in comment')
if (checkUrlsSameHost(url, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
}
if (checkUrlsSameHost(body.id, url) !== true) {
throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const comment = new VideoCommentModel({
url: body.id,

View File

@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { doRequest } from '../../helpers/requests'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { ActorModel } from '../../models/activitypub/actor'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0
await Bluebird.map(actorUrls, async actorUrl => {
await Bluebird.map(ratesUrl, async rateUrl => {
try {
// Fetch url
const { body } = await doRequest({
uri: rateUrl,
json: true,
activityPub: true
})
if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
}
if (checkUrlsSameHost(body.id, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const [ , created ] = await AccountVideoRateModel
.findOrCreate({
where: {
@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
defaults: {
videoId: video.id,
accountId: actor.Account.id,
type: rate
type: rate,
url: body.id
}
})
if (created) rateCounts += 1
} catch (err) {
logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err })
logger.warn('Cannot add rate %s.', rateUrl, { err })
}
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
if (dislikes > 0) await sendCreateDislike(actor, video, t)
}
function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
}
export {
getRateUrl,
createRates,
sendVideoRateChange
}

View File

@ -29,6 +29,7 @@ import { createRates } from './video-rates'
import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { AccountModel } from '../../models/account/account'
import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
// If the video is not private and published, we federate it
@ -63,7 +64,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
const { response, body } = await doRequest(options)
if (sanitizeAndCheckVideoTorrentObject(body) === false) {
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body })
return { response, videoObject: undefined }
}
@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
const channel = videoObject.attributedTo.find(a => a.type === 'Group')
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
}
return getOrCreateActorAndServerAndModel(channel.id, 'all')
}

View File

@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
'activity': items => processActivities(items),
'activity': items => processActivities(items, { outboxUrl: payload.uri }),
'video-likes': items => createRates(items, video, 'like'),
'video-dislikes': items => createRates(items, video, 'dislike'),
'video-shares': items => addVideoShares(items, video),

View File

@ -5,4 +5,6 @@ export * from './video-channels'
export * from './video-comments'
export * from './video-imports'
export * from './video-watch'
export * from './video-rates'
export * from './video-shares'
export * from './videos'

View File

@ -0,0 +1,55 @@
import * as express from 'express'
import 'express-validator'
import { body, param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { areValidationErrors } from '../utils'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoRateType } from '../../../../shared/models/videos'
import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
const videoUpdateRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const getAccountVideoRateValidator = function (rateType: VideoRateType) {
return [
param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
if (areValidationErrors(req, res)) return
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
if (!rate) {
return res.status(404)
.json({ error: 'Video rate not found' })
.end()
}
res.locals.accountVideoRate = rate
return next()
}
]
}
// ---------------------------------------------------------------------------
export {
videoUpdateRateValidator,
getAccountVideoRateValidator
}

View File

@ -0,0 +1,38 @@
import * as express from 'express'
import 'express-validator'
import { param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { VideoShareModel } from '../../../models/video/video-share'
import { areValidationErrors } from '../utils'
import { VideoModel } from '../../../models/video/video'
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video: VideoModel = res.locals.video
const share = await VideoShareModel.load(req.params.actorId, video.id)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
// ---------------------------------------------------------------------------
export {
videosShareValidator
}

View File

@ -26,14 +26,12 @@ import {
isVideoLicenceValid,
isVideoNameValid,
isVideoPrivacyValid,
isVideoRatingTypeValid,
isVideoSupportValid,
isVideoTagsValid
} from '../../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { VideoShareModel } from '../../../models/video/video-share'
import { authenticate } from '../../oauth'
import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
@ -188,41 +186,6 @@ const videosRemoveValidator = [
}
]
const videoRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
const videosChangeOwnershipValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@ -415,9 +378,6 @@ export {
videosGetValidator,
videosCustomGetValidator,
videosRemoveValidator,
videosShareValidator,
videoRateValidator,
videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator,

View File

@ -1,12 +1,14 @@
import { values } from 'lodash'
import { Transaction } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
import { VideoRateType } from '../../../shared/models/videos'
import { VIDEO_RATE_TYPES } from '../../initializers'
import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
import { VideoModel } from '../video/video'
import { AccountModel } from './account'
import { ActorModel } from '../activitypub/actor'
import { throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
/*
Account rates per video.
@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
},
{
fields: [ 'videoId', 'type' ]
},
{
fields: [ 'url' ],
unique: true
}
]
})
@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
@Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
type: VideoRateType
@AllowNull(false)
@Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
url: string
@CreatedAt
createdAt: Date
@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
})
Account: AccountModel
static load (accountId: number, videoId: number, transaction: Transaction) {
static load (accountId: number, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
accountId,
@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
return AccountVideoRateModel.findOne(options)
}
static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
videoId,
type: rateType
},
include: [
{
model: AccountModel.unscoped(),
required: true,
include: [
{
attributes: [ 'id', 'url', 'preferredUsername' ],
model: ActorModel.unscoped(),
required: true,
where: {
preferredUsername: accountName
}
}
]
},
{
model: VideoModel.unscoped(),
required: true
}
]
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static loadByUrl (url: string, transaction: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
url
}
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
const query = {
offset: start,

View File

@ -47,7 +47,7 @@ enum ScopeNames {
required: true,
include: [
{
attributes: [ 'id' ],
attributes: [ 'id', 'url' ],
model: () => ActorModel.unscoped(),
required: true
}

View File

@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
})
Video: VideoModel
static load (actorId: number, videoId: number, t: Sequelize.Transaction) {
static load (actorId: number, videoId: number, t?: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: {
actorId,

View File

@ -2,7 +2,7 @@
import 'mocha'
import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
import { HTTP_SIGNATURE } from '../../../initializers'
import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import * as chai from 'chai'
@ -63,7 +63,7 @@ describe('Test ActivityPub security', function () {
Digest: buildDigest({ hello: 'coucou' })
}
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
@ -73,7 +73,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(body)
headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
@ -85,7 +85,7 @@ describe('Test ActivityPub security', function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body)
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
@ -97,7 +97,7 @@ describe('Test ActivityPub security', function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body)
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204)
})
@ -126,7 +126,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
@ -147,7 +147,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403)
})
@ -163,7 +163,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204)
})

View File

@ -3,7 +3,7 @@ import { HTTP_SIGNATURE } from '../../../initializers'
import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import { activityPubContextify } from '../../../helpers/activitypub'
function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) {
function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
const options = {
method: 'POST',
uri: url,
@ -34,10 +34,10 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
}
const headers = buildGlobalHeaders(body)
return makeAPRequest(to.url, body, httpSignature, headers)
return makePOSTAPRequest(to.url, body, httpSignature, headers)
}
export {
makeAPRequest,
makePOSTAPRequest,
makeFollowRequest
}

View File

@ -1,5 +1,6 @@
export interface DislikeObject {
type: 'Dislike',
id: string
type: 'Dislike'
actor: string
object: string
}

View File

@ -1 +1 @@
export type VideoRateType = 'like' | 'dislike' | 'none'
export type VideoRateType = 'like' | 'dislike'