aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-11-10 14:34:45 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-11-27 19:40:51 +0100
commit0d0e8dd0904b380b70e19ebcb4763d65601c4632 (patch)
treeacb625d7c88fbe863fa14bf6783fafe4a8e35137 /server/lib/activitypub
parente4f97babf701481b55cc10fb3448feab5f97c867 (diff)
downloadPeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.tar.gz
PeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.tar.zst
PeerTube-0d0e8dd0904b380b70e19ebcb4763d65601c4632.zip
Continue activitypub
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/misc.ts77
-rw-r--r--server/lib/activitypub/process-add.ts72
-rw-r--r--server/lib/activitypub/process-create.ts104
-rw-r--r--server/lib/activitypub/process-update.ts127
4 files changed, 298 insertions, 82 deletions
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts
new file mode 100644
index 000000000..05e77ebc3
--- /dev/null
+++ b/server/lib/activitypub/misc.ts
@@ -0,0 +1,77 @@
1import * as magnetUtil from 'magnet-uri'
2import * as Sequelize from 'sequelize'
3import { VideoTorrentObject } from '../../../shared'
4import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
5import { database as db } from '../../initializers'
6import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants'
7import { VideoChannelInstance } from '../../models/video/video-channel-interface'
8import { VideoFileAttributes } from '../../models/video/video-file-interface'
9import { VideoAttributes, VideoInstance } from '../../models/video/video-interface'
10
11async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelInstance, videoObject: VideoTorrentObject, t: Sequelize.Transaction) {
12 const videoFromDatabase = await db.Video.loadByUUIDOrURL(videoObject.uuid, videoObject.id, t)
13 if (videoFromDatabase) throw new Error('Video with this UUID/Url already exists.')
14
15 const duration = videoObject.duration.replace(/[^\d]+/, '')
16 const videoData: VideoAttributes = {
17 name: videoObject.name,
18 uuid: videoObject.uuid,
19 url: videoObject.id,
20 category: parseInt(videoObject.category.identifier, 10),
21 licence: parseInt(videoObject.licence.identifier, 10),
22 language: parseInt(videoObject.language.identifier, 10),
23 nsfw: videoObject.nsfw,
24 description: videoObject.content,
25 channelId: videoChannel.id,
26 duration: parseInt(duration, 10),
27 createdAt: videoObject.published,
28 // FIXME: updatedAt does not seems to be considered by Sequelize
29 updatedAt: videoObject.updated,
30 views: videoObject.views,
31 likes: 0,
32 dislikes: 0,
33 // likes: videoToCreateData.likes,
34 // dislikes: videoToCreateData.dislikes,
35 remote: true,
36 privacy: 1
37 // privacy: videoToCreateData.privacy
38 }
39
40 return videoData
41}
42
43function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) {
44 const fileUrls = videoObject.url
45 .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1)
46
47 const attributes: VideoFileAttributes[] = []
48 for (const url of fileUrls) {
49 // Fetch associated magnet uri
50 const magnet = videoObject.url
51 .find(u => {
52 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width
53 })
54 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url)
55
56 const parsed = magnetUtil.decode(magnet.url)
57 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url)
58
59 const attribute = {
60 extname: VIDEO_MIMETYPE_EXT[url.mimeType],
61 infoHash: parsed.infoHash,
62 resolution: url.width,
63 size: url.size,
64 videoId: videoCreated.id
65 }
66 attributes.push(attribute)
67 }
68
69 return attributes
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 videoFileActivityUrlToDBAttributes,
76 videoActivityObjectToDBAttributes
77}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
new file mode 100644
index 000000000..40541aca3
--- /dev/null
+++ b/server/lib/activitypub/process-add.ts
@@ -0,0 +1,72 @@
1import { VideoTorrentObject } from '../../../shared'
2import { ActivityAdd } from '../../../shared/models/activitypub/activity'
3import { generateThumbnailFromUrl, logger, retryTransactionWrapper, getOrCreateAccount } from '../../helpers'
4import { database as db } from '../../initializers'
5import { AccountInstance } from '../../models/account/account-interface'
6import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
7import Bluebird = require('bluebird')
8
9async function processAddActivity (activity: ActivityAdd) {
10 const activityObject = activity.object
11 const activityType = activityObject.type
12 const account = await getOrCreateAccount(activity.actor)
13
14 if (activityType === 'Video') {
15 return processAddVideo(account, activity.id, activityObject as VideoTorrentObject)
16 }
17
18 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
19 return Promise.resolve(undefined)
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 processAddActivity
26}
27
28// ---------------------------------------------------------------------------
29
30function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
31 const options = {
32 arguments: [ account, videoChannelUrl ,video ],
33 errorMessage: 'Cannot insert the remote video with many retries.'
34 }
35
36 return retryTransactionWrapper(addRemoteVideo, options)
37}
38
39async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) {
40 logger.debug('Adding remote video %s.', videoToCreateData.url)
41
42 await db.sequelize.transaction(async t => {
43 const sequelizeOptions = {
44 transaction: t
45 }
46
47 const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t)
48 if (!videoChannel) throw new Error('Video channel not found.')
49
50 if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.')
51
52 const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t)
53 const video = db.Video.build(videoData)
54
55 // Don't block on request
56 generateThumbnailFromUrl(video, videoToCreateData.icon)
57 .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
58
59 const videoCreated = await video.save(sequelizeOptions)
60
61 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData)
62
63 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
64 await Promise.all(tasks)
65
66 const tags = videoToCreateData.tag.map(t => t.name)
67 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
68 await videoCreated.setTags(tagInstances, sequelizeOptions)
69 })
70
71 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid)
72}
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts
index 114ff1848..471674ead 100644
--- a/server/lib/activitypub/process-create.ts
+++ b/server/lib/activitypub/process-create.ts
@@ -1,23 +1,23 @@
1import { 1import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared'
2 ActivityCreate, 2import { ActivityAdd } from '../../../shared/models/activitypub/activity'
3 VideoTorrentObject, 3import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers'
4 VideoChannelObject
5} from '../../../shared'
6import { database as db } from '../../initializers' 4import { database as db } from '../../initializers'
7import { logger, retryTransactionWrapper } from '../../helpers' 5import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
6import Bluebird = require('bluebird')
7import { AccountInstance } from '../../models/account/account-interface'
8import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
8 9
9function processCreateActivity (activity: ActivityCreate) { 10async function processCreateActivity (activity: ActivityCreate) {
10 const activityObject = activity.object 11 const activityObject = activity.object
11 const activityType = activityObject.type 12 const activityType = activityObject.type
13 const account = await getOrCreateAccount(activity.actor)
12 14
13 if (activityType === 'Video') { 15 if (activityType === 'VideoChannel') {
14 return processCreateVideo(activityObject as VideoTorrentObject) 16 return processCreateVideoChannel(account, activityObject as VideoChannelObject)
15 } else if (activityType === 'VideoChannel') {
16 return processCreateVideoChannel(activityObject as VideoChannelObject)
17 } 17 }
18 18
19 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 19 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
20 return Promise.resolve() 20 return Promise.resolve(undefined)
21} 21}
22 22
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
@@ -28,77 +28,37 @@ export {
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31function processCreateVideo (video: VideoTorrentObject) { 31function processCreateVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
32 const options = { 32 const options = {
33 arguments: [ video ], 33 arguments: [ account, videoChannelToCreateData ],
34 errorMessage: 'Cannot insert the remote video with many retries.' 34 errorMessage: 'Cannot insert the remote video channel with many retries.'
35 } 35 }
36 36
37 return retryTransactionWrapper(addRemoteVideo, options) 37 return retryTransactionWrapper(addRemoteVideoChannel, options)
38} 38}
39 39
40async function addRemoteVideo (videoToCreateData: VideoTorrentObject) { 40async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) {
41 logger.debug('Adding remote video %s.', videoToCreateData.url) 41 logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid)
42 42
43 await db.sequelize.transaction(async t => { 43 await db.sequelize.transaction(async t => {
44 const sequelizeOptions = { 44 let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t)
45 transaction: t 45 if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.')
46 } 46
47 47 const videoChannelData = {
48 const videoFromDatabase = await db.Video.loadByUUID(videoToCreateData.uuid) 48 name: videoChannelToCreateData.name,
49 if (videoFromDatabase) throw new Error('UUID already exists.') 49 description: videoChannelToCreateData.content,
50 50 uuid: videoChannelToCreateData.uuid,
51 const videoChannel = await db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) 51 createdAt: videoChannelToCreateData.published,
52 if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') 52 updatedAt: videoChannelToCreateData.updated,
53
54 const tags = videoToCreateData.tags
55 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
56
57 const videoData = {
58 name: videoToCreateData.name,
59 uuid: videoToCreateData.uuid,
60 category: videoToCreateData.category,
61 licence: videoToCreateData.licence,
62 language: videoToCreateData.language,
63 nsfw: videoToCreateData.nsfw,
64 description: videoToCreateData.truncatedDescription,
65 channelId: videoChannel.id,
66 duration: videoToCreateData.duration,
67 createdAt: videoToCreateData.createdAt,
68 // FIXME: updatedAt does not seems to be considered by Sequelize
69 updatedAt: videoToCreateData.updatedAt,
70 views: videoToCreateData.views,
71 likes: videoToCreateData.likes,
72 dislikes: videoToCreateData.dislikes,
73 remote: true, 53 remote: true,
74 privacy: videoToCreateData.privacy 54 accountId: account.id
75 }
76
77 const video = db.Video.build(videoData)
78 await db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData)
79 const videoCreated = await video.save(sequelizeOptions)
80
81 const tasks = []
82 for (const fileData of videoToCreateData.files) {
83 const videoFileInstance = db.VideoFile.build({
84 extname: fileData.extname,
85 infoHash: fileData.infoHash,
86 resolution: fileData.resolution,
87 size: fileData.size,
88 videoId: videoCreated.id
89 })
90
91 tasks.push(videoFileInstance.save(sequelizeOptions))
92 } 55 }
93 56
94 await Promise.all(tasks) 57 videoChannel = db.VideoChannel.build(videoChannelData)
58 videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid)
95 59
96 await videoCreated.setTags(tagInstances, sequelizeOptions) 60 await videoChannel.save({ transaction: t })
97 }) 61 })
98 62
99 logger.info('Remote video with uuid %s inserted.', videoToCreateData.uuid) 63 logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
100}
101
102function processCreateVideoChannel (videoChannel: VideoChannelObject) {
103
104} 64}
diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process-update.ts
index 187c7be7c..cd8a4b8e2 100644
--- a/server/lib/activitypub/process-update.ts
+++ b/server/lib/activitypub/process-update.ts
@@ -1,15 +1,25 @@
1import { 1import { VideoChannelObject, VideoTorrentObject } from '../../../shared'
2 ActivityCreate, 2import { ActivityUpdate } from '../../../shared/models/activitypub/activity'
3 VideoTorrentObject, 3import { getOrCreateAccount } from '../../helpers/activitypub'
4 VideoChannelObject 4import { retryTransactionWrapper } from '../../helpers/database-utils'
5} from '../../../shared' 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)
6 15
7function processUpdateActivity (activity: ActivityCreate) {
8 if (activity.object.type === 'Video') { 16 if (activity.object.type === 'Video') {
9 return processUpdateVideo(activity.object) 17 return processUpdateVideo(account, activity.object)
10 } else if (activity.object.type === 'VideoChannel') { 18 } else if (activity.object.type === 'VideoChannel') {
11 return processUpdateVideoChannel(activity.object) 19 return processUpdateVideoChannel(account, activity.object)
12 } 20 }
21
22 return undefined
13} 23}
14 24
15// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
@@ -20,10 +30,107 @@ export {
20 30
21// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
22 32
23function processUpdateVideo (video: VideoTorrentObject) { 33function processUpdateVideo (account: AccountInstance, video: VideoTorrentObject) {
34 const options = {
35 arguments: [ account, video ],
36 errorMessage: 'Cannot update the remote video with many retries'
37 }
24 38
39 return retryTransactionWrapper(updateRemoteVideo, options)
25} 40}
26 41
27function processUpdateVideoChannel (videoChannel: VideoChannelObject) { 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.loadByUrl(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, t)
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 // videoInstance.set('privacy', videoData.privacy)
74
75 await videoInstance.save(sequelizeOptions)
76
77 // Remove old video files
78 const videoFileDestroyTasks: Bluebird<void>[] = []
79 for (const videoFile of videoInstance.VideoFiles) {
80 videoFileDestroyTasks.push(videoFile.destroy(sequelizeOptions))
81 }
82 await Promise.all(videoFileDestroyTasks)
83
84 const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
85 const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f))
86 await Promise.all(tasks)
87
88 const tags = videoAttributesToUpdate.tag.map(t => t.name)
89 const tagInstances = await db.Tag.findOrCreateTags(tags, t)
90 await videoInstance.setTags(tagInstances, sequelizeOptions)
91 })
92
93 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
94 } catch (err) {
95 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
96 resetSequelizeInstance(videoInstance, videoFieldsSave)
97 }
98
99 // This is just a debug because we will retry the insert
100 logger.debug('Cannot update the remote video.', err)
101 throw err
102 }
103}
104
105async function processUpdateVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
106 const options = {
107 arguments: [ account, videoChannel ],
108 errorMessage: 'Cannot update the remote video channel with many retries.'
109 }
110
111 await retryTransactionWrapper(updateRemoteVideoChannel, options)
112}
113
114async function updateRemoteVideoChannel (account: AccountInstance, videoChannel: VideoChannelObject) {
115 logger.debug('Updating remote video channel "%s".', videoChannel.uuid)
116
117 await db.sequelize.transaction(async t => {
118 const sequelizeOptions = { transaction: t }
119
120 const videoChannelInstance = await db.VideoChannel.loadByUrl(videoChannel.id)
121 if (!videoChannelInstance) throw new Error('Video ' + videoChannel.id + ' not found.')
122
123 if (videoChannelInstance.Account.id !== account.id) {
124 throw new Error('Account ' + account.id + ' does not own video channel ' + videoChannelInstance.url)
125 }
126
127 videoChannelInstance.set('name', videoChannel.name)
128 videoChannelInstance.set('description', videoChannel.content)
129 videoChannelInstance.set('createdAt', videoChannel.published)
130 videoChannelInstance.set('updatedAt', videoChannel.updated)
131
132 await videoChannelInstance.save(sequelizeOptions)
133 })
28 134
135 logger.info('Remote video channel with uuid %s updated', videoChannel.uuid)
29} 136}