]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/activitypub.ts
Refractor activity pub lib/helpers
[github/Chocobozzz/PeerTube.git] / server / helpers / activitypub.ts
CommitLineData
571389d4
C
1import { join } from 'path'
2import * as request from 'request'
efc32059 3import * as Sequelize from 'sequelize'
e4f97bab 4import * as url from 'url'
571389d4 5import { ActivityIconObject } from '../../shared/index'
afffe988 6import { Activity } from '../../shared/models/activitypub/activity'
e4f97bab 7import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
20494f12 8import { VideoChannelObject } from '../../shared/models/activitypub/objects/video-channel-object'
e4f97bab 9import { ResultList } from '../../shared/models/result-list.model'
571389d4 10import { database as db, REMOTE_SCHEME } from '../initializers'
9a27cdc2 11import { ACTIVITY_PUB, CONFIG, STATIC_PATHS } from '../initializers/constants'
54141398
C
12import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/process/misc'
13import { sendVideoAnnounce } from '../lib/activitypub/send/send-announce'
20494f12 14import { sendVideoChannelAnnounce } from '../lib/index'
54141398 15import { AccountFollowInstance } from '../models/account/account-follow-interface'
20494f12 16import { AccountInstance } from '../models/account/account-interface'
54141398 17import { VideoAbuseInstance } from '../models/video/video-abuse-interface'
efc32059 18import { VideoChannelInstance } from '../models/video/video-channel-interface'
0d0e8dd0 19import { VideoInstance } from '../models/video/video-interface'
571389d4
C
20import { isRemoteAccountValid } from './custom-validators'
21import { logger } from './logger'
afffe988 22import { signObject } from './peertube-crypto'
571389d4 23import { doRequest, doRequestAndSaveToFile } from './requests'
efc32059 24import { getServerAccount } from './utils'
54141398 25import { isVideoChannelObjectValid } from './custom-validators/activitypub/video-channels'
0d0e8dd0
C
26
27function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
28 const thumbnailName = video.getThumbnailName()
29 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
30
31 const options = {
32 method: 'GET',
33 uri: icon.url
34 }
35 return doRequestAndSaveToFile(options, thumbnailPath)
36}
37
efc32059
C
38async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
39 const serverAccount = await getServerAccount()
40
41 await db.VideoChannelShare.create({
42 accountId: serverAccount.id,
43 videoChannelId: videoChannel.id
44 }, { transaction: t })
45
20494f12 46 return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
efc32059
C
47}
48
49async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
50 const serverAccount = await getServerAccount()
51
52 await db.VideoShare.create({
53 accountId: serverAccount.id,
54 videoId: video.id
55 }, { transaction: t })
56
20494f12 57 return sendVideoAnnounce(serverAccount, video, t)
efc32059
C
58}
59
54141398
C
60function getVideoActivityPubUrl (video: VideoInstance) {
61 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
62}
63
64function getVideoChannelActivityPubUrl (videoChannel: VideoChannelInstance) {
65 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid
66}
67
68function getAccountActivityPubUrl (accountName: string) {
69 return CONFIG.WEBSERVER.URL + '/account/' + accountName
70}
71
72function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) {
73 return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
74}
75
76function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
77 const me = accountFollow.AccountFollower
78 const following = accountFollow.AccountFollowing
79
80 return me.url + '#follows/' + following.id
81}
82
83function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) {
84 const follower = accountFollow.AccountFollower
85 const me = accountFollow.AccountFollowing
86
87 return follower.url + '#accepts/follows/' + me.id
88}
89
90function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) {
91 return originalUrl + '#announces/' + byAccount.id
92}
93
94function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
95 return originalUrl + '#updates/' + updatedAt
96}
0d0e8dd0 97
54141398
C
98function getUndoActivityPubUrl (originalUrl: string) {
99 return originalUrl + '/undo'
0d0e8dd0
C
100}
101
102async function getOrCreateAccount (accountUrl: string) {
103 let account = await db.Account.loadByUrl(accountUrl)
104
105 // We don't have this account in our database, fetch it on remote
106 if (!account) {
60862425 107 const res = await fetchRemoteAccountAndCreateServer(accountUrl)
350e31d6 108 if (res === undefined) throw new Error('Cannot fetch remote account.')
0d0e8dd0
C
109
110 // Save our new account in database
20494f12 111 account = await res.account.save()
0d0e8dd0
C
112 }
113
114 return account
115}
e4f97bab 116
20494f12
C
117async function getOrCreateVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
118 let videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl)
119
120 // We don't have this account in our database, fetch it on remote
121 if (!videoChannel) {
122 videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl)
123 if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.')
124
125 // Save our new video channel in database
126 await videoChannel.save()
127 }
128
129 return videoChannel
130}
131
60862425 132async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
e4f97bab
C
133 const options = {
134 uri: accountUrl,
350e31d6
C
135 method: 'GET',
136 headers: {
9a27cdc2 137 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
350e31d6 138 }
e4f97bab
C
139 }
140
350e31d6
C
141 logger.info('Fetching remote account %s.', accountUrl)
142
e4f97bab
C
143 let requestResult
144 try {
145 requestResult = await doRequest(options)
146 } catch (err) {
350e31d6 147 logger.warn('Cannot fetch remote account %s.', accountUrl, err)
e4f97bab
C
148 return undefined
149 }
150
350e31d6
C
151 const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
152 if (isRemoteAccountValid(accountJSON) === false) {
153 logger.debug('Remote account JSON is not valid.', { accountJSON })
154 return undefined
155 }
e4f97bab
C
156
157 const followersCount = await fetchAccountCount(accountJSON.followers)
158 const followingCount = await fetchAccountCount(accountJSON.following)
159
160 const account = db.Account.build({
161 uuid: accountJSON.uuid,
162 name: accountJSON.preferredUsername,
163 url: accountJSON.url,
164 publicKey: accountJSON.publicKey.publicKeyPem,
165 privateKey: null,
166 followersCount: followersCount,
167 followingCount: followingCount,
168 inboxUrl: accountJSON.inbox,
169 outboxUrl: accountJSON.outbox,
170 sharedInboxUrl: accountJSON.endpoints.sharedInbox,
171 followersUrl: accountJSON.followers,
172 followingUrl: accountJSON.following
173 })
174
175 const accountHost = url.parse(account.url).host
60862425 176 const serverOptions = {
e4f97bab
C
177 where: {
178 host: accountHost
179 },
180 defaults: {
181 host: accountHost
182 }
183 }
60862425
C
184 const [ server ] = await db.Server.findOrCreate(serverOptions)
185 account.set('serverId', server.id)
e4f97bab 186
60862425 187 return { account, server }
e4f97bab
C
188}
189
20494f12
C
190async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
191 const options = {
192 uri: videoChannelUrl,
193 method: 'GET',
194 headers: {
9a27cdc2 195 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
20494f12
C
196 }
197 }
198
199 logger.info('Fetching remote video channel %s.', videoChannelUrl)
200
201 let requestResult
202 try {
203 requestResult = await doRequest(options)
204 } catch (err) {
205 logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err)
206 return undefined
207 }
208
209 const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body)
210 if (isVideoChannelObjectValid(videoChannelJSON) === false) {
211 logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON })
212 return undefined
213 }
214
215 const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount)
216 const videoChannel = db.VideoChannel.build(videoChannelAttributes)
217 videoChannel.Account = ownerAccount
218
219 return videoChannel
220}
221
571389d4
C
222function fetchRemoteVideoPreview (video: VideoInstance) {
223 // FIXME: use url
60862425 224 const host = video.VideoChannel.Account.Server.host
571389d4
C
225 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
226
227 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
228}
229
230async function fetchRemoteVideoDescription (video: VideoInstance) {
975e6e0e
C
231 // FIXME: use url
232 const host = video.VideoChannel.Account.Server.host
233 const path = video.getDescriptionPath()
571389d4 234 const options = {
975e6e0e
C
235 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
236 json: true
571389d4
C
237 }
238
239 const { body } = await doRequest(options)
240 return body.description ? body.description : ''
241}
242
243function activityPubContextify <T> (data: T) {
e4f97bab
C
244 return Object.assign(data,{
245 '@context': [
246 'https://www.w3.org/ns/activitystreams',
247 'https://w3id.org/security/v1',
248 {
249 'Hashtag': 'as:Hashtag',
250 'uuid': 'http://schema.org/identifier',
251 'category': 'http://schema.org/category',
252 'licence': 'http://schema.org/license',
253 'nsfw': 'as:sensitive',
254 'language': 'http://schema.org/inLanguage',
255 'views': 'http://schema.org/Number',
8e13fa7d
C
256 'size': 'http://schema.org/Number',
257 'VideoChannel': 'https://peertu.be/ns/VideoChannel'
e4f97bab
C
258 }
259 ]
260 })
261}
262
263function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
264 const baseUrl = url.split('?').shift
265
266 const obj = {
267 id: baseUrl,
268 type: 'Collection',
269 totalItems: result.total,
270 first: {
271 id: baseUrl + '?page=' + page,
272 type: 'CollectionPage',
273 totalItems: result.total,
274 next: baseUrl + '?page=' + (page + 1),
275 partOf: baseUrl,
276 items: result.data
277 }
278 }
279
280 return activityPubContextify(obj)
281}
282
afffe988
C
283function buildSignedActivity (byAccount: AccountInstance, data: Object) {
284 const activity = activityPubContextify(data)
285
286 return signObject(byAccount, activity) as Promise<Activity>
287}
288
e4f97bab
C
289// ---------------------------------------------------------------------------
290
291export {
60862425 292 fetchRemoteAccountAndCreateServer,
e4f97bab 293 activityPubContextify,
0d0e8dd0 294 activityPubCollectionPagination,
0d0e8dd0 295 generateThumbnailFromUrl,
571389d4
C
296 getOrCreateAccount,
297 fetchRemoteVideoPreview,
efc32059
C
298 fetchRemoteVideoDescription,
299 shareVideoChannelByServer,
20494f12 300 shareVideoByServer,
afffe988 301 getOrCreateVideoChannel,
54141398
C
302 buildSignedActivity,
303 getVideoActivityPubUrl,
304 getVideoChannelActivityPubUrl,
305 getAccountActivityPubUrl,
306 getVideoAbuseActivityPubUrl,
307 getAccountFollowActivityPubUrl,
308 getAccountFollowAcceptActivityPubUrl,
309 getAnnounceActivityPubUrl,
310 getUpdateActivityPubUrl,
311 getUndoActivityPubUrl
e4f97bab
C
312}
313
314// ---------------------------------------------------------------------------
315
316async function fetchAccountCount (url: string) {
317 const options = {
318 uri: url,
319 method: 'GET'
320 }
321
322 let requestResult
323 try {
324 requestResult = await doRequest(options)
325 } catch (err) {
350e31d6 326 logger.warn('Cannot fetch remote account count %s.', url, err)
e4f97bab
C
327 return undefined
328 }
329
330 return requestResult.totalItems ? requestResult.totalItems : 0
331}