From 0032ebe94aa83fab761c7de3ceb6210ac4532824 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 23 Nov 2017 14:19:55 +0100 Subject: Federate likes/dislikes --- server/lib/activitypub/process/index.ts | 1 + server/lib/activitypub/process/process-create.ts | 35 +++++++- server/lib/activitypub/process/process-like.ts | 50 +++++++++++ server/lib/activitypub/process/process-undo.ts | 99 +++++++++++++++++++--- server/lib/activitypub/process/process.ts | 6 +- server/lib/activitypub/send/index.ts | 2 + server/lib/activitypub/send/misc.ts | 27 +++++- server/lib/activitypub/send/send-create.ts | 62 +++++++++++--- server/lib/activitypub/send/send-like.ts | 60 +++++++++++++ server/lib/activitypub/send/send-undo.ts | 71 ++++++++++++++-- server/lib/activitypub/url.ts | 12 ++- server/lib/activitypub/videos.ts | 47 +++++++++- .../activitypub-http-broadcast-handler.ts | 9 +- .../activitypub-http-job-scheduler.ts | 24 +++++- .../activitypub-http-unicast-handler.ts | 9 +- 15 files changed, 475 insertions(+), 39 deletions(-) create mode 100644 server/lib/activitypub/process/process-like.ts create mode 100644 server/lib/activitypub/send/send-like.ts (limited to 'server/lib') diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts index c68312053..e25c261cc 100644 --- a/server/lib/activitypub/process/index.ts +++ b/server/lib/activitypub/process/index.ts @@ -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' diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1777733a0..147bbd132 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -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) diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts new file mode 100644 index 000000000..d77b30f24 --- /dev/null +++ b/server/lib/activitypub/process/process-like.ts @@ -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) + }) +} diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 610b800fb..caa835714 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -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 + }) +} diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index 613597341..942bce0e6 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts @@ -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 } = { 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) { diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts index 5f15dd4b5..ee8f3ad7e 100644 --- a/server/lib/activitypub/send/index.ts +++ b/server/lib/activitypub/send/index.ts @@ -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' diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index f3dc5c148..41a039b19 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts @@ -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 } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index e5fb212b7..6afe67ee6 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -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) + + 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) +} - // 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) +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 } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts new file mode 100644 index 000000000..70a7d886f --- /dev/null +++ b/server/lib/activitypub/send/send-like.ts @@ -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 +} diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 77bee6639..53fddd0cb 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -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, diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index d98561e33..17395a99b 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -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 } diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 944244893..acee4fe16 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -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[] = [] + + // 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[] = [] + + // 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 } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts index 111fc88a4..5b4c65b81 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts @@ -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) + } } } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts index aef217ce7..ccf109935 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts @@ -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 } = { 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 } diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts index 8d3b755ad..f7f3dabbd 100644 --- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts +++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts @@ -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) { -- cgit v1.2.3