Federate likes/dislikes

pull/128/head
Chocobozzz 2017-11-23 14:19:55 +01:00
parent d52eb8f656
commit 0032ebe94a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
27 changed files with 548 additions and 62 deletions

View File

@ -3,6 +3,7 @@ import { UserVideoRateUpdate } from '../../../../shared'
import { logger, retryTransactionWrapper } from '../../../helpers'
import { VIDEO_RATE_TYPES } from '../../../initializers'
import { database as db } from '../../../initializers/database'
import { sendVideoRateChangeToFollowers, sendVideoRateChangeToOrigin } from '../../../lib/activitypub/videos'
import { asyncMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
import { AccountInstance } from '../../../models/account/account-interface'
import { VideoInstance } from '../../../models/video/video-interface'
@ -82,10 +83,10 @@ async function rateVideo (req: express.Request, res: express.Response) {
// It is useful for the user to have a feedback
await videoInstance.increment(incrementQuery, sequelizeOptions)
if (videoInstance.isOwned() === false) {
// TODO: Send a event to original server
if (videoInstance.isOwned()) {
await sendVideoRateChangeToFollowers(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
} else {
// TODO: Send update to followers
await sendVideoRateChangeToOrigin(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
}
})

View File

@ -1,9 +1,9 @@
import * as validator from 'validator'
import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
import { isAnnounceValid } from './announce'
import { isAnnounceActivityValid } from './announce'
import { isActivityPubUrlValid } from './misc'
import { isUndoValid } from './undo'
import { isUndoActivityValid } from './undo'
import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
import {
isVideoFlagValid,
@ -12,6 +12,7 @@ import {
isVideoTorrentUpdateActivityValid
} from './videos'
import { isViewActivityValid } from './view'
import { isDislikeActivityValid, isLikeActivityValid } from './rate'
function isRootActivityValid (activity: any) {
return Array.isArray(activity['@context']) &&
@ -34,7 +35,8 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean
Follow: checkFollowActivity,
Accept: checkAcceptActivity,
Announce: checkAnnounceActivity,
Undo: checkUndoActivity
Undo: checkUndoActivity,
Like: checkLikeActivity
}
function isActivityValid (activity: any) {
@ -55,9 +57,10 @@ export {
// ---------------------------------------------------------------------------
function checkCreateActivity (activity: any) {
return isVideoChannelCreateActivityValid(activity) ||
isVideoFlagValid(activity) ||
isViewActivityValid(activity)
return isViewActivityValid(activity) ||
isDislikeActivityValid(activity) ||
isVideoChannelCreateActivityValid(activity) ||
isVideoFlagValid(activity)
}
function checkAddActivity (activity: any) {
@ -84,9 +87,13 @@ function checkAcceptActivity (activity: any) {
}
function checkAnnounceActivity (activity: any) {
return isAnnounceValid(activity)
return isAnnounceActivityValid(activity)
}
function checkUndoActivity (activity: any) {
return isUndoValid(activity)
return isUndoActivityValid(activity)
}
function checkLikeActivity (activity: any) {
return isLikeActivityValid(activity)
}

View File

@ -2,7 +2,7 @@ import { isBaseActivityValid } from './misc'
import { isVideoTorrentAddActivityValid } from './videos'
import { isVideoChannelCreateActivityValid } from './video-channels'
function isAnnounceValid (activity: any) {
function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
(
isVideoChannelCreateActivityValid(activity.object) ||
@ -11,5 +11,5 @@ function isAnnounceValid (activity: any) {
}
export {
isAnnounceValid
isAnnounceActivityValid
}

View File

@ -0,0 +1,20 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isLikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Like') &&
isActivityPubUrlValid(activity.object)
}
function isDislikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
activity.object.type === 'Dislike' &&
isActivityPubUrlValid(activity.object.actor) &&
isActivityPubUrlValid(activity.object.object)
}
// ---------------------------------------------------------------------------
export {
isLikeActivityValid,
isDislikeActivityValid
}

View File

@ -1,13 +1,16 @@
import { isAccountFollowActivityValid } from './account'
import { isBaseActivityValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate'
function isUndoValid (activity: any) {
function isUndoActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Undo') &&
(
isAccountFollowActivityValid(activity.object)
isAccountFollowActivityValid(activity.object) ||
isLikeActivityValid(activity.object) ||
isDislikeActivityValid(activity.object)
)
}
export {
isUndoValid
isUndoActivityValid
}

View File

@ -229,6 +229,7 @@ const ACTIVITY_PUB = {
PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 100,
MAX_HTTP_ATTEMPT: 5,
URL_MIME_TYPES: {
VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
TORRENT: [ 'application/x-bittorrent' ],

View File

@ -5,5 +5,6 @@ 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

@ -5,9 +5,10 @@ import { logger, retryTransactionWrapper } from '../../../helpers'
import { database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface'
import { getOrCreateAccountAndServer } from '../account'
import { sendCreateViewToVideoFollowers } from '../send/send-create'
import { sendCreateDislikeToVideoFollowers, sendCreateViewToVideoFollowers } from '../send/send-create'
import { getVideoChannelActivityPubUrl } from '../url'
import { videoChannelActivityObjectToDBAttributes } from './misc'
import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object
@ -16,6 +17,8 @@ async function processCreateActivity (activity: ActivityCreate) {
if (activityType === 'View') {
return processCreateView(activityObject as ViewObject)
} else if (activityType === 'Dislike') {
return processCreateDislike(account, activityObject as DislikeObject)
} else if (activityType === 'VideoChannel') {
return processCreateVideoChannel(account, activityObject as VideoChannelObject)
} else if (activityType === 'Flag') {
@ -34,6 +37,36 @@ export {
// ---------------------------------------------------------------------------
async function processCreateDislike (byAccount: AccountInstance, dislike: DislikeObject) {
const options = {
arguments: [ byAccount, dislike ],
errorMessage: 'Cannot dislike the video with many retries.'
}
return retryTransactionWrapper(createVideoDislike, options)
}
function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject) {
return db.sequelize.transaction(async t => {
const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object)
if (!video) throw new Error('Unknown video ' + dislike.object)
const rate = {
type: 'dislike' as 'dislike',
videoId: video.id,
accountId: byAccount.id
}
const [ , created ] = await db.AccountVideoRate.findOrCreate({
where: rate,
defaults: rate
})
await video.increment('dislikes')
if (video.isOwned() && created === true) await sendCreateDislikeToVideoFollowers(byAccount, video, undefined)
})
}
async function processCreateView (view: ViewObject) {
const video = await db.Video.loadByUrlAndPopulateAccount(view.object)

View File

@ -0,0 +1,50 @@
import { ActivityLike } from '../../../../shared/models/activitypub/activity'
import { database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface'
import { getOrCreateAccountAndServer } from '../account'
import { sendLikeToVideoFollowers } from '../send/send-like'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
async function processLikeActivity (activity: ActivityLike) {
const account = await getOrCreateAccountAndServer(activity.actor)
return processLikeVideo(account, activity.object)
}
// ---------------------------------------------------------------------------
export {
processLikeActivity
}
// ---------------------------------------------------------------------------
async function processLikeVideo (byAccount: AccountInstance, videoUrl: string) {
const options = {
arguments: [ byAccount, videoUrl ],
errorMessage: 'Cannot like the video with many retries.'
}
return retryTransactionWrapper(createVideoLike, options)
}
function createVideoLike (byAccount: AccountInstance, videoUrl: string) {
return db.sequelize.transaction(async t => {
const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl)
if (!video) throw new Error('Unknown video ' + videoUrl)
const rate = {
type: 'like' as 'like',
videoId: video.id,
accountId: byAccount.id
}
const [ , created ] = await db.AccountVideoRate.findOrCreate({
where: rate,
defaults: rate
})
await video.increment('likes')
if (video.isOwned() && created === true) await sendLikeToVideoFollowers(byAccount, video, undefined)
})
}

View File

@ -1,20 +1,20 @@
import { ActivityUndo } from '../../../../shared/models/activitypub/activity'
import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
import { logger } from '../../../helpers/logger'
import { database as db } from '../../../initializers'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
import { sendUndoLikeToVideoFollowers } from '../send/send-undo'
import { sendUndoDislikeToVideoFollowers } from '../index'
async function processUndoActivity (activity: ActivityUndo) {
const activityToUndo = activity.object
if (activityToUndo.type === 'Follow') {
const follower = await db.Account.loadByUrl(activity.actor)
const following = await db.Account.loadByUrl(activityToUndo.object)
const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id)
if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`)
await accountFollow.destroy()
return undefined
if (activityToUndo.type === 'Like') {
return processUndoLike(activity.actor, activityToUndo)
} else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') {
return processUndoDislike(activity.actor, activityToUndo.object)
} else if (activityToUndo.type === 'Follow') {
return processUndoFollow(activity.actor, activityToUndo)
}
logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
@ -29,3 +29,80 @@ export {
}
// ---------------------------------------------------------------------------
function processUndoLike (actor: string, likeActivity: ActivityLike) {
const options = {
arguments: [ actor, likeActivity ],
errorMessage: 'Cannot undo like with many retries.'
}
return retryTransactionWrapper(undoLike, options)
}
function undoLike (actor: string, likeActivity: ActivityLike) {
return db.sequelize.transaction(async t => {
const byAccount = await db.Account.loadByUrl(actor, t)
if (!byAccount) throw new Error('Unknown account ' + actor)
const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object)
if (!video) throw new Error('Unknown video ' + likeActivity.actor)
const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })
await video.decrement('likes')
if (video.isOwned()) await sendUndoLikeToVideoFollowers(byAccount, video, t)
})
}
function processUndoDislike (actor: string, dislikeCreateActivity: DislikeObject) {
const options = {
arguments: [ actor, dislikeCreateActivity ],
errorMessage: 'Cannot undo dislike with many retries.'
}
return retryTransactionWrapper(undoDislike, options)
}
function undoDislike (actor: string, dislike: DislikeObject) {
return db.sequelize.transaction(async t => {
const byAccount = await db.Account.loadByUrl(actor, t)
if (!byAccount) throw new Error('Unknown account ' + actor)
const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object)
if (!video) throw new Error('Unknown video ' + dislike.actor)
const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
await rate.destroy({ transaction: t })
await video.decrement('dislikes')
if (video.isOwned()) await sendUndoDislikeToVideoFollowers(byAccount, video, t)
})
}
function processUndoFollow (actor: string, followActivity: ActivityFollow) {
const options = {
arguments: [ actor, followActivity ],
errorMessage: 'Cannot undo follow with many retries.'
}
return retryTransactionWrapper(undoFollow, options)
}
function undoFollow (actor: string, followActivity: ActivityFollow) {
return db.sequelize.transaction(async t => {
const follower = await db.Account.loadByUrl(actor, t)
const following = await db.Account.loadByUrl(followActivity.object, t)
const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id, t)
if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`)
await accountFollow.destroy({ transaction: t })
return undefined
})
}

View File

@ -1,4 +1,5 @@
import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
import { logger } from '../../../helpers/logger'
import { AccountInstance } from '../../../models/account/account-interface'
import { processAcceptActivity } from './process-accept'
import { processAddActivity } from './process-add'
@ -6,9 +7,9 @@ import { processAnnounceActivity } from './process-announce'
import { processCreateActivity } from './process-create'
import { processDeleteActivity } from './process-delete'
import { processFollowActivity } from './process-follow'
import { processLikeActivity } from './process-like'
import { processUndoActivity } from './process-undo'
import { processUpdateActivity } from './process-update'
import { logger } from '../../../helpers/logger'
const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
Create: processCreateActivity,
@ -18,7 +19,8 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
Follow: processFollowActivity,
Accept: processAcceptActivity,
Announce: processAnnounceActivity,
Undo: processUndoActivity
Undo: processUndoActivity,
Like: processLikeActivity
}
async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {

View File

@ -4,4 +4,6 @@ export * from './send-announce'
export * from './send-create'
export * from './send-delete'
export * from './send-follow'
export * from './send-like'
export * from './send-undo'
export * from './send-update'

View File

@ -3,6 +3,7 @@ import { logger } from '../../../helpers/logger'
import { ACTIVITY_PUB, database as db } from '../../../initializers'
import { AccountInstance } from '../../../models/account/account-interface'
import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler'
import { VideoInstance } from '../../../models/video/video-interface'
async function broadcastToFollowers (
data: any,
@ -41,6 +42,27 @@ async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: s
return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
}
function getOriginVideoAudience (video: VideoInstance) {
return {
to: [ video.VideoChannel.Account.url ],
cc: [ video.VideoChannel.Account.url + '/followers' ]
}
}
function getVideoFollowersAudience (video: VideoInstance) {
return {
to: [ video.VideoChannel.Account.url + '/followers' ],
cc: []
}
}
async function getAccountsToForwardVideoAction (byAccount: AccountInstance, video: VideoInstance) {
const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
accountsToForwardView.push(video.VideoChannel.Account)
return accountsToForwardView
}
async function getAudience (accountSender: AccountInstance, isPublic = true) {
const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
@ -64,5 +86,8 @@ async function getAudience (accountSender: AccountInstance, isPublic = true) {
export {
broadcastToFollowers,
unicastTo,
getAudience
getAudience,
getOriginVideoAudience,
getAccountsToForwardVideoAction,
getVideoFollowersAudience
}

View File

@ -1,11 +1,17 @@
import { Transaction } from 'sequelize'
import { ActivityCreate } from '../../../../shared/models/activitypub/activity'
import { getServerAccount } from '../../../helpers/utils'
import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface'
import { broadcastToFollowers, getAudience, unicastTo } from './misc'
import { getVideoAbuseActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import { getServerAccount } from '../../../helpers/utils'
import { database as db } from '../../../initializers'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import {
broadcastToFollowers,
getAccountsToForwardVideoAction,
getAudience,
getOriginVideoAudience,
getVideoFollowersAudience,
unicastTo
} from './misc'
async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
const byAccount = videoChannel.Account
@ -29,7 +35,7 @@ async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoI
const url = getVideoViewActivityPubUrl(byAccount, video)
const viewActivity = createViewActivityData(byAccount, video)
const audience = { to: [ video.VideoChannel.Account.url ], cc: [ video.VideoChannel.Account.url + '/followers' ] }
const audience = getOriginVideoAudience(video)
const data = await createActivityData(url, byAccount, viewActivity, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@ -39,16 +45,35 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video
const url = getVideoViewActivityPubUrl(byAccount, video)
const viewActivity = createViewActivityData(byAccount, video)
const audience = { to: [ video.VideoChannel.Account.url + '/followers' ], cc: [] }
const audience = getVideoFollowersAudience(video)
const data = await createActivityData(url, byAccount, viewActivity, audience)
const serverAccount = await getServerAccount()
const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
accountsToForwardView.push(video.VideoChannel.Account)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
// Don't forward view to server that sent it to us
const index = accountsToForwardView.findIndex(a => a.id === byAccount.id)
if (index) accountsToForwardView.splice(index, 1)
const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
}
async function sendCreateDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoDislikeActivityPubUrl(byAccount, video)
const dislikeActivity = createDislikeActivityData(byAccount, video)
const audience = getOriginVideoAudience(video)
const data = await createActivityData(url, byAccount, dislikeActivity, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
}
async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoDislikeActivityPubUrl(byAccount, video)
const dislikeActivity = createDislikeActivityData(byAccount, video)
const audience = getVideoFollowersAudience(video)
const data = await createActivityData(url, byAccount, dislikeActivity, audience)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
@ -71,6 +96,16 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje
return activity
}
function createDislikeActivityData (byAccount: AccountInstance, video: VideoInstance) {
const obj = {
type: 'Dislike',
actor: byAccount.url,
object: video.url
}
return obj
}
// ---------------------------------------------------------------------------
export {
@ -78,7 +113,10 @@ export {
sendVideoAbuse,
createActivityData,
sendCreateViewToOrigin,
sendCreateViewToVideoFollowers
sendCreateViewToVideoFollowers,
sendCreateDislikeToOrigin,
sendCreateDislikeToVideoFollowers,
createDislikeActivityData
}
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,60 @@
import { Transaction } from 'sequelize'
import { ActivityLike } from '../../../../shared/models/activitypub/activity'
import { getServerAccount } from '../../../helpers/utils'
import { AccountInstance, VideoInstance } from '../../../models'
import { getVideoLikeActivityPubUrl } from '../url'
import {
broadcastToFollowers,
getAccountsToForwardVideoAction,
getAudience,
getOriginVideoAudience,
getVideoFollowersAudience,
unicastTo
} from './misc'
async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video)
const audience = getOriginVideoAudience(video)
const data = await likeActivityData(url, byAccount, video, audience)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
}
async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const url = getVideoLikeActivityPubUrl(byAccount, video)
const audience = getVideoFollowersAudience(video)
const data = await likeActivityData(url, byAccount, video, audience)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
}
async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) {
if (!audience) {
audience = await getAudience(byAccount)
}
const activity: ActivityLike = {
type: 'Like',
id: url,
actor: byAccount.url,
to: audience.to,
cc: audience.cc,
object: video.url
}
return activity
}
// ---------------------------------------------------------------------------
export {
sendLikeToOrigin,
sendLikeToVideoFollowers,
likeActivityData
}

View File

@ -1,10 +1,14 @@
import { Transaction } from 'sequelize'
import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity'
import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
import { AccountInstance } from '../../../models'
import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
import { unicastTo } from './misc'
import { broadcastToFollowers, getAccountsToForwardVideoAction, unicastTo } from './misc'
import { followActivityData } from './send-follow'
import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../url'
import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
import { VideoInstance } from '../../../models/video/video-interface'
import { likeActivityData } from './send-like'
import { createActivityData, createDislikeActivityData } from './send-create'
import { getServerAccount } from '../../../helpers/utils'
async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) {
const me = accountFollow.AccountFollower
@ -19,15 +23,72 @@ async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transact
return unicastTo(data, me, following.inboxUrl, t)
}
async function sendUndoLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
const undoUrl = getUndoActivityPubUrl(likeUrl)
const object = await likeActivityData(likeUrl, byAccount, video)
const data = await undoActivityData(undoUrl, byAccount, object)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
}
async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
const undoUrl = getUndoActivityPubUrl(likeUrl)
const object = await likeActivityData(likeUrl, byAccount, video)
const data = await undoActivityData(undoUrl, byAccount, object)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
}
async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
const undoUrl = getUndoActivityPubUrl(dislikeUrl)
const dislikeActivity = createDislikeActivityData(byAccount, video)
const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
const data = await undoActivityData(undoUrl, byAccount, object)
return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
}
async function sendUndoDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
const undoUrl = getUndoActivityPubUrl(dislikeUrl)
const dislikeActivity = createDislikeActivityData(byAccount, video)
const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
const data = await undoActivityData(undoUrl, byAccount, object)
const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
const serverAccount = await getServerAccount()
const followersException = [ byAccount ]
return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
}
// ---------------------------------------------------------------------------
export {
sendUndoFollow
sendUndoFollow,
sendUndoLikeToOrigin,
sendUndoLikeToVideoFollowers,
sendUndoDislikeToOrigin,
sendUndoDislikeToVideoFollowers
}
// ---------------------------------------------------------------------------
async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) {
async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow | ActivityLike | ActivityCreate) {
const activity: ActivityUndo = {
type: 'Undo',
id: url,

View File

@ -25,6 +25,14 @@ function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoIns
return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString()
}
function getVideoLikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
return byAccount.url + '#likes/' + video.id
}
function getVideoDislikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
return byAccount.url + '#dislikes/' + video.id
}
function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
const me = accountFollow.AccountFollower
const following = accountFollow.AccountFollowing
@ -61,5 +69,7 @@ export {
getAnnounceActivityPubUrl,
getUpdateActivityPubUrl,
getUndoActivityPubUrl,
getVideoViewActivityPubUrl
getVideoViewActivityPubUrl,
getVideoLikeActivityPubUrl,
getVideoDislikeActivityPubUrl
}

View File

@ -1,9 +1,20 @@
import { join } from 'path'
import * as request from 'request'
import { Transaction } from 'sequelize'
import { ActivityIconObject } from '../../../shared/index'
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
import { CONFIG, REMOTE_SCHEME, STATIC_PATHS } from '../../initializers/constants'
import { AccountInstance } from '../../models/account/account-interface'
import { VideoInstance } from '../../models/video/video-interface'
import { sendLikeToOrigin } from './index'
import { sendCreateDislikeToOrigin, sendCreateDislikeToVideoFollowers } from './send/send-create'
import { sendLikeToVideoFollowers } from './send/send-like'
import {
sendUndoDislikeToOrigin,
sendUndoDislikeToVideoFollowers,
sendUndoLikeToOrigin,
sendUndoLikeToVideoFollowers
} from './send/send-undo'
function fetchRemoteVideoPreview (video: VideoInstance) {
// FIXME: use url
@ -37,8 +48,42 @@ function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObjec
return doRequestAndSaveToFile(options, thumbnailPath)
}
function sendVideoRateChangeToFollowers (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
const tasks: Promise<any>[] = []
// Undo Like
if (likes < 0) tasks.push(sendUndoLikeToVideoFollowers(account, video, t))
// Like
if (likes > 0) tasks.push(sendLikeToVideoFollowers(account, video, t))
// Undo Dislike
if (dislikes < 0) tasks.push(sendUndoDislikeToVideoFollowers(account, video, t))
// Dislike
if (dislikes > 0) tasks.push(sendCreateDislikeToVideoFollowers(account, video, t))
return Promise.all(tasks)
}
function sendVideoRateChangeToOrigin (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
const tasks: Promise<any>[] = []
// Undo Like
if (likes < 0) tasks.push(sendUndoLikeToOrigin(account, video, t))
// Like
if (likes > 0) tasks.push(sendLikeToOrigin(account, video, t))
// Undo Dislike
if (dislikes < 0) tasks.push(sendUndoDislikeToOrigin(account, video, t))
// Dislike
if (dislikes > 0) tasks.push(sendCreateDislikeToOrigin(account, video, t))
return Promise.all(tasks)
}
export {
fetchRemoteVideoPreview,
fetchRemoteVideoDescription,
generateThumbnailFromUrl
generateThumbnailFromUrl,
sendVideoRateChangeToFollowers,
sendVideoRateChangeToOrigin
}

View File

@ -2,7 +2,7 @@ import { logger } from '../../../helpers'
import { buildSignedActivity } from '../../../helpers/activitypub'
import { doRequest } from '../../../helpers/requests'
import { database as db } from '../../../initializers'
import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler'
import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
async function process (payload: ActivityPubHttpPayload, jobId: number) {
logger.info('Processing ActivityPub broadcast in job %d.', jobId)
@ -20,7 +20,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
for (const uri of payload.uris) {
options.uri = uri
await doRequest(options)
try {
await doRequest(options)
} catch (err) {
await maybeRetryRequestLater(err, payload, uri)
}
}
}

View File

@ -4,12 +4,16 @@ import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-h
import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler'
import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler'
import { JobCategory } from '../../../../shared'
import { ACTIVITY_PUB } from '../../../initializers/constants'
import { logger } from '../../../helpers/logger'
type ActivityPubHttpPayload = {
uris: string[]
signatureAccountId?: number
body?: any
attemptNumber?: number
}
const jobHandlers: { [ handlerName: string ]: JobHandler<ActivityPubHttpPayload, void> } = {
activitypubHttpBroadcastHandler,
activitypubHttpUnicastHandler,
@ -19,7 +23,25 @@ const jobCategory: JobCategory = 'activitypub-http'
const activitypubHttpJobScheduler = new JobScheduler(jobCategory, jobHandlers)
function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, uri: string) {
logger.warn('Cannot make request to %s.', uri, err)
let attemptNumber = payload.attemptNumber || 1
attemptNumber += 1
if (attemptNumber < ACTIVITY_PUB.MAX_HTTP_ATTEMPT) {
logger.debug('Retrying request to %s (attempt %d/%d).', uri, attemptNumber, ACTIVITY_PUB.MAX_HTTP_ATTEMPT, err)
const newPayload = Object.assign(payload, {
uris: [ uri ],
attemptNumber
})
return activitypubHttpJobScheduler.createJob(undefined, 'activitypubHttpUnicastHandler', newPayload)
}
}
export {
ActivityPubHttpPayload,
activitypubHttpJobScheduler
activitypubHttpJobScheduler,
maybeRetryRequestLater
}

View File

@ -1,6 +1,6 @@
import { logger } from '../../../helpers'
import { doRequest } from '../../../helpers/requests'
import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler'
import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
import { database as db } from '../../../initializers/database'
import { buildSignedActivity } from '../../../helpers/activitypub'
@ -18,7 +18,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
json: signedBody
}
await doRequest(options)
try {
await doRequest(options)
} catch (err) {
await maybeRetryRequestLater(err, payload, uri)
throw err
}
}
function onError (err: Error, jobId: number) {

View File

@ -5,7 +5,11 @@ import { ResultList } from '../../../shared/models/result-list.model'
import { AccountInstance } from './account-interface'
export namespace AccountFollowMethods {
export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance>
export type LoadByAccountAndTarget = (
accountId: number,
targetAccountId: number,
t?: Sequelize.Transaction
) => Bluebird<AccountFollowInstance>
export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>

View File

@ -93,7 +93,7 @@ toFormattedJSON = function (this: AccountFollowInstance) {
return json
}
loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
const query = {
where: {
accountId,
@ -110,7 +110,8 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
required: true,
as: 'AccountFollowing'
}
]
],
transaction: t
}
return AccountFollow.findOne(query)

