]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/activitypub.ts
Refractor activity pub lib/helpers
[github/Chocobozzz/PeerTube.git] / server / helpers / activitypub.ts
1 import { join } from 'path'
2 import * as request from 'request'
3 import * as Sequelize from 'sequelize'
4 import * as url from 'url'
5 import { ActivityIconObject } from '../../shared/index'
6 import { Activity } from '../../shared/models/activitypub/activity'
7 import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
8 import { VideoChannelObject } from '../../shared/models/activitypub/objects/video-channel-object'
9 import { ResultList } from '../../shared/models/result-list.model'
10 import { database as db, REMOTE_SCHEME } from '../initializers'
11 import { ACTIVITY_PUB, CONFIG, STATIC_PATHS } from '../initializers/constants'
12 import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/process/misc'
13 import { sendVideoAnnounce } from '../lib/activitypub/send/send-announce'
14 import { sendVideoChannelAnnounce } from '../lib/index'
15 import { AccountFollowInstance } from '../models/account/account-follow-interface'
16 import { AccountInstance } from '../models/account/account-interface'
17 import { VideoAbuseInstance } from '../models/video/video-abuse-interface'
18 import { VideoChannelInstance } from '../models/video/video-channel-interface'
19 import { VideoInstance } from '../models/video/video-interface'
20 import { isRemoteAccountValid } from './custom-validators'
21 import { logger } from './logger'
22 import { signObject } from './peertube-crypto'
23 import { doRequest, doRequestAndSaveToFile } from './requests'
24 import { getServerAccount } from './utils'
25 import { isVideoChannelObjectValid } from './custom-validators/activitypub/video-channels'
26
27 function 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
38 async 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
46 return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
47 }
48
49 async 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
57 return sendVideoAnnounce(serverAccount, video, t)
58 }
59
60 function getVideoActivityPubUrl (video: VideoInstance) {
61 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
62 }
63
64 function getVideoChannelActivityPubUrl (videoChannel: VideoChannelInstance) {
65 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid
66 }
67
68 function getAccountActivityPubUrl (accountName: string) {
69 return CONFIG.WEBSERVER.URL + '/account/' + accountName
70 }
71
72 function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) {
73 return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
74 }
75
76 function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
77 const me = accountFollow.AccountFollower
78 const following = accountFollow.AccountFollowing
79
80 return me.url + '#follows/' + following.id
81 }
82
83 function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) {
84 const follower = accountFollow.AccountFollower
85 const me = accountFollow.AccountFollowing
86
87 return follower.url + '#accepts/follows/' + me.id
88 }
89
90 function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) {
91 return originalUrl + '#announces/' + byAccount.id
92 }
93
94 function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
95 return originalUrl + '#updates/' + updatedAt
96 }
97
98 function getUndoActivityPubUrl (originalUrl: string) {
99 return originalUrl + '/undo'
100 }
101
102 async 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) {
107 const res = await fetchRemoteAccountAndCreateServer(accountUrl)
108 if (res === undefined) throw new Error('Cannot fetch remote account.')
109
110 // Save our new account in database
111 account = await res.account.save()
112 }
113
114 return account
115 }
116
117 async 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
132 async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
133 const options = {
134 uri: accountUrl,
135 method: 'GET',
136 headers: {
137 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
138 }
139 }
140
141 logger.info('Fetching remote account %s.', accountUrl)
142
143 let requestResult
144 try {
145 requestResult = await doRequest(options)
146 } catch (err) {
147 logger.warn('Cannot fetch remote account %s.', accountUrl, err)
148 return undefined
149 }
150
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 }
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
176 const serverOptions = {
177 where: {
178 host: accountHost
179 },
180 defaults: {
181 host: accountHost
182 }
183 }
184 const [ server ] = await db.Server.findOrCreate(serverOptions)
185 account.set('serverId', server.id)
186
187 return { account, server }
188 }
189
190 async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
191 const options = {
192 uri: videoChannelUrl,
193 method: 'GET',
194 headers: {
195 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
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
222 function fetchRemoteVideoPreview (video: VideoInstance) {
223 // FIXME: use url
224 const host = video.VideoChannel.Account.Server.host
225 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
226
227 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
228 }
229
230 async function fetchRemoteVideoDescription (video: VideoInstance) {
231 // FIXME: use url
232 const host = video.VideoChannel.Account.Server.host
233 const path = video.getDescriptionPath()
234 const options = {
235 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
236 json: true
237 }
238
239 const { body } = await doRequest(options)
240 return body.description ? body.description : ''
241 }
242
243 function activityPubContextify <T> (data: T) {
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',
256 'size': 'http://schema.org/Number',
257 'VideoChannel': 'https://peertu.be/ns/VideoChannel'
258 }
259 ]
260 })
261 }
262
263 function 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
283 function buildSignedActivity (byAccount: AccountInstance, data: Object) {
284 const activity = activityPubContextify(data)
285
286 return signObject(byAccount, activity) as Promise<Activity>
287 }
288
289 // ---------------------------------------------------------------------------
290
291 export {
292 fetchRemoteAccountAndCreateServer,
293 activityPubContextify,
294 activityPubCollectionPagination,
295 generateThumbnailFromUrl,
296 getOrCreateAccount,
297 fetchRemoteVideoPreview,
298 fetchRemoteVideoDescription,
299 shareVideoChannelByServer,
300 shareVideoByServer,
301 getOrCreateVideoChannel,
302 buildSignedActivity,
303 getVideoActivityPubUrl,
304 getVideoChannelActivityPubUrl,
305 getAccountActivityPubUrl,
306 getVideoAbuseActivityPubUrl,
307 getAccountFollowActivityPubUrl,
308 getAccountFollowAcceptActivityPubUrl,
309 getAnnounceActivityPubUrl,
310 getUpdateActivityPubUrl,
311 getUndoActivityPubUrl
312 }
313
314 // ---------------------------------------------------------------------------
315
316 async 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) {
326 logger.warn('Cannot fetch remote account count %s.', url, err)
327 return undefined
328 }
329
330 return requestResult.totalItems ? requestResult.totalItems : 0
331 }