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'
// 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)
}
})
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,
isVideoTorrentUpdateActivityValid
} from './videos'
import { isViewActivityValid } from './view'
+import { isDislikeActivityValid, isLikeActivityValid } from './rate'
function isRootActivityValid (activity: any) {
return Array.isArray(activity['@context']) &&
Follow: checkFollowActivity,
Accept: checkAcceptActivity,
Announce: checkAnnounceActivity,
- Undo: checkUndoActivity
+ Undo: checkUndoActivity,
+ Like: checkLikeActivity
}
function isActivityValid (activity: any) {
// ---------------------------------------------------------------------------
function checkCreateActivity (activity: any) {
- return isVideoChannelCreateActivityValid(activity) ||
- isVideoFlagValid(activity) ||
- isViewActivityValid(activity)
+ return isViewActivityValid(activity) ||
+ isDislikeActivityValid(activity) ||
+ isVideoChannelCreateActivityValid(activity) ||
+ isVideoFlagValid(activity)
}
function checkAddActivity (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)
}
import { isVideoTorrentAddActivityValid } from './videos'
import { isVideoChannelCreateActivityValid } from './video-channels'
-function isAnnounceValid (activity: any) {
+function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
(
isVideoChannelCreateActivityValid(activity.object) ||
}
export {
- isAnnounceValid
+ isAnnounceActivityValid
}
--- /dev/null
+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
+}
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
}
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' ],
export * from './process-create'
export * from './process-delete'
export * from './process-follow'
+export * from './process-like'
export * from './process-undo'
export * from './process-update'
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
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') {
// ---------------------------------------------------------------------------
+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)
--- /dev/null
+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)
+ })
+}
-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 })
}
// ---------------------------------------------------------------------------
+
+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
+ })
+}
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'
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,
Follow: processFollowActivity,
Accept: processAcceptActivity,
Announce: processAnnounceActivity,
- Undo: processUndoActivity
+ Undo: processUndoActivity,
+ Like: processLikeActivity
}
async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {
export * from './send-create'
export * from './send-delete'
export * from './send-follow'
+export * from './send-like'
+export * from './send-undo'
export * from './send-update'
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,
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()
export {
broadcastToFollowers,
unicastTo,
- getAudience
+ getAudience,
+ getOriginVideoAudience,
+ getAccountsToForwardVideoAction,
+ getVideoFollowersAudience
}
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
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)
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)
return activity
}
+function createDislikeActivityData (byAccount: AccountInstance, video: VideoInstance) {
+ const obj = {
+ type: 'Dislike',
+ actor: byAccount.url,
+ object: video.url
+ }
+
+ return obj
+}
+
// ---------------------------------------------------------------------------
export {
sendVideoAbuse,
createActivityData,
sendCreateViewToOrigin,
- sendCreateViewToVideoFollowers
+ sendCreateViewToVideoFollowers,
+ sendCreateDislikeToOrigin,
+ sendCreateDislikeToVideoFollowers,
+ createDislikeActivityData
}
// ---------------------------------------------------------------------------
--- /dev/null
+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
+}
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
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,
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
getAnnounceActivityPubUrl,
getUpdateActivityPubUrl,
getUndoActivityPubUrl,
- getVideoViewActivityPubUrl
+ getVideoViewActivityPubUrl,
+ getVideoLikeActivityPubUrl,
+ getVideoDislikeActivityPubUrl
}
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
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
}
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)
for (const uri of payload.uris) {
options.uri = uri
- await doRequest(options)
+
+ try {
+ await doRequest(options)
+ } catch (err) {
+ await maybeRetryRequestLater(err, payload, uri)
+ }
}
}
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,
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
}
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'
json: signedBody
}
- await doRequest(options)
+ try {
+ await doRequest(options)
+ } catch (err) {
+ await maybeRetryRequestLater(err, payload, uri)
+ throw err
+ }
}
function onError (err: Error, jobId: number) {
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>>
return json
}
-loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
+loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
const query = {
where: {
accountId,
required: true,
as: 'AccountFollowing'
}
- ]
+ ],
+ transaction: t
}
return AccountFollow.findOne(query)
// Order of the tests we want to execute
// import './multiple-servers'
import './video-transcoder'
+import './multiple-servers'
import './follows'
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[]
export interface ActivityCreate extends BaseActivity {
type: 'Create'
- object: VideoChannelObject | VideoAbuseObject | ViewObject
+ object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject
}
export interface ActivityAdd extends BaseActivity {
export interface ActivityUndo extends BaseActivity {
type: 'Undo',
- object: ActivityFollow
+ object: ActivityFollow | ActivityLike | ActivityCreate
+}
+
+export interface ActivityLike extends BaseActivity {
+ type: 'Like',
+ object: string
}
--- /dev/null
+export interface DislikeObject {
+ type: 'Dislike',
+ actor: string
+ object: string
+}
export * from './video-channel-object'
export * from './video-torrent-object'
export * from './view-object'
+export * from './dislike-object'