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