Send server announce when users upload a video
[oweals/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 { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
7 import { ResultList } from '../../shared/models/result-list.model'
8 import { database as db, REMOTE_SCHEME } from '../initializers'
9 import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
10 import { sendAnnounce } from '../lib/activitypub/send-request'
11 import { VideoChannelInstance } from '../models/video/video-channel-interface'
12 import { VideoInstance } from '../models/video/video-interface'
13 import { isRemoteAccountValid } from './custom-validators'
14 import { logger } from './logger'
15 import { doRequest, doRequestAndSaveToFile } from './requests'
16 import { getServerAccount } from './utils'
17
18 function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
19   const thumbnailName = video.getThumbnailName()
20   const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
21
22   const options = {
23     method: 'GET',
24     uri: icon.url
25   }
26   return doRequestAndSaveToFile(options, thumbnailPath)
27 }
28
29 async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
30   const serverAccount = await getServerAccount()
31
32   await db.VideoChannelShare.create({
33     accountId: serverAccount.id,
34     videoChannelId: videoChannel.id
35   }, { transaction: t })
36
37   return sendAnnounce(serverAccount, videoChannel, t)
38 }
39
40 async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
41   const serverAccount = await getServerAccount()
42
43   await db.VideoShare.create({
44     accountId: serverAccount.id,
45     videoId: video.id
46   }, { transaction: t })
47
48   return sendAnnounce(serverAccount, video, t)
49 }
50
51 function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) {
52   if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + id
53   else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + id
54   else if (type === 'account') return CONFIG.WEBSERVER.URL + '/account/' + id
55   else if (type === 'videoAbuse') return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + id
56
57   return ''
58 }
59
60 async function getOrCreateAccount (accountUrl: string) {
61   let account = await db.Account.loadByUrl(accountUrl)
62
63   // We don't have this account in our database, fetch it on remote
64   if (!account) {
65     const res = await fetchRemoteAccountAndCreateServer(accountUrl)
66     if (res === undefined) throw new Error('Cannot fetch remote account.')
67
68     // Save our new account in database
69     const account = res.account
70     await account.save()
71   }
72
73   return account
74 }
75
76 async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
77   const options = {
78     uri: accountUrl,
79     method: 'GET',
80     headers: {
81       'Accept': ACTIVITY_PUB_ACCEPT_HEADER
82     }
83   }
84
85   logger.info('Fetching remote account %s.', accountUrl)
86
87   let requestResult
88   try {
89     requestResult = await doRequest(options)
90   } catch (err) {
91     logger.warn('Cannot fetch remote account %s.', accountUrl, err)
92     return undefined
93   }
94
95   const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
96   if (isRemoteAccountValid(accountJSON) === false) {
97     logger.debug('Remote account JSON is not valid.', { accountJSON })
98     return undefined
99   }
100
101   const followersCount = await fetchAccountCount(accountJSON.followers)
102   const followingCount = await fetchAccountCount(accountJSON.following)
103
104   const account = db.Account.build({
105     uuid: accountJSON.uuid,
106     name: accountJSON.preferredUsername,
107     url: accountJSON.url,
108     publicKey: accountJSON.publicKey.publicKeyPem,
109     privateKey: null,
110     followersCount: followersCount,
111     followingCount: followingCount,
112     inboxUrl: accountJSON.inbox,
113     outboxUrl: accountJSON.outbox,
114     sharedInboxUrl: accountJSON.endpoints.sharedInbox,
115     followersUrl: accountJSON.followers,
116     followingUrl: accountJSON.following
117   })
118
119   const accountHost = url.parse(account.url).host
120   const serverOptions = {
121     where: {
122       host: accountHost
123     },
124     defaults: {
125       host: accountHost
126     }
127   }
128   const [ server ] = await db.Server.findOrCreate(serverOptions)
129   account.set('serverId', server.id)
130
131   return { account, server }
132 }
133
134 function fetchRemoteVideoPreview (video: VideoInstance) {
135   // FIXME: use url
136   const host = video.VideoChannel.Account.Server.host
137   const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
138
139   return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
140 }
141
142 async function fetchRemoteVideoDescription (video: VideoInstance) {
143   const options = {
144     uri: video.url
145   }
146
147   const { body } = await doRequest(options)
148   return body.description ? body.description : ''
149 }
150
151 function activityPubContextify <T> (data: T) {
152   return Object.assign(data,{
153     '@context': [
154       'https://www.w3.org/ns/activitystreams',
155       'https://w3id.org/security/v1',
156       {
157         'Hashtag': 'as:Hashtag',
158         'uuid': 'http://schema.org/identifier',
159         'category': 'http://schema.org/category',
160         'licence': 'http://schema.org/license',
161         'nsfw': 'as:sensitive',
162         'language': 'http://schema.org/inLanguage',
163         'views': 'http://schema.org/Number',
164         'size': 'http://schema.org/Number',
165         'VideoChannel': 'https://peertu.be/ns/VideoChannel'
166       }
167     ]
168   })
169 }
170
171 function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
172   const baseUrl = url.split('?').shift
173
174   const obj = {
175     id: baseUrl,
176     type: 'Collection',
177     totalItems: result.total,
178     first: {
179       id: baseUrl + '?page=' + page,
180       type: 'CollectionPage',
181       totalItems: result.total,
182       next: baseUrl + '?page=' + (page + 1),
183       partOf: baseUrl,
184       items: result.data
185     }
186   }
187
188   return activityPubContextify(obj)
189 }
190
191 // ---------------------------------------------------------------------------
192
193 export {
194   fetchRemoteAccountAndCreateServer,
195   activityPubContextify,
196   activityPubCollectionPagination,
197   getActivityPubUrl,
198   generateThumbnailFromUrl,
199   getOrCreateAccount,
200   fetchRemoteVideoPreview,
201   fetchRemoteVideoDescription,
202   shareVideoChannelByServer,
203   shareVideoByServer
204 }
205
206 // ---------------------------------------------------------------------------
207
208 async function fetchAccountCount (url: string) {
209   const options = {
210     uri: url,
211     method: 'GET'
212   }
213
214   let requestResult
215   try {
216     requestResult = await doRequest(options)
217   } catch (err) {
218     logger.warn('Cannot fetch remote account count %s.', url, err)
219     return undefined
220   }
221
222   return requestResult.totalItems ? requestResult.totalItems : 0
223 }