diff options
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/activitypub/misc.ts | 77 | ||||
-rw-r--r-- | server/lib/activitypub/process-add.ts | 72 | ||||
-rw-r--r-- | server/lib/activitypub/process-create.ts | 104 | ||||
-rw-r--r-- | server/lib/activitypub/process-update.ts | 127 |
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 @@ | |||
1 | import * as magnetUtil from 'magnet-uri' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { VideoTorrentObject } from '../../../shared' | ||
4 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
5 | import { database as db } from '../../initializers' | ||
6 | import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants' | ||
7 | import { VideoChannelInstance } from '../../models/video/video-channel-interface' | ||
8 | import { VideoFileAttributes } from '../../models/video/video-file-interface' | ||
9 | import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' | ||
10 | |||
11 | async 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 | |||
43 | function 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 | |||
74 | export { | ||
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 @@ | |||
1 | import { VideoTorrentObject } from '../../../shared' | ||
2 | import { ActivityAdd } from '../../../shared/models/activitypub/activity' | ||
3 | import { generateThumbnailFromUrl, logger, retryTransactionWrapper, getOrCreateAccount } from '../../helpers' | ||
4 | import { database as db } from '../../initializers' | ||
5 | import { AccountInstance } from '../../models/account/account-interface' | ||
6 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | ||
7 | import Bluebird = require('bluebird') | ||
8 | |||
9 | async 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 | |||
24 | export { | ||
25 | processAddActivity | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | function 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 | |||
39 | async 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 @@ | |||
1 | import { | 1 | import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared' |
2 | ActivityCreate, | 2 | import { ActivityAdd } from '../../../shared/models/activitypub/activity' |
3 | VideoTorrentObject, | 3 | import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers' |
4 | VideoChannelObject | ||
5 | } from '../../../shared' | ||
6 | import { database as db } from '../../initializers' | 4 | import { database as db } from '../../initializers' |
7 | import { logger, retryTransactionWrapper } from '../../helpers' | 5 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
6 | import Bluebird = require('bluebird') | ||
7 | import { AccountInstance } from '../../models/account/account-interface' | ||
8 | import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' | ||
8 | 9 | ||
9 | function processCreateActivity (activity: ActivityCreate) { | 10 | async 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 | ||
31 | function processCreateVideo (video: VideoTorrentObject) { | 31 | function 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 | ||
40 | async function addRemoteVideo (videoToCreateData: VideoTorrentObject) { | 40 | async 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 | |||
102 | function 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 @@ | |||
1 | import { | 1 | import { VideoChannelObject, VideoTorrentObject } from '../../../shared' |
2 | ActivityCreate, | 2 | import { ActivityUpdate } from '../../../shared/models/activitypub/activity' |
3 | VideoTorrentObject, | 3 | import { getOrCreateAccount } from '../../helpers/activitypub' |
4 | VideoChannelObject | 4 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
5 | } from '../../../shared' | 5 | import { logger } from '../../helpers/logger' |
6 | import { resetSequelizeInstance } from '../../helpers/utils' | ||
7 | import { database as db } from '../../initializers' | ||
8 | import { AccountInstance } from '../../models/account/account-interface' | ||
9 | import { VideoInstance } from '../../models/video/video-interface' | ||
10 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | ||
11 | import Bluebird = require('bluebird') | ||
12 | |||
13 | async function processUpdateActivity (activity: ActivityUpdate) { | ||
14 | const account = await getOrCreateAccount(activity.actor) | ||
6 | 15 | ||
7 | function 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 | ||
23 | function processUpdateVideo (video: VideoTorrentObject) { | 33 | function 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 | ||
27 | function processUpdateVideoChannel (videoChannel: VideoChannelObject) { | 42 | async 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 | |||
105 | async 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 | |||
114 | async 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 | } |