diff options
Diffstat (limited to 'server/lib/activitypub/process')
-rw-r--r-- | server/lib/activitypub/process/index.ts | 8 | ||||
-rw-r--r-- | server/lib/activitypub/process/misc.ts | 101 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-accept.ts | 27 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-add.ts | 87 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-announce.ts | 46 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 88 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-delete.ts | 105 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-follow.ts | 59 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-undo.ts | 31 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 135 |
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 @@ | |||
1 | export * from './process-accept' | ||
2 | export * from './process-add' | ||
3 | export * from './process-announce' | ||
4 | export * from './process-create' | ||
5 | export * from './process-delete' | ||
6 | export * from './process-follow' | ||
7 | export * from './process-undo' | ||
8 | 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 @@ | |||
1 | import * as magnetUtil from 'magnet-uri' | ||
2 | import { VideoTorrentObject } from '../../../../shared' | ||
3 | import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object' | ||
4 | import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos' | ||
5 | import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants' | ||
6 | import { AccountInstance } from '../../../models/account/account-interface' | ||
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 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | ||
11 | |||
12 | function 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 | |||
25 | async 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 | |||
60 | function 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 | |||
97 | export { | ||
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 @@ | |||
1 | import { ActivityAccept } from '../../../../shared/models/activitypub/activity' | ||
2 | import { database as db } from '../../../initializers' | ||
3 | import { AccountInstance } from '../../../models/account/account-interface' | ||
4 | |||
5 | async 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 | |||
15 | export { | ||
16 | processAcceptActivity | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { VideoTorrentObject } from '../../../../shared' | ||
3 | import { ActivityAdd } from '../../../../shared/models/activitypub/activity' | ||
4 | import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../../helpers' | ||
5 | import { getOrCreateVideoChannel } from '../../../helpers/activitypub' | ||
6 | import { database as db } from '../../../initializers' | ||
7 | import { AccountInstance } from '../../../models/account/account-interface' | ||
8 | import { VideoChannelInstance } from '../../../models/video/video-channel-interface' | ||
9 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | ||
10 | |||
11 | async 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 | |||
29 | export { | ||
30 | processAddActivity | ||
31 | } | ||
32 | |||
33 | // --------------------------------------------------------------------------- | ||
34 | |||
35 | function 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 | |||
44 | function 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 @@ | |||
1 | import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity' | ||
2 | import { getOrCreateAccount } from '../../../helpers/activitypub' | ||
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { database as db } from '../../../initializers/index' | ||
5 | import { VideoInstance } from '../../../models/index' | ||
6 | import { VideoChannelInstance } from '../../../models/video/video-channel-interface' | ||
7 | import { processAddActivity } from './process-add' | ||
8 | import { processCreateActivity } from './process-create' | ||
9 | |||
10 | async 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 | |||
44 | export { | ||
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 @@ | |||
1 | import { ActivityCreate, VideoChannelObject } from '../../../../shared' | ||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' | ||
3 | import { logger, retryTransactionWrapper } from '../../../helpers' | ||
4 | import { getOrCreateAccount, getVideoChannelActivityPubUrl } from '../../../helpers/activitypub' | ||
5 | import { database as db } from '../../../initializers' | ||
6 | import { AccountInstance } from '../../../models/account/account-interface' | ||
7 | import { videoChannelActivityObjectToDBAttributes } from './misc' | ||
8 | |||
9 | async 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 | |||
26 | export { | ||
27 | processCreateActivity | ||
28 | } | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | function 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 | |||
41 | function 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 | |||
59 | function 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 | |||
68 | function 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 @@ | |||
1 | import { ActivityDelete } from '../../../../shared/models/activitypub/activity' | ||
2 | import { getOrCreateAccount } from '../../../helpers/activitypub' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { database as db } from '../../../initializers' | ||
6 | import { AccountInstance } from '../../../models/account/account-interface' | ||
7 | import { VideoChannelInstance } from '../../../models/video/video-channel-interface' | ||
8 | import { VideoInstance } from '../../../models/video/video-interface' | ||
9 | |||
10 | async 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 | |||
36 | export { | ||
37 | processDeleteActivity | ||
38 | } | ||
39 | |||
40 | // --------------------------------------------------------------------------- | ||
41 | |||
42 | async 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 | |||
51 | async 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 | |||
65 | async 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 | |||
74 | async 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 | |||
88 | async 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 | |||
97 | async 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 @@ | |||
1 | import { ActivityFollow } from '../../../../shared/models/activitypub/activity' | ||
2 | import { getOrCreateAccount, retryTransactionWrapper } from '../../../helpers' | ||
3 | import { database as db } from '../../../initializers' | ||
4 | import { AccountInstance } from '../../../models/account/account-interface' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { sendAccept } from '../send/send-accept' | ||
7 | |||
8 | async 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 | |||
17 | export { | ||
18 | processFollowActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | function 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 | |||
32 | async 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 @@ | |||
1 | import { ActivityUndo } from '../../../../shared/models/activitypub/activity' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { database as db } from '../../../initializers' | ||
4 | |||
5 | async 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 | |||
27 | export { | ||
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 @@ | |||
1 | import { VideoChannelObject, VideoTorrentObject } from '../../../../shared' | ||
2 | import { ActivityUpdate } from '../../../../shared/models/activitypub/activity' | ||
3 | import { getOrCreateAccount } from '../../../helpers/activitypub' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
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) | ||
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 | |||
27 | export { | ||
28 | processUpdateActivity | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
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 | } | ||
38 | |||
39 | return retryTransactionWrapper(updateRemoteVideo, options) | ||
40 | } | ||
41 | |||
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.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 | |||
104 | async 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 | |||
113 | async 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 | } | ||