aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-01-03 16:38:50 +0100
committerChocobozzz <me@florianbigard.com>2018-01-03 16:38:50 +0100
commit265ba139ebf56bbdc1c65f6ea4f367774c691fc0 (patch)
treec7c52d1ae48a35b8f9aa06a9fa2335a6ba502fd1 /server/lib/activitypub
parent9bce811268cd74b402176ae9fcd8b77ac887576e (diff)
downloadPeerTube-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.ts86
-rw-r--r--server/lib/activitypub/process/process-update.ts94
-rw-r--r--server/lib/activitypub/send/send-update.ts16
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'
5import * as uuidv4 from 'uuid/v4' 5import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' 8import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper } from '../../helpers/database-utils' 10import { retryTransactionWrapper } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14import { CONFIG, sequelizeTypescript } from '../../initializers' 14import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
15import { AccountModel } from '../../models/account/account' 15import { AccountModel } from '../../models/account/account'
16import { ActorModel } from '../../models/activitypub/actor' 16import { ActorModel } from '../../models/activitypub/actor'
17import { AvatarModel } from '../../models/avatar/avatar' 17import { AvatarModel } from '../../models/avatar/avatar'
@@ -84,10 +84,52 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
84 }) 84 })
85} 85}
86 86
87async 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
106async 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
87export { 127export {
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
220async 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
239function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { 247function 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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityUpdate } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 5import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
5import { resetSequelizeInstance } from '../../../helpers/utils' 7import { resetSequelizeInstance } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
11import { AvatarModel } from '../../../models/avatar/avatar'
8import { TagModel } from '../../../models/video/tag' 12import { TagModel } from '../../../models/video/tag'
9import { VideoModel } from '../../../models/video/video' 13import { VideoModel } from '../../../models/video/video'
10import { VideoFileModel } from '../../../models/video/video-file' 14import { VideoFileModel } from '../../../models/video/video-file'
11import { getOrCreateActorAndServerAndModel } from '../actor' 15import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor'
12import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' 16import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
13 17
14async function processUpdateActivity (activity: ActivityUpdate) { 18async 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
41async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { 47async 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
114function 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
123async 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 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { UserModel } from '../../../models/account/user'
4import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
6import { VideoShareModel } from '../../../models/video/video-share' 7import { 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
26async 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
27export { 42export {
43 sendUpdateUser,
28 sendUpdateVideo 44 sendUpdateVideo
29} 45}
30 46