View File

@ -1,4 +1,5 @@
// Order of the tests we want to execute
// import './multiple-servers'
import './video-transcoder'
import './multiple-servers'
import './follows'

View File

@ -1,13 +1,14 @@
import { ActivityPubSignature } from './activitypub-signature'
import { VideoChannelObject, VideoTorrentObject } from './objects'
import { DislikeObject } from './objects/dislike-object'
import { VideoAbuseObject } from './objects/video-abuse-object'
import { ViewObject } from './objects/view-object'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
ActivityUndo
ActivityUndo | ActivityLike
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo'
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like'
export interface BaseActivity {
'@context'?: any[]
@ -21,7 +22,7 @@ export interface BaseActivity {
export interface ActivityCreate extends BaseActivity {
type: 'Create'
object: VideoChannelObject | VideoAbuseObject | ViewObject
object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject
}
export interface ActivityAdd extends BaseActivity {
@ -55,5 +56,10 @@ export interface ActivityAnnounce extends BaseActivity {
export interface ActivityUndo extends BaseActivity {
type: 'Undo',
object: ActivityFollow
object: ActivityFollow | ActivityLike | ActivityCreate
}
export interface ActivityLike extends BaseActivity {
type: 'Like',
object: string
}

View File

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

View File

@ -3,3 +3,4 @@ export * from './video-abuse-object'
export * from './video-channel-object'
export * from './video-torrent-object'
export * from './view-object'
export * from './dislike-object'