Skip to content

Commit 9d4c99a

Browse files
Merge pull request #113 from Amruth-Vamshi/feature/WA-provider
Add Fonada SMS service integration and update .env.sample
2 parents f54c259 + f6c0889 commit 9d4c99a

File tree

3 files changed

+247
-1
lines changed

3 files changed

+247
-1
lines changed

.env.sample

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ CDAC_SERVICE_URL=
1212
CDAC_OTP_TEMPLATE_ID="123456"
1313
CDAC_OTP_TEMPLATE="Respected User, The OTP to reset password for %phone% is %code%."
1414

15+
# FONADA
16+
FONADA_SERVICE_URL=
17+
FONADA_OTP_TEMPLATE=
18+
FONADA_OTP_TEMPLATE_ID=
19+
FONADA_USERNAME=
20+
FONADA_PASSWORD=
21+
1522
# SMS Adapter
1623
SMS_ADAPTER_TYPE= # CDAC or GUPSHUP or RAJAI
1724
SMS_TOTP_SECRET= # any random string, needed for CDAC

src/api/api.module.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CdacService } from './sms/cdac/cdac.service';
1414
import { RajaiOtpService } from '../user/sms/rajaiOtpService/rajaiOtpService.service';
1515
import { GupshupWhatsappService } from './sms/gupshupWhatsapp/gupshupWhatsapp.service';
1616
import { TelemetryService } from 'src/telemetry/telemetry.service';
17+
import { FonadaService } from './sms/fonada/fonada.service';
1718

1819
const otpServiceFactory = {
1920
provide: OtpService,
@@ -40,7 +41,17 @@ const otpServiceFactory = {
4041
},
4142
inject: [],
4243
}.useFactory(config.get('RAJAI_USERNAME'), config.get('RAJAI_PASSWORD'), config.get('RAJAI_BASEURL'));
43-
}
44+
} else if (config.get<string>('SMS_ADAPTER_TYPE') == 'FONADA') {
45+
factory = {
46+
provide: 'OtpService',
47+
useFactory: () => {
48+
return new FonadaService(
49+
config
50+
);
51+
},
52+
inject: [],
53+
}.useFactory();
54+
}
4455
else {
4556
factory = {
4657
provide: 'OtpService',

src/api/sms/fonada/fonada.service.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import {
2+
OTPResponse,
3+
SMS,
4+
SMSData,
5+
SMSError,
6+
SMSProvider,
7+
SMSResponse,
8+
SMSResponseStatus,
9+
SMSType,
10+
TrackResponse,
11+
} from '../sms.interface';
12+
13+
import { HttpException, Injectable } from '@nestjs/common';
14+
import { SmsService } from '../sms.service';
15+
import { ConfigService } from '@nestjs/config';
16+
import got, {Got} from 'got';
17+
import * as speakeasy from 'speakeasy';
18+
19+
@Injectable()
20+
export class FonadaService extends SmsService implements SMS {
21+
baseURL: string;
22+
path = '';
23+
data: SMSData;
24+
httpClient: Got;
25+
auth: any;
26+
constructor(
27+
private configService: ConfigService,
28+
) {
29+
super();
30+
this.baseURL = configService.get<string>('FONADA_SERVICE_URL');
31+
this.auth = {
32+
userid: configService.get<string>('FONADA_USERNAME'),
33+
password: configService.get<string>('FONADA_PASSWORD'),
34+
}
35+
this.httpClient = got;
36+
}
37+
38+
send(data: SMSData): Promise<SMSResponse> {
39+
if (!data) {
40+
throw new Error('Data cannot be empty');
41+
}
42+
this.data = data;
43+
if (this.data.type === SMSType.otp) return this.doOTPRequest(data);
44+
else return this.doRequest();
45+
}
46+
47+
track(data: SMSData): Promise<SMSResponse> {
48+
if (!data) {
49+
throw new Error('Data cannot be null');
50+
}
51+
this.data = data;
52+
if (this.data.type === SMSType.otp) return this.verifyOTP(data);
53+
else return this.doRequest();
54+
}
55+
56+
private getTotpSecret(phone): string {
57+
return `${this.configService.get<string>('SMS_TOTP_SECRET')}${phone}`
58+
}
59+
60+
private doOTPRequest(data: SMSData): Promise<any> {
61+
let otp = '';
62+
try {
63+
otp = speakeasy.totp({
64+
secret: this.getTotpSecret(data.phone),
65+
encoding: 'base32',
66+
step: this.configService.get<string>('SMS_TOTP_EXPIRY'),
67+
digits: 4,
68+
});
69+
} catch (error) {
70+
throw new HttpException('TOTP generation failed!', 500);
71+
}
72+
73+
const payload = this.configService.get<string>('FONADA_OTP_TEMPLATE')
74+
.replace('%phone%', data.phone)
75+
.replace('%code%', otp + '');
76+
const params = new URLSearchParams({
77+
username:this.auth.userid,
78+
password:this.auth.password,
79+
unicode:"true",
80+
from:"CMPTKM",
81+
to:data.phone,
82+
text:payload,
83+
dltContentId:this.configService.get<string>('FONADA_OTP_TEMPLATE_ID'),
84+
});
85+
this.path = '/fe/api/v1/send'
86+
const url = `${this.baseURL}${this.path}?${params.toString()}`;
87+
88+
const status: OTPResponse = {} as OTPResponse;
89+
status.provider = SMSProvider.cdac;
90+
status.phone = data.phone;
91+
92+
// noinspection DuplicatedCode
93+
return this.httpClient.get(url, {})
94+
.then((response): OTPResponse => {
95+
status.networkResponseCode = 200;
96+
const r = this.parseResponse(response.body);
97+
status.messageID = r.messageID;
98+
status.error = r.error;
99+
status.providerResponseCode = r.providerResponseCode;
100+
status.providerSuccessResponse = r.providerSuccessResponse;
101+
status.status = r.status;
102+
return status;
103+
})
104+
.catch((e: Error): OTPResponse => {
105+
const error: SMSError = {
106+
errorText: `Uncaught Exception :: ${e.message}`,
107+
errorCode: 'CUSTOM ERROR',
108+
};
109+
status.networkResponseCode = 200;
110+
status.messageID = null;
111+
status.error = error;
112+
status.providerResponseCode = null;
113+
status.providerSuccessResponse = null;
114+
status.status = SMSResponseStatus.failure;
115+
return status;
116+
});
117+
}
118+
119+
doRequest(): Promise<SMSResponse> {
120+
throw new Error('Method not implemented.');
121+
}
122+
123+
parseResponse(response: any) {
124+
response = JSON.parse(response);
125+
try {
126+
if (response.state == 'SUBMIT_ACCEPTED') {
127+
return {
128+
providerResponseCode: response.state,
129+
status: SMSResponseStatus.success,
130+
messageID: response.transactionId,
131+
error: null,
132+
providerSuccessResponse: null,
133+
};
134+
} else {
135+
const error: SMSError = {
136+
errorText: response.description,
137+
errorCode: response.state,
138+
};
139+
return {
140+
providerResponseCode: response.state,
141+
status: SMSResponseStatus.failure,
142+
messageID: response.transactionId,
143+
error,
144+
providerSuccessResponse: null,
145+
};
146+
}
147+
} catch (e) {
148+
const error: SMSError = {
149+
errorText: `CDAC response could not be parsed :: ${e.message}; Provider Response - ${response}`,
150+
errorCode: 'CUSTOM ERROR',
151+
};
152+
return {
153+
providerResponseCode: null,
154+
status: SMSResponseStatus.failure,
155+
messageID: null,
156+
error,
157+
providerSuccessResponse: null,
158+
};
159+
}
160+
}
161+
162+
verifyOTP(data: SMSData): Promise<TrackResponse> {
163+
if(
164+
process.env.ALLOW_DEFAULT_OTP === 'true' &&
165+
process.env.DEFAULT_OTP_USERS
166+
){
167+
if(JSON.parse(process.env.DEFAULT_OTP_USERS).indexOf(data.phone)!=-1){
168+
if(data.params.otp == process.env.DEFAULT_OTP) {
169+
return new Promise(resolve => {
170+
const status: TrackResponse = {} as TrackResponse;
171+
status.provider = SMSProvider.cdac;
172+
status.phone = data.phone;
173+
status.networkResponseCode = 200;
174+
status.messageID = Date.now() + '';
175+
status.error = null;
176+
status.providerResponseCode = null;
177+
status.providerSuccessResponse = 'OTP matched.';
178+
status.status = SMSResponseStatus.success;
179+
resolve(status);
180+
});
181+
}
182+
}
183+
}
184+
185+
let verified = false;
186+
try {
187+
verified = speakeasy.totp.verify({
188+
secret: this.getTotpSecret(data.phone.replace(/^\+\d{1,3}[-\s]?/, '')),
189+
encoding: 'base32',
190+
token: data.params.otp,
191+
step: this.configService.get<string>('SMS_TOTP_EXPIRY'),
192+
digits: 4,
193+
});
194+
if (verified) {
195+
return new Promise(resolve => {
196+
const status: TrackResponse = {} as TrackResponse;
197+
status.provider = SMSProvider.cdac;
198+
status.phone = data.phone;
199+
status.messageID = '';
200+
status.error = null;
201+
status.providerResponseCode = null;
202+
status.providerSuccessResponse = null;
203+
status.status = SMSResponseStatus.success;
204+
resolve(status);
205+
});
206+
} else {
207+
return new Promise(resolve => {
208+
const status: TrackResponse = {} as TrackResponse;
209+
status.provider = SMSProvider.cdac;
210+
status.phone = data.phone;
211+
status.networkResponseCode = 200;
212+
status.messageID = '';
213+
status.error = {
214+
errorText: 'Invalid or expired OTP.',
215+
errorCode: '400'
216+
};
217+
status.providerResponseCode = '400';
218+
status.providerSuccessResponse = null;
219+
status.status = SMSResponseStatus.failure;
220+
resolve(status);
221+
});
222+
}
223+
} catch(error) {
224+
throw new HttpException(error, 500);
225+
}
226+
}
227+
}
228+

0 commit comments

Comments
 (0)