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