@tiledesk/tiledesk-voice-twilio-connector 0.1.28 → 0.2.0-rc6

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.
Files changed (49) hide show
  1. package/LICENSE +179 -0
  2. package/README.md +515 -0
  3. package/index.js +7 -1562
  4. package/package.json +23 -21
  5. package/src/app.js +154 -0
  6. package/src/config/index.js +32 -0
  7. package/src/controllers/VoiceController.js +493 -0
  8. package/src/middlewares/httpLogger.js +43 -0
  9. package/src/models/KeyValueStore.js +78 -0
  10. package/src/routes/manageApp.js +298 -0
  11. package/src/routes/voice.js +22 -0
  12. package/src/services/AiService.js +219 -0
  13. package/src/services/AiService.sdk.js +367 -0
  14. package/src/services/IntegrationService.js +74 -0
  15. package/src/services/MessageService.js +139 -0
  16. package/src/services/README_SDK.md +107 -0
  17. package/src/services/SessionService.js +143 -0
  18. package/src/services/SpeechService.js +134 -0
  19. package/src/services/TiledeskMessageBuilder.js +135 -0
  20. package/src/services/TwilioService.js +129 -0
  21. package/src/services/UploadService.js +78 -0
  22. package/src/services/channels/TiledeskChannel.js +268 -0
  23. package/{tiledesk → src/services/channels}/VoiceChannel.js +20 -59
  24. package/src/services/clients/TiledeskSubscriptionClient.js +78 -0
  25. package/src/services/index.js +45 -0
  26. package/src/services/translators/TiledeskTwilioTranslator.js +514 -0
  27. package/src/utils/fileUtils.js +24 -0
  28. package/src/utils/logger.js +32 -0
  29. package/{tiledesk → src/utils}/utils-message.js +6 -21
  30. package/logs/app.log +0 -3082
  31. package/routes/manageApp.js +0 -419
  32. package/tiledesk/KVBaseMongo.js +0 -101
  33. package/tiledesk/TiledeskChannel.js +0 -363
  34. package/tiledesk/TiledeskSubscriptionClient.js +0 -135
  35. package/tiledesk/TiledeskTwilioTranslator.js +0 -707
  36. package/tiledesk/fileUtils.js +0 -55
  37. package/tiledesk/services/AiService.js +0 -230
  38. package/tiledesk/services/IntegrationService.js +0 -81
  39. package/tiledesk/services/UploadService.js +0 -88
  40. /package/{winston.js → src/config/logger.js} +0 -0
  41. /package/{tiledesk → src}/services/voiceEventEmitter.js +0 -0
  42. /package/{template → src/template}/configure.html +0 -0
  43. /package/{template → src/template}/css/configure.css +0 -0
  44. /package/{template → src/template}/css/error.css +0 -0
  45. /package/{template → src/template}/css/style.css +0 -0
  46. /package/{template → src/template}/error.html +0 -0
  47. /package/{tiledesk → src/utils}/constants.js +0 -0
  48. /package/{tiledesk → src/utils}/errors.js +0 -0
  49. /package/{tiledesk → src/utils}/utils.js +0 -0
