]>
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' |
9a27cdc2 | 11 | import { ACTIVITY_PUB, CONFIG, STATIC_PATHS } from '../initializers/constants' |
54141398 C |
12 | import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/process/misc' |
13 | import { sendVideoAnnounce } from '../lib/activitypub/send/send-announce' | |
20494f12 | 14 | import { sendVideoChannelAnnounce } from '../lib/index' |
54141398 | 15 | import { AccountFollowInstance } from '../models/account/account-follow-interface' |
20494f12 | 16 | import { AccountInstance } from '../models/account/account-interface' |
54141398 | 17 | import { VideoAbuseInstance } from '../models/video/video-abuse-interface' |
efc32059 | 18 | import { VideoChannelInstance } from '../models/video/video-channel-interface' |
0d0e8dd0 | 19 | import { VideoInstance } from '../models/video/video-interface' |
571389d4 C |
20 | import { isRemoteAccountValid } from './custom-validators' |
21 | import { logger } from './logger' | |
afffe988 | 22 | import { signObject } from './peertube-crypto' |
571389d4 | 23 | import { doRequest, doRequestAndSaveToFile } from './requests' |
efc32059 | 24 | import { getServerAccount } from './utils' |
54141398 | 25 | import { isVideoChannelObjectValid } from './custom-validators/activitypub/video-channels' |
0d0e8dd0 C |
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 | ||
efc32059 C |
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 | ||
20494f12 | 46 | return sendVideoChannelAnnounce(serverAccount, videoChannel, t) |
efc32059 C |
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 | ||
20494f12 | 57 | return sendVideoAnnounce(serverAccount, video, t) |
efc32059 C |
58 | } |
59 | ||
54141398 C |
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 | } | |
0d0e8dd0 | 97 | |
54141398 C |
98 | function getUndoActivityPubUrl (originalUrl: string) { |
99 | return originalUrl + '/undo' | |
0d0e8dd0 C |
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) { | |
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 |
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 | ||
60862425 | 132 | async 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 |
190 | async 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 |
222 | function 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 | ||
230 | async 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 | ||
243 | function 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 | ||
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 | ||
afffe988 C |
283 | function 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 | ||
291 | export { | |
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 | ||
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) { | |
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 | } |