From 54141398354e6e7b94aa3065a705a1251390111c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 20 Nov 2017 09:43:39 +0100 Subject: Refractor activity pub lib/helpers --- server/lib/activitypub/index.ts | 10 +- server/lib/activitypub/misc.ts | 101 -------- server/lib/activitypub/process-accept.ts | 27 -- server/lib/activitypub/process-add.ts | 87 ------- server/lib/activitypub/process-announce.ts | 45 ---- server/lib/activitypub/process-create.ts | 88 ------- server/lib/activitypub/process-delete.ts | 105 -------- server/lib/activitypub/process-follow.ts | 57 ----- server/lib/activitypub/process-update.ts | 135 ---------- server/lib/activitypub/process/index.ts | 8 + server/lib/activitypub/process/misc.ts | 101 ++++++++ server/lib/activitypub/process/process-accept.ts | 27 ++ server/lib/activitypub/process/process-add.ts | 87 +++++++ server/lib/activitypub/process/process-announce.ts | 46 ++++ server/lib/activitypub/process/process-create.ts | 88 +++++++ server/lib/activitypub/process/process-delete.ts | 105 ++++++++ server/lib/activitypub/process/process-follow.ts | 59 +++++ server/lib/activitypub/process/process-undo.ts | 31 +++ server/lib/activitypub/process/process-update.ts | 135 ++++++++++ server/lib/activitypub/send-request.ts | 275 --------------------- server/lib/activitypub/send/index.ts | 7 + server/lib/activitypub/send/misc.ts | 58 +++++ server/lib/activitypub/send/send-accept.ts | 34 +++ server/lib/activitypub/send/send-add.ts | 38 +++ server/lib/activitypub/send/send-announce.ts | 45 ++++ server/lib/activitypub/send/send-create.ts | 44 ++++ server/lib/activitypub/send/send-delete.ts | 53 ++++ server/lib/activitypub/send/send-follow.ts | 34 +++ server/lib/activitypub/send/send-undo.ts | 39 +++ server/lib/activitypub/send/send-update.ts | 55 +++++ 30 files changed, 1096 insertions(+), 928 deletions(-) delete mode 100644 server/lib/activitypub/misc.ts delete mode 100644 server/lib/activitypub/process-accept.ts delete mode 100644 server/lib/activitypub/process-add.ts delete mode 100644 server/lib/activitypub/process-announce.ts delete mode 100644 server/lib/activitypub/process-create.ts delete mode 100644 server/lib/activitypub/process-delete.ts delete mode 100644 server/lib/activitypub/process-follow.ts delete mode 100644 server/lib/activitypub/process-update.ts create mode 100644 server/lib/activitypub/process/index.ts create mode 100644 server/lib/activitypub/process/misc.ts create mode 100644 server/lib/activitypub/process/process-accept.ts create mode 100644 server/lib/activitypub/process/process-add.ts create mode 100644 server/lib/activitypub/process/process-announce.ts create mode 100644 server/lib/activitypub/process/process-create.ts create mode 100644 server/lib/activitypub/process/process-delete.ts create mode 100644 server/lib/activitypub/process/process-follow.ts create mode 100644 server/lib/activitypub/process/process-undo.ts create mode 100644 server/lib/activitypub/process/process-update.ts delete mode 100644 server/lib/activitypub/send-request.ts create mode 100644 server/lib/activitypub/send/index.ts create mode 100644 server/lib/activitypub/send/misc.ts create mode 100644 server/lib/activitypub/send/send-accept.ts create mode 100644 server/lib/activitypub/send/send-add.ts create mode 100644 server/lib/activitypub/send/send-announce.ts create mode 100644 server/lib/activitypub/send/send-create.ts create mode 100644 server/lib/activitypub/send/send-delete.ts create mode 100644 server/lib/activitypub/send/send-follow.ts create mode 100644 server/lib/activitypub/send/send-undo.ts create mode 100644 server/lib/activitypub/send/send-update.ts (limited to 'server/lib/activitypub') diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts index e08108aac..1bea0a412 100644 --- a/server/lib/activitypub/index.ts +++ b/server/lib/activitypub/index.ts @@ -1,8 +1,2 @@ -export * from './process-accept' -export * from './process-add' -export * from './process-announce' -export * from './process-create' -export * from './process-delete' -export * from './process-follow' -export * from './process-update' -export * from './send-request' +export * from './process' +export * from './send' diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts deleted file mode 100644 index 4c210eb10..000000000 --- a/server/lib/activitypub/misc.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as magnetUtil from 'magnet-uri' -import { VideoTorrentObject } from '../../../shared' -import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' -import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' -import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../initializers/constants' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { VideoFileAttributes } from '../../models/video/video-file-interface' -import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' -import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum' - -function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { - return { - name: videoChannelObject.name, - description: videoChannelObject.content, - uuid: videoChannelObject.uuid, - url: videoChannelObject.id, - createdAt: new Date(videoChannelObject.published), - updatedAt: new Date(videoChannelObject.updated), - remote: true, - accountId: account.id - } -} - -async function videoActivityObjectToDBAttributes ( - videoChannel: VideoChannelInstance, - videoObject: VideoTorrentObject, - to: string[] = [], - cc: string[] = [] -) { - let privacy = VideoPrivacy.PRIVATE - if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC - else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED - - const duration = videoObject.duration.replace(/[^\d]+/, '') - const videoData: VideoAttributes = { - name: videoObject.name, - uuid: videoObject.uuid, - url: videoObject.id, - category: parseInt(videoObject.category.identifier, 10), - licence: parseInt(videoObject.licence.identifier, 10), - language: parseInt(videoObject.language.identifier, 10), - nsfw: videoObject.nsfw, - description: videoObject.content, - channelId: videoChannel.id, - duration: parseInt(duration, 10), - createdAt: new Date(videoObject.published), - // FIXME: updatedAt does not seems to be considered by Sequelize - updatedAt: new Date(videoObject.updated), - views: videoObject.views, - likes: 0, - dislikes: 0, - remote: true, - privacy - } - - return videoData -} - -function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { - const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) - const fileUrls = videoObject.url.filter(u => { - return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') - }) - - if (fileUrls.length === 0) { - throw new Error('Cannot find video files for ' + videoCreated.url) - } - - const attributes: VideoFileAttributes[] = [] - for (const fileUrl of fileUrls) { - // Fetch associated magnet uri - const magnet = videoObject.url.find(u => { - return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width - }) - - if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url) - - const parsed = magnetUtil.decode(magnet.url) - if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) - - const attribute = { - extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType], - infoHash: parsed.infoHash, - resolution: fileUrl.width, - size: fileUrl.size, - videoId: videoCreated.id - } - attributes.push(attribute) - } - - return attributes -} - -// --------------------------------------------------------------------------- - -export { - videoFileActivityUrlToDBAttributes, - videoActivityObjectToDBAttributes, - videoChannelActivityObjectToDBAttributes -} diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process-accept.ts deleted file mode 100644 index 9e0cd4032..000000000 --- a/server/lib/activitypub/process-accept.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ActivityAccept } from '../../../shared/models/activitypub/activity' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' - -async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) { - if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.') - - const targetAccount = await db.Account.loadByUrl(activity.actor) - - return processAccept(inboxAccount, targetAccount) -} - -// --------------------------------------------------------------------------- - -export { - processAcceptActivity -} - -// --------------------------------------------------------------------------- - -async function processAccept (account: AccountInstance, targetAccount: AccountInstance) { - const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id) - if (!follow) throw new Error('Cannot find associated follow.') - - follow.set('state', 'accepted') - await follow.save() -} diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts deleted file mode 100644 index e1769bee8..000000000 --- a/server/lib/activitypub/process-add.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as Bluebird from 'bluebird' -import { VideoTorrentObject } from '../../../shared' -import { ActivityAdd } from '../../../shared/models/activitypub/activity' -import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../helpers' -import { getOrCreateVideoChannel } from '../../helpers/activitypub' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' - -async function processAddActivity (activity: ActivityAdd) { - const activityObject = activity.object - const activityType = activityObject.type - const account = await getOrCreateAccount(activity.actor) - - if (activityType === 'Video') { - const videoChannelUrl = activity.target - const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl) - - return processAddVideo(account, activity, videoChannel, activityObject) - } - - logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) - return Promise.resolve(undefined) -} - -// --------------------------------------------------------------------------- - -export { - processAddActivity -} - -// --------------------------------------------------------------------------- - -function processAddVideo (account: AccountInstance, activity: ActivityAdd, videoChannel: VideoChannelInstance, video: VideoTorrentObject) { - const options = { - arguments: [ account, activity, videoChannel, video ], - errorMessage: 'Cannot insert the remote video with many retries.' - } - - return retryTransactionWrapper(addRemoteVideo, options) -} - -function addRemoteVideo ( - account: AccountInstance, - activity: ActivityAdd, - videoChannel: VideoChannelInstance, - videoToCreateData: VideoTorrentObject -) { - logger.debug('Adding remote video %s.', videoToCreateData.url) - - return db.sequelize.transaction(async t => { - const sequelizeOptions = { - transaction: t - } - - if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') - - const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t) - if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.') - - const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc) - const video = db.Video.build(videoData) - - // Don't block on request - generateThumbnailFromUrl(video, videoToCreateData.icon) - .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err)) - - const videoCreated = await video.save(sequelizeOptions) - - const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) - if (videoFileAttributes.length === 0) { - throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url) - } - - const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t })) - await Promise.all(tasks) - - const tags = videoToCreateData.tag.map(t => t.name) - const tagInstances = await db.Tag.findOrCreateTags(tags, t) - await videoCreated.setTags(tagInstances, sequelizeOptions) - - logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) - - return videoCreated - }) -} diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process-announce.ts deleted file mode 100644 index eb38aecca..000000000 --- a/server/lib/activitypub/process-announce.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ActivityAnnounce } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers/index' -import { VideoInstance } from '../../models/index' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { processAddActivity } from './process-add' -import { processCreateActivity } from './process-create' - -async function processAnnounceActivity (activity: ActivityAnnounce) { - const announcedActivity = activity.object - const accountAnnouncer = await getOrCreateAccount(activity.actor) - - if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { - // Add share entry - const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) - await db.VideoChannelShare.create({ - accountId: accountAnnouncer.id, - videoChannelId: videoChannel.id - }) - - return undefined - } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') { - // Add share entry - const video: VideoInstance = await processAddActivity(announcedActivity) - await db.VideoShare.create({ - accountId: accountAnnouncer.id, - videoId: video.id - }) - - return undefined - } - - logger.warn( - 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type, - { activity: activity.id } - ) - return Promise.resolve(undefined) -} - -// --------------------------------------------------------------------------- - -export { - processAnnounceActivity -} diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts deleted file mode 100644 index de8e09adf..000000000 --- a/server/lib/activitypub/process-create.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ActivityCreate, VideoChannelObject } from '../../../shared' -import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' -import { logger, retryTransactionWrapper } from '../../helpers' -import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { videoChannelActivityObjectToDBAttributes } from './misc' - -async function processCreateActivity (activity: ActivityCreate) { - const activityObject = activity.object - const activityType = activityObject.type - const account = await getOrCreateAccount(activity.actor) - - if (activityType === 'VideoChannel') { - return processCreateVideoChannel(account, activityObject as VideoChannelObject) - } else if (activityType === 'Flag') { - return processCreateVideoAbuse(account, activityObject as VideoAbuseObject) - } - - logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) - return Promise.resolve(undefined) -} - -// --------------------------------------------------------------------------- - -export { - processCreateActivity -} - -// --------------------------------------------------------------------------- - -function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { - const options = { - arguments: [ account, videoChannelToCreateData ], - errorMessage: 'Cannot insert the remote video channel with many retries.' - } - - return retryTransactionWrapper(addRemoteVideoChannel, options) -} - -function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { - logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) - - return db.sequelize.transaction(async t => { - let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) - if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') - - const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) - videoChannel = db.VideoChannel.build(videoChannelData) - videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) - - videoChannel = await videoChannel.save({ transaction: t }) - logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid) - - return videoChannel - }) -} - -function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { - const options = { - arguments: [ account, videoAbuseToCreateData ], - errorMessage: 'Cannot insert the remote video abuse with many retries.' - } - - return retryTransactionWrapper(addRemoteVideoAbuse, options) -} - -function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { - logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) - - return db.sequelize.transaction(async t => { - const video = await db.Video.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t) - if (!video) { - logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object) - return undefined - } - - const videoAbuseData = { - reporterAccountId: account.id, - reason: videoAbuseToCreateData.content, - videoId: video.id - } - - await db.VideoAbuse.create(videoAbuseData) - - logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) - }) -} diff --git a/server/lib/activitypub/process-delete.ts b/server/lib/activitypub/process-delete.ts deleted file mode 100644 index 0d5756e9c..000000000 --- a/server/lib/activitypub/process-delete.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ActivityDelete } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { retryTransactionWrapper } from '../../helpers/database-utils' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { VideoInstance } from '../../models/video/video-interface' - -async function processDeleteActivity (activity: ActivityDelete) { - const account = await getOrCreateAccount(activity.actor) - - if (account.url === activity.id) { - return processDeleteAccount(account) - } - - { - let videoObject = await db.Video.loadByUrlAndPopulateAccount(activity.id) - if (videoObject !== undefined) { - return processDeleteVideo(account, videoObject) - } - } - - { - let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id) - if (videoChannelObject !== undefined) { - return processDeleteVideoChannel(account, videoChannelObject) - } - } - - return -} - -// --------------------------------------------------------------------------- - -export { - processDeleteActivity -} - -// --------------------------------------------------------------------------- - -async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) { - const options = { - arguments: [ account, videoToDelete ], - errorMessage: 'Cannot remove the remote video with many retries.' - } - - await retryTransactionWrapper(deleteRemoteVideo, options) -} - -async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) { - logger.debug('Removing remote video "%s".', videoToDelete.uuid) - - await db.sequelize.transaction(async t => { - if (videoToDelete.VideoChannel.Account.id !== account.id) { - throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url) - } - - await videoToDelete.destroy({ transaction: t }) - }) - - logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) -} - -async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { - const options = { - arguments: [ account, videoChannelToRemove ], - errorMessage: 'Cannot remove the remote video channel with many retries.' - } - - await retryTransactionWrapper(deleteRemoteVideoChannel, options) -} - -async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { - logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid) - - await db.sequelize.transaction(async t => { - if (videoChannelToRemove.Account.id !== account.id) { - throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url) - } - - await videoChannelToRemove.destroy({ transaction: t }) - }) - - logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid) -} - -async function processDeleteAccount (accountToRemove: AccountInstance) { - const options = { - arguments: [ accountToRemove ], - errorMessage: 'Cannot remove the remote account with many retries.' - } - - await retryTransactionWrapper(deleteRemoteAccount, options) -} - -async function deleteRemoteAccount (accountToRemove: AccountInstance) { - logger.debug('Removing remote account "%s".', accountToRemove.uuid) - - await db.sequelize.transaction(async t => { - await accountToRemove.destroy({ transaction: t }) - }) - - logger.info('Remote account with uuid %s removed.', accountToRemove.uuid) -} diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts deleted file mode 100644 index a805c0757..000000000 --- a/server/lib/activitypub/process-follow.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ActivityFollow } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { sendAccept } from './send-request' -import { logger } from '../../helpers/logger' - -async function processFollowActivity (activity: ActivityFollow) { - const activityObject = activity.object - const account = await getOrCreateAccount(activity.actor) - - return processFollow(account, activityObject) -} - -// --------------------------------------------------------------------------- - -export { - processFollowActivity -} - -// --------------------------------------------------------------------------- - -function processFollow (account: AccountInstance, targetAccountURL: string) { - const options = { - arguments: [ account, targetAccountURL ], - errorMessage: 'Cannot follow with many retries.' - } - - return retryTransactionWrapper(follow, options) -} - -async function follow (account: AccountInstance, targetAccountURL: string) { - await db.sequelize.transaction(async t => { - const targetAccount = await db.Account.loadByUrl(targetAccountURL, t) - - if (targetAccount === undefined) throw new Error('Unknown account') - if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') - - await db.AccountFollow.findOrCreate({ - where: { - accountId: account.id, - targetAccountId: targetAccount.id - }, - defaults: { - accountId: account.id, - targetAccountId: targetAccount.id, - state: 'accepted' - }, - transaction: t - }) - - // Target sends to account he accepted the follow request - return sendAccept(targetAccount, account, t) - }) - - logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL) -} diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts deleted file mode 100644 index a9aa5eeb4..000000000 --- a/server/lib/activitypub/process-update.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { VideoChannelObject, VideoTorrentObject } from '../../../shared' -import { ActivityUpdate } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { retryTransactionWrapper } from '../../helpers/database-utils' -import { logger } from '../../helpers/logger' -import { resetSequelizeInstance } from '../../helpers/utils' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoInstance } from '../../models/video/video-interface' -import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' -import Bluebird = require('bluebird') - -async function processUpdateActivity (activity: ActivityUpdate) { - const account = await getOrCreateAccount(activity.actor) - - if (activity.object.type === 'Video') { - return processUpdateVideo(account, activity.object) - } else if (activity.object.type === 'VideoChannel') { - return processUpdateVideoChannel(account, activity.object) - } - - return undefined -} - -// --------------------------------------------------------------------------- - -export { - processUpdateActivity -} - -// --------------------------------------------------------------------------- - -function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) { - const options = { - arguments: [ account, video ], - errorMessage: 'Cannot update the remote video with many retries' - } - - return retryTransactionWrapper(updateRemoteVideo, options) -} - -async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) { - logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) - let videoInstance: VideoInstance - let videoFieldsSave: object - - try { - await db.sequelize.transaction(async t => { - const sequelizeOptions = { - transaction: t - } - - const videoInstance = await db.Video.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) - if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') - - if (videoInstance.VideoChannel.Account.id !== account.id) { - throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url) - } - - const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate) - videoInstance.set('name', videoData.name) - videoInstance.set('category', videoData.category) - videoInstance.set('licence', videoData.licence) - videoInstance.set('language', videoData.language) - videoInstance.set('nsfw', videoData.nsfw) - videoInstance.set('description', videoData.description) - videoInstance.set('duration', videoData.duration) - videoInstance.set('createdAt', videoData.createdAt) - videoInstance.set('updatedAt', videoData.updatedAt) - videoInstance.set('views', videoData.views) - // videoInstance.set('likes', videoData.likes) - // videoInstance.set('dislikes', videoData.dislikes) - - await videoInstance.save(sequelizeOptions) - - // Remove old video files - const videoFileDestroyTasks: Bluebird[] = [] - for (const videoFile of videoInstance.VideoFiles) { - videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions)) - } - await Promise.all(videoFileDestroyTasks) - - const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) - const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f)) - await Promise.all(tasks) - - const tags = videoAttributesToUpdate.tag.map(t => t.name) - const tagInstances = await db.Tag.findOrCreateTags(tags, t) - await videoInstance.setTags(tagInstances, sequelizeOptions) - }) - - logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) - } catch (err) { - if (videoInstance !== undefined && videoFieldsSave !== undefined) { - resetSequelizeInstance(videoInstance, videoFieldsSave) - } - - // This is just a debug because we will retry the insert - logger.debug('Cannot update the remote video.', err) - throw err - } -} - -async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { - const options = { - arguments: [ account, videoChannel ], - errorMessage: 'Cannot update the remote video channel with many retries.' - } - - await retryTransactionWrapper(updateRemoteVideoChannel, options) -} - -async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { - logger.debug('Updating remote video channel "%s".', videoChannel.uuid) - - await db.sequelize.transaction(async t => { - const sequelizeOptions = { transaction: t } - - const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id) - if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.') - - if (videoChannelInstance.Account.id !== account.id) { - throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url) - } - - videoChannelInstance.set('name', videoChannel.name) - videoChannelInstance.set('description', videoChannel.content) - videoChannelInstance.set('createdAt', videoChannel.published) - videoChannelInstance.set('updatedAt', videoChannel.updated) - - await videoChannelInstance.save(sequelizeOptions) - }) - - logger.info('Remote video channel with uuid %s updated', videoChannel.uuid) -} diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts new file mode 100644 index 000000000..e80b46b6f --- /dev/null +++ b/server/lib/activitypub/process/index.ts @@ -0,0 +1,8 @@ +export * from './process-accept' +export * from './process-add' +export * from './process-announce' +export * from './process-create' +export * from './process-delete' +export * from './process-follow' +export * from './process-undo' +export * from './process-update' diff --git a/server/lib/activitypub/process/misc.ts b/server/lib/activitypub/process/misc.ts new file mode 100644 index 000000000..e90a793fc --- /dev/null +++ b/server/lib/activitypub/process/misc.ts @@ -0,0 +1,101 @@ +import * as magnetUtil from 'magnet-uri' +import { VideoTorrentObject } from '../../../../shared' +import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object' +import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos' +import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { VideoFileAttributes } from '../../../models/video/video-file-interface' +import { VideoAttributes, VideoInstance } from '../../../models/video/video-interface' +import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' + +function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { + return { + name: videoChannelObject.name, + description: videoChannelObject.content, + uuid: videoChannelObject.uuid, + url: videoChannelObject.id, + createdAt: new Date(videoChannelObject.published), + updatedAt: new Date(videoChannelObject.updated), + remote: true, + accountId: account.id + } +} + +async function videoActivityObjectToDBAttributes ( + videoChannel: VideoChannelInstance, + videoObject: VideoTorrentObject, + to: string[] = [], + cc: string[] = [] +) { + let privacy = VideoPrivacy.PRIVATE + if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC + else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED + + const duration = videoObject.duration.replace(/[^\d]+/, '') + const videoData: VideoAttributes = { + name: videoObject.name, + uuid: videoObject.uuid, + url: videoObject.id, + category: parseInt(videoObject.category.identifier, 10), + licence: parseInt(videoObject.licence.identifier, 10), + language: parseInt(videoObject.language.identifier, 10), + nsfw: videoObject.nsfw, + description: videoObject.content, + channelId: videoChannel.id, + duration: parseInt(duration, 10), + createdAt: new Date(videoObject.published), + // FIXME: updatedAt does not seems to be considered by Sequelize + updatedAt: new Date(videoObject.updated), + views: videoObject.views, + likes: 0, + dislikes: 0, + remote: true, + privacy + } + + return videoData +} + +function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { + const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) + const fileUrls = videoObject.url.filter(u => { + return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') + }) + + if (fileUrls.length === 0) { + throw new Error('Cannot find video files for ' + videoCreated.url) + } + + const attributes: VideoFileAttributes[] = [] + for (const fileUrl of fileUrls) { + // Fetch associated magnet uri + const magnet = videoObject.url.find(u => { + return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width + }) + + if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url) + + const parsed = magnetUtil.decode(magnet.url) + if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) + + const attribute = { + extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType], + infoHash: parsed.infoHash, + resolution: fileUrl.width, + size: fileUrl.size, + videoId: videoCreated.id + } + attributes.push(attribute) + } + + return attributes +} + +// --------------------------------------------------------------------------- + +export { + videoFileActivityUrlToDBAttributes, + videoActivityObjectToDBAttributes, + videoChannelActivityObjectToDBAttributes +} diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts new file mode 100644 index 000000000..e159c41b5 --- /dev/null +++ b/server/lib/activitypub/process/process-accept.ts @@ -0,0 +1,27 @@ +import { ActivityAccept } from '../../../../shared/models/activitypub/activity' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' + +async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) { + if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.') + + const targetAccount = await db.Account.loadByUrl(activity.actor) + + return processAccept(inboxAccount, targetAccount) +} + +// --------------------------------------------------------------------------- + +export { + processAcceptActivity +} + +// --------------------------------------------------------------------------- + +async function processAccept (account: AccountInstance, targetAccount: AccountInstance) { + const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id) + if (!follow) throw new Error('Cannot find associated follow.') + + follow.set('state', 'accepted') + await follow.save() +} diff --git a/server/lib/activitypub/process/process-add.ts b/server/lib/activitypub/process/process-add.ts new file mode 100644 index 000000000..f064c1ab6 --- /dev/null +++ b/server/lib/activitypub/process/process-add.ts @@ -0,0 +1,87 @@ +import * as Bluebird from 'bluebird' +import { VideoTorrentObject } from '../../../../shared' +import { ActivityAdd } from '../../../../shared/models/activitypub/activity' +import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../../helpers' +import { getOrCreateVideoChannel } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' + +async function processAddActivity (activity: ActivityAdd) { + const activityObject = activity.object + const activityType = activityObject.type + const account = await getOrCreateAccount(activity.actor) + + if (activityType === 'Video') { + const videoChannelUrl = activity.target + const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl) + + return processAddVideo(account, activity, videoChannel, activityObject) + } + + logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) + return Promise.resolve(undefined) +} + +// --------------------------------------------------------------------------- + +export { + processAddActivity +} + +// --------------------------------------------------------------------------- + +function processAddVideo (account: AccountInstance, activity: ActivityAdd, videoChannel: VideoChannelInstance, video: VideoTorrentObject) { + const options = { + arguments: [ account, activity, videoChannel, video ], + errorMessage: 'Cannot insert the remote video with many retries.' + } + + return retryTransactionWrapper(addRemoteVideo, options) +} + +function addRemoteVideo ( + account: AccountInstance, + activity: ActivityAdd, + videoChannel: VideoChannelInstance, + videoToCreateData: VideoTorrentObject +) { + logger.debug('Adding remote video %s.', videoToCreateData.url) + + return db.sequelize.transaction(async t => { + const sequelizeOptions = { + transaction: t + } + + if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') + + const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t) + if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.') + + const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc) + const video = db.Video.build(videoData) + + // Don't block on request + generateThumbnailFromUrl(video, videoToCreateData.icon) + .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err)) + + const videoCreated = await video.save(sequelizeOptions) + + const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) + if (videoFileAttributes.length === 0) { + throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url) + } + + const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t })) + await Promise.all(tasks) + + const tags = videoToCreateData.tag.map(t => t.name) + const tagInstances = await db.Tag.findOrCreateTags(tags, t) + await videoCreated.setTags(tagInstances, sequelizeOptions) + + logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) + + return videoCreated + }) +} diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts new file mode 100644 index 000000000..656db08a9 --- /dev/null +++ b/server/lib/activitypub/process/process-announce.ts @@ -0,0 +1,46 @@ +import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers/index' +import { VideoInstance } from '../../../models/index' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { processAddActivity } from './process-add' +import { processCreateActivity } from './process-create' + +async function processAnnounceActivity (activity: ActivityAnnounce) { + const announcedActivity = activity.object + const accountAnnouncer = await getOrCreateAccount(activity.actor) + + if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { + // Add share entry + const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) + await db.VideoChannelShare.create({ + accountId: accountAnnouncer.id, + videoChannelId: videoChannel.id + }) + + return undefined + } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') { + // Add share entry + const video: VideoInstance = await processAddActivity(announcedActivity) + await db.VideoShare.create({ + accountId: accountAnnouncer.id, + videoId: video.id + }) + + return undefined + } + + logger.warn( + 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type, + { activity: activity.id } + ) + + return undefined +} + +// --------------------------------------------------------------------------- + +export { + processAnnounceActivity +} diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts new file mode 100644 index 000000000..aac941a6c --- /dev/null +++ b/server/lib/activitypub/process/process-create.ts @@ -0,0 +1,88 @@ +import { ActivityCreate, VideoChannelObject } from '../../../../shared' +import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' +import { logger, retryTransactionWrapper } from '../../../helpers' +import { getOrCreateAccount, getVideoChannelActivityPubUrl } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { videoChannelActivityObjectToDBAttributes } from './misc' + +async function processCreateActivity (activity: ActivityCreate) { + const activityObject = activity.object + const activityType = activityObject.type + const account = await getOrCreateAccount(activity.actor) + + if (activityType === 'VideoChannel') { + return processCreateVideoChannel(account, activityObject as VideoChannelObject) + } else if (activityType === 'Flag') { + return processCreateVideoAbuse(account, activityObject as VideoAbuseObject) + } + + logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) + return Promise.resolve(undefined) +} + +// --------------------------------------------------------------------------- + +export { + processCreateActivity +} + +// --------------------------------------------------------------------------- + +function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { + const options = { + arguments: [ account, videoChannelToCreateData ], + errorMessage: 'Cannot insert the remote video channel with many retries.' + } + + return retryTransactionWrapper(addRemoteVideoChannel, options) +} + +function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { + logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) + + return db.sequelize.transaction(async t => { + let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) + if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') + + const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) + videoChannel = db.VideoChannel.build(videoChannelData) + videoChannel.url = getVideoChannelActivityPubUrl(videoChannel) + + videoChannel = await videoChannel.save({ transaction: t }) + logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid) + + return videoChannel + }) +} + +function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { + const options = { + arguments: [ account, videoAbuseToCreateData ], + errorMessage: 'Cannot insert the remote video abuse with many retries.' + } + + return retryTransactionWrapper(addRemoteVideoAbuse, options) +} + +function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { + logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) + + return db.sequelize.transaction(async t => { + const video = await db.Video.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t) + if (!video) { + logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object) + return undefined + } + + const videoAbuseData = { + reporterAccountId: account.id, + reason: videoAbuseToCreateData.content, + videoId: video.id + } + + await db.VideoAbuse.create(videoAbuseData) + + logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) + }) +} diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts new file mode 100644 index 000000000..af5d964d4 --- /dev/null +++ b/server/lib/activitypub/process/process-delete.ts @@ -0,0 +1,105 @@ +import { ActivityDelete } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { VideoInstance } from '../../../models/video/video-interface' + +async function processDeleteActivity (activity: ActivityDelete) { + const account = await getOrCreateAccount(activity.actor) + + if (account.url === activity.id) { + return processDeleteAccount(account) + } + + { + let videoObject = await db.Video.loadByUrlAndPopulateAccount(activity.id) + if (videoObject !== undefined) { + return processDeleteVideo(account, videoObject) + } + } + + { + let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id) + if (videoChannelObject !== undefined) { + return processDeleteVideoChannel(account, videoChannelObject) + } + } + + return +} + +// --------------------------------------------------------------------------- + +export { + processDeleteActivity +} + +// --------------------------------------------------------------------------- + +async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) { + const options = { + arguments: [ account, videoToDelete ], + errorMessage: 'Cannot remove the remote video with many retries.' + } + + await retryTransactionWrapper(deleteRemoteVideo, options) +} + +async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) { + logger.debug('Removing remote video "%s".', videoToDelete.uuid) + + await db.sequelize.transaction(async t => { + if (videoToDelete.VideoChannel.Account.id !== account.id) { + throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url) + } + + await videoToDelete.destroy({ transaction: t }) + }) + + logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) +} + +async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { + const options = { + arguments: [ account, videoChannelToRemove ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } + + await retryTransactionWrapper(deleteRemoteVideoChannel, options) +} + +async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { + logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid) + + await db.sequelize.transaction(async t => { + if (videoChannelToRemove.Account.id !== account.id) { + throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url) + } + + await videoChannelToRemove.destroy({ transaction: t }) + }) + + logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid) +} + +async function processDeleteAccount (accountToRemove: AccountInstance) { + const options = { + arguments: [ accountToRemove ], + errorMessage: 'Cannot remove the remote account with many retries.' + } + + await retryTransactionWrapper(deleteRemoteAccount, options) +} + +async function deleteRemoteAccount (accountToRemove: AccountInstance) { + logger.debug('Removing remote account "%s".', accountToRemove.uuid) + + await db.sequelize.transaction(async t => { + await accountToRemove.destroy({ transaction: t }) + }) + + logger.info('Remote account with uuid %s removed.', accountToRemove.uuid) +} diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts new file mode 100644 index 000000000..553639580 --- /dev/null +++ b/server/lib/activitypub/process/process-follow.ts @@ -0,0 +1,59 @@ +import { ActivityFollow } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount, retryTransactionWrapper } from '../../../helpers' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { logger } from '../../../helpers/logger' +import { sendAccept } from '../send/send-accept' + +async function processFollowActivity (activity: ActivityFollow) { + const activityObject = activity.object + const account = await getOrCreateAccount(activity.actor) + + return processFollow(account, activityObject) +} + +// --------------------------------------------------------------------------- + +export { + processFollowActivity +} + +// --------------------------------------------------------------------------- + +function processFollow (account: AccountInstance, targetAccountURL: string) { + const options = { + arguments: [ account, targetAccountURL ], + errorMessage: 'Cannot follow with many retries.' + } + + return retryTransactionWrapper(follow, options) +} + +async function follow (account: AccountInstance, targetAccountURL: string) { + await db.sequelize.transaction(async t => { + const targetAccount = await db.Account.loadByUrl(targetAccountURL, t) + + if (!targetAccount) throw new Error('Unknown account') + if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') + + const [ accountFollow ] = await db.AccountFollow.findOrCreate({ + where: { + accountId: account.id, + targetAccountId: targetAccount.id + }, + defaults: { + accountId: account.id, + targetAccountId: targetAccount.id, + state: 'accepted' + }, + transaction: t + }) + accountFollow.AccountFollower = account + accountFollow.AccountFollowing = targetAccount + + // Target sends to account he accepted the follow request + return sendAccept(accountFollow, t) + }) + + logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL) +} diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts new file mode 100644 index 000000000..5d09423e1 --- /dev/null +++ b/server/lib/activitypub/process/process-undo.ts @@ -0,0 +1,31 @@ +import { ActivityUndo } from '../../../../shared/models/activitypub/activity' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers' + +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 + } + + logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) + + return undefined +} + +// --------------------------------------------------------------------------- + +export { + processUndoActivity +} + +// --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts new file mode 100644 index 000000000..a3bfb1baf --- /dev/null +++ b/server/lib/activitypub/process/process-update.ts @@ -0,0 +1,135 @@ +import { VideoChannelObject, VideoTorrentObject } from '../../../../shared' +import { ActivityUpdate } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { logger } from '../../../helpers/logger' +import { resetSequelizeInstance } from '../../../helpers/utils' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoInstance } from '../../../models/video/video-interface' +import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' +import Bluebird = require('bluebird') + +async function processUpdateActivity (activity: ActivityUpdate) { + const account = await getOrCreateAccount(activity.actor) + + if (activity.object.type === 'Video') { + return processUpdateVideo(account, activity.object) + } else if (activity.object.type === 'VideoChannel') { + return processUpdateVideoChannel(account, activity.object) + } + + return undefined +} + +// --------------------------------------------------------------------------- + +export { + processUpdateActivity +} + +// --------------------------------------------------------------------------- + +function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) { + const options = { + arguments: [ account, video ], + errorMessage: 'Cannot update the remote video with many retries' + } + + return retryTransactionWrapper(updateRemoteVideo, options) +} + +async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) { + logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) + let videoInstance: VideoInstance + let videoFieldsSave: object + + try { + await db.sequelize.transaction(async t => { + const sequelizeOptions = { + transaction: t + } + + const videoInstance = await db.Video.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) + if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') + + if (videoInstance.VideoChannel.Account.id !== account.id) { + throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url) + } + + const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate) + videoInstance.set('name', videoData.name) + videoInstance.set('category', videoData.category) + videoInstance.set('licence', videoData.licence) + videoInstance.set('language', videoData.language) + videoInstance.set('nsfw', videoData.nsfw) + videoInstance.set('description', videoData.description) + videoInstance.set('duration', videoData.duration) + videoInstance.set('createdAt', videoData.createdAt) + videoInstance.set('updatedAt', videoData.updatedAt) + videoInstance.set('views', videoData.views) + // videoInstance.set('likes', videoData.likes) + // videoInstance.set('dislikes', videoData.dislikes) + + await videoInstance.save(sequelizeOptions) + + // Remove old video files + const videoFileDestroyTasks: Bluebird[] = [] + for (const videoFile of videoInstance.VideoFiles) { + videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions)) + } + await Promise.all(videoFileDestroyTasks) + + const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) + const tasks: Bluebird[] = videoFileAttributes.map(f => db.VideoFile.create(f)) + await Promise.all(tasks) + + const tags = videoAttributesToUpdate.tag.map(t => t.name) + const tagInstances = await db.Tag.findOrCreateTags(tags, t) + await videoInstance.setTags(tagInstances, sequelizeOptions) + }) + + logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) + } catch (err) { + if (videoInstance !== undefined && videoFieldsSave !== undefined) { + resetSequelizeInstance(videoInstance, videoFieldsSave) + } + + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video.', err) + throw err + } +} + +async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { + const options = { + arguments: [ account, videoChannel ], + errorMessage: 'Cannot update the remote video channel with many retries.' + } + + await retryTransactionWrapper(updateRemoteVideoChannel, options) +} + +async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) { + logger.debug('Updating remote video channel "%s".', videoChannel.uuid) + + await db.sequelize.transaction(async t => { + const sequelizeOptions = { transaction: t } + + const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id) + if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.') + + if (videoChannelInstance.Account.id !== account.id) { + throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url) + } + + videoChannelInstance.set('name', videoChannel.name) + videoChannelInstance.set('description', videoChannel.content) + videoChannelInstance.set('createdAt', videoChannel.published) + videoChannelInstance.set('updatedAt', videoChannel.updated) + + await videoChannelInstance.save(sequelizeOptions) + }) + + logger.info('Remote video channel with uuid %s updated', videoChannel.uuid) +} diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts deleted file mode 100644 index 261ff04ab..000000000 --- a/server/lib/activitypub/send-request.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { Transaction } from 'sequelize' -import { - ActivityAccept, - ActivityAdd, - ActivityCreate, - ActivityDelete, - ActivityFollow, - ActivityUpdate -} from '../../../shared/models/activitypub/activity' -import { getActivityPubUrl } from '../../helpers/activitypub' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers' -import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../models' -import { VideoAbuseInstance } from '../../models/video/video-abuse-interface' -import { activitypubHttpJobScheduler } from '../jobs' -import { ACTIVITY_PUB } from '../../initializers/constants' -import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum' - -async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const videoChannelObject = videoChannel.toActivityPubObject() - const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject) - - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const videoChannelObject = videoChannel.toActivityPubObject() - const data = await updateActivityData(videoChannel.url, byAccount, videoChannelObject) - - const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const data = await deleteActivityData(videoChannel.url, byAccount) - - const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendAddVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const videoObject = video.toActivityPubObject() - const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject) - - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendUpdateVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const videoObject = video.toActivityPubObject() - const data = await updateActivityData(video.url, byAccount, videoObject) - - const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const data = await deleteActivityData(video.url, byAccount) - - const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteAccount (account: AccountInstance, t: Transaction) { - const data = await deleteActivityData(account.url, account) - - return broadcastToFollowers(data, account, [ account ], t) -} - -async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { - const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce' - const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) - - const data = await announceActivityData(url, byAccount, announcedActivity) - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { - const url = getActivityPubUrl('video', video.uuid) + '#announce' - - const videoChannel = video.VideoChannel - const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) - - const data = await announceActivityData(url, byAccount, announcedActivity) - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) { - const url = getActivityPubUrl('videoAbuse', videoAbuse.id.toString()) - const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject()) - - return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) -} - -async function sendAccept (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) { - const data = await acceptActivityData(byAccount) - - return unicastTo(data, byAccount, toAccount.inboxUrl, t) -} - -async function sendFollow (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) { - const data = await followActivityData(toAccount.url, byAccount) - - return unicastTo(data, byAccount, toAccount.inboxUrl, t) -} - -// --------------------------------------------------------------------------- - -export { - sendCreateVideoChannel, - sendUpdateVideoChannel, - sendDeleteVideoChannel, - sendAddVideo, - sendUpdateVideo, - sendDeleteVideo, - sendDeleteAccount, - sendAccept, - sendFollow, - sendVideoAbuse, - sendVideoChannelAnnounce, - sendVideoAnnounce -} - -// --------------------------------------------------------------------------- - -async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) { - const toAccountFollowerIds = toAccountFollowers.map(a => a.id) - const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) - if (result.data.length === 0) { - logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) - return undefined - } - - const jobPayload = { - uris: result.data, - signatureAccountId: byAccount.id, - body: data - } - - return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) -} - -async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { - const jobPayload = { - uris: [ toAccountUrl ], - signatureAccountId: byAccount.id, - body: data - } - - return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) -} - -async function getAudience (accountSender: AccountInstance, isPublic = true) { - const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() - - // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47 - let to = [] - let cc = [] - - if (isPublic) { - to = [ ACTIVITY_PUB.PUBLIC ] - cc = followerInboxUrls - } else { // Unlisted - to = followerInboxUrls - cc = [ ACTIVITY_PUB.PUBLIC ] - } - - return { to, cc } -} - -async function createActivityData (url: string, byAccount: AccountInstance, object: any) { - const { to, cc } = await getAudience(byAccount) - const activity: ActivityCreate = { - type: 'Create', - id: url, - actor: byAccount.url, - to, - cc, - object - } - - return activity -} - -async function updateActivityData (url: string, byAccount: AccountInstance, object: any) { - const { to, cc } = await getAudience(byAccount) - const activity: ActivityUpdate = { - type: 'Update', - id: url, - actor: byAccount.url, - to, - cc, - object - } - - return activity -} - -async function deleteActivityData (url: string, byAccount: AccountInstance) { - const activity: ActivityDelete = { - type: 'Delete', - id: url, - actor: byAccount.url - } - - return activity -} - -async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) { - const videoPublic = video.privacy === VideoPrivacy.PUBLIC - - const { to, cc } = await getAudience(byAccount, videoPublic) - const activity: ActivityAdd = { - type: 'Add', - id: url, - actor: byAccount.url, - to, - cc, - object, - target - } - - return activity -} - -async function announceActivityData (url: string, byAccount: AccountInstance, object: any) { - const activity = { - type: 'Announce', - id: url, - actor: byAccount.url, - object - } - - return activity -} - -async function followActivityData (url: string, byAccount: AccountInstance) { - const activity: ActivityFollow = { - type: 'Follow', - id: byAccount.url, - actor: byAccount.url, - object: url - } - - return activity -} - -async function acceptActivityData (byAccount: AccountInstance) { - const activity: ActivityAccept = { - type: 'Accept', - id: byAccount.url, - actor: byAccount.url - } - - return activity -} diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts new file mode 100644 index 000000000..5f15dd4b5 --- /dev/null +++ b/server/lib/activitypub/send/index.ts @@ -0,0 +1,7 @@ +export * from './send-accept' +export * from './send-add' +export * from './send-announce' +export * from './send-create' +export * from './send-delete' +export * from './send-follow' +export * from './send-update' diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts new file mode 100644 index 000000000..bea955b67 --- /dev/null +++ b/server/lib/activitypub/send/misc.ts @@ -0,0 +1,58 @@ +import { Transaction } from 'sequelize' +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' + +async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) { + const toAccountFollowerIds = toAccountFollowers.map(a => a.id) + const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) + if (result.data.length === 0) { + logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) + return undefined + } + + const jobPayload = { + uris: result.data, + signatureAccountId: byAccount.id, + body: data + } + + return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) +} + +async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { + const jobPayload = { + uris: [ toAccountUrl ], + signatureAccountId: byAccount.id, + body: data + } + + return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) +} + +async function getAudience (accountSender: AccountInstance, isPublic = true) { + const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() + + // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47 + let to = [] + let cc = [] + + if (isPublic) { + to = [ ACTIVITY_PUB.PUBLIC ] + cc = followerInboxUrls + } else { // Unlisted + to = followerInboxUrls + cc = [ ACTIVITY_PUB.PUBLIC ] + } + + return { to, cc } +} + +// --------------------------------------------------------------------------- + +export { + broadcastToFollowers, + unicastTo, + getAudience +} diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts new file mode 100644 index 000000000..0324a30fa --- /dev/null +++ b/server/lib/activitypub/send/send-accept.ts @@ -0,0 +1,34 @@ +import { Transaction } from 'sequelize' +import { ActivityAccept } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowAcceptActivityPubUrl } from '../../../helpers/activitypub' + +async function sendAccept (accountFollow: AccountFollowInstance, t: Transaction) { + const follower = accountFollow.AccountFollower + const me = accountFollow.AccountFollowing + + const url = getAccountFollowAcceptActivityPubUrl(accountFollow) + const data = await acceptActivityData(url, me) + + return unicastTo(data, me, follower.inboxUrl, t) +} + +// --------------------------------------------------------------------------- + +export { + sendAccept +} + +// --------------------------------------------------------------------------- + +async function acceptActivityData (url: string, byAccount: AccountInstance) { + const activity: ActivityAccept = { + type: 'Accept', + id: url, + actor: byAccount.url + } + + return activity +} diff --git a/server/lib/activitypub/send/send-add.ts b/server/lib/activitypub/send/send-add.ts new file mode 100644 index 000000000..3012b7533 --- /dev/null +++ b/server/lib/activitypub/send/send-add.ts @@ -0,0 +1,38 @@ +import { Transaction } from 'sequelize' +import { ActivityAdd } from '../../../../shared/models/activitypub/activity' +import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' +import { AccountInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers, getAudience } from './misc' + +async function sendAddVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const videoObject = video.toActivityPubObject() + const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject) + + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) { + const videoPublic = video.privacy === VideoPrivacy.PUBLIC + + const { to, cc } = await getAudience(byAccount, videoPublic) + const activity: ActivityAdd = { + type: 'Add', + id: url, + actor: byAccount.url, + to, + cc, + object, + target + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + addActivityData, + sendAddVideo +} diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts new file mode 100644 index 000000000..b9217e4f6 --- /dev/null +++ b/server/lib/activitypub/send/send-announce.ts @@ -0,0 +1,45 @@ +import { Transaction } from 'sequelize' +import { AccountInstance, VideoInstance } from '../../../models' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { broadcastToFollowers } from './misc' +import { addActivityData } from './send-add' +import { createActivityData } from './send-create' +import { getAnnounceActivityPubUrl } from '../../../helpers/activitypub' + +async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(video.url, byAccount) + + const videoChannel = video.VideoChannel + const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) + + const data = await announceActivityData(url, byAccount, announcedActivity) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount) + const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) + + const data = await announceActivityData(url, byAccount, announcedActivity) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +// --------------------------------------------------------------------------- + +export { + sendVideoAnnounce, + sendVideoChannelAnnounce +} + +// --------------------------------------------------------------------------- + +async function announceActivityData (url: string, byAccount: AccountInstance, object: any) { + const activity = { + type: 'Announce', + id: url, + actor: byAccount.url, + object + } + + return activity +} diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts new file mode 100644 index 000000000..66bfeee89 --- /dev/null +++ b/server/lib/activitypub/send/send-create.ts @@ -0,0 +1,44 @@ +import { Transaction } from 'sequelize' +import { ActivityCreate } from '../../../../shared/models/activitypub/activity' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' +import { broadcastToFollowers, getAudience, unicastTo } from './misc' +import { getVideoAbuseActivityPubUrl } from '../../../helpers/activitypub' + +async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const videoChannelObject = videoChannel.toActivityPubObject() + const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject) + + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) { + const url = getVideoAbuseActivityPubUrl(videoAbuse) + const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject()) + + return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) +} + +async function createActivityData (url: string, byAccount: AccountInstance, object: any) { + const { to, cc } = await getAudience(byAccount) + const activity: ActivityCreate = { + type: 'Create', + id: url, + actor: byAccount.url, + to, + cc, + object + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + sendCreateVideoChannel, + sendVideoAbuse, + createActivityData +} diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts new file mode 100644 index 000000000..5be0e2d24 --- /dev/null +++ b/server/lib/activitypub/send/send-delete.ts @@ -0,0 +1,53 @@ +import { Transaction } from 'sequelize' +import { ActivityDelete } from '../../../../shared/models/activitypub/activity' +import { database as db } from '../../../initializers' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers } from './misc' + +async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const data = await deleteActivityData(videoChannel.url, byAccount) + + const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendDeleteVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const data = await deleteActivityData(video.url, byAccount) + + const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendDeleteAccount (account: AccountInstance, t: Transaction) { + const data = await deleteActivityData(account.url, account) + + return broadcastToFollowers(data, account, [ account ], t) +} + +// --------------------------------------------------------------------------- + +export { + sendDeleteVideoChannel, + sendDeleteVideo, + sendDeleteAccount +} + +// --------------------------------------------------------------------------- + +async function deleteActivityData (url: string, byAccount: AccountInstance) { + const activity: ActivityDelete = { + type: 'Delete', + id: url, + actor: byAccount.url + } + + return activity +} diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts new file mode 100644 index 000000000..48d641c22 --- /dev/null +++ b/server/lib/activitypub/send/send-follow.ts @@ -0,0 +1,34 @@ +import { Transaction } from 'sequelize' +import { ActivityFollow } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowActivityPubUrl } from '../../../helpers/activitypub' + +async function sendFollow (accountFollow: AccountFollowInstance, t: Transaction) { + const me = accountFollow.AccountFollower + const following = accountFollow.AccountFollowing + + const url = getAccountFollowActivityPubUrl(accountFollow) + const data = await followActivityData(url, me, following) + + return unicastTo(data, me, following.inboxUrl, t) +} + +async function followActivityData (url: string, byAccount: AccountInstance, targetAccount: AccountInstance) { + const activity: ActivityFollow = { + type: 'Follow', + id: url, + actor: byAccount.url, + object: targetAccount.url + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + sendFollow, + followActivityData +} diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts new file mode 100644 index 000000000..39da824f3 --- /dev/null +++ b/server/lib/activitypub/send/send-undo.ts @@ -0,0 +1,39 @@ +import { Transaction } from 'sequelize' +import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../../../helpers/activitypub' +import { followActivityData } from './send-follow' + +async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) { + const me = accountFollow.AccountFollower + const following = accountFollow.AccountFollowing + + const followUrl = getAccountFollowActivityPubUrl(accountFollow) + const undoUrl = getUndoActivityPubUrl(followUrl) + + const object = await followActivityData(followUrl, me, following) + const data = await undoActivityData(undoUrl, me, object) + + return unicastTo(data, me, following.inboxUrl, t) +} + +// --------------------------------------------------------------------------- + +export { + sendUndoFollow +} + +// --------------------------------------------------------------------------- + +async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) { + const activity: ActivityUndo = { + type: 'Undo', + id: url, + actor: byAccount.url, + object + } + + return activity +} diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts new file mode 100644 index 000000000..42738f973 --- /dev/null +++ b/server/lib/activitypub/send/send-update.ts @@ -0,0 +1,55 @@ +import { Transaction } from 'sequelize' +import { ActivityUpdate } from '../../../../shared/models/activitypub/activity' +import { getUpdateActivityPubUrl } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers, getAudience } from './misc' + +async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const url = getUpdateActivityPubUrl(videoChannel.url, videoChannel.updatedAt.toISOString()) + const videoChannelObject = videoChannel.toActivityPubObject() + const data = await updateActivityData(url, byAccount, videoChannelObject) + + const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendUpdateVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) + const videoObject = video.toActivityPubObject() + const data = await updateActivityData(url, byAccount, videoObject) + + const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +// --------------------------------------------------------------------------- + +export { + sendUpdateVideoChannel, + sendUpdateVideo +} + +// --------------------------------------------------------------------------- + +async function updateActivityData (url: string, byAccount: AccountInstance, object: any) { + const { to, cc } = await getAudience(byAccount) + const activity: ActivityUpdate = { + type: 'Update', + id: url, + actor: byAccount.url, + to, + cc, + object + } + + return activity +} -- cgit v1.2.3