@@ -0,0 +1,143 @@
1
+ const logger = require('../utils/logger');
2
+ const { CHANNEL_NAME, VOICE_LANGUAGE, VOICE_NAME } = require('../utils/constants');
3
+
4
+ /**
5
+ * Service responsible for managing voice call session lifecycle.
6
+ * Handles session creation, retrieval, and cleanup.
7
+ */
8
+ class SessionService {
9
+ constructor({ voiceChannel, integrationService, tdChannel, config }) {
10
+ if (!voiceChannel) throw new Error('[SessionService] voiceChannel is required');
11
+ if (!integrationService) throw new Error('[SessionService] integrationService is required');
12
+ if (!tdChannel) throw new Error('[SessionService] tdChannel is required');
13
+ if (!config) throw new Error('[SessionService] config is required');
14
+
15
+ this.voiceChannel = voiceChannel;
16
+ this.integrationService = integrationService;
17
+ this.tdChannel = tdChannel;
18
+ this.config = config;
19
+ }
20
+
21
+ /**
22
+ * Retrieve integration keys (OpenAI, ElevenLabs) for a project.
23
+ * @param {string} projectId - The project ID
24
+ * @param {string} token - Authorization token
25
+ * @returns {Promise<Array>} Array of integration objects
26
+ */
27
+ async getIntegrationKeys(projectId, token) {
28
+ const integrations = [];
29
+
30
+ // Retrieve OpenAI key with fallback chain
31
+ const openaiKey = await this._getOpenAIKey(projectId, token);
32
+ if (openaiKey) {
33
+ integrations.push(openaiKey);
34
+ }
35
+
36
+ // Retrieve ElevenLabs key
37
+ const elevenLabsKey = await this._getElevenLabsKey(projectId, token);
38
+ if (elevenLabsKey) {
39
+ integrations.push(elevenLabsKey);
40
+ }
41
+
42
+ return integrations;
43
+ }
44
+
45
+ /**
46
+ * Get OpenAI key with fallback: Integrations -> KB Settings -> Public Key
47
+ */
48
+ async _getOpenAIKey(projectId, token) {
49
+ try {
50
+ let key = await this.integrationService.getKeyFromIntegrations(projectId, 'openai', token);
51
+
52
+ if (!key) {
53
+ // logger.debug('[SessionService] OpenAI key not in Integrations, checking KB settings...');
54
+ key = await this.integrationService.getKeyFromKbSettings(projectId, token);
55
+ }
56
+
57
+ if (!key) {
58
+ // logger.debug('[SessionService] Using public GPT key');
59
+ return { type: 'openai', key: this.config.GPT_KEY, publicKey: true };
60
+ }
61
+
62
+ return { type: 'openai', key, publicKey: false };
63
+ } catch (error) {
64
+ logger.error('[SessionService] Error retrieving OpenAI key:', error);
65
+ return { type: 'openai', key: this.config.GPT_KEY, publicKey: true };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get ElevenLabs key from integrations
71
+ */
72
+ async _getElevenLabsKey(projectId, token) {
73
+ try {
74
+ const key = await this.integrationService.getKeyFromIntegrations(projectId, 'elevenlabs', token);
75
+ if (key) {
76
+ // logger.debug('[SessionService] ElevenLabs key found');
77
+ return { type: 'elevenlabs', key, publicKey: false };
78
+ }
79
+ return null;
80
+ } catch (error) {
81
+ logger.error('[SessionService] Error retrieving ElevenLabs key:', error);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Create and store a new call session.
88
+ * @param {Object} params - Session parameters
89
+ * @returns {Promise<Object>} The created session data
90
+ */
91
+ async createSession({ callSid, from, to, projectId, user, conversationId, integrations }) {
92
+ const sessionData = {
93
+ from,
94
+ to,
95
+ callSid,
96
+ project_id: projectId,
97
+ user,
98
+ conversation_id: conversationId,
99
+ integrations
100
+ };
101
+
102
+ await this.voiceChannel.setSessionForCallId(callSid, sessionData);
103
+ // logger.debug(`[SessionService] Session created for callSid: ${callSid}`);
104
+
105
+ return sessionData;
106
+ }
107
+
108
+ /**
109
+ * Retrieve session context for a call.
110
+ * @param {string} callSid - The call session ID
111
+ * @returns {Promise<Object>} Session context with vxmlAttributes
112
+ */
113
+ async getSessionContext(callSid) {
114
+ const sessionInfo = await this.voiceChannel.getSessionForCallId(callSid);
115
+
116
+ if (!sessionInfo) {
117
+ throw new Error(`[SessionService] No session found for callSid: ${callSid}`);
118
+ }
119
+
120
+ const { project_id, from, conversation_id, user } = sessionInfo;
121
+
122
+ const vxmlAttributes = {
123
+ TTS_VOICE_LANGUAGE: VOICE_LANGUAGE,
124
+ TTS_VOICE_NAME: VOICE_NAME,
125
+ callSid
126
+ };
127
+
128
+ return { sessionInfo, project_id, from, conversation_id, user, vxmlAttributes };
129
+ }
130
+
131
+ /**
132
+ * Clean up session data for a completed call.
133
+ * @param {string} callSid - The call session ID
134
+ * @param {string} conversationId - The conversation ID
135
+ */
136
+ async cleanupSession(callSid, conversationId) {
137
+ await this.voiceChannel.deleteCallKeys(callSid);
138
+ await this.tdChannel.clearQueue(conversationId);
139
+ // logger.debug(`[SessionService] Session cleaned up for callSid: ${callSid}`);
140
+ }
141
+ }
142
+
143
+ module.exports = { SessionService };
@@ -0,0 +1,134 @@
1
+ const logger = require('../utils/logger');
2
+ const { CHANNEL_NAME, VOICE_PROVIDER } = require('../utils/constants');
3
+
4
+ /**
5
+ * Service responsible for Speech-to-Text operations.
6
+ * Encapsulates provider-specific logic for OpenAI and ElevenLabs.
7
+ */
8
+ class SpeechService {
9
+ constructor({ aiService, voiceChannel }) {
10
+ if (!aiService) throw new Error('[SpeechService] aiService is required');
11
+ if (!voiceChannel) throw new Error('[SpeechService] voiceChannel is required');
12
+
13
+ this.aiService = aiService;
14
+ this.voiceChannel = voiceChannel;
15
+ }
16
+
17
+ /**
18
+ * Convert audio recording to text using configured provider.
19
+ *
20
+ * @param {string} audioFileUrl - URL of the audio recording
21
+ * @param {string} callSid - The call session ID
22
+ * @param {Object} sessionInfo - Session information including integrations
23
+ * @param {Object} settings - Project settings
24
+ * @returns {Promise<Object|null>} Tiledesk message object or null if STT failed
25
+ */
26
+ async transcribeAudio(audioFileUrl, callSid, sessionInfo, settings) {
27
+ const attributes = await this.voiceChannel.getSettingsForCallId(callSid);
28
+ // logger.debug(`[SpeechService] Transcribing audio: ${audioFileUrl}, provider: ${attributes.VOICE_PROVIDER}`);
29
+
30
+ try {
31
+ const text = await this._performSTT(audioFileUrl, attributes, sessionInfo, settings);
32
+
33
+ if (!text) {
34
+ // logger.debug('[SpeechService] STT returned empty text');
35
+ return null;
36
+ }
37
+
38
+ return this._buildTextMessage(text, sessionInfo.from);
39
+
40
+ } catch (error) {
41
+ logger.error('[SpeechService] Transcription error:', error);
42
+
43
+ if (error.code === 'QUOTA_EXCEEDED') {
44
+ return this._buildCloseMessage(sessionInfo.from, 'quota_exceeded');
45
+ }
46
+
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Perform STT based on configured provider.
53
+ */
54
+ async _performSTT(audioFileUrl, attributes, sessionInfo, settings) {
55
+ const provider = attributes.VOICE_PROVIDER;
56
+
57
+ switch (provider) {
58
+ case VOICE_PROVIDER.OPENAI:
59
+ return this._transcribeWithOpenAI(audioFileUrl, attributes, sessionInfo, settings);
60
+
61
+ case VOICE_PROVIDER.ELEVENLABS:
62
+ return this._transcribeWithElevenLabs(audioFileUrl, attributes, sessionInfo);
63
+
64
+ default:
65
+ throw new Error(`[SpeechService] Unsupported provider: ${provider}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Transcribe audio using OpenAI Whisper.
71
+ */
72
+ async _transcribeWithOpenAI(audioFileUrl, attributes, sessionInfo, settings) {
73
+ const integration = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.OPENAI);
74
+ const apiKey = integration?.key;
75
+ const isPublicKey = integration?.publicKey;
76
+
77
+ // Check quota for public keys
78
+ if (isPublicKey) {
79
+ const hasQuota = await this.aiService.checkQuoteAvailability(sessionInfo.project_id, settings.token);
80
+ // logger.debug(`[SpeechService] Quota check: ${hasQuota}`);
81
+
82
+ if (!hasQuota) {
83
+ const error = new Error('No quota available');
84
+ error.code = 'QUOTA_EXCEEDED';
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ return this.aiService.speechToText(audioFileUrl, attributes.STT_MODEL, apiKey);
90
+ }
91
+
92
+ /**
93
+ * Transcribe audio using ElevenLabs.
94
+ */
95
+ async _transcribeWithElevenLabs(audioFileUrl, attributes, sessionInfo) {
96
+ const integration = sessionInfo.integrations.find(i => i.type === VOICE_PROVIDER.ELEVENLABS);
97
+ const apiKey = integration?.key;
98
+ const language = attributes.TTS_LANGUAGE || 'en';
99
+
100
+ return this.aiService.speechToTextElevenLabs(audioFileUrl, attributes.STT_MODEL, language, apiKey);
101
+ }
102
+
103
+ /**
104
+ * Build a standard text message.
105
+ */
106
+ _buildTextMessage(text, senderFullname) {
107
+ return {
108
+ text,
109
+ senderFullname,
110
+ type: 'text',
111
+ channel: { name: CHANNEL_NAME }
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Build a close/event message.
117
+ */
118
+ _buildCloseMessage(senderFullname, event) {
119
+ return {
120
+ text: '/close',
121
+ senderFullname,
122
+ type: 'text',
123
+ channel: { name: CHANNEL_NAME },
124
+ attributes: {
125
+ subtype: 'info',
126
+ action: `close${JSON.stringify({ event })}`,
127
+ payload: { catchEvent: event },
128
+ timestamp: Date.now()
129
+ }
130
+ };
131
+ }
132
+ }
133
+
134
+ module.exports = { SpeechService };
@@ -0,0 +1,135 @@
1
+ const { CHANNEL_NAME } = require('../utils/constants');
2
+
3
+ /**
4
+ * Factory for building Tiledesk message objects.
5
+ * Centralizes message construction to ensure consistency.
6
+ */
7
+ class TiledeskMessageBuilder {
8
+ /**
9
+ * Build a start message for initiating a conversation.
10
+ * @param {string} from - Sender identifier
11
+ * @param {string} departmentId - Department ID
12
+ * @param {Object} payload - Additional payload data
13
+ * @returns {Object} Tiledesk message object
14
+ */
15
+ static buildStartMessage(from, departmentId, payload = {}) {
16
+ return {
17
+ text: '/start',
18
+ senderFullname: from,
19
+ type: 'text',
20
+ attributes: {
21
+ subtype: 'info',
22
+ payload
23
+ },
24
+ channel: { name: CHANNEL_NAME },
25
+ departmentid: departmentId
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Build a standard text message.
31
+ * @param {string} text - Message text
32
+ * @param {string} from - Sender identifier
33
+ * @param {Object} attributes - Optional attributes
34
+ * @returns {Object} Tiledesk message object
35
+ */
36
+ static buildTextMessage(text, from, attributes = {}) {
37
+ return {
38
+ text,
39
+ senderFullname: from,
40
+ type: 'text',
41
+ channel: { name: CHANNEL_NAME },
42
+ attributes
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Build an event message (e.g., no_input, close).
48
+ * @param {string} event - Event name
49
+ * @param {string} from - Sender identifier
50
+ * @param {Object} options - Event options
51
+ * @returns {Object} Tiledesk message object
52
+ */
53
+ static buildEventMessage(event, from, { action = '', payload = {} } = {}) {
54
+ return {
55
+ text: `/${event}`,
56
+ senderFullname: from,
57
+ type: 'text',
58
+ channel: { name: CHANNEL_NAME },
59
+ attributes: {
60
+ type: 'info',
61
+ action,
62
+ payload
63
+ }
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Build a close message.
69
+ * @param {string} from - Sender identifier
70
+ * @param {string} event - The close event type
71
+ * @returns {Object} Tiledesk message object
72
+ */
73
+ static buildCloseMessage(from, event) {
74
+ return {
75
+ text: '/close',
76
+ senderFullname: from,
77
+ type: 'text',
78
+ channel: { name: CHANNEL_NAME },
79
+ attributes: {
80
+ subtype: 'info',
81
+ action: `close${JSON.stringify({ event })}`,
82
+ payload: { catchEvent: event },
83
+ timestamp: Date.now()
84
+ }
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Build a transfer event message.
90
+ * @param {string} from - Sender identifier
91
+ * @param {string} event - Event name
92
+ * @param {Object} options - Transfer options
93
+ * @returns {Object} Tiledesk message object
94
+ */
95
+ static buildTransferMessage(from, event, { action, intentName, timestamp }) {
96
+ return {
97
+ text: `/${event}`,
98
+ senderFullname: from,
99
+ type: 'text',
100
+ channel: { name: CHANNEL_NAME },
101
+ attributes: {
102
+ subtype: 'info',
103
+ action,
104
+ payload: {
105
+ event,
106
+ lastBlock: intentName,
107
+ lastTimestamp: timestamp
108
+ }
109
+ }
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Build a no-input fallback message.
115
+ * @param {string} from - Sender identifier
116
+ * @param {string} buttonAction - The fallback action
117
+ * @param {Object} payload - Event payload
118
+ * @returns {Object} Tiledesk message object
119
+ */
120
+ static buildNoInputMessage(from, buttonAction, payload = {}) {
121
+ return {
122
+ text: '/no_input',
123
+ senderFullname: from,
124
+ type: 'text',
125
+ channel: { name: CHANNEL_NAME },
126
+ attributes: {
127
+ type: 'info',
128
+ action: buttonAction,
129
+ payload
130
+ }
131
+ };
132
+ }
133
+ }
134
+
135
+ module.exports = { TiledeskMessageBuilder };
@@ -0,0 +1,129 @@
1
+ const { twiml } = require('twilio');
2
+ const logger = require('../utils/logger');
3
+
4
+ /**
5
+ * Service responsible for Twilio API operations.
6
+ * Encapsulates call management and redirects.
7
+ */
8
+ class TwilioService {
9
+ constructor({ db, config }) {
10
+ if (!db) throw new Error('[TwilioService] db is required');
11
+ if (!config) throw new Error('[TwilioService] config is required');
12
+
13
+ this.db = db;
14
+ this.config = config;
15
+ this._twilioClients = new Map(); // Cache clients per account
16
+ }
17
+
18
+ /**
19
+ * Get or create a Twilio client for the given credentials.
20
+ * @param {string} accountSid - Twilio account SID
21
+ * @param {string} authToken - Twilio auth token
22
+ * @returns {Object} Twilio client instance
23
+ */
24
+ _getClient(accountSid, authToken) {
25
+ if (!this._twilioClients.has(accountSid)) {
26
+ const twilio = require('twilio')(accountSid, authToken);
27
+ this._twilioClients.set(accountSid, twilio);
28
+ }
29
+ return this._twilioClients.get(accountSid);
30
+ }
31
+
32
+ /**
33
+ * Redirect an active call to a new URL.
34
+ * @param {string} callSid - The call SID to redirect
35
+ * @param {string} url - The URL to redirect to
36
+ * @param {Object} settings - Project settings containing Twilio credentials
37
+ * @returns {Promise<boolean>} True if redirect succeeded
38
+ */
39
+ async redirectCall(callSid, url, settings) {
40
+ try {
41
+ const client = this._getClient(settings.account_sid, settings.auth_token);
42
+
43
+ await client.calls(callSid).update({
44
+ url,
45
+ method: 'POST'
46
+ });
47
+
48
+ // logger.debug(`[TwilioService] Call ${callSid} redirected to ${url}`);
49
+ return true;
50
+ } catch (error) {
51
+ logger.error(`[TwilioService] Error redirecting call ${callSid}:`, error);
52
+ return false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Build a nextblock redirect URL with query parameters.
58
+ * @param {string} callSid - The call SID
59
+ * @param {string} intentName - The intent name
60
+ * @returns {string} The complete redirect URL
61
+ */
62
+ buildNextBlockUrl(callSid, intentName) {
63
+ const querystring = require('querystring');
64
+ const queryParams = `?intentName=${querystring.encode(intentName)}&previousIntentTimestamp=${Date.now()}`;
65
+ return `${this.config.BASE_URL}/nextblock/${callSid}${queryParams}`;
66
+ }
67
+
68
+ /**
69
+ * Handle play verb redirect logic.
70
+ * Redirects call to nextblock when a 'play' verb completes.
71
+ * @param {Object} tiledeskMessage - The Tiledesk message
72
+ * @param {string} projectId - The project ID
73
+ * @param {string} contentKey - The settings key
74
+ * @param {Object} lastCallSidVerb - Map of callSid to last verb
75
+ * @returns {Promise<boolean>} True if redirect was performed
76
+ */
77
+ async handlePlayRedirect(tiledeskMessage, projectId, contentKey, lastCallSidVerb) {
78
+ const flowAttrs = tiledeskMessage.attributes?.flowAttributes;
79
+ const callSid = flowAttrs?.CallSid;
80
+
81
+ if (!callSid || lastCallSidVerb[callSid] !== 'play_loop') {
82
+ return false;
83
+ }
84
+
85
+ const settings = await this.db.get(contentKey);
86
+ if (!settings) {
87
+ logger.warn(`[TwilioService] No settings found for ${contentKey}`);
88
+ return false;
89
+ }
90
+
91
+ const intentName = tiledeskMessage.attributes?.intentName || '';
92
+ const redirectUrl = this.buildNextBlockUrl(callSid, intentName);
93
+
94
+ return this.redirectCall(callSid, redirectUrl, settings);
95
+ }
96
+
97
+ generateRedirectTwiML(callSid, intentName) {
98
+ const response = new twiml.VoiceResponse();
99
+ response.redirect({ method: 'POST' }, this.buildNextBlockUrl(callSid, intentName));
100
+ return response.toString();
101
+ }
102
+
103
+ /**
104
+ * Get call information.
105
+ * @param {string} callSid - The call SID
106
+ * @param {Object} settings - Project settings
107
+ * @returns {Promise<Object|null>} Call info or null
108
+ */
109
+ async getCallInfo(callSid, settings) {
110
+ try {
111
+ const client = this._getClient(settings.account_sid, settings.auth_token);
112
+ const call = await client.calls(callSid).fetch();
113
+ return call;
114
+ } catch (error) {
115
+ logger.error(`[TwilioService] Error fetching call ${callSid}:`, error);
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Clear cached Twilio client for an account.
122
+ * @param {string} accountSid - The account SID to clear
123
+ */
124
+ clearClient(accountSid) {
125
+ this._twilioClients.delete(accountSid);
126
+ }
127
+ }
128
+
129
+ module.exports = { TwilioService };
@@ -0,0 +1,78 @@
1
+ const winston = require('../utils/logger');
2
+ const axios = require("axios").default;
3
+ const FormData = require('form-data');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { ServiceError } = require('../utils/errors');
8
+
9
+ class UploadService {
10
+
11
+ constructor(config) {
12
+
13
+ if (!config) {
14
+ throw new Error("[UploadService] config is mandatory");
15
+ }
16
+ if (!config.API_URL) {
17
+ throw new Error("[UploadService] config.API_URL is mandatory");
18
+ }
19
+
20
+ if (config.user) {
21
+ this.user = config.user
22
+ }
23
+
24
+ this.API_URL = config.API_URL;
25
+
26
+ }
27
+
28
+ async upload(id, file, user) {
29
+ // logger.debug(`[UploadService] upload for id ${id} and user ${user._id}`);
30
+
31
+ const tempFilePath = path.join(os.tmpdir(), `speech_${user._id}_${id}.wav`);
32
+
33
+ try {
34
+ // Step 2: Save file locally (temporarily)
35
+ fs.writeFileSync(tempFilePath, file);
36
+
37
+ // Step 3: Upload file to server
38
+ const formData = new FormData();
39
+ formData.append('file', fs.createReadStream(tempFilePath), {
40
+ filename: `audiofile_${user._id}_${id}.wav`,
41
+ contentType: 'audio/mpeg'
42
+ });
43
+
44
+ const response = await axios({
45
+ url: this.API_URL + "/files/users",
46
+ headers: {
47
+ ...formData.getHeaders(),
48
+ "Authorization": user.token
49
+ },
50
+ data: formData,
51
+ method: 'POST'
52
+ });
53
+
54
+ if (response.data) {
55
+ let fileUrl = this.API_URL + "/files?path=" + response.data.filename;
56
+ return fileUrl;
57
+ } else {
58
+ throw new Error("No data in response");
59
+ }
60
+
61
+ } catch (err) {
62
+ winston.error('[UploadService] upload error', err);
63
+ throw new ServiceError('UPLOADSERVICE_FAILED', 'UploadService /files/users API failed with err:', err);
64
+ } finally {
65
+ // Step 4: Clean up temporary file
66
+ if (fs.existsSync(tempFilePath)) {
67
+ try {
68
+ fs.unlinkSync(tempFilePath);
69
+ } catch (e) {
70
+ winston.error("[UploadService] Error deleting temp file:", e);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ }
77
+
78
+ module.exports = { UploadService };