@tiledesk/tiledesk-voice-twilio-connector 0.1.28 → 0.2.0-rc3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +179 -0
- package/README.md +44 -0
- package/index.js +7 -1562
- package/package.json +23 -22
- package/src/app.js +146 -0
- package/src/config/index.js +32 -0
- package/src/controllers/VoiceController.js +488 -0
- package/src/controllers/VoiceController.original.js +811 -0
- package/src/middlewares/httpLogger.js +31 -0
- package/src/models/KeyValueStore.js +78 -0
- package/src/routes/manageApp.js +298 -0
- package/src/routes/voice.js +22 -0
- package/src/services/AiService.js +219 -0
- package/src/services/AiService.sdk.js +367 -0
- package/src/services/IntegrationService.js +74 -0
- package/src/services/MessageService.js +133 -0
- package/src/services/README_SDK.md +107 -0
- package/src/services/SessionService.js +143 -0
- package/src/services/SpeechService.js +134 -0
- package/src/services/TiledeskMessageBuilder.js +135 -0
- package/src/services/TwilioService.js +122 -0
- package/src/services/UploadService.js +78 -0
- package/src/services/channels/TiledeskChannel.js +269 -0
- package/{tiledesk → src/services/channels}/VoiceChannel.js +17 -56
- package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
- package/src/services/index.js +45 -0
- package/src/services/translators/TiledeskTwilioTranslator.js +509 -0
- package/{tiledesk/TiledeskTwilioTranslator.js → src/services/translators/TiledeskTwilioTranslator.original.js} +119 -212
- package/src/utils/fileUtils.js +24 -0
- package/src/utils/logger.js +32 -0
- package/{tiledesk → src/utils}/utils-message.js +6 -21
- package/logs/app.log +0 -3082
- package/routes/manageApp.js +0 -419
- package/tiledesk/KVBaseMongo.js +0 -101
- package/tiledesk/TiledeskChannel.js +0 -363
- package/tiledesk/TiledeskSubscriptionClient.js +0 -135
- package/tiledesk/fileUtils.js +0 -55
- package/tiledesk/services/AiService.js +0 -230
- package/tiledesk/services/IntegrationService.js +0 -81
- package/tiledesk/services/UploadService.js +0 -88
- /package/{winston.js → src/config/logger.js} +0 -0
- /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
- /package/{template → src/template}/configure.html +0 -0
- /package/{template → src/template}/css/configure.css +0 -0
- /package/{template → src/template}/css/error.css +0 -0
- /package/{template → src/template}/css/style.css +0 -0
- /package/{template → src/template}/error.html +0 -0
- /package/{tiledesk → src/utils}/constants.js +0 -0
- /package/{tiledesk → src/utils}/errors.js +0 -0
- /package/{tiledesk → src/utils}/utils.js +0 -0
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
var winston = require('../../winston');
|
|
2
|
-
const axios = require("axios").default;
|
|
3
|
-
const FormData = require('form-data');
|
|
4
|
-
|
|
5
|
-
/*ERROR HANDLER*/
|
|
6
|
-
const { ServiceError } = require('../errors');
|
|
7
|
-
|
|
8
|
-
/*UTILS*/
|
|
9
|
-
const fileUtils = require('../fileUtils.js')
|
|
10
|
-
|
|
11
|
-
class AiService {
|
|
12
|
-
|
|
13
|
-
constructor(config) {
|
|
14
|
-
|
|
15
|
-
if (!config) {
|
|
16
|
-
throw new Error("[AiService] config is mandatory");
|
|
17
|
-
}
|
|
18
|
-
if (!config.OPENAI_ENDPOINT) {
|
|
19
|
-
throw new Error("[AiService] config.OPENAI_ENDPOINT is mandatory");
|
|
20
|
-
}
|
|
21
|
-
if(!config.ELEVENLABS_ENDPOINT){
|
|
22
|
-
throw new Error("[AiService] config.ELEVENLABS_ENDPOINT is mandatory");
|
|
23
|
-
}
|
|
24
|
-
if (!config.API_URL) {
|
|
25
|
-
throw new Error("[AiService] config.API_URL is mandatory");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.OPENAI_ENDPOINT = config.OPENAI_ENDPOINT;
|
|
30
|
-
this.ELEVENLABS_ENDPOINT = config.ELEVENLABS_ENDPOINT;
|
|
31
|
-
this.API_URL = config.API_URL;
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async speechToText(fileUrl, model, GPT_KEY) {
|
|
36
|
-
let start_time = new Date();
|
|
37
|
-
winston.debug("[AiService] speechToText url: "+ fileUrl);
|
|
38
|
-
|
|
39
|
-
return new Promise(async (resolve, reject) => {
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
43
|
-
winston.error("[AiService] err while downloadFromUrl: ", err)
|
|
44
|
-
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
if (!file) {
|
|
48
|
-
winston.debug('[AiService] OPENAI speechToText file NOT EXIST: . . . return')
|
|
49
|
-
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const formData = new FormData();
|
|
53
|
-
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
54
|
-
formData.append('model', model);
|
|
55
|
-
|
|
56
|
-
axios({
|
|
57
|
-
url: `${this.OPENAI_ENDPOINT}/audio/transcriptions`,
|
|
58
|
-
headers: {
|
|
59
|
-
...formData.getHeaders(),
|
|
60
|
-
"Authorization": "Bearer " + GPT_KEY
|
|
61
|
-
},
|
|
62
|
-
data: formData,
|
|
63
|
-
method: 'POST'
|
|
64
|
-
}).then((resbody) => {
|
|
65
|
-
resolve(resbody.data.text);
|
|
66
|
-
let end_time = new Date();
|
|
67
|
-
winston.verbose(`-----> [AiService] OpenAI speechToText time elapsed: ${end_time - start_time} ms`);
|
|
68
|
-
}).catch((err) => {
|
|
69
|
-
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI /audio/transcriptions API failed with err:', err));
|
|
70
|
-
})
|
|
71
|
-
} catch (error) {
|
|
72
|
-
winston.error("[AiService] OpenAI STT error", err.message);
|
|
73
|
-
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI STT service failed with err:', err));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async textToSpeech(text, name, model, GPT_KEY){
|
|
81
|
-
let start_time = new Date();
|
|
82
|
-
winston.debug('[AiService] textToSpeech text:'+ text)
|
|
83
|
-
|
|
84
|
-
const data = {
|
|
85
|
-
model: model,
|
|
86
|
-
input: text,
|
|
87
|
-
voice: name,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
winston.debug('[AiService] textToSpeech config:', data)
|
|
91
|
-
|
|
92
|
-
return new Promise((resolve, reject) => {
|
|
93
|
-
axios({
|
|
94
|
-
url: `${this.OPENAI_ENDPOINT}/audio/speech`,
|
|
95
|
-
headers: {
|
|
96
|
-
"Content-Type": "application/json",
|
|
97
|
-
"Authorization": "Bearer " + GPT_KEY
|
|
98
|
-
},
|
|
99
|
-
responseType: 'arraybuffer',
|
|
100
|
-
data: data,
|
|
101
|
-
method: "POST",
|
|
102
|
-
}).then( async (response) => {
|
|
103
|
-
//console.log('[AiService] textToSpeech result', response?.data)
|
|
104
|
-
resolve(response?.data)
|
|
105
|
-
let end_time = new Date();
|
|
106
|
-
winston.verbose(`-----> [AiService] textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
107
|
-
})
|
|
108
|
-
.catch((err) => {
|
|
109
|
-
winston.error("[AiService] textToSpeech error: ", err.response?.data);
|
|
110
|
-
reject(new ServiceError('AISERVICE_FAILED', 'OpenAI textToSpeech API failed with err:', err));
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
async speechToTextElevenLabs(fileUrl, model, language, API_KEY) {
|
|
119
|
-
let start_time = new Date();
|
|
120
|
-
winston.debug("[AiService] ELEVEN Labs speechToText url: "+ fileUrl);
|
|
121
|
-
|
|
122
|
-
return new Promise(async (resolve, reject) => {
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
let file = await fileUtils.downloadFromUrl(fileUrl).catch((err) => {
|
|
126
|
-
winston.error("[AiService] err: ", err)
|
|
127
|
-
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file:', fileUrl));
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
if (!file) {
|
|
131
|
-
winston.debug('[AiService] ELEVEN Labs speechToText file NOT EXIST: . . . return')
|
|
132
|
-
return reject(new ServiceError('AISERVICE_FAILED', 'Cannot download audio file: file is null'));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const formData = new FormData();
|
|
137
|
-
formData.append('file', file, { filename: 'audiofile.wav', contentType: 'audio/wav' });
|
|
138
|
-
formData.append('model_id', "scribe_v1");
|
|
139
|
-
formData.append('language_code', language)
|
|
140
|
-
|
|
141
|
-
axios({
|
|
142
|
-
url: `${this.ELEVENLABS_ENDPOINT}/v1/speech-to-text`,
|
|
143
|
-
headers: {
|
|
144
|
-
...formData.getHeaders(),
|
|
145
|
-
"xi-api-key": API_KEY
|
|
146
|
-
},
|
|
147
|
-
data: formData,
|
|
148
|
-
method: 'POST'
|
|
149
|
-
}).then((resbody) => {
|
|
150
|
-
resolve(resbody.data.text);
|
|
151
|
-
let end_time = new Date();
|
|
152
|
-
winston.verbose(`-----> [AiService] ELEVEN Labs speechToText time elapsed: ${end_time - start_time} ms`);
|
|
153
|
-
}).catch((err) => {
|
|
154
|
-
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs /speech-to-text API failed with err:', err));
|
|
155
|
-
})
|
|
156
|
-
} catch (error) {
|
|
157
|
-
winston.error("[AiService] ElevenLabs STT error", err.message);
|
|
158
|
-
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs STT service failed with err:', err));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async textToSpeechElevenLabs(text, voice_id, model, language_code, API_KEY){
|
|
165
|
-
let start_time = new Date();
|
|
166
|
-
const data = {
|
|
167
|
-
model_id: model,
|
|
168
|
-
text: text,
|
|
169
|
-
language_code: language_code
|
|
170
|
-
};
|
|
171
|
-
winston.debug('[AiService] ELEVEN Labs textToSpeech config:', data)
|
|
172
|
-
|
|
173
|
-
return new Promise((resolve, reject) => {
|
|
174
|
-
axios({
|
|
175
|
-
url: `${this.ELEVENLABS_ENDPOINT}/v1/text-to-speech/${voice_id}?output_format=mp3_44100_128`,
|
|
176
|
-
headers: {
|
|
177
|
-
"Content-Type": "application/json",
|
|
178
|
-
"xi-api-key": API_KEY
|
|
179
|
-
},
|
|
180
|
-
responseType: 'arraybuffer',
|
|
181
|
-
data: data,
|
|
182
|
-
method: "POST",
|
|
183
|
-
}).then( async (response) => {
|
|
184
|
-
resolve(response?.data)
|
|
185
|
-
let end_time = new Date();
|
|
186
|
-
winston.verbose(`-----> [AiService] ELEVEN Labs textToSpeech time elapsed: ${end_time - start_time} ms`);
|
|
187
|
-
})
|
|
188
|
-
.catch((err) => {
|
|
189
|
-
winston.error("[AiService] ELEVEN Labs textToSpeech error: ", err);
|
|
190
|
-
reject(new ServiceError('AISERVICE_FAILED', 'ElevenLabs textToSpeech API failed with err:', err));
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
async checkQuoteAvailability(projectId, token) {
|
|
199
|
-
|
|
200
|
-
winston.debug("[AiService] checkQuoteAvailability for project: "+ projectId);
|
|
201
|
-
|
|
202
|
-
return new Promise((resolve, reject) => {
|
|
203
|
-
|
|
204
|
-
axios({
|
|
205
|
-
url: `${this.API_URL}/${projectId}/quotes/tokens`,
|
|
206
|
-
headers: {
|
|
207
|
-
'Content-Type': 'application/json',
|
|
208
|
-
'Authorization': token
|
|
209
|
-
},
|
|
210
|
-
method: 'GET'
|
|
211
|
-
}).then((resbody) => {
|
|
212
|
-
if (resbody && resbody.data?.isAvailable === true) {
|
|
213
|
-
resolve(true)
|
|
214
|
-
} else {
|
|
215
|
-
resolve(false)
|
|
216
|
-
}
|
|
217
|
-
}).catch((err) => {
|
|
218
|
-
winston.error("[AiService] checkQuoteAvailability error: ", err.response?.data);
|
|
219
|
-
reject(new ServiceError('AISERVICE_FAILED', 'checkQuoteAvailability API failed with err:', err));
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
module.exports = { AiService };
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
var winston = require('../../winston');
|
|
3
|
-
const axios = require("axios").default;
|
|
4
|
-
const FormData = require('form-data');
|
|
5
|
-
|
|
6
|
-
/*UTILS*/
|
|
7
|
-
const fileUtils = require('../fileUtils.js')
|
|
8
|
-
|
|
9
|
-
class IntegrationService {
|
|
10
|
-
|
|
11
|
-
constructor(config) {
|
|
12
|
-
|
|
13
|
-
if (!config) {
|
|
14
|
-
throw new Error("[IntegrationService] config is mandatory");
|
|
15
|
-
}
|
|
16
|
-
if (!config.API_URL) {
|
|
17
|
-
throw new Error("[IntegrationService] config.API_URL is mandatory");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
this.API_URL = config.API_URL;
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
async getKeyFromIntegrations(id_project, integration_name, token){
|
|
26
|
-
|
|
27
|
-
winston.debug('[IntegrationService] getKeyFromIntegrations id_project:'+ id_project + ' ' + integration_name)
|
|
28
|
-
|
|
29
|
-
return await axios({
|
|
30
|
-
url: this.API_URL + "/"+ id_project + "/integration/name/" + integration_name,
|
|
31
|
-
headers: {
|
|
32
|
-
'Content-Type': 'application/json',
|
|
33
|
-
'Authorization': token
|
|
34
|
-
},
|
|
35
|
-
data: {},
|
|
36
|
-
method: "GET",
|
|
37
|
-
}).then( async (response) => {
|
|
38
|
-
if (!response.data || !response.data?.value) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return response.data?.value?.apikey
|
|
43
|
-
})
|
|
44
|
-
.catch((err) => {
|
|
45
|
-
winston.error("[IntegrationService] getKeyFromIntegrations error: ", err.response?.data);
|
|
46
|
-
return null;
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async getKeyFromKbSettings(id_project, token) {
|
|
53
|
-
|
|
54
|
-
winston.debug('[IntegrationService] getKeyFromIntegrations id_project:', id_project)
|
|
55
|
-
|
|
56
|
-
return await axios({
|
|
57
|
-
url: this.API_URL + "/"+ id_project + "/kbsettings",
|
|
58
|
-
headers: {
|
|
59
|
-
'Content-Type': 'application/json',
|
|
60
|
-
'Authorization': token
|
|
61
|
-
},
|
|
62
|
-
data: {},
|
|
63
|
-
method: "GET",
|
|
64
|
-
}).then( async (response) => {
|
|
65
|
-
if (!response.data || response.data?.gptkey) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return response.data?.gptkey
|
|
70
|
-
})
|
|
71
|
-
.catch((err) => {
|
|
72
|
-
return null;
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = { IntegrationService };
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
var winston = require('../../winston');
|
|
2
|
-
const axios = require("axios").default;
|
|
3
|
-
const FormData = require('form-data');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
/*UTILS*/
|
|
8
|
-
const fileUtils = require('../fileUtils.js')
|
|
9
|
-
|
|
10
|
-
/*ERROR HANDLER*/
|
|
11
|
-
const { ServiceError } = require('../errors');
|
|
12
|
-
|
|
13
|
-
class UploadService {
|
|
14
|
-
|
|
15
|
-
constructor(config) {
|
|
16
|
-
|
|
17
|
-
if (!config) {
|
|
18
|
-
throw new Error("[UploadService] config is mandatory");
|
|
19
|
-
}
|
|
20
|
-
if (!config.API_URL) {
|
|
21
|
-
throw new Error("[UploadService] config.API_URL is mandatory");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (config.user) {
|
|
25
|
-
this.user = config.user
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
this.API_URL = config.API_URL;
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async upload(id, file, user) {
|
|
33
|
-
|
|
34
|
-
winston.debug(`[UploadService] upload for id ${id} and user ${user}`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Step 2: Salva il file MP3 localmente (temporaneamente)
|
|
38
|
-
const tempFilePath = path.join(__dirname, `speech_${user._id}_${id}.wav`);
|
|
39
|
-
fs.writeFileSync(tempFilePath, file);
|
|
40
|
-
|
|
41
|
-
// Step 3: Carica il file al tuo server
|
|
42
|
-
const formData = new FormData();
|
|
43
|
-
formData.append('file', fs.createReadStream(tempFilePath), {
|
|
44
|
-
filename: `audiofile_${user._id}_${id}.wav`,
|
|
45
|
-
contentType: 'audio/mpeg'
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
|
|
51
|
-
//const formData = new FormData();
|
|
52
|
-
//formData.append('file', file, { filename: 'audiofile_'+user._id+'_'+id+'.mp3', contentType: 'audio/mpeg' });
|
|
53
|
-
user.token = 'JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWM1ZjExNjlmYWYyZDA0Y2Q3ZGE1MjciLCJlbWFpbCI6ImdhYnJpZWxlQHRpbGVkZXNrLmNvbSIsImZpcnN0bmFtZSI6IkdhYnJpZWxlIiwibGFzdG5hbWUiOiJQYW5pY28iLCJlbWFpbHZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3NDgyNTY2MTUsImF1ZCI6Imh0dHBzOi8vdGlsZWRlc2suY29tIiwiaXNzIjoiaHR0cHM6Ly90aWxlZGVzay5jb20iLCJzdWIiOiJ1c2VyIiwianRpIjoiNWUyZDhhYmUtYzQ0YS00MjJiLWE3MjUtYWYwMjcxNDgyZTczIn0.AcT1tNbE3AcfctJXfOsfUbytRNUQlhBqPUctxzXMjehZOS2ORJThWaPqPxrvqTTIyeOU2l6eoTw8_tqfRJGlp6X4m9KLio87axGl1z3WYBgh8bSMIkAw2zSIUuJmpjBuT8EZdjXZClXRUAliAvAoFRgCmhWJ1tODVvBynLiSb37sB_zscqWH5L5eF1vdt6HHizEO4HbGABQS00I2hEPn99ssC9Y3W4_UhDcitZG80ACwS_Bpl6uk8OxAFybZ1DHHkBS1AK-lCO2P2JJCFRyM33mcvTgb9B6pADETzgJT2qfgOU4-1Pm0l55Mij1LS-h7QTj95DTFQMM7DD6elP0WcA'
|
|
54
|
-
|
|
55
|
-
axios({
|
|
56
|
-
url: this.API_URL + "/files/users",
|
|
57
|
-
headers: {
|
|
58
|
-
...formData.getHeaders(),
|
|
59
|
-
"Authorization": user.token
|
|
60
|
-
},
|
|
61
|
-
data: formData,
|
|
62
|
-
method: 'POST'
|
|
63
|
-
}).then((resbody) => {
|
|
64
|
-
if(resbody?.data){
|
|
65
|
-
|
|
66
|
-
// Step 4: Pulisci il file temporaneo
|
|
67
|
-
fs.unlinkSync(tempFilePath);
|
|
68
|
-
|
|
69
|
-
let fileUrl = this.API_URL + "/files?path="+resbody?.data.filename
|
|
70
|
-
resolve(fileUrl);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
}).catch((err) => {
|
|
74
|
-
console.log('err', err)
|
|
75
|
-
reject(new ServiceError('UPLOADSERVICE_FAILED', 'UploadService /files/users API failed with err:', err) );
|
|
76
|
-
}).finally(() => {
|
|
77
|
-
// Sempre eseguito
|
|
78
|
-
if (fs.existsSync(tempFilePath)) {
|
|
79
|
-
fs.unlinkSync(tempFilePath);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
module.exports = { UploadService };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|