-
Notifications
You must be signed in to change notification settings - Fork 359
/
verification.ts
229 lines (205 loc) · 7.25 KB
/
verification.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import { retryAsyncWithBackOff } from '@celo/utils/lib/async'
import { messaging } from 'firebase-admin'
import { PhoneNumberUtil } from 'google-libphonenumber'
import sleep from 'sleep-promise'
import {
alwaysUseTwilio,
appSignature,
getTwilioClient,
sendSmsWithNexmo,
smsAckTimeout,
twilioPhoneNum,
} from './config'
import {
deleteMessage,
getActiveVerifiers,
getMessagesForPhoneNumber,
incrementVerifierAttemptCount,
isMessageSent,
saveMessage,
setVerifierProperties,
} from './database'
import { MobileVerifier } from './types'
const SMS_LENGTH_LIMIT = 160
const NUM_VERIFIERS_TO_WAKE = 3
const MAX_VERIFIER_ATTEMPT_COUNT = 20
const phoneUtil = PhoneNumberUtil.getInstance()
export async function sendSmsCode(address: string, phoneNumber: string, message: string) {
console.info('Attempting to send sms verification code.')
message = getFormattedMessage(message)
if (alwaysUseTwilio) {
console.info('Config set to always use Twilio')
await sendViaTextProvider(phoneNumber, message)
return 'Twilio'
}
const verifiers = await getRandomActiveVerifiers(NUM_VERIFIERS_TO_WAKE, phoneNumber)
if (verifiers === null || verifiers.length === 0) {
console.info('No suitable verifiers found. Using Twilio')
await sendViaTextProvider(phoneNumber, message)
return 'Twilio'
}
const veriferIds = verifiers.map((v) => v.id).join(',')
const messageId = await saveMessage(phoneNumber, address, message, veriferIds)
await triggerVerifiersSendSms(verifiers, messageId)
await sleep(smsAckTimeout)
const messageSent = await isMessageSent(messageId)
if (messageSent) {
console.info('Message was sent by verifier.')
return messageId
} else {
console.info('SMS timeout reached and message was not yet sent. Sending via Text provider')
await deleteMessage(messageId)
await sendViaTextProvider(phoneNumber, message)
return 'Twilio'
}
}
function getFormattedMessage(message: string) {
// Add app signature to enable SMS retriever API
message = `<#> ${message} ${appSignature}`
if (message.length >= SMS_LENGTH_LIMIT) {
console.warn('SMS too long, attempting to shorten', message)
// TODO remove when miner nodes don't include this string anymore
message = message.replace('Celo verification code: ', '')
console.info('New message', message)
}
return message
}
const NEXMO_COUNTRY_CODES = ['MX', 'US']
async function sendViaTextProvider(phoneNumber: string, messageText: string) {
try {
console.info('Sending message via text provider')
const countryCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber))
if (countryCode === undefined) {
throw new Error('Could not detect country code of ' + phoneNumber)
}
if (NEXMO_COUNTRY_CODES.indexOf(countryCode) === -1) {
await getTwilioClient().messages.create({
body: messageText,
from: twilioPhoneNum,
to: phoneNumber,
})
console.info('Message sent via Twilio')
} else {
await retryAsyncWithBackOff(
sendSmsWithNexmo,
10,
[countryCode, phoneNumber, messageText],
1000
)
console.info('Message sent via Nexmo')
}
} catch (e) {
console.error('Failed to send text message via txt provider', e)
throw new Error('Failed to send text message via txt provider' + e)
}
}
async function getRandomActiveVerifiers(numToSelect: number, phoneNumber: string) {
const verifiers = await getActiveVerifiers()
if (!verifiers || Object.keys(verifiers).length === 0) {
console.info('No verifiers found in database')
return null
}
// Firebase DB queries only allows for a single filter so we do additional filtering here
// Find active verifiers in the regionCode that aren't already assigned to a message
// for that same target phone number
const regionCode = phoneUtil.getRegionCodeForNumber(phoneUtil.parse(phoneNumber))
console.info(`Detected region code ${regionCode} for phone ${phoneNumber}`)
const preAssignedVerifiers = await getVerifiersAssignedToNumber(phoneNumber)
const regionalVerifiers = Object.keys(verifiers)
.map((id) => {
verifiers[id].id = id // assign for convinience
return verifiers[id]
})
.filter(
(verifier) =>
verifier.supportedRegion === regionCode &&
!preAssignedVerifiers.has(verifier.id) &&
verifier.phoneNum !== phoneNumber
)
console.info(`Found ${regionalVerifiers.length} regional active verifiers`)
if (!regionalVerifiers || regionalVerifiers.length === 0) {
return null
}
// Select some number of verifiers randomly from the those eligible
numToSelect = Math.min(numToSelect, regionalVerifiers.length)
const selectedVerifiers: MobileVerifier[] = []
for (let i = 0; i < numToSelect; i++) {
const index = Math.floor(Math.random() * regionalVerifiers.length)
selectedVerifiers.push(regionalVerifiers[index])
regionalVerifiers.splice(index, 1)
}
return selectedVerifiers
}
async function getVerifiersAssignedToNumber(phoneNumber: string) {
const assignedVerifiers = new Set()
const messages = await getMessagesForPhoneNumber(phoneNumber)
if (messages) {
// For every message, add each of it's verifier candidates
for (const id of Object.keys(messages)) {
const candidates = messages[id].verifierCandidates
if (!candidates) {
continue
}
candidates.split(',').map((verifierId: string) => assignedVerifiers.add(verifierId))
}
}
return assignedVerifiers
}
async function triggerVerifiersSendSms(verifiers: MobileVerifier[], messageId: string) {
await Promise.all<any>(
verifiers.map((v) => sendVerifierPushNotification(messageId, v.fcmToken, v.id))
)
return Promise.all<any>(verifiers.map((v) => incrementVerifierAttemptCount(v.id)))
}
async function sendVerifierPushNotification(
messageId: string,
fcmToken: string,
verifierId: string
) {
if (!messageId) {
console.error('No messageId provided to notifiy for')
return
}
if (!fcmToken) {
console.error('No fcm token provided for verifier')
return
}
console.info(`Sending notification to fcm token ${fcmToken} for message ${messageId}`)
// Prepare a message to be sent.
const message: messaging.Message = {
data: {
messageId,
},
android: {
ttl: 3600 * 1000, // 1 hour in milliseconds
priority: 'high',
},
token: fcmToken,
}
try {
await messaging().send(message)
} catch (error) {
console.warn('Failed to send notification message', error)
if (error.message && error.message.includes('Requested entity was not found')) {
console.warn('Disabling the verifier that could not be reached to prevent retries')
setVerifierProperties(verifierId, { isVerifying: false })
}
}
}
// Disable verifiers that have too high attempt count
export async function disableInactiveVerifers() {
console.info('Finding verifiers with attempt count past threshold')
const verifiers = await getActiveVerifiers()
if (!verifiers) {
return
}
return Promise.all<any>(
Object.keys(verifiers).map((id) => {
if (verifiers[id].attemptCount >= MAX_VERIFIER_ATTEMPT_COUNT) {
console.info('Attempt count exceeded for verifier, disabling:', id)
return setVerifierProperties(id, { isVerifying: false })
}
return null
})
)
}