diff options
Diffstat (limited to 'server/lib/activitypub/process')
-rw-r--r-- | server/lib/activitypub/process/index.ts | 1 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-accept.ts | 32 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-announce.ts | 75 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 170 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-delete.ts | 153 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-dislike.ts | 58 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-flag.ts | 103 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-follow.ts | 156 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-like.ts | 60 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-reject.ts | 33 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-undo.ts | 183 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 119 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-view.ts | 42 | ||||
-rw-r--r-- | server/lib/activitypub/process/process.ts | 92 |
14 files changed, 0 insertions, 1277 deletions
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts deleted file mode 100644 index 5466739c1..000000000 --- a/server/lib/activitypub/process/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './process' | ||
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts deleted file mode 100644 index 077b01eda..000000000 --- a/server/lib/activitypub/process/process-accept.ts +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | import { ActivityAccept } from '../../../../shared/models/activitypub' | ||
2 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
3 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
4 | import { MActorDefault, MActorSignature } from '../../../types/models' | ||
5 | import { addFetchOutboxJob } from '../outbox' | ||
6 | |||
7 | async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) { | ||
8 | const { byActor: targetActor, inboxActor } = options | ||
9 | if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.') | ||
10 | |||
11 | return processAccept(inboxActor, targetActor) | ||
12 | } | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
17 | processAcceptActivity | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | async function processAccept (actor: MActorDefault, targetActor: MActorSignature) { | ||
23 | const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id) | ||
24 | if (!follow) throw new Error('Cannot find associated follow.') | ||
25 | |||
26 | if (follow.state !== 'accepted') { | ||
27 | follow.state = 'accepted' | ||
28 | await follow.save() | ||
29 | |||
30 | await addFetchOutboxJob(targetActor) | ||
31 | } | ||
32 | } | ||
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts deleted file mode 100644 index 9cc87ee27..000000000 --- a/server/lib/activitypub/process/process-announce.ts +++ /dev/null | |||
@@ -1,75 +0,0 @@ | |||
1 | import { getAPId } from '@server/lib/activitypub/activity' | ||
2 | import { ActivityAnnounce } from '../../../../shared/models/activitypub' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { sequelizeTypescript } from '../../../initializers/database' | ||
6 | import { VideoShareModel } from '../../../models/video/video-share' | ||
7 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
8 | import { MActorSignature, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | ||
9 | import { Notifier } from '../../notifier' | ||
10 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
11 | import { getOrCreateAPVideo } from '../videos' | ||
12 | |||
13 | async function processAnnounceActivity (options: APProcessorOptions<ActivityAnnounce>) { | ||
14 | const { activity, byActor: actorAnnouncer } = options | ||
15 | // Only notify if it is not from a fetcher job | ||
16 | const notify = options.fromFetch !== true | ||
17 | |||
18 | // Announces on accounts are not supported | ||
19 | if (actorAnnouncer.type !== 'Application' && actorAnnouncer.type !== 'Group') return | ||
20 | |||
21 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity, notify) | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | export { | ||
27 | processAnnounceActivity | ||
28 | } | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function processVideoShare (actorAnnouncer: MActorSignature, activity: ActivityAnnounce, notify: boolean) { | ||
33 | const objectUri = getAPId(activity.object) | ||
34 | |||
35 | let video: MVideoAccountLightBlacklistAllFiles | ||
36 | let videoCreated: boolean | ||
37 | |||
38 | try { | ||
39 | const result = await getOrCreateAPVideo({ videoObject: objectUri }) | ||
40 | video = result.video | ||
41 | videoCreated = result.created | ||
42 | } catch (err) { | ||
43 | logger.debug('Cannot process share of %s. Maybe this is not a video object, so just skipping.', objectUri, { err }) | ||
44 | return | ||
45 | } | ||
46 | |||
47 | await sequelizeTypescript.transaction(async t => { | ||
48 | // Add share entry | ||
49 | |||
50 | const share = { | ||
51 | actorId: actorAnnouncer.id, | ||
52 | videoId: video.id, | ||
53 | url: activity.id | ||
54 | } | ||
55 | |||
56 | const [ , created ] = await VideoShareModel.findOrCreate({ | ||
57 | where: { | ||
58 | url: activity.id | ||
59 | }, | ||
60 | defaults: share, | ||
61 | transaction: t | ||
62 | }) | ||
63 | |||
64 | if (video.isOwned() && created === true) { | ||
65 | // Don't resend the activity to the sender | ||
66 | const exceptions = [ actorAnnouncer ] | ||
67 | |||
68 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
69 | } | ||
70 | |||
71 | return undefined | ||
72 | }) | ||
73 | |||
74 | if (videoCreated && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) | ||
75 | } | ||
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts deleted file mode 100644 index 5f980de65..000000000 --- a/server/lib/activitypub/process/process-create.ts +++ /dev/null | |||
@@ -1,170 +0,0 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
2 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
3 | import { VideoModel } from '@server/models/video/video' | ||
4 | import { | ||
5 | AbuseObject, | ||
6 | ActivityCreate, | ||
7 | ActivityCreateObject, | ||
8 | ActivityObject, | ||
9 | CacheFileObject, | ||
10 | PlaylistObject, | ||
11 | VideoCommentObject, | ||
12 | VideoObject, | ||
13 | WatchActionObject | ||
14 | } from '@shared/models' | ||
15 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
16 | import { logger } from '../../../helpers/logger' | ||
17 | import { sequelizeTypescript } from '../../../initializers/database' | ||
18 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
19 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | ||
20 | import { Notifier } from '../../notifier' | ||
21 | import { fetchAPObjectIfNeeded } from '../activity' | ||
22 | import { createOrUpdateCacheFile } from '../cache-file' | ||
23 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' | ||
24 | import { createOrUpdateVideoPlaylist } from '../playlists' | ||
25 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
26 | import { resolveThread } from '../video-comments' | ||
27 | import { getOrCreateAPVideo } from '../videos' | ||
28 | |||
29 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) { | ||
30 | const { activity, byActor } = options | ||
31 | |||
32 | // Only notify if it is not from a fetcher job | ||
33 | const notify = options.fromFetch !== true | ||
34 | const activityObject = await fetchAPObjectIfNeeded<Exclude<ActivityObject, AbuseObject>>(activity.object) | ||
35 | const activityType = activityObject.type | ||
36 | |||
37 | if (activityType === 'Video') { | ||
38 | return processCreateVideo(activityObject, notify) | ||
39 | } | ||
40 | |||
41 | if (activityType === 'Note') { | ||
42 | // Comments will be fetched from videos | ||
43 | if (options.fromFetch) return | ||
44 | |||
45 | return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify) | ||
46 | } | ||
47 | |||
48 | if (activityType === 'WatchAction') { | ||
49 | return retryTransactionWrapper(processCreateWatchAction, activityObject) | ||
50 | } | ||
51 | |||
52 | if (activityType === 'CacheFile') { | ||
53 | return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor) | ||
54 | } | ||
55 | |||
56 | if (activityType === 'Playlist') { | ||
57 | return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor) | ||
58 | } | ||
59 | |||
60 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | ||
61 | return Promise.resolve(undefined) | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | export { | ||
67 | processCreateActivity | ||
68 | } | ||
69 | |||
70 | // --------------------------------------------------------------------------- | ||
71 | |||
72 | async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) { | ||
73 | const syncParam = { rates: false, shares: false, comments: false, refreshVideo: false } | ||
74 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) | ||
75 | |||
76 | if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) | ||
77 | |||
78 | return video | ||
79 | } | ||
80 | |||
81 | async function processCreateCacheFile ( | ||
82 | activity: ActivityCreate<CacheFileObject | string>, | ||
83 | cacheFile: CacheFileObject, | ||
84 | byActor: MActorSignature | ||
85 | ) { | ||
86 | if (await isRedundancyAccepted(activity, byActor) !== true) return | ||
87 | |||
88 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) | ||
89 | |||
90 | await sequelizeTypescript.transaction(async t => { | ||
91 | return createOrUpdateCacheFile(cacheFile, video, byActor, t) | ||
92 | }) | ||
93 | |||
94 | if (video.isOwned()) { | ||
95 | // Don't resend the activity to the sender | ||
96 | const exceptions = [ byActor ] | ||
97 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
98 | } | ||
99 | } | ||
100 | |||
101 | async function processCreateWatchAction (watchAction: WatchActionObject) { | ||
102 | if (watchAction.actionStatus !== 'CompletedActionStatus') return | ||
103 | |||
104 | const video = await VideoModel.loadByUrl(watchAction.object) | ||
105 | if (video.remote) return | ||
106 | |||
107 | await sequelizeTypescript.transaction(async t => { | ||
108 | return createOrUpdateLocalVideoViewer(watchAction, video, t) | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | async function processCreateVideoComment ( | ||
113 | activity: ActivityCreate<VideoCommentObject | string>, | ||
114 | commentObject: VideoCommentObject, | ||
115 | byActor: MActorSignature, | ||
116 | notify: boolean | ||
117 | ) { | ||
118 | const byAccount = byActor.Account | ||
119 | |||
120 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) | ||
121 | |||
122 | let video: MVideoAccountLightBlacklistAllFiles | ||
123 | let created: boolean | ||
124 | let comment: MCommentOwnerVideo | ||
125 | |||
126 | try { | ||
127 | const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false }) | ||
128 | if (!resolveThreadResult) return // Comment not accepted | ||
129 | |||
130 | video = resolveThreadResult.video | ||
131 | created = resolveThreadResult.commentCreated | ||
132 | comment = resolveThreadResult.comment | ||
133 | } catch (err) { | ||
134 | logger.debug( | ||
135 | 'Cannot process video comment because we could not resolve thread %s. Maybe it was not a video thread, so skip it.', | ||
136 | commentObject.inReplyTo, | ||
137 | { err } | ||
138 | ) | ||
139 | return | ||
140 | } | ||
141 | |||
142 | // Try to not forward unwanted comments on our videos | ||
143 | if (video.isOwned()) { | ||
144 | if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { | ||
145 | logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) | ||
146 | return | ||
147 | } | ||
148 | |||
149 | if (created === true) { | ||
150 | // Don't resend the activity to the sender | ||
151 | const exceptions = [ byActor ] | ||
152 | |||
153 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
154 | } | ||
155 | } | ||
156 | |||
157 | if (created && notify) Notifier.Instance.notifyOnNewComment(comment) | ||
158 | } | ||
159 | |||
160 | async function processCreatePlaylist ( | ||
161 | activity: ActivityCreate<PlaylistObject | string>, | ||
162 | playlistObject: PlaylistObject, | ||
163 | byActor: MActorSignature | ||
164 | ) { | ||
165 | const byAccount = byActor.Account | ||
166 | |||
167 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | ||
168 | |||
169 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) | ||
170 | } | ||
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts deleted file mode 100644 index ac0e7e235..000000000 --- a/server/lib/activitypub/process/process-delete.ts +++ /dev/null | |||
@@ -1,153 +0,0 @@ | |||
1 | import { ActivityDelete } from '../../../../shared/models/activitypub' | ||
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { sequelizeTypescript } from '../../../initializers/database' | ||
5 | import { ActorModel } from '../../../models/actor/actor' | ||
6 | import { VideoModel } from '../../../models/video/video' | ||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | ||
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
10 | import { | ||
11 | MAccountActor, | ||
12 | MActor, | ||
13 | MActorFull, | ||
14 | MActorSignature, | ||
15 | MChannelAccountActor, | ||
16 | MChannelActor, | ||
17 | MCommentOwnerVideo | ||
18 | } from '../../../types/models' | ||
19 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
20 | |||
21 | async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { | ||
22 | const { activity, byActor } = options | ||
23 | |||
24 | const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id | ||
25 | |||
26 | if (activity.actor === objectUrl) { | ||
27 | // We need more attributes (all the account and channel) | ||
28 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | ||
29 | |||
30 | if (byActorFull.type === 'Person') { | ||
31 | if (!byActorFull.Account) throw new Error('Actor ' + byActorFull.url + ' is a person but we cannot find it in database.') | ||
32 | |||
33 | const accountToDelete = byActorFull.Account as MAccountActor | ||
34 | accountToDelete.Actor = byActorFull | ||
35 | |||
36 | return retryTransactionWrapper(processDeleteAccount, accountToDelete) | ||
37 | } else if (byActorFull.type === 'Group') { | ||
38 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') | ||
39 | |||
40 | const channelToDelete = byActorFull.VideoChannel as MChannelAccountActor & { Actor: MActorFull } | ||
41 | channelToDelete.Actor = byActorFull | ||
42 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) | ||
43 | } | ||
44 | } | ||
45 | |||
46 | { | ||
47 | const videoCommentInstance = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(objectUrl) | ||
48 | if (videoCommentInstance) { | ||
49 | return retryTransactionWrapper(processDeleteVideoComment, byActor, videoCommentInstance, activity) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | { | ||
54 | const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(objectUrl) | ||
55 | if (videoInstance) { | ||
56 | if (videoInstance.isOwned()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`) | ||
57 | |||
58 | return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance) | ||
59 | } | ||
60 | } | ||
61 | |||
62 | { | ||
63 | const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl) | ||
64 | if (videoPlaylist) { | ||
65 | if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`) | ||
66 | |||
67 | return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist) | ||
68 | } | ||
69 | } | ||
70 | |||
71 | return undefined | ||
72 | } | ||
73 | |||
74 | // --------------------------------------------------------------------------- | ||
75 | |||
76 | export { | ||
77 | processDeleteActivity | ||
78 | } | ||
79 | |||
80 | // --------------------------------------------------------------------------- | ||
81 | |||
82 | async function processDeleteVideo (actor: MActor, videoToDelete: VideoModel) { | ||
83 | logger.debug('Removing remote video "%s".', videoToDelete.uuid) | ||
84 | |||
85 | await sequelizeTypescript.transaction(async t => { | ||
86 | if (videoToDelete.VideoChannel.Account.Actor.id !== actor.id) { | ||
87 | throw new Error('Account ' + actor.url + ' does not own video channel ' + videoToDelete.VideoChannel.Actor.url) | ||
88 | } | ||
89 | |||
90 | await videoToDelete.destroy({ transaction: t }) | ||
91 | }) | ||
92 | |||
93 | logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) | ||
94 | } | ||
95 | |||
96 | async function processDeleteVideoPlaylist (actor: MActor, playlistToDelete: VideoPlaylistModel) { | ||
97 | logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid) | ||
98 | |||
99 | await sequelizeTypescript.transaction(async t => { | ||
100 | if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) { | ||
101 | throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url) | ||
102 | } | ||
103 | |||
104 | await playlistToDelete.destroy({ transaction: t }) | ||
105 | }) | ||
106 | |||
107 | logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid) | ||
108 | } | ||
109 | |||
110 | async function processDeleteAccount (accountToRemove: MAccountActor) { | ||
111 | logger.debug('Removing remote account "%s".', accountToRemove.Actor.url) | ||
112 | |||
113 | await sequelizeTypescript.transaction(async t => { | ||
114 | await accountToRemove.destroy({ transaction: t }) | ||
115 | }) | ||
116 | |||
117 | logger.info('Remote account %s removed.', accountToRemove.Actor.url) | ||
118 | } | ||
119 | |||
120 | async function processDeleteVideoChannel (videoChannelToRemove: MChannelActor) { | ||
121 | logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url) | ||
122 | |||
123 | await sequelizeTypescript.transaction(async t => { | ||
124 | await videoChannelToRemove.destroy({ transaction: t }) | ||
125 | }) | ||
126 | |||
127 | logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url) | ||
128 | } | ||
129 | |||
130 | function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCommentOwnerVideo, activity: ActivityDelete) { | ||
131 | // Already deleted | ||
132 | if (videoComment.isDeleted()) return Promise.resolve() | ||
133 | |||
134 | logger.debug('Removing remote video comment "%s".', videoComment.url) | ||
135 | |||
136 | return sequelizeTypescript.transaction(async t => { | ||
137 | if (byActor.Account.id !== videoComment.Account.id && byActor.Account.id !== videoComment.Video.VideoChannel.accountId) { | ||
138 | throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`) | ||
139 | } | ||
140 | |||
141 | videoComment.markAsDeleted() | ||
142 | |||
143 | await videoComment.save({ transaction: t }) | ||
144 | |||
145 | if (videoComment.Video.isOwned()) { | ||
146 | // Don't resend the activity to the sender | ||
147 | const exceptions = [ byActor ] | ||
148 | await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video) | ||
149 | } | ||
150 | |||
151 | logger.info('Remote video comment %s removed.', videoComment.url) | ||
152 | }) | ||
153 | } | ||
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts deleted file mode 100644 index 4e270f917..000000000 --- a/server/lib/activitypub/process/process-dislike.ts +++ /dev/null | |||
@@ -1,58 +0,0 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | ||
2 | import { ActivityDislike } from '@shared/models' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { sequelizeTypescript } from '../../../initializers/database' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
6 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
7 | import { MActorSignature } from '../../../types/models' | ||
8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | ||
9 | |||
10 | async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) { | ||
11 | const { activity, byActor } = options | ||
12 | return retryTransactionWrapper(processDislike, activity, byActor) | ||
13 | } | ||
14 | |||
15 | // --------------------------------------------------------------------------- | ||
16 | |||
17 | export { | ||
18 | processDislikeActivity | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | |||
23 | async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { | ||
24 | const dislikeObject = activity.object | ||
25 | const byAccount = byActor.Account | ||
26 | |||
27 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | ||
28 | |||
29 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' }) | ||
30 | |||
31 | // We don't care about dislikes of remote videos | ||
32 | if (!onlyVideo.isOwned()) return | ||
33 | |||
34 | return sequelizeTypescript.transaction(async t => { | ||
35 | const video = await VideoModel.loadFull(onlyVideo.id, t) | ||
36 | |||
37 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) | ||
38 | if (existingRate && existingRate.type === 'dislike') return | ||
39 | |||
40 | await video.increment('dislikes', { transaction: t }) | ||
41 | video.dislikes++ | ||
42 | |||
43 | if (existingRate && existingRate.type === 'like') { | ||
44 | await video.decrement('likes', { transaction: t }) | ||
45 | video.likes-- | ||
46 | } | ||
47 | |||
48 | const rate = existingRate || new AccountVideoRateModel() | ||
49 | rate.type = 'dislike' | ||
50 | rate.videoId = video.id | ||
51 | rate.accountId = byAccount.id | ||
52 | rate.url = activity.id | ||
53 | |||
54 | await rate.save({ transaction: t }) | ||
55 | |||
56 | await federateVideoIfNeeded(video, false, t) | ||
57 | }) | ||
58 | } | ||
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts deleted file mode 100644 index bea285670..000000000 --- a/server/lib/activitypub/process/process-flag.ts +++ /dev/null | |||
@@ -1,103 +0,0 @@ | |||
1 | import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { VideoModel } from '@server/models/video/video' | ||
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | ||
6 | import { AbuseState, ActivityFlag } from '@shared/models' | ||
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | import { sequelizeTypescript } from '../../../initializers/database' | ||
10 | import { getAPId } from '../../../lib/activitypub/activity' | ||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' | ||
13 | |||
14 | async function processFlagActivity (options: APProcessorOptions<ActivityFlag>) { | ||
15 | const { activity, byActor } = options | ||
16 | |||
17 | return retryTransactionWrapper(processCreateAbuse, activity, byActor) | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | processFlagActivity | ||
24 | } | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) { | ||
29 | const account = byActor.Account | ||
30 | if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) | ||
31 | |||
32 | const reporterAccount = await AccountModel.load(account.id) | ||
33 | |||
34 | const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ] | ||
35 | |||
36 | const tags = Array.isArray(flag.tag) ? flag.tag : [] | ||
37 | const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name]) | ||
38 | .filter(v => !isNaN(v)) | ||
39 | |||
40 | const startAt = flag.startAt | ||
41 | const endAt = flag.endAt | ||
42 | |||
43 | for (const object of objects) { | ||
44 | try { | ||
45 | const uri = getAPId(object) | ||
46 | |||
47 | logger.debug('Reporting remote abuse for object %s.', uri) | ||
48 | |||
49 | await sequelizeTypescript.transaction(async t => { | ||
50 | const video = await VideoModel.loadByUrlAndPopulateAccount(uri, t) | ||
51 | let videoComment: MCommentOwnerVideo | ||
52 | let flaggedAccount: MAccountDefault | ||
53 | |||
54 | if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri, t) | ||
55 | if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri, t) | ||
56 | |||
57 | if (!video && !videoComment && !flaggedAccount) { | ||
58 | logger.warn('Cannot flag unknown entity %s.', object) | ||
59 | return | ||
60 | } | ||
61 | |||
62 | const baseAbuse = { | ||
63 | reporterAccountId: reporterAccount.id, | ||
64 | reason: flag.content, | ||
65 | state: AbuseState.PENDING, | ||
66 | predefinedReasons | ||
67 | } | ||
68 | |||
69 | if (video) { | ||
70 | return createVideoAbuse({ | ||
71 | baseAbuse, | ||
72 | startAt, | ||
73 | endAt, | ||
74 | reporterAccount, | ||
75 | transaction: t, | ||
76 | videoInstance: video, | ||
77 | skipNotification: false | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | if (videoComment) { | ||
82 | return createVideoCommentAbuse({ | ||
83 | baseAbuse, | ||
84 | reporterAccount, | ||
85 | transaction: t, | ||
86 | commentInstance: videoComment, | ||
87 | skipNotification: false | ||
88 | }) | ||
89 | } | ||
90 | |||
91 | return await createAccountAbuse({ | ||
92 | baseAbuse, | ||
93 | reporterAccount, | ||
94 | transaction: t, | ||
95 | accountInstance: flaggedAccount, | ||
96 | skipNotification: false | ||
97 | }) | ||
98 | }) | ||
99 | } catch (err) { | ||
100 | logger.debug('Cannot process report of %s', getAPId(object), { err }) | ||
101 | } | ||
102 | } | ||
103 | } | ||
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts deleted file mode 100644 index 7def753d5..000000000 --- a/server/lib/activitypub/process/process-follow.ts +++ /dev/null | |||
@@ -1,156 +0,0 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
3 | import { AccountModel } from '@server/models/account/account' | ||
4 | import { getServerActor } from '@server/models/application/application' | ||
5 | import { ActivityFollow } from '../../../../shared/models/activitypub' | ||
6 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
7 | import { logger } from '../../../helpers/logger' | ||
8 | import { CONFIG } from '../../../initializers/config' | ||
9 | import { sequelizeTypescript } from '../../../initializers/database' | ||
10 | import { getAPId } from '../../../lib/activitypub/activity' | ||
11 | import { ActorModel } from '../../../models/actor/actor' | ||
12 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
13 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
14 | import { MActorFollow, MActorFull, MActorId, MActorSignature } from '../../../types/models' | ||
15 | import { Notifier } from '../../notifier' | ||
16 | import { autoFollowBackIfNeeded } from '../follow' | ||
17 | import { sendAccept, sendReject } from '../send' | ||
18 | |||
19 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { | ||
20 | const { activity, byActor } = options | ||
21 | |||
22 | const activityId = activity.id | ||
23 | const objectId = getAPId(activity.object) | ||
24 | |||
25 | return retryTransactionWrapper(processFollow, byActor, activityId, objectId) | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | export { | ||
31 | processFollowActivity | ||
32 | } | ||
33 | |||
34 | // --------------------------------------------------------------------------- | ||
35 | |||
36 | async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) { | ||
37 | const { actorFollow, created, targetActor } = await sequelizeTypescript.transaction(async t => { | ||
38 | const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) | ||
39 | |||
40 | if (!targetActor) throw new Error('Unknown actor') | ||
41 | if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') | ||
42 | |||
43 | if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined } | ||
44 | if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined } | ||
45 | |||
46 | const [ actorFollow, created ] = await ActorFollowModel.findOrCreateCustom({ | ||
47 | byActor, | ||
48 | targetActor, | ||
49 | activityId, | ||
50 | state: await isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL | ||
51 | ? 'pending' | ||
52 | : 'accepted', | ||
53 | transaction: t | ||
54 | }) | ||
55 | |||
56 | if (rejectIfAlreadyRejected(actorFollow, byActor, activityId, targetActor)) return { actorFollow: undefined } | ||
57 | |||
58 | await acceptIfNeeded(actorFollow, targetActor, t) | ||
59 | |||
60 | await fixFollowURLIfNeeded(actorFollow, activityId, t) | ||
61 | |||
62 | actorFollow.ActorFollower = byActor | ||
63 | actorFollow.ActorFollowing = targetActor | ||
64 | |||
65 | // Target sends to actor he accepted the follow request | ||
66 | if (actorFollow.state === 'accepted') { | ||
67 | sendAccept(actorFollow) | ||
68 | |||
69 | await autoFollowBackIfNeeded(actorFollow, t) | ||
70 | } | ||
71 | |||
72 | return { actorFollow, created, targetActor } | ||
73 | }) | ||
74 | |||
75 | // Rejected | ||
76 | if (!actorFollow) return | ||
77 | |||
78 | if (created) { | ||
79 | const follower = await ActorModel.loadFull(byActor.id) | ||
80 | const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower }) | ||
81 | |||
82 | if (await isFollowingInstance(targetActor)) { | ||
83 | Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull) | ||
84 | } else { | ||
85 | Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url) | ||
90 | } | ||
91 | |||
92 | async function rejectIfInstanceFollowDisabled (byActor: MActorSignature, activityId: string, targetActor: MActorFull) { | ||
93 | if (await isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) { | ||
94 | logger.info('Rejecting %s because instance followers are disabled.', targetActor.url) | ||
95 | |||
96 | sendReject(activityId, byActor, targetActor) | ||
97 | |||
98 | return true | ||
99 | } | ||
100 | |||
101 | return false | ||
102 | } | ||
103 | |||
104 | async function rejectIfMuted (byActor: MActorSignature, activityId: string, targetActor: MActorFull) { | ||
105 | const followerAccount = await AccountModel.load(byActor.Account.id) | ||
106 | const followingAccountId = targetActor.Account | ||
107 | |||
108 | if (followerAccount && await isBlockedByServerOrAccount(followerAccount, followingAccountId)) { | ||
109 | logger.info('Rejecting %s because follower is muted.', byActor.url) | ||
110 | |||
111 | sendReject(activityId, byActor, targetActor) | ||
112 | |||
113 | return true | ||
114 | } | ||
115 | |||
116 | return false | ||
117 | } | ||
118 | |||
119 | function rejectIfAlreadyRejected (actorFollow: MActorFollow, byActor: MActorSignature, activityId: string, targetActor: MActorFull) { | ||
120 | // Already rejected | ||
121 | if (actorFollow.state === 'rejected') { | ||
122 | logger.info('Rejecting %s because follow is already rejected.', byActor.url) | ||
123 | |||
124 | sendReject(activityId, byActor, targetActor) | ||
125 | |||
126 | return true | ||
127 | } | ||
128 | |||
129 | return false | ||
130 | } | ||
131 | |||
132 | async function acceptIfNeeded (actorFollow: MActorFollow, targetActor: MActorFull, transaction: Transaction) { | ||
133 | // Set the follow as accepted if the remote actor follows a channel or account | ||
134 | // Or if the instance automatically accepts followers | ||
135 | if (actorFollow.state === 'accepted') return | ||
136 | if (!await isFollowingInstance(targetActor)) return | ||
137 | if (CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === true && await isFollowingInstance(targetActor)) return | ||
138 | |||
139 | actorFollow.state = 'accepted' | ||
140 | |||
141 | await actorFollow.save({ transaction }) | ||
142 | } | ||
143 | |||
144 | async function fixFollowURLIfNeeded (actorFollow: MActorFollow, activityId: string, transaction: Transaction) { | ||
145 | // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows | ||
146 | if (!actorFollow.url) { | ||
147 | actorFollow.url = activityId | ||
148 | await actorFollow.save({ transaction }) | ||
149 | } | ||
150 | } | ||
151 | |||
152 | async function isFollowingInstance (targetActor: MActorId) { | ||
153 | const serverActor = await getServerActor() | ||
154 | |||
155 | return targetActor.id === serverActor.id | ||
156 | } | ||
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts deleted file mode 100644 index 580a05bcd..000000000 --- a/server/lib/activitypub/process/process-like.ts +++ /dev/null | |||
@@ -1,60 +0,0 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | ||
2 | import { ActivityLike } from '../../../../shared/models/activitypub' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { sequelizeTypescript } from '../../../initializers/database' | ||
5 | import { getAPId } from '../../../lib/activitypub/activity' | ||
6 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
7 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
8 | import { MActorSignature } from '../../../types/models' | ||
9 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | ||
10 | |||
11 | async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { | ||
12 | const { activity, byActor } = options | ||
13 | |||
14 | return retryTransactionWrapper(processLikeVideo, byActor, activity) | ||
15 | } | ||
16 | |||
17 | // --------------------------------------------------------------------------- | ||
18 | |||
19 | export { | ||
20 | processLikeActivity | ||
21 | } | ||
22 | |||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | async function processLikeVideo (byActor: MActorSignature, activity: ActivityLike) { | ||
26 | const videoUrl = getAPId(activity.object) | ||
27 | |||
28 | const byAccount = byActor.Account | ||
29 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) | ||
30 | |||
31 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' }) | ||
32 | |||
33 | // We don't care about likes of remote videos | ||
34 | if (!onlyVideo.isOwned()) return | ||
35 | |||
36 | return sequelizeTypescript.transaction(async t => { | ||
37 | const video = await VideoModel.loadFull(onlyVideo.id, t) | ||
38 | |||
39 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) | ||
40 | if (existingRate && existingRate.type === 'like') return | ||
41 | |||
42 | if (existingRate && existingRate.type === 'dislike') { | ||
43 | await video.decrement('dislikes', { transaction: t }) | ||
44 | video.dislikes-- | ||
45 | } | ||
46 | |||
47 | await video.increment('likes', { transaction: t }) | ||
48 | video.likes++ | ||
49 | |||
50 | const rate = existingRate || new AccountVideoRateModel() | ||
51 | rate.type = 'like' | ||
52 | rate.videoId = video.id | ||
53 | rate.accountId = byAccount.id | ||
54 | rate.url = activity.id | ||
55 | |||
56 | await rate.save({ transaction: t }) | ||
57 | |||
58 | await federateVideoIfNeeded(video, false, t) | ||
59 | }) | ||
60 | } | ||
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts deleted file mode 100644 index db7ff24d8..000000000 --- a/server/lib/activitypub/process/process-reject.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import { ActivityReject } from '../../../../shared/models/activitypub/activity' | ||
2 | import { sequelizeTypescript } from '../../../initializers/database' | ||
3 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
4 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
5 | import { MActor } from '../../../types/models' | ||
6 | |||
7 | async function processRejectActivity (options: APProcessorOptions<ActivityReject>) { | ||
8 | const { byActor: targetActor, inboxActor } = options | ||
9 | if (inboxActor === undefined) throw new Error('Need to reject on explicit inbox.') | ||
10 | |||
11 | return processReject(inboxActor, targetActor) | ||
12 | } | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
17 | processRejectActivity | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | async function processReject (follower: MActor, targetActor: MActor) { | ||
23 | return sequelizeTypescript.transaction(async t => { | ||
24 | const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, targetActor.id, t) | ||
25 | |||
26 | if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${targetActor.id}.`) | ||
27 | |||
28 | actorFollow.state = 'rejected' | ||
29 | await actorFollow.save({ transaction: t }) | ||
30 | |||
31 | return undefined | ||
32 | }) | ||
33 | } | ||
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts deleted file mode 100644 index a9d8199de..000000000 --- a/server/lib/activitypub/process/process-undo.ts +++ /dev/null | |||
@@ -1,183 +0,0 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | ||
2 | import { | ||
3 | ActivityAnnounce, | ||
4 | ActivityCreate, | ||
5 | ActivityDislike, | ||
6 | ActivityFollow, | ||
7 | ActivityLike, | ||
8 | ActivityUndo, | ||
9 | ActivityUndoObject, | ||
10 | CacheFileObject | ||
11 | } from '../../../../shared/models/activitypub' | ||
12 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
13 | import { logger } from '../../../helpers/logger' | ||
14 | import { sequelizeTypescript } from '../../../initializers/database' | ||
15 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | ||
16 | import { ActorModel } from '../../../models/actor/actor' | ||
17 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
18 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | ||
19 | import { VideoShareModel } from '../../../models/video/video-share' | ||
20 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
21 | import { MActorSignature } from '../../../types/models' | ||
22 | import { fetchAPObjectIfNeeded } from '../activity' | ||
23 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
24 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | ||
25 | |||
26 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo<ActivityUndoObject>>) { | ||
27 | const { activity, byActor } = options | ||
28 | const activityToUndo = activity.object | ||
29 | |||
30 | if (activityToUndo.type === 'Like') { | ||
31 | return retryTransactionWrapper(processUndoLike, byActor, activity) | ||
32 | } | ||
33 | |||
34 | if (activityToUndo.type === 'Create') { | ||
35 | const objectToUndo = await fetchAPObjectIfNeeded<CacheFileObject>(activityToUndo.object) | ||
36 | |||
37 | if (objectToUndo.type === 'CacheFile') { | ||
38 | return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | if (activityToUndo.type === 'Dislike') { | ||
43 | return retryTransactionWrapper(processUndoDislike, byActor, activity) | ||
44 | } | ||
45 | |||
46 | if (activityToUndo.type === 'Follow') { | ||
47 | return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) | ||
48 | } | ||
49 | |||
50 | if (activityToUndo.type === 'Announce') { | ||
51 | return retryTransactionWrapper(processUndoAnnounce, byActor, activityToUndo) | ||
52 | } | ||
53 | |||
54 | logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) | ||
55 | |||
56 | return undefined | ||
57 | } | ||
58 | |||
59 | // --------------------------------------------------------------------------- | ||
60 | |||
61 | export { | ||
62 | processUndoActivity | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
67 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo<ActivityLike>) { | ||
68 | const likeActivity = activity.object | ||
69 | |||
70 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) | ||
71 | // We don't care about likes of remote videos | ||
72 | if (!onlyVideo.isOwned()) return | ||
73 | |||
74 | return sequelizeTypescript.transaction(async t => { | ||
75 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | ||
76 | |||
77 | const video = await VideoModel.loadFull(onlyVideo.id, t) | ||
78 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t) | ||
79 | if (!rate || rate.type !== 'like') { | ||
80 | logger.warn('Unknown like by account %d for video %d.', byActor.Account.id, video.id) | ||
81 | return | ||
82 | } | ||
83 | |||
84 | await rate.destroy({ transaction: t }) | ||
85 | await video.decrement('likes', { transaction: t }) | ||
86 | |||
87 | video.likes-- | ||
88 | await federateVideoIfNeeded(video, false, t) | ||
89 | }) | ||
90 | } | ||
91 | |||
92 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo<ActivityDislike>) { | ||
93 | const dislikeActivity = activity.object | ||
94 | |||
95 | const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object }) | ||
96 | // We don't care about likes of remote videos | ||
97 | if (!onlyVideo.isOwned()) return | ||
98 | |||
99 | return sequelizeTypescript.transaction(async t => { | ||
100 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | ||
101 | |||
102 | const video = await VideoModel.loadFull(onlyVideo.id, t) | ||
103 | const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t) | ||
104 | if (!rate || rate.type !== 'dislike') { | ||
105 | logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) | ||
106 | return | ||
107 | } | ||
108 | |||
109 | await rate.destroy({ transaction: t }) | ||
110 | await video.decrement('dislikes', { transaction: t }) | ||
111 | video.dislikes-- | ||
112 | |||
113 | await federateVideoIfNeeded(video, false, t) | ||
114 | }) | ||
115 | } | ||
116 | |||
117 | // --------------------------------------------------------------------------- | ||
118 | |||
119 | async function processUndoCacheFile ( | ||
120 | byActor: MActorSignature, | ||
121 | activity: ActivityUndo<ActivityCreate<CacheFileObject>>, | ||
122 | cacheFileObject: CacheFileObject | ||
123 | ) { | ||
124 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) | ||
125 | |||
126 | return sequelizeTypescript.transaction(async t => { | ||
127 | const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) | ||
128 | if (!cacheFile) { | ||
129 | logger.debug('Cannot undo unknown video cache %s.', cacheFileObject.id) | ||
130 | return | ||
131 | } | ||
132 | |||
133 | if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.') | ||
134 | |||
135 | await cacheFile.destroy({ transaction: t }) | ||
136 | |||
137 | if (video.isOwned()) { | ||
138 | // Don't resend the activity to the sender | ||
139 | const exceptions = [ byActor ] | ||
140 | |||
141 | await forwardVideoRelatedActivity(activity, t, exceptions, video) | ||
142 | } | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) { | ||
147 | return sequelizeTypescript.transaction(async t => { | ||
148 | const share = await VideoShareModel.loadByUrl(announceActivity.id, t) | ||
149 | if (!share) { | ||
150 | logger.warn('Unknown video share %d', announceActivity.id) | ||
151 | return | ||
152 | } | ||
153 | |||
154 | if (share.actorId !== byActor.id) throw new Error(`${share.url} is not shared by ${byActor.url}.`) | ||
155 | |||
156 | await share.destroy({ transaction: t }) | ||
157 | |||
158 | if (share.Video.isOwned()) { | ||
159 | // Don't resend the activity to the sender | ||
160 | const exceptions = [ byActor ] | ||
161 | |||
162 | await forwardVideoRelatedActivity(announceActivity, t, exceptions, share.Video) | ||
163 | } | ||
164 | }) | ||
165 | } | ||
166 | |||
167 | // --------------------------------------------------------------------------- | ||
168 | |||
169 | function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) { | ||
170 | return sequelizeTypescript.transaction(async t => { | ||
171 | const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t) | ||
172 | const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t) | ||
173 | |||
174 | if (!actorFollow) { | ||
175 | logger.warn('Unknown actor follow %d -> %d.', follower.id, following.id) | ||
176 | return | ||
177 | } | ||
178 | |||
179 | await actorFollow.destroy({ transaction: t }) | ||
180 | |||
181 | return undefined | ||
182 | }) | ||
183 | } | ||
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts deleted file mode 100644 index 304ed9de6..000000000 --- a/server/lib/activitypub/process/process-update.ts +++ /dev/null | |||
@@ -1,119 +0,0 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
2 | import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' | ||
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | ||
4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | ||
6 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | ||
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | import { sequelizeTypescript } from '../../../initializers/database' | ||
10 | import { ActorModel } from '../../../models/actor/actor' | ||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
12 | import { MActorFull, MActorSignature } from '../../../types/models' | ||
13 | import { fetchAPObjectIfNeeded } from '../activity' | ||
14 | import { APActorUpdater } from '../actors/updater' | ||
15 | import { createOrUpdateCacheFile } from '../cache-file' | ||
16 | import { createOrUpdateVideoPlaylist } from '../playlists' | ||
17 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
18 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' | ||
19 | |||
20 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) { | ||
21 | const { activity, byActor } = options | ||
22 | |||
23 | const object = await fetchAPObjectIfNeeded(activity.object) | ||
24 | const objectType = object.type | ||
25 | |||
26 | if (objectType === 'Video') { | ||
27 | return retryTransactionWrapper(processUpdateVideo, activity) | ||
28 | } | ||
29 | |||
30 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { | ||
31 | // We need more attributes | ||
32 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | ||
33 | return retryTransactionWrapper(processUpdateActor, byActorFull, object) | ||
34 | } | ||
35 | |||
36 | if (objectType === 'CacheFile') { | ||
37 | // We need more attributes | ||
38 | const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) | ||
39 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity, object) | ||
40 | } | ||
41 | |||
42 | if (objectType === 'Playlist') { | ||
43 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object) | ||
44 | } | ||
45 | |||
46 | return undefined | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | export { | ||
52 | processUpdateActivity | ||
53 | } | ||
54 | |||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | async function processUpdateVideo (activity: ActivityUpdate<VideoObject | string>) { | ||
58 | const videoObject = activity.object as VideoObject | ||
59 | |||
60 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { | ||
61 | logger.debug('Video sent by update is not valid.', { videoObject }) | ||
62 | return undefined | ||
63 | } | ||
64 | |||
65 | const { video, created } = await getOrCreateAPVideo({ | ||
66 | videoObject: videoObject.id, | ||
67 | allowRefresh: false, | ||
68 | fetchType: 'all' | ||
69 | }) | ||
70 | // We did not have this video, it has been created so no need to update | ||
71 | if (created) return | ||
72 | |||
73 | const updater = new APVideoUpdater(videoObject, video) | ||
74 | return updater.update(activity.to) | ||
75 | } | ||
76 | |||
77 | async function processUpdateCacheFile ( | ||
78 | byActor: MActorSignature, | ||
79 | activity: ActivityUpdate<CacheFileObject | string>, | ||
80 | cacheFileObject: CacheFileObject | ||
81 | ) { | ||
82 | if (await isRedundancyAccepted(activity, byActor) !== true) return | ||
83 | |||
84 | if (!isCacheFileObjectValid(cacheFileObject)) { | ||
85 | logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) | ||
86 | return undefined | ||
87 | } | ||
88 | |||
89 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) | ||
90 | |||
91 | await sequelizeTypescript.transaction(async t => { | ||
92 | await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) | ||
93 | }) | ||
94 | |||
95 | if (video.isOwned()) { | ||
96 | // Don't resend the activity to the sender | ||
97 | const exceptions = [ byActor ] | ||
98 | |||
99 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) { | ||
104 | logger.debug('Updating remote account "%s".', actorObject.url) | ||
105 | |||
106 | const updater = new APActorUpdater(actorObject, actor) | ||
107 | return updater.update() | ||
108 | } | ||
109 | |||
110 | async function processUpdatePlaylist ( | ||
111 | byActor: MActorSignature, | ||
112 | activity: ActivityUpdate<PlaylistObject | string>, | ||
113 | playlistObject: PlaylistObject | ||
114 | ) { | ||
115 | const byAccount = byActor.Account | ||
116 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | ||
117 | |||
118 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) | ||
119 | } | ||
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts deleted file mode 100644 index e49506d82..000000000 --- a/server/lib/activitypub/process/process-view.ts +++ /dev/null | |||
@@ -1,42 +0,0 @@ | |||
1 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
2 | import { ActivityView } from '../../../../shared/models/activitypub' | ||
3 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
4 | import { MActorSignature } from '../../../types/models' | ||
5 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | ||
6 | import { getOrCreateAPVideo } from '../videos' | ||
7 | |||
8 | async function processViewActivity (options: APProcessorOptions<ActivityView>) { | ||
9 | const { activity, byActor } = options | ||
10 | |||
11 | return processCreateView(activity, byActor) | ||
12 | } | ||
13 | |||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
16 | export { | ||
17 | processViewActivity | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | async function processCreateView (activity: ActivityView, byActor: MActorSignature) { | ||
23 | const videoObject = activity.object | ||
24 | |||
25 | const { video } = await getOrCreateAPVideo({ | ||
26 | videoObject, | ||
27 | fetchType: 'only-video', | ||
28 | allowRefresh: false | ||
29 | }) | ||
30 | |||
31 | const viewerExpires = activity.expires | ||
32 | ? new Date(activity.expires) | ||
33 | : undefined | ||
34 | |||
35 | await VideoViewsManager.Instance.processRemoteView({ video, viewerId: activity.id, viewerExpires }) | ||
36 | |||
37 | if (video.isOwned()) { | ||
38 | // Forward the view but don't resend the activity to the sender | ||
39 | const exceptions = [ byActor ] | ||
40 | await forwardVideoRelatedActivity(activity, undefined, exceptions, video) | ||
41 | } | ||
42 | } | ||
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts deleted file mode 100644 index 2bc3dce03..000000000 --- a/server/lib/activitypub/process/process.ts +++ /dev/null | |||
@@ -1,92 +0,0 @@ | |||
1 | import { StatsManager } from '@server/lib/stat-manager' | ||
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | ||
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
5 | import { MActorDefault, MActorSignature } from '../../../types/models' | ||
6 | import { getAPId } from '../activity' | ||
7 | import { getOrCreateAPActor } from '../actors' | ||
8 | import { checkUrlsSameHost } from '../url' | ||
9 | import { processAcceptActivity } from './process-accept' | ||
10 | import { processAnnounceActivity } from './process-announce' | ||
11 | import { processCreateActivity } from './process-create' | ||
12 | import { processDeleteActivity } from './process-delete' | ||
13 | import { processDislikeActivity } from './process-dislike' | ||
14 | import { processFlagActivity } from './process-flag' | ||
15 | import { processFollowActivity } from './process-follow' | ||
16 | import { processLikeActivity } from './process-like' | ||
17 | import { processRejectActivity } from './process-reject' | ||
18 | import { processUndoActivity } from './process-undo' | ||
19 | import { processUpdateActivity } from './process-update' | ||
20 | import { processViewActivity } from './process-view' | ||
21 | |||
22 | const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = { | ||
23 | Create: processCreateActivity, | ||
24 | Update: processUpdateActivity, | ||
25 | Delete: processDeleteActivity, | ||
26 | Follow: processFollowActivity, | ||
27 | Accept: processAcceptActivity, | ||
28 | Reject: processRejectActivity, | ||
29 | Announce: processAnnounceActivity, | ||
30 | Undo: processUndoActivity, | ||
31 | Like: processLikeActivity, | ||
32 | Dislike: processDislikeActivity, | ||
33 | Flag: processFlagActivity, | ||
34 | View: processViewActivity | ||
35 | } | ||
36 | |||
37 | async function processActivities ( | ||
38 | activities: Activity[], | ||
39 | options: { | ||
40 | signatureActor?: MActorSignature | ||
41 | inboxActor?: MActorDefault | ||
42 | outboxUrl?: string | ||
43 | fromFetch?: boolean | ||
44 | } = {} | ||
45 | ) { | ||
46 | const { outboxUrl, signatureActor, inboxActor, fromFetch = false } = options | ||
47 | |||
48 | const actorsCache: { [ url: string ]: MActorSignature } = {} | ||
49 | |||
50 | for (const activity of activities) { | ||
51 | if (!signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { | ||
52 | logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) | ||
53 | continue | ||
54 | } | ||
55 | |||
56 | const actorUrl = getAPId(activity.actor) | ||
57 | |||
58 | // When we fetch remote data, we don't have signature | ||
59 | if (signatureActor && actorUrl !== signatureActor.url) { | ||
60 | logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, signatureActor.url) | ||
61 | continue | ||
62 | } | ||
63 | |||
64 | if (outboxUrl && checkUrlsSameHost(outboxUrl, actorUrl) !== true) { | ||
65 | logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', outboxUrl, actorUrl) | ||
66 | continue | ||
67 | } | ||
68 | |||
69 | const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateAPActor(actorUrl) | ||
70 | actorsCache[actorUrl] = byActor | ||
71 | |||
72 | const activityProcessor = processActivity[activity.type] | ||
73 | if (activityProcessor === undefined) { | ||
74 | logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id }) | ||
75 | continue | ||
76 | } | ||
77 | |||
78 | try { | ||
79 | await activityProcessor({ activity, byActor, inboxActor, fromFetch }) | ||
80 | |||
81 | StatsManager.Instance.addInboxProcessedSuccess(activity.type) | ||
82 | } catch (err) { | ||
83 | logger.warn('Cannot process activity %s.', activity.type, { err }) | ||
84 | |||
85 | StatsManager.Instance.addInboxProcessedError(activity.type) | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | |||
90 | export { | ||
91 | processActivities | ||
92 | } | ||