aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/process
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-11-20 09:43:39 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-11-27 19:40:52 +0100
commit54141398354e6e7b94aa3065a705a1251390111c (patch)
tree8d30d1b9ea8acbe04f6d404125b04fc0c9897b70 /server/lib/activitypub/process
parenteb8b27c93e61a896a08923dc1ca3c87ba8cf4948 (diff)
downloadPeerTube-54141398354e6e7b94aa3065a705a1251390111c.tar.gz
PeerTube-54141398354e6e7b94aa3065a705a1251390111c.tar.zst
PeerTube-54141398354e6e7b94aa3065a705a1251390111c.zip
Refractor activity pub lib/helpers
Diffstat (limited to 'server/lib/activitypub/process')
-rw-r--r--server/lib/activitypub/process/index.ts8
-rw-r--r--server/lib/activitypub/process/misc.ts101
-rw-r--r--server/lib/activitypub/process/process-accept.ts27
-rw-r--r--server/lib/activitypub/process/process-add.ts87
-rw-r--r--server/lib/activitypub/process/process-announce.ts46
-rw-r--r--server/lib/activitypub/process/process-create.ts88
-rw-r--r--server/lib/activitypub/process/process-delete.ts105
-rw-r--r--server/lib/activitypub/process/process-follow.ts59
-rw-r--r--server/lib/activitypub/process/process-undo.ts31
-rw-r--r--server/lib/activitypub/process/process-update.ts135
10 files changed, 687 insertions, 0 deletions
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 @@
1export * from './process-accept'
2export * from './process-add'
3export * from './process-announce'
4export * from './process-create'
5export * from './process-delete'
6export * from './process-follow'
7export * from './process-undo'
8export * 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 @@
1import * as magnetUtil from 'magnet-uri'
2import { VideoTorrentObject } from '../../../../shared'
3import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object'
4import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos'
5import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants'
6import { AccountInstance } from '../../../models/account/account-interface'
7import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
8import { VideoFileAttributes } from '../../../models/video/video-file-interface'
9import { VideoAttributes, VideoInstance } from '../../../models/video/video-interface'
10import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
11
12function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) {
13 return {
14 name: videoChannelObject.name,
15 description: videoChannelObject.content,
16 uuid: videoChannelObject.uuid,
17 url: videoChannelObject.id,
18 createdAt: new Date(videoChannelObject.published),
19 updatedAt: new Date(videoChannelObject.updated),
20 remote: true,
21 accountId: account.id
22 }
23}
24
25async function videoActivityObjectToDBAttributes (
26 videoChannel: VideoChannelInstance,
27 videoObject: VideoTorrentObject,
28 to: string[] = [],
29 cc: string[] = []
30) {
31 let privacy = VideoPrivacy.PRIVATE
32 if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC
33 else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED
34
35 const duration = videoObject.duration.replace(/[^\d]+/, '')
36 const videoData: VideoAttributes = {
37 name: videoObject.name,
38 uuid: videoObject.uuid,
39 url: videoObject.id,
40 category: parseInt(videoObject.category.identifier, 10),
41 licence: parseInt(videoObject.licence.identifier, 10),
42 language: parseInt(videoObject.language.identifier, 10),
43 nsfw: videoObject.nsfw,
44 description: videoObject.content,
45 channelId: videoChannel.id,
46 duration: parseInt(duration, 10),
47 createdAt: new Date(videoObject.published),
48 // FIXME: updatedAt does not seems to be considered by Sequelize
49 updatedAt: new Date(videoObject.updated),
50 views: videoObject.views,
51 likes: 0,
52 dislikes: 0,
53 remote: true,
54 privacy
55 }
56
57 return videoData
58}
59
60function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
61 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT)
62 const fileUrls = videoObject.url.filter(u => {
63 return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/')
64 })
65
66 if (fileUrls.length === 0) {
67 throw new Error('Cannot find video files for ' + videoCreated.url)
68 }
69
70 const attributes: VideoFileAttributes[] = []
71 for (const fileUrl of fileUrls) {
72 // Fetch associated magnet uri
73 const magnet = videoObject.url.find(u => {
74 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width
75 })
76
77 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url)
78
79 const parsed = magnetUtil.decode(magnet.url)
80 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
81
82 const attribute = {
83 extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType],
84 infoHash: parsed.infoHash,
85 resolution: fileUrl.width,
86 size: fileUrl.size,
87 videoId: videoCreated.id
88 }
89 attributes.push(attribute)
90 }
91
92 return attributes
93}
94
95// ---------------------------------------------------------------------------
96
97export {
98 videoFileActivityUrlToDBAttributes,
99 videoActivityObjectToDBAttributes,
100 videoChannelActivityObjectToDBAttributes
101}
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 @@
1import { ActivityAccept } from '../../../../shared/models/activitypub/activity'
2import { database as db } from '../../../initializers'
3import { AccountInstance } from '../../../models/account/account-interface'
4
5async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) {
6 if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
7
8 const targetAccount = await db.Account.loadByUrl(activity.actor)
9
10 return processAccept(inboxAccount, targetAccount)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 processAcceptActivity
17}
18
19// ---------------------------------------------------------------------------
20
21async function processAccept (account: AccountInstance, targetAccount: AccountInstance) {
22 const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
23 if (!follow) throw new Error('Cannot find associated follow.')
24
25 follow.set('state', 'accepted')
26 await follow.save()
27}
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 @@
1import * as Bluebird from 'bluebird'
2import { VideoTorrentObject } from '../../../../shared'
3import { ActivityAdd } from '../../../../shared/models/activitypub/activity'
4import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../../helpers'
5import { getOrCreateVideoChannel } from '../../../helpers/activitypub'
6import { database as db } from '../../../initializers'
7import { AccountInstance } from '../../../models/account/account-interface'
8import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
9import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
10
11async function processAddActivity (activity: ActivityAdd) {
12 const activityObject = activity.object
13 const activityType = activityObject.type
14 const account = await getOrCreateAccount(activity.actor)
15
16 if (activityType === 'Video') {
17 const videoChannelUrl = activity.target
18 const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl)
19
20 return processAddVideo(account, activity, videoChannel, activityObject)
21 }
22
23 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
24 return Promise.resolve(undefined)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 processAddActivity
31}
32
33// ---------------------------------------------------------------------------
34
35function processAddVideo (account: AccountInstance, activity: ActivityAdd, videoChannel: VideoChannelInstance, video: VideoTorrentObject) {
36 const options = {
37 arguments: [ account, activity, videoChannel, video ],
38 errorMessage: 'Cannot insert the remote video with many retries.'
39 }
40
41 return retryTransactionWrapper(addRemoteVideo, options)
42}
43
44function addRemoteVideo (
45 account: AccountInstance,
46 activity: ActivityAdd,
47 videoChannel: VideoChannelInstance,
48 videoToCreateData: VideoTorrentObject
49) {
50 logger.debug('Adding remote video %s.', videoToCreateData.url)
51
52 return db.sequelize.transaction(async t => {
53 const sequelizeOptions = {
54 transaction: t
55 }
56
57 if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
58
59 const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoToCreateData.uuid, videoToCreateData.id, t)
60 if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.')
61
62 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, activity.to, activity.cc)
63 const video = db.Video.build(videoData)
64
65 // Don't block on request
66 generateThumbnailFromUrl(video, videoToCreateData.icon)
67 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
68
69 const videoCreated = await video.save(sequelizeOptions)
70
71 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
72 if (videoFileAttributes.length === 0) {
73 throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url)
74 }
75
76 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t }))
77 await Promise.all(tasks)
78
79 const tags = videoToCreateData.tag.map(t => t.name)
80 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
81 await videoCreated.setTags(tagInstances, sequelizeOptions)
82
83 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
84
85 return videoCreated
86 })
87}
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 @@
1import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity'
2import { getOrCreateAccount } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger'
4import { database as db } from '../../../initializers/index'
5import { VideoInstance } from '../../../models/index'
6import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
7import { processAddActivity } from './process-add'
8import { processCreateActivity } from './process-create'
9
10async function processAnnounceActivity (activity: ActivityAnnounce) {
11 const announcedActivity = activity.object
12 const accountAnnouncer = await getOrCreateAccount(activity.actor)
13
14 if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') {
15 // Add share entry
16 const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity)
17 await db.VideoChannelShare.create({
18 accountId: accountAnnouncer.id,
19 videoChannelId: videoChannel.id
20 })
21
22 return undefined
23 } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') {
24 // Add share entry
25 const video: VideoInstance = await processAddActivity(announcedActivity)
26 await db.VideoShare.create({
27 accountId: accountAnnouncer.id,
28 videoId: video.id
29 })
30
31 return undefined
32 }
33
34 logger.warn(
35 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type,
36 { activity: activity.id }
37 )
38
39 return undefined
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 processAnnounceActivity
46}
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 @@
1import { ActivityCreate, VideoChannelObject } from '../../../../shared'
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object'
3import { logger, retryTransactionWrapper } from '../../../helpers'
4import { getOrCreateAccount, getVideoChannelActivityPubUrl } from '../../../helpers/activitypub'
5import { database as db } from '../../../initializers'
6import { AccountInstance } from '../../../models/account/account-interface'
7import { videoChannelActivityObjectToDBAttributes } from './misc'
8
9async function processCreateActivity (activity: ActivityCreate) {
10 const activityObject = activity.object
11 const activityType = activityObject.type
12 const account = await getOrCreateAccount(activity.actor)
13
14 if (activityType === 'VideoChannel') {
15 return processCreateVideoChannel(account, activityObject as VideoChannelObject)
16 } else if (activityType === 'Flag') {
17 return processCreateVideoAbuse(account, activityObject as VideoAbuseObject)
18 }
19
20 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
21 return Promise.resolve(undefined)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 processCreateActivity
28}
29
30// ---------------------------------------------------------------------------
31
32function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
33 const options = {
34 arguments: [ account, videoChannelToCreateData ],
35 errorMessage: 'Cannot insert the remote video channel with many retries.'
36 }
37
38 return retryTransactionWrapper(addRemoteVideoChannel, options)
39}
40
41function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
42 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
43
44 return db.sequelize.transaction(async t => {
45 let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
46 if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
47
48 const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account)
49 videoChannel = db.VideoChannel.build(videoChannelData)
50 videoChannel.url = getVideoChannelActivityPubUrl(videoChannel)
51
52 videoChannel = await videoChannel.save({ transaction: t })
53 logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
54
55 return videoChannel
56 })
57}
58
59function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
60 const options = {
61 arguments: [ account, videoAbuseToCreateData ],
62 errorMessage: 'Cannot insert the remote video abuse with many retries.'
63 }
64
65 return retryTransactionWrapper(addRemoteVideoAbuse, options)
66}
67
68function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
69 logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
70
71 return db.sequelize.transaction(async t => {
72 const video = await db.Video.loadByUrlAndPopulateAccount(videoAbuseToCreateData.object, t)
73 if (!video) {
74 logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object)
75 return undefined
76 }
77
78 const videoAbuseData = {
79 reporterAccountId: account.id,
80 reason: videoAbuseToCreateData.content,
81 videoId: video.id
82 }
83
84 await db.VideoAbuse.create(videoAbuseData)
85
86 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
87 })
88}
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 @@
1import { ActivityDelete } from '../../../../shared/models/activitypub/activity'
2import { getOrCreateAccount } from '../../../helpers/activitypub'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger'
5import { database as db } from '../../../initializers'
6import { AccountInstance } from '../../../models/account/account-interface'
7import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
8import { VideoInstance } from '../../../models/video/video-interface'
9
10async function processDeleteActivity (activity: ActivityDelete) {
11 const account = await getOrCreateAccount(activity.actor)
12
13 if (account.url === activity.id) {
14 return processDeleteAccount(account)
15 }
16
17 {
18 let videoObject = await db.Video.loadByUrlAndPopulateAccount(activity.id)
19 if (videoObject !== undefined) {
20 return processDeleteVideo(account, videoObject)
21 }
22 }
23
24 {
25 let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id)
26 if (videoChannelObject !== undefined) {
27 return processDeleteVideoChannel(account, videoChannelObject)
28 }
29 }
30
31 return
32}
33
34// ---------------------------------------------------------------------------
35
36export {
37 processDeleteActivity
38}
39
40// ---------------------------------------------------------------------------
41
42async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
43 const options = {
44 arguments: [ account, videoToDelete ],
45 errorMessage: 'Cannot remove the remote video with many retries.'
46 }
47
48 await retryTransactionWrapper(deleteRemoteVideo, options)
49}
50
51async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
52 logger.debug('Removing remote video "%s".', videoToDelete.uuid)
53
54 await db.sequelize.transaction(async t => {
55 if (videoToDelete.VideoChannel.Account.id !== account.id) {
56 throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
57 }
58
59 await videoToDelete.destroy({ transaction: t })
60 })
61
62 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
63}
64
65async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
66 const options = {
67 arguments: [ account, videoChannelToRemove ],
68 errorMessage: 'Cannot remove the remote video channel with many retries.'
69 }
70
71 await retryTransactionWrapper(deleteRemoteVideoChannel, options)
72}
73
74async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
75 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
76
77 await db.sequelize.transaction(async t => {
78 if (videoChannelToRemove.Account.id !== account.id) {
79 throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
80 }
81
82 await videoChannelToRemove.destroy({ transaction: t })
83 })
84
85 logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
86}
87
88async function processDeleteAccount (accountToRemove: AccountInstance) {
89 const options = {
90 arguments: [ accountToRemove ],
91 errorMessage: 'Cannot remove the remote account with many retries.'
92 }
93
94 await retryTransactionWrapper(deleteRemoteAccount, options)
95}
96
97async function deleteRemoteAccount (accountToRemove: AccountInstance) {
98 logger.debug('Removing remote account "%s".', accountToRemove.uuid)
99
100 await db.sequelize.transaction(async t => {
101 await accountToRemove.destroy({ transaction: t })
102 })
103
104 logger.info('Remote account with uuid %s removed.', accountToRemove.uuid)
105}
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 @@
1import { ActivityFollow } from '../../../../shared/models/activitypub/activity'
2import { getOrCreateAccount, retryTransactionWrapper } from '../../../helpers'
3import { database as db } from '../../../initializers'
4import { AccountInstance } from '../../../models/account/account-interface'
5import { logger } from '../../../helpers/logger'
6import { sendAccept } from '../send/send-accept'
7
8async function processFollowActivity (activity: ActivityFollow) {
9 const activityObject = activity.object
10 const account = await getOrCreateAccount(activity.actor)
11
12 return processFollow(account, activityObject)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 processFollowActivity
19}
20
21// ---------------------------------------------------------------------------
22
23function processFollow (account: AccountInstance, targetAccountURL: string) {
24 const options = {
25 arguments: [ account, targetAccountURL ],
26 errorMessage: 'Cannot follow with many retries.'
27 }
28
29 return retryTransactionWrapper(follow, options)
30}
31
32async function follow (account: AccountInstance, targetAccountURL: string) {
33 await db.sequelize.transaction(async t => {
34 const targetAccount = await db.Account.loadByUrl(targetAccountURL, t)
35
36 if (!targetAccount) throw new Error('Unknown account')
37 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
38
39 const [ accountFollow ] = await db.AccountFollow.findOrCreate({
40 where: {
41 accountId: account.id,
42 targetAccountId: targetAccount.id
43 },
44 defaults: {
45 accountId: account.id,
46 targetAccountId: targetAccount.id,
47 state: 'accepted'
48 },
49 transaction: t
50 })
51 accountFollow.AccountFollower = account
52 accountFollow.AccountFollowing = targetAccount
53
54 // Target sends to account he accepted the follow request
55 return sendAccept(accountFollow, t)
56 })
57
58 logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL)
59}
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 @@
1import { ActivityUndo } from '../../../../shared/models/activitypub/activity'
2import { logger } from '../../../helpers/logger'
3import { database as db } from '../../../initializers'
4
5async function processUndoActivity (activity: ActivityUndo) {
6 const activityToUndo = activity.object
7
8 if (activityToUndo.type === 'Follow') {
9 const follower = await db.Account.loadByUrl(activity.actor)
10 const following = await db.Account.loadByUrl(activityToUndo.object)
11 const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id)
12
13 if (!accountFollow) throw new Error(`'Unknown account follow (${follower.id} -> ${following.id}.`)
14
15 await accountFollow.destroy()
16
17 return undefined
18 }
19
20 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
21
22 return undefined
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 processUndoActivity
29}
30
31// ---------------------------------------------------------------------------
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 @@
1import { VideoChannelObject, VideoTorrentObject } from '../../../../shared'
2import { ActivityUpdate } from '../../../../shared/models/activitypub/activity'
3import { getOrCreateAccount } from '../../../helpers/activitypub'
4import { retryTransactionWrapper } from '../../../helpers/database-utils'
5import { logger } from '../../../helpers/logger'
6import { resetSequelizeInstance } from '../../../helpers/utils'
7import { database as db } from '../../../initializers'
8import { AccountInstance } from '../../../models/account/account-interface'
9import { VideoInstance } from '../../../models/video/video-interface'
10import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
11import Bluebird = require('bluebird')
12
13async function processUpdateActivity (activity: ActivityUpdate) {
14 const account = await getOrCreateAccount(activity.actor)
15
16 if (activity.object.type === 'Video') {
17 return processUpdateVideo(account, activity.object)
18 } else if (activity.object.type === 'VideoChannel') {
19 return processUpdateVideoChannel(account, activity.object)
20 }
21
22 return undefined
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 processUpdateActivity
29}
30
31// ---------------------------------------------------------------------------
32
33function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
34 const options = {
35 arguments: [ account, video ],
36 errorMessage: 'Cannot update the remote video with many retries'
37 }
38
39 return retryTransactionWrapper(updateRemoteVideo, options)
40}
41
42async function updateRemoteVideo (account: AccountInstance, videoAttributesToUpdate: VideoTorrentObject) {
43 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
44 let videoInstance: VideoInstance
45 let videoFieldsSave: object
46
47 try {
48 await db.sequelize.transaction(async t => {
49 const sequelizeOptions = {
50 transaction: t
51 }
52
53 const videoInstance = await db.Video.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
54 if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
55
56 if (videoInstance.VideoChannel.Account.id !== account.id) {
57 throw new Error('Account ' + account.url + ' does not own video channel ' + videoInstance.VideoChannel.url)
58 }
59
60 const videoData = await videoActivityObjectToDBAttributes(videoInstance.VideoChannel, videoAttributesToUpdate)
61 videoInstance.set('name', videoData.name)
62 videoInstance.set('category', videoData.category)
63 videoInstance.set('licence', videoData.licence)
64 videoInstance.set('language', videoData.language)
65 videoInstance.set('nsfw', videoData.nsfw)
66 videoInstance.set('description', videoData.description)
67 videoInstance.set('duration', videoData.duration)
68 videoInstance.set('createdAt', videoData.createdAt)
69 videoInstance.set('updatedAt', videoData.updatedAt)
70 videoInstance.set('views', videoData.views)
71 // videoInstance.set('likes', videoData.likes)
72 // videoInstance.set('dislikes', videoData.dislikes)
73
74 await videoInstance.save(sequelizeOptions)
75
76 // Remove old video files
77 const videoFileDestroyTasks: Bluebird<void>[] = []
78 for (const videoFile of videoInstance.VideoFiles) {
79 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
80 }
81 await Promise.all(videoFileDestroyTasks)
82
83 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
84 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
85 await Promise.all(tasks)
86
87 const tags = videoAttributesToUpdate.tag.map(t => t.name)
88 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
89 await videoInstance.setTags(tagInstances, sequelizeOptions)
90 })
91
92 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
93 } catch (err) {
94 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
95 resetSequelizeInstance(videoInstance, videoFieldsSave)
96 }
97
98 // This is just a debug because we will retry the insert
99 logger.debug('Cannot update the remote video.', err)
100 throw err
101 }
102}
103
104async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
105 const options = {
106 arguments: [ account, videoChannel ],
107 errorMessage: 'Cannot update the remote video channel with many retries.'
108 }
109
110 await retryTransactionWrapper(updateRemoteVideoChannel, options)
111}
112
113async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
114 logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
115
116 await db.sequelize.transaction(async t => {
117 const sequelizeOptions = { transaction: t }
118
119 const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
120 if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
121
122 if (videoChannelInstance.Account.id !== account.id) {
123 throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
124 }
125
126 videoChannelInstance.set('name', videoChannel.name)
127 videoChannelInstance.set('description', videoChannel.content)
128 videoChannelInstance.set('createdAt', videoChannel.published)
129 videoChannelInstance.set('updatedAt', videoChannel.updated)
130
131 await videoChannelInstance.save(sequelizeOptions)
132 })
133
134 logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
135}