Split types and typings
[oweals/peertube.git] / server / helpers / activitypub.ts
1 import * as Bluebird from 'bluebird'
2 import validator from 'validator'
3 import { ResultList } from '../../shared/models'
4 import { Activity } from '../../shared/models/activitypub'
5 import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
6 import { signJsonLDObject } from './peertube-crypto'
7 import { pageToStartAndCount } from './core-utils'
8 import { URL } from 'url'
9 import { MActor, MVideoAccountLight } from '../types/models'
10 import { ContextType } from '@shared/models/activitypub/context'
11
12 function getContextData (type: ContextType) {
13   const context: any[] = [
14     'https://www.w3.org/ns/activitystreams',
15     'https://w3id.org/security/v1',
16     {
17       RsaSignature2017: 'https://w3id.org/security#RsaSignature2017'
18     }
19   ]
20
21   if (type !== 'View' && type !== 'Announce') {
22     const additional = {
23       pt: 'https://joinpeertube.org/ns#',
24       sc: 'http://schema.org#'
25     }
26
27     if (type === 'CacheFile') {
28       Object.assign(additional, {
29         expires: 'sc:expires',
30         CacheFile: 'pt:CacheFile'
31       })
32     } else {
33       Object.assign(additional, {
34         Hashtag: 'as:Hashtag',
35         uuid: 'sc:identifier',
36         category: 'sc:category',
37         licence: 'sc:license',
38         subtitleLanguage: 'sc:subtitleLanguage',
39         sensitive: 'as:sensitive',
40         language: 'sc:inLanguage',
41
42         Infohash: 'pt:Infohash',
43         Playlist: 'pt:Playlist',
44         PlaylistElement: 'pt:PlaylistElement',
45
46         originallyPublishedAt: 'sc:datePublished',
47         views: {
48           '@type': 'sc:Number',
49           '@id': 'pt:views'
50         },
51         state: {
52           '@type': 'sc:Number',
53           '@id': 'pt:state'
54         },
55         size: {
56           '@type': 'sc:Number',
57           '@id': 'pt:size'
58         },
59         fps: {
60           '@type': 'sc:Number',
61           '@id': 'pt:fps'
62         },
63         startTimestamp: {
64           '@type': 'sc:Number',
65           '@id': 'pt:startTimestamp'
66         },
67         stopTimestamp: {
68           '@type': 'sc:Number',
69           '@id': 'pt:stopTimestamp'
70         },
71         position: {
72           '@type': 'sc:Number',
73           '@id': 'pt:position'
74         },
75         commentsEnabled: {
76           '@type': 'sc:Boolean',
77           '@id': 'pt:commentsEnabled'
78         },
79         downloadEnabled: {
80           '@type': 'sc:Boolean',
81           '@id': 'pt:downloadEnabled'
82         },
83         waitTranscoding: {
84           '@type': 'sc:Boolean',
85           '@id': 'pt:waitTranscoding'
86         },
87         support: {
88           '@type': 'sc:Text',
89           '@id': 'pt:support'
90         },
91         likes: {
92           '@id': 'as:likes',
93           '@type': '@id'
94         },
95         dislikes: {
96           '@id': 'as:dislikes',
97           '@type': '@id'
98         },
99         playlists: {
100           '@id': 'pt:playlists',
101           '@type': '@id'
102         },
103         shares: {
104           '@id': 'as:shares',
105           '@type': '@id'
106         },
107         comments: {
108           '@id': 'as:comments',
109           '@type': '@id'
110         }
111       })
112     }
113
114     context.push(additional)
115   }
116
117   return {
118     '@context': context
119   }
120 }
121
122 function activityPubContextify <T> (data: T, type: ContextType = 'All') {
123   return Object.assign({}, data, getContextData(type))
124 }
125
126 type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
127 async function activityPubCollectionPagination (
128   baseUrl: string,
129   handler: ActivityPubCollectionPaginationHandler,
130   page?: any,
131   size = ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE
132 ) {
133   if (!page || !validator.isInt(page)) {
134     // We just display the first page URL, we only need the total items
135     const result = await handler(0, 1)
136
137     return {
138       id: baseUrl,
139       type: 'OrderedCollectionPage',
140       totalItems: result.total,
141       first: baseUrl + '?page=1'
142     }
143   }
144
145   const { start, count } = pageToStartAndCount(page, size)
146   const result = await handler(start, count)
147
148   let next: string | undefined
149   let prev: string | undefined
150
151   // Assert page is a number
152   page = parseInt(page, 10)
153
154   // There are more results
155   if (result.total > page * size) {
156     next = baseUrl + '?page=' + (page + 1)
157   }
158
159   if (page > 1) {
160     prev = baseUrl + '?page=' + (page - 1)
161   }
162
163   return {
164     id: baseUrl + '?page=' + page,
165     type: 'OrderedCollectionPage',
166     prev,
167     next,
168     partOf: baseUrl,
169     orderedItems: result.data,
170     totalItems: result.total
171   }
172
173 }
174
175 function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) {
176   const activity = activityPubContextify(data, contextType)
177
178   return signJsonLDObject(byActor, activity) as Promise<Activity>
179 }
180
181 function getAPId (activity: string | { id: string }) {
182   if (typeof activity === 'string') return activity
183
184   return activity.id
185 }
186
187 function checkUrlsSameHost (url1: string, url2: string) {
188   const idHost = new URL(url1).host
189   const actorHost = new URL(url2).host
190
191   return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
192 }
193
194 function buildRemoteVideoBaseUrl (video: MVideoAccountLight, path: string) {
195   const host = video.VideoChannel.Account.Actor.Server.host
196
197   return REMOTE_SCHEME.HTTP + '://' + host + path
198 }
199
200 // ---------------------------------------------------------------------------
201
202 export {
203   checkUrlsSameHost,
204   getAPId,
205   activityPubContextify,
206   activityPubCollectionPagination,
207   buildSignedActivity,
208   buildRemoteVideoBaseUrl
209 }