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'
27 function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) {
28 const thumbnailName = video.getThumbnailName()
29 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
35 return doRequestAndSaveToFile(options, thumbnailPath)
38 async function shareVideoChannelByServer (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
39 const serverAccount = await getServerAccount()
41 await db.VideoChannelShare.create({
42 accountId: serverAccount.id,
43 videoChannelId: videoChannel.id
44 }, { transaction: t })
46 return sendVideoChannelAnnounce(serverAccount, videoChannel, t)
49 async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transaction) {
50 const serverAccount = await getServerAccount()
52 await db.VideoShare.create({
53 accountId: serverAccount.id,
55 }, { transaction: t })
57 return sendVideoAnnounce(serverAccount, video, t)
60 function getVideoActivityPubUrl (video: VideoInstance) {
61 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
64 function getVideoChannelActivityPubUrl (videoChannel: VideoChannelInstance) {
65 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid
68 function getAccountActivityPubUrl (accountName: string) {
69 return CONFIG.WEBSERVER.URL + '/account/' + accountName
72 function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) {
73 return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
76 function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
77 const me = accountFollow.AccountFollower
78 const following = accountFollow.AccountFollowing
80 return me.url + '#follows/' + following.id
83 function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) {
84 const follower = accountFollow.AccountFollower
85 const me = accountFollow.AccountFollowing
87 return follower.url + '#accepts/follows/' + me.id
90 function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) {
91 return originalUrl + '#announces/' + byAccount.id
94 function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
95 return originalUrl + '#updates/' + updatedAt
98 function getUndoActivityPubUrl (originalUrl: string) {
99 return originalUrl + '/undo'
102 async function getOrCreateAccount (accountUrl: string) {
103 let account = await db.Account.loadByUrl(accountUrl)
105 // We don't have this account in our database, fetch it on remote
107 const res = await fetchRemoteAccountAndCreateServer(accountUrl)
108 if (res === undefined) throw new Error('Cannot fetch remote account.')
110 // Save our new account in database
111 account = await res.account.save()
117 async function getOrCreateVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
118 let videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl)
120 // We don't have this account in our database, fetch it on remote
122 videoChannel = await fetchRemoteVideoChannel(ownerAccount, videoChannelUrl)
123 if (videoChannel === undefined) throw new Error('Cannot fetch remote video channel.')
125 // Save our new video channel in database
126 await videoChannel.save()
132 async function fetchRemoteAccountAndCreateServer (accountUrl: string) {
137 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
141 logger.info('Fetching remote account %s.', accountUrl)
145 requestResult = await doRequest(options)
147 logger.warn('Cannot fetch remote account %s.', accountUrl, err)
151 const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
152 if (isRemoteAccountValid(accountJSON) === false) {
153 logger.debug('Remote account JSON is not valid.', { accountJSON })
157 const followersCount = await fetchAccountCount(accountJSON.followers)
158 const followingCount = await fetchAccountCount(accountJSON.following)
160 const account = db.Account.build({
161 uuid: accountJSON.uuid,
162 name: accountJSON.preferredUsername,
163 url: accountJSON.url,
164 publicKey: accountJSON.publicKey.publicKeyPem,
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
175 const accountHost = url.parse(account.url).host
176 const serverOptions = {
184 const [ server ] = await db.Server.findOrCreate(serverOptions)
185 account.set('serverId', server.id)
187 return { account, server }
190 async function fetchRemoteVideoChannel (ownerAccount: AccountInstance, videoChannelUrl: string) {
192 uri: videoChannelUrl,
195 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
199 logger.info('Fetching remote video channel %s.', videoChannelUrl)
203 requestResult = await doRequest(options)
205 logger.warn('Cannot fetch remote video channel %s.', videoChannelUrl, err)
209 const videoChannelJSON: VideoChannelObject = JSON.parse(requestResult.body)
210 if (isVideoChannelObjectValid(videoChannelJSON) === false) {
211 logger.debug('Remote video channel JSON is not valid.', { videoChannelJSON })
215 const videoChannelAttributes = videoChannelActivityObjectToDBAttributes(videoChannelJSON, ownerAccount)
216 const videoChannel = db.VideoChannel.build(videoChannelAttributes)
217 videoChannel.Account = ownerAccount
222 function fetchRemoteVideoPreview (video: VideoInstance) {
224 const host = video.VideoChannel.Account.Server.host
225 const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName())
227 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path)
230 async function fetchRemoteVideoDescription (video: VideoInstance) {
232 const host = video.VideoChannel.Account.Server.host
233 const path = video.getDescriptionPath()
235 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
239 const { body } = await doRequest(options)
240 return body.description ? body.description : ''
243 function activityPubContextify <T> (data: T) {
244 return Object.assign(data,{
246 'https://www.w3.org/ns/activitystreams',
247 'https://w3id.org/security/v1',
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'
263 function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
264 const baseUrl = url.split('?').shift
269 totalItems: result.total,
271 id: baseUrl + '?page=' + page,
272 type: 'CollectionPage',
273 totalItems: result.total,
274 next: baseUrl + '?page=' + (page + 1),
280 return activityPubContextify(obj)
283 function buildSignedActivity (byAccount: AccountInstance, data: Object) {
284 const activity = activityPubContextify(data)
286 return signObject(byAccount, activity) as Promise<Activity>
289 // ---------------------------------------------------------------------------
292 fetchRemoteAccountAndCreateServer,
293 activityPubContextify,
294 activityPubCollectionPagination,
295 generateThumbnailFromUrl,
297 fetchRemoteVideoPreview,
298 fetchRemoteVideoDescription,
299 shareVideoChannelByServer,
301 getOrCreateVideoChannel,
303 getVideoActivityPubUrl,
304 getVideoChannelActivityPubUrl,
305 getAccountActivityPubUrl,
306 getVideoAbuseActivityPubUrl,
307 getAccountFollowActivityPubUrl,
308 getAccountFollowAcceptActivityPubUrl,
309 getAnnounceActivityPubUrl,
310 getUpdateActivityPubUrl,
311 getUndoActivityPubUrl
314 // ---------------------------------------------------------------------------
316 async function fetchAccountCount (url: string) {
324 requestResult = await doRequest(options)
326 logger.warn('Cannot fetch remote account count %s.', url, err)
330 return requestResult.totalItems ? requestResult.totalItems : 0