Merge branch 'release/v1.0.0' into develop
[oweals/peertube.git] / server / helpers / custom-validators / activitypub / actor.ts
1 import * as validator from 'validator'
2 import { CONSTRAINTS_FIELDS } from '../../../initializers'
3 import { exists, isArray } from '../misc'
4 import { truncate } from 'lodash'
5 import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
6 import { isHostValid } from '../servers'
7
8 function isActorEndpointsObjectValid (endpointObject: any) {
9   return isActivityPubUrlValid(endpointObject.sharedInbox)
10 }
11
12 function isActorPublicKeyObjectValid (publicKeyObject: any) {
13   return isActivityPubUrlValid(publicKeyObject.id) &&
14     isActivityPubUrlValid(publicKeyObject.owner) &&
15     isActorPublicKeyValid(publicKeyObject.publicKeyPem)
16 }
17
18 function isActorTypeValid (type: string) {
19   return type === 'Person' || type === 'Application' || type === 'Group'
20 }
21
22 function isActorPublicKeyValid (publicKey: string) {
23   return exists(publicKey) &&
24     typeof publicKey === 'string' &&
25     publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
26     publicKey.indexOf('-----END PUBLIC KEY-----') !== -1 &&
27     validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
28 }
29
30 const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$')
31 function isActorPreferredUsernameValid (preferredUsername: string) {
32   return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
33 }
34
35 function isActorPrivateKeyValid (privateKey: string) {
36   return exists(privateKey) &&
37     typeof privateKey === 'string' &&
38     privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
39     // Sometimes there is a \n at the end, so just assert the string contains the end mark
40     privateKey.indexOf('-----END RSA PRIVATE KEY-----') !== -1 &&
41     validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY)
42 }
43
44 function isActorObjectValid (actor: any) {
45   return exists(actor) &&
46     isActivityPubUrlValid(actor.id) &&
47     isActorTypeValid(actor.type) &&
48     isActivityPubUrlValid(actor.following) &&
49     isActivityPubUrlValid(actor.followers) &&
50     isActivityPubUrlValid(actor.inbox) &&
51     isActivityPubUrlValid(actor.outbox) &&
52     isActorPreferredUsernameValid(actor.preferredUsername) &&
53     isActivityPubUrlValid(actor.url) &&
54     isActorPublicKeyObjectValid(actor.publicKey) &&
55     isActorEndpointsObjectValid(actor.endpoints) &&
56     setValidAttributedTo(actor) &&
57
58     // If this is not an account, it should be attributed to an account
59     // In PeerTube we use this to attach a video channel to a specific account
60     (actor.type === 'Person' || actor.attributedTo.length !== 0)
61 }
62
63 function isActorFollowingCountValid (value: string) {
64   return exists(value) && validator.isInt('' + value, { min: 0 })
65 }
66
67 function isActorFollowersCountValid (value: string) {
68   return exists(value) && validator.isInt('' + value, { min: 0 })
69 }
70
71 function isActorDeleteActivityValid (activity: any) {
72   return isBaseActivityValid(activity, 'Delete')
73 }
74
75 function isActorFollowActivityValid (activity: any) {
76   return isBaseActivityValid(activity, 'Follow') &&
77     isActivityPubUrlValid(activity.object)
78 }
79
80 function isActorAcceptActivityValid (activity: any) {
81   return isBaseActivityValid(activity, 'Accept')
82 }
83
84 function isActorRejectActivityValid (activity: any) {
85   return isBaseActivityValid(activity, 'Reject')
86 }
87
88 function isActorUpdateActivityValid (activity: any) {
89   normalizeActor(activity.object)
90
91   return isBaseActivityValid(activity, 'Update') &&
92     isActorObjectValid(activity.object)
93 }
94
95 function normalizeActor (actor: any) {
96   if (!actor || !actor.url) return
97
98   if (typeof actor.url !== 'string') {
99     actor.url = actor.url.href || actor.url.url
100   }
101
102   if (actor.summary && typeof actor.summary === 'string') {
103     actor.summary = truncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
104
105     if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
106       actor.summary = null
107     }
108   }
109
110   return
111 }
112
113 function isValidActorHandle (handle: string) {
114   if (!exists(handle)) return false
115
116   const parts = handle.split('@')
117   if (parts.length !== 2) return false
118
119   return isHostValid(parts[1])
120 }
121
122 function areValidActorHandles (handles: string[]) {
123   return isArray(handles) && handles.every(h => isValidActorHandle(h))
124 }
125
126 // ---------------------------------------------------------------------------
127
128 export {
129   normalizeActor,
130   areValidActorHandles,
131   isActorEndpointsObjectValid,
132   isActorPublicKeyObjectValid,
133   isActorTypeValid,
134   isActorPublicKeyValid,
135   isActorPreferredUsernameValid,
136   isActorPrivateKeyValid,
137   isActorObjectValid,
138   isActorFollowingCountValid,
139   isActorFollowersCountValid,
140   isActorFollowActivityValid,
141   isActorAcceptActivityValid,
142   isActorRejectActivityValid,
143   isActorDeleteActivityValid,
144   isActorUpdateActivityValid,
145   isValidActorHandle
146 }