diff options
author | Chocobozzz <me@florianbigard.com> | 2018-01-03 16:38:50 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-01-03 16:38:50 +0100 |
commit | 265ba139ebf56bbdc1c65f6ea4f367774c691fc0 (patch) | |
tree | c7c52d1ae48a35b8f9aa06a9fa2335a6ba502fd1 /server/lib/activitypub | |
parent | 9bce811268cd74b402176ae9fcd8b77ac887576e (diff) | |
download | PeerTube-265ba139ebf56bbdc1c65f6ea4f367774c691fc0.tar.gz PeerTube-265ba139ebf56bbdc1c65f6ea4f367774c691fc0.tar.zst PeerTube-265ba139ebf56bbdc1c65f6ea4f367774c691fc0.zip |
Send account activitypub update events
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r-- | server/lib/activitypub/actor.ts | 86 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 94 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-update.ts | 16 |
3 files changed, 154 insertions, 42 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index e557896e8..b6ba2cc22 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -5,13 +5,13 @@ import * as url from 'url' | |||
5 | import * as uuidv4 from 'uuid/v4' | 5 | import * as uuidv4 from 'uuid/v4' |
6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
8 | import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
11 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
14 | import { CONFIG, sequelizeTypescript } from '../../initializers' | 14 | import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers' |
15 | import { AccountModel } from '../../models/account/account' | 15 | import { AccountModel } from '../../models/account/account' |
16 | import { ActorModel } from '../../models/activitypub/actor' | 16 | import { ActorModel } from '../../models/activitypub/actor' |
17 | import { AvatarModel } from '../../models/avatar/avatar' | 17 | import { AvatarModel } from '../../models/avatar/avatar' |
@@ -84,10 +84,52 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
84 | }) | 84 | }) |
85 | } | 85 | } |
86 | 86 | ||
87 | async function fetchActorTotalItems (url: string) { | ||
88 | const options = { | ||
89 | uri: url, | ||
90 | method: 'GET', | ||
91 | json: true, | ||
92 | activityPub: true | ||
93 | } | ||
94 | |||
95 | let requestResult | ||
96 | try { | ||
97 | requestResult = await doRequest(options) | ||
98 | } catch (err) { | ||
99 | logger.warn('Cannot fetch remote actor count %s.', url, err) | ||
100 | return undefined | ||
101 | } | ||
102 | |||
103 | return requestResult.totalItems ? requestResult.totalItems : 0 | ||
104 | } | ||
105 | |||
106 | async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { | ||
107 | if ( | ||
108 | actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && | ||
109 | isActivityPubUrlValid(actorJSON.icon.url) | ||
110 | ) { | ||
111 | const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] | ||
112 | |||
113 | const avatarName = uuidv4() + extension | ||
114 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | ||
115 | |||
116 | await doRequestAndSaveToFile({ | ||
117 | method: 'GET', | ||
118 | uri: actorJSON.icon.url | ||
119 | }, destPath) | ||
120 | |||
121 | return avatarName | ||
122 | } | ||
123 | |||
124 | return undefined | ||
125 | } | ||
126 | |||
87 | export { | 127 | export { |
88 | getOrCreateActorAndServerAndModel, | 128 | getOrCreateActorAndServerAndModel, |
89 | buildActorInstance, | 129 | buildActorInstance, |
90 | setAsyncActorKeys | 130 | setAsyncActorKeys, |
131 | fetchActorTotalItems, | ||
132 | fetchAvatarIfExists | ||
91 | } | 133 | } |
92 | 134 | ||
93 | // --------------------------------------------------------------------------- | 135 | // --------------------------------------------------------------------------- |
@@ -166,7 +208,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu | |||
166 | const requestResult = await doRequest(options) | 208 | const requestResult = await doRequest(options) |
167 | const actorJSON: ActivityPubActor = requestResult.body | 209 | const actorJSON: ActivityPubActor = requestResult.body |
168 | 210 | ||
169 | if (isRemoteActorValid(actorJSON) === false) { | 211 | if (isActorObjectValid(actorJSON) === false) { |
170 | logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) | 212 | logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) |
171 | return undefined | 213 | return undefined |
172 | } | 214 | } |
@@ -190,22 +232,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu | |||
190 | followingUrl: actorJSON.following | 232 | followingUrl: actorJSON.following |
191 | }) | 233 | }) |
192 | 234 | ||
193 | // Fetch icon? | 235 | const avatarName = await fetchAvatarIfExists(actorJSON) |
194 | let avatarName: string = undefined | ||
195 | if ( | ||
196 | actorJSON.icon && actorJSON.icon.type === 'Image' && actorJSON.icon.mediaType === 'image/png' && | ||
197 | isActivityPubUrlValid(actorJSON.icon.url) | ||
198 | ) { | ||
199 | const extension = actorJSON.icon.mediaType === 'image/png' ? '.png' : '.jpg' | ||
200 | |||
201 | avatarName = uuidv4() + extension | ||
202 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | ||
203 | |||
204 | await doRequestAndSaveToFile({ | ||
205 | method: 'GET', | ||
206 | uri: actorJSON.icon.url | ||
207 | }, destPath) | ||
208 | } | ||
209 | 236 | ||
210 | const name = actorJSON.name || actorJSON.preferredUsername | 237 | const name = actorJSON.name || actorJSON.preferredUsername |
211 | return { | 238 | return { |
@@ -217,25 +244,6 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu | |||
217 | } | 244 | } |
218 | } | 245 | } |
219 | 246 | ||
220 | async function fetchActorTotalItems (url: string) { | ||
221 | const options = { | ||
222 | uri: url, | ||
223 | method: 'GET', | ||
224 | json: true, | ||
225 | activityPub: true | ||
226 | } | ||
227 | |||
228 | let requestResult | ||
229 | try { | ||
230 | requestResult = await doRequest(options) | ||
231 | } catch (err) { | ||
232 | logger.warn('Cannot fetch remote actor count %s.', url, err) | ||
233 | return undefined | ||
234 | } | ||
235 | |||
236 | return requestResult.totalItems ? requestResult.totalItems : 0 | ||
237 | } | ||
238 | |||
239 | function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { | 247 | function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { |
240 | const account = new AccountModel({ | 248 | const account = new AccountModel({ |
241 | name: result.name, | 249 | name: result.name, |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index bc8ae5cc6..05ea7d272 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -1,14 +1,18 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | ||
4 | import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
5 | import { resetSequelizeInstance } from '../../../helpers/utils' | 7 | import { resetSequelizeInstance } from '../../../helpers/utils' |
6 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import { AccountModel } from '../../../models/account/account' | ||
7 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
11 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
8 | import { TagModel } from '../../../models/video/tag' | 12 | import { TagModel } from '../../../models/video/tag' |
9 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
10 | import { VideoFileModel } from '../../../models/video/video-file' | 14 | import { VideoFileModel } from '../../../models/video/video-file' |
11 | import { getOrCreateActorAndServerAndModel } from '../actor' | 15 | import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor' |
12 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | 16 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
13 | 17 | ||
14 | async function processUpdateActivity (activity: ActivityUpdate) { | 18 | async function processUpdateActivity (activity: ActivityUpdate) { |
@@ -16,6 +20,8 @@ async function processUpdateActivity (activity: ActivityUpdate) { | |||
16 | 20 | ||
17 | if (activity.object.type === 'Video') { | 21 | if (activity.object.type === 'Video') { |
18 | return processUpdateVideo(actor, activity) | 22 | return processUpdateVideo(actor, activity) |
23 | } else if (activity.object.type === 'Person') { | ||
24 | return processUpdateAccount(actor, activity) | ||
19 | } | 25 | } |
20 | 26 | ||
21 | return | 27 | return |
@@ -39,11 +45,11 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) { | |||
39 | } | 45 | } |
40 | 46 | ||
41 | async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { | 47 | async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { |
42 | const videoAttributesToUpdate = activity.object | 48 | const videoAttributesToUpdate = activity.object as VideoTorrentObject |
43 | 49 | ||
44 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) | 50 | logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) |
45 | let videoInstance: VideoModel | 51 | let videoInstance: VideoModel |
46 | let videoFieldsSave: object | 52 | let videoFieldsSave: any |
47 | 53 | ||
48 | try { | 54 | try { |
49 | await sequelizeTypescript.transaction(async t => { | 55 | await sequelizeTypescript.transaction(async t => { |
@@ -54,6 +60,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { | |||
54 | const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) | 60 | const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) |
55 | if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') | 61 | if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') |
56 | 62 | ||
63 | videoFieldsSave = videoInstance.toJSON() | ||
64 | |||
57 | const videoChannel = videoInstance.VideoChannel | 65 | const videoChannel = videoInstance.VideoChannel |
58 | if (videoChannel.Account.Actor.id !== actor.id) { | 66 | if (videoChannel.Account.Actor.id !== actor.id) { |
59 | throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) | 67 | throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) |
@@ -102,3 +110,83 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { | |||
102 | throw err | 110 | throw err |
103 | } | 111 | } |
104 | } | 112 | } |
113 | |||
114 | function processUpdateAccount (actor: ActorModel, activity: ActivityUpdate) { | ||
115 | const options = { | ||
116 | arguments: [ actor, activity ], | ||
117 | errorMessage: 'Cannot update the remote account with many retries' | ||
118 | } | ||
119 | |||
120 | return retryTransactionWrapper(updateRemoteAccount, options) | ||
121 | } | ||
122 | |||
123 | async function updateRemoteAccount (actor: ActorModel, activity: ActivityUpdate) { | ||
124 | const accountAttributesToUpdate = activity.object as ActivityPubActor | ||
125 | |||
126 | logger.debug('Updating remote account "%s".', accountAttributesToUpdate.uuid) | ||
127 | let actorInstance: ActorModel | ||
128 | let accountInstance: AccountModel | ||
129 | let actorFieldsSave: object | ||
130 | let accountFieldsSave: object | ||
131 | |||
132 | // Fetch icon? | ||
133 | const avatarName = await fetchAvatarIfExists(accountAttributesToUpdate) | ||
134 | |||
135 | try { | ||
136 | await sequelizeTypescript.transaction(async t => { | ||
137 | actorInstance = await ActorModel.loadByUrl(accountAttributesToUpdate.id, t) | ||
138 | if (!actorInstance) throw new Error('Actor ' + accountAttributesToUpdate.id + ' not found.') | ||
139 | |||
140 | actorFieldsSave = actorInstance.toJSON() | ||
141 | accountInstance = actorInstance.Account | ||
142 | accountFieldsSave = actorInstance.Account.toJSON() | ||
143 | |||
144 | const followersCount = await fetchActorTotalItems(accountAttributesToUpdate.followers) | ||
145 | const followingCount = await fetchActorTotalItems(accountAttributesToUpdate.following) | ||
146 | |||
147 | actorInstance.set('type', accountAttributesToUpdate.type) | ||
148 | actorInstance.set('uuid', accountAttributesToUpdate.uuid) | ||
149 | actorInstance.set('preferredUsername', accountAttributesToUpdate.preferredUsername) | ||
150 | actorInstance.set('url', accountAttributesToUpdate.id) | ||
151 | actorInstance.set('publicKey', accountAttributesToUpdate.publicKey.publicKeyPem) | ||
152 | actorInstance.set('followersCount', followersCount) | ||
153 | actorInstance.set('followingCount', followingCount) | ||
154 | actorInstance.set('inboxUrl', accountAttributesToUpdate.inbox) | ||
155 | actorInstance.set('outboxUrl', accountAttributesToUpdate.outbox) | ||
156 | actorInstance.set('sharedInboxUrl', accountAttributesToUpdate.endpoints.sharedInbox) | ||
157 | actorInstance.set('followersUrl', accountAttributesToUpdate.followers) | ||
158 | actorInstance.set('followingUrl', accountAttributesToUpdate.following) | ||
159 | |||
160 | if (avatarName !== undefined) { | ||
161 | if (actorInstance.avatarId) { | ||
162 | await actorInstance.Avatar.destroy({ transaction: t }) | ||
163 | } | ||
164 | |||
165 | const avatar = await AvatarModel.create({ | ||
166 | filename: avatarName | ||
167 | }, { transaction: t }) | ||
168 | |||
169 | actor.set('avatarId', avatar.id) | ||
170 | } | ||
171 | |||
172 | await actor.save({ transaction: t }) | ||
173 | |||
174 | actor.Account.set('name', accountAttributesToUpdate.name || accountAttributesToUpdate.preferredUsername) | ||
175 | await actor.Account.save({ transaction: t }) | ||
176 | }) | ||
177 | |||
178 | logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid) | ||
179 | } catch (err) { | ||
180 | if (actorInstance !== undefined && actorFieldsSave !== undefined) { | ||
181 | resetSequelizeInstance(actorInstance, actorFieldsSave) | ||
182 | } | ||
183 | |||
184 | if (accountInstance !== undefined && accountFieldsSave !== undefined) { | ||
185 | resetSequelizeInstance(accountInstance, accountFieldsSave) | ||
186 | } | ||
187 | |||
188 | // This is just a debug because we will retry the insert | ||
189 | logger.debug('Cannot update the remote account.', err) | ||
190 | throw err | ||
191 | } | ||
192 | } | ||
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index b623fec6c..e8f11edd0 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' | 2 | import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' |
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../../shared/models/videos' |
4 | import { UserModel } from '../../../models/account/user' | ||
4 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
6 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { VideoShareModel } from '../../../models/video/video-share' |
@@ -22,9 +23,24 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) { | |||
22 | return broadcastToFollowers(data, byActor, actorsInvolved, t) | 23 | return broadcastToFollowers(data, byActor, actorsInvolved, t) |
23 | } | 24 | } |
24 | 25 | ||
26 | async function sendUpdateUser (user: UserModel, t: Transaction) { | ||
27 | const byActor = user.Account.Actor | ||
28 | |||
29 | const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString()) | ||
30 | const accountObject = user.Account.toActivityPubObject() | ||
31 | const audience = await getAudience(byActor, t) | ||
32 | const data = await updateActivityData(url, byActor, accountObject, t, audience) | ||
33 | |||
34 | const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) | ||
35 | actorsInvolved.push(byActor) | ||
36 | |||
37 | return broadcastToFollowers(data, byActor, actorsInvolved, t) | ||
38 | } | ||
39 | |||
25 | // --------------------------------------------------------------------------- | 40 | // --------------------------------------------------------------------------- |
26 | 41 | ||
27 | export { | 42 | export { |
43 | sendUpdateUser, | ||
28 | sendUpdateVideo | 44 | sendUpdateVideo |
29 | } | 45 | } |
30 | 46 | ||