@peopl-health/nexus 1.0.3 → 1.1.1

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.
@@ -0,0 +1,582 @@
1
+ const { Message } = require('../models/messageModel');
2
+ const { sendMessage } = require('../services/whatsappService');
3
+ const { fetchConversationData, processConversations } = require('../services/conversationService');
4
+
5
+ const getConversationController = async (req, res) => {
6
+ const startTime = Date.now();
7
+ console.log('Starting getConversationController at', new Date().toISOString());
8
+
9
+ try {
10
+ // Parse pagination parameters
11
+ const limit = parseInt(req.query.limit) || 100;
12
+ const page = parseInt(req.query.page) || 1;
13
+ const skip = (page - 1) * limit;
14
+ const filter = req.query.filter || 'all';
15
+
16
+ console.log(`Pagination: page ${page}, limit ${limit}, skip ${skip}, filter: ${filter}`);
17
+
18
+ // Check if messages exist
19
+ const messageCount = await Message.countDocuments({});
20
+ console.log('Total message count:', messageCount);
21
+
22
+ if (messageCount === 0) {
23
+ console.log('No messages found in database, returning empty conversations list');
24
+ return res.status(200).json({
25
+ success: true,
26
+ conversations: [],
27
+ emptyDatabase: true
28
+ });
29
+ }
30
+
31
+ // Fetch conversation data
32
+ const { conversations, total, nameMap, unreadMap } = await fetchConversationData(filter, skip, limit);
33
+
34
+ if (!conversations || conversations.length === 0) {
35
+ console.log('No conversations found, returning empty list');
36
+ return res.status(200).json({
37
+ success: true,
38
+ conversations: [],
39
+ noConversations: true
40
+ });
41
+ }
42
+
43
+ // Process conversations for response
44
+ const processedConversations = await processConversations(conversations, nameMap, unreadMap);
45
+
46
+ const totalPages = Math.ceil(total / limit);
47
+ const totalTime = Date.now() - startTime;
48
+
49
+ console.log('Number of conversations found:', conversations?.length || 0);
50
+ console.log(`Total controller execution time: ${totalTime}ms`);
51
+ console.log(`Filter: ${filter}, Pagination: ${conversations.length} of ${total} conversations (page ${page}/${totalPages})`);
52
+
53
+ res.status(200).json({
54
+ success: true,
55
+ conversations: processedConversations,
56
+ filter: filter,
57
+ pagination: {
58
+ page,
59
+ limit,
60
+ total,
61
+ totalPages,
62
+ hasNext: page < totalPages,
63
+ hasPrev: page > 1
64
+ }
65
+ });
66
+ console.log('Response sent successfully!');
67
+
68
+ } catch (error) {
69
+ console.error('Error fetching conversations:', error);
70
+ console.error('Error stack:', error.stack);
71
+ res.status(500).json({
72
+ success: false,
73
+ error: error.message || 'Failed to fetch conversations'
74
+ });
75
+ }
76
+ };
77
+
78
+ const getConversationMessagesController = async (req, res) => {
79
+ console.log('Starting getConversationMessagesController at', new Date().toISOString());
80
+ try {
81
+ const { phoneNumber } = req.params;
82
+ console.log('Requested conversation for phone number:', phoneNumber);
83
+
84
+ if (!phoneNumber) {
85
+ return res.status(400).json({
86
+ success: false,
87
+ error: 'Phone number is required'
88
+ });
89
+ }
90
+
91
+ const limit = parseInt(req.query.limit) || 50;
92
+ const { before } = req.query;
93
+
94
+ const query = { numero: phoneNumber, is_group: false };
95
+ if (before) {
96
+ try {
97
+ query.createdAt = { $lt: new Date(before) };
98
+ } catch (parseError) {
99
+ console.warn('Invalid date format for before parameter:', before, parseError);
100
+ query.createdAt = { $lt: before };
101
+ }
102
+ }
103
+
104
+ console.log('Fetching conversation messages with query:', JSON.stringify(query));
105
+ console.log('Using limit:', limit);
106
+
107
+ console.log('About to execute Message.find with query:', JSON.stringify(query));
108
+ let messages = [];
109
+
110
+ try {
111
+ const countResult = await Message.countDocuments(query);
112
+ console.log(`Found ${countResult} messages matching query criteria`);
113
+
114
+ if (countResult > 0) {
115
+ messages = await Message.find(query)
116
+ .sort({ createdAt: -1 })
117
+ .limit(limit)
118
+ .lean();
119
+
120
+ console.log(`Retrieved ${messages.length} messages, inspecting for potential issues...`);
121
+
122
+ const problematicMessages = messages.filter(msg => {
123
+ if (!msg || !msg.numero || !msg.createdAt) {
124
+ console.warn('Found message missing required fields:', msg?._id || 'unknown');
125
+ return true;
126
+ }
127
+
128
+ if (msg.is_media === true) {
129
+ if (!msg.media || typeof msg.media !== 'object') {
130
+ console.warn('Found media message with invalid media data:', msg._id);
131
+ return true;
132
+ }
133
+
134
+ if (msg.media && (typeof msg.media.data === 'function' || msg.media.data instanceof Buffer)) {
135
+ console.warn('Found media message with Buffer data that might cause serialization issues:', msg._id);
136
+ return true;
137
+ }
138
+ }
139
+
140
+ return false;
141
+ });
142
+
143
+ if (problematicMessages.length > 0) {
144
+ console.warn(`Found ${problematicMessages.length} potentially problematic messages`);
145
+ console.log('First problematic message IDs:', problematicMessages.slice(0, 3).map(m => m?._id || 'unknown'));
146
+ }
147
+ } else {
148
+ console.log('No messages found for this query');
149
+ }
150
+ } catch (err) {
151
+ console.error('Database query error in message retrieval:', err);
152
+ messages = [];
153
+ }
154
+
155
+ console.log(`Retrieved a total of ${messages?.length || 0} messages for phone number: ${phoneNumber}`);
156
+
157
+ const sanitizedMessages = (messages || []).map(msg => {
158
+ try {
159
+ JSON.stringify(msg);
160
+ return msg;
161
+ } catch (serializationError) {
162
+ console.error(`Found non-serializable message with ID ${msg._id}:`, serializationError);
163
+ return {
164
+ _id: msg._id?.toString() || 'unknown',
165
+ numero: msg.numero || phoneNumber,
166
+ body: msg.body || '[Message content unavailable]',
167
+ createdAt: msg.createdAt || new Date(),
168
+ from_me: !!msg.from_me,
169
+ is_media: !!msg.is_media,
170
+ error: 'Message contained non-serializable data'
171
+ };
172
+ }
173
+ });
174
+
175
+ const reversedMessages = sanitizedMessages.reverse();
176
+ console.log(`Sending ${reversedMessages.length} sanitized messages in response`);
177
+
178
+ res.status(200).json({
179
+ success: true,
180
+ phoneNumber,
181
+ messages: reversedMessages
182
+ });
183
+ console.log('Successfully sent conversation messages response');
184
+ } catch (error) {
185
+ console.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, error);
186
+ res.status(500).json({
187
+ success: false,
188
+ error: error.message || 'Failed to fetch conversation'
189
+ });
190
+ }
191
+ };
192
+
193
+ const getConversationReplyController = async (req, res) => {
194
+ console.log('Starting getConversationReplyController at', new Date().toISOString());
195
+ try {
196
+ const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
197
+ console.log('Reply request params:', {
198
+ phoneNumber,
199
+ hasMessage: !!message,
200
+ hasMediaData: !!mediaData,
201
+ hasContentSid: !!contentSid,
202
+ hasVariables: !!variables
203
+ });
204
+
205
+ if (!phoneNumber || (!message && !mediaData && !contentSid)) {
206
+ return res.status(400).json({
207
+ success: false,
208
+ error: 'Phone number and either message, media data, or template ID are required'
209
+ });
210
+ }
211
+
212
+ const formattedPhoneNumber = phoneNumber?.startsWith('whatsapp:')
213
+ ? phoneNumber
214
+ : `whatsapp:${phoneNumber}`;
215
+ console.log('Formatted phone number:', formattedPhoneNumber);
216
+
217
+ const messageData = {
218
+ code: formattedPhoneNumber,
219
+ fileType: 'text'
220
+ };
221
+
222
+ // Handle template message (contentSid provided)
223
+ if (contentSid) {
224
+ console.log('Processing template message with contentSid:', contentSid);
225
+ messageData.contentSid = contentSid;
226
+
227
+ if (variables && Object.keys(variables).length > 0) {
228
+ console.log('Template variables:', JSON.stringify(variables));
229
+ messageData.variables = variables;
230
+ }
231
+
232
+ const now = new Date();
233
+ const timestamp = now.getTime();
234
+ const uniqueId = `${Math.floor(Math.random() * 10000)}`;
235
+
236
+ messageData.templateMetadata = {
237
+ timestamp: timestamp,
238
+ uniqueId: uniqueId,
239
+ variableCount: variables ? Object.keys(variables).length : 0
240
+ };
241
+ }
242
+ // Handle media message
243
+ else if (mediaData && mediaData.fileUrl) {
244
+ console.log('Processing media data:', { fileType: mediaData.fileType, hasFileUrl: !!mediaData.fileUrl, fileName: mediaData.fileName });
245
+ messageData.fileUrl = mediaData.fileUrl;
246
+ messageData.fileType = mediaData.fileType || 'image';
247
+
248
+ if (mediaData.fileName) {
249
+ messageData.fileName = mediaData.fileName;
250
+ }
251
+
252
+ if (message && message.trim() !== '') {
253
+ messageData.message = message;
254
+ }
255
+ }
256
+ // Handle text message
257
+ else if (message) {
258
+ messageData.message = message;
259
+ }
260
+
261
+ console.log('Sending message with data:', JSON.stringify(messageData));
262
+ await sendMessage(messageData);
263
+ console.log('Message sent successfully');
264
+
265
+ res.status(200).json({
266
+ success: true,
267
+ message: 'Reply sent successfully'
268
+ });
269
+ } catch (error) {
270
+ console.error('Error sending reply:', error);
271
+ console.log('Request body:', JSON.stringify(req.body || {}));
272
+ const errorMsg = error.message || 'Failed to send reply';
273
+ console.error('Responding with error:', errorMsg);
274
+ res.status(500).json({
275
+ success: false,
276
+ error: errorMsg
277
+ });
278
+ }
279
+ };
280
+
281
+ const searchConversationsController = async (req, res) => {
282
+ try {
283
+ const { query, limit = 50 } = req.query;
284
+
285
+ if (!query || query.trim().length === 0) {
286
+ return res.status(400).json({
287
+ success: false,
288
+ error: 'Search query is required'
289
+ });
290
+ }
291
+
292
+ console.log(`Searching conversations for query: "${query}"`);
293
+ const searchStartTime = Date.now();
294
+
295
+ const escapedQuery = query.replace(/\+/g, '\\+');
296
+
297
+ // Search through all conversations in the database
298
+ const conversations = await Message.aggregate([
299
+ { $match: {
300
+ is_group: false,
301
+ $or: [
302
+ { numero: { $regex: escapedQuery, $options: 'i' } },
303
+ { nombre_whatsapp: { $regex: escapedQuery, $options: 'i' } },
304
+ { body: { $regex: escapedQuery, $options: 'i' } }
305
+ ]
306
+ }},
307
+ { $project: {
308
+ numero: 1,
309
+ body: 1,
310
+ createdAt: 1,
311
+ timestamp: 1,
312
+ is_media: 1,
313
+ media: 1,
314
+ nombre_whatsapp: 1,
315
+ from_me: 1
316
+ }},
317
+ { $group: {
318
+ _id: '$numero',
319
+ latestMessage: { $first: '$$ROOT' },
320
+ messageCount: { $sum: 1 }
321
+ }},
322
+ { $sort: { 'latestMessage.createdAt': -1 } },
323
+ { $limit: parseInt(limit) }
324
+ ]);
325
+
326
+ const searchTime = Date.now() - searchStartTime;
327
+ console.log(`Search completed in ${searchTime}ms, found ${conversations.length} conversations`);
328
+
329
+ // Process conversations for response
330
+ const processedConversations = conversations.map(conv => {
331
+ if (!conv || !conv.latestMessage) {
332
+ return {
333
+ phoneNumber: conv?._id || 'unknown',
334
+ name: 'Unknown',
335
+ lastMessage: '',
336
+ lastMessageTime: new Date(),
337
+ messageCount: 0,
338
+ unreadCount: 0,
339
+ isLastMessageMedia: false,
340
+ lastMessageType: null,
341
+ lastMessageFromMe: false
342
+ };
343
+ }
344
+
345
+ const isMedia = conv.latestMessage.is_media === true;
346
+ let mediaType = null;
347
+
348
+ if (isMedia && conv?.latestMessage?.media) {
349
+ if (conv.latestMessage.media.mediaType) {
350
+ mediaType = conv.latestMessage.media.mediaType;
351
+ } else if (conv.latestMessage.media.contentType) {
352
+ const contentType = conv.latestMessage.media.contentType;
353
+ const contentTypeParts = contentType?.split('/') || ['unknown'];
354
+ mediaType = contentTypeParts[0] || 'unknown';
355
+
356
+ if (mediaType === 'application') {
357
+ mediaType = 'document';
358
+ } else if (contentTypeParts[1] === 'webp') {
359
+ mediaType = 'sticker';
360
+ }
361
+ }
362
+ }
363
+
364
+ return {
365
+ phoneNumber: conv._id,
366
+ name: conv?.latestMessage?.nombre_whatsapp || 'Unknown',
367
+ lastMessage: conv?.latestMessage?.body || '',
368
+ lastMessageTime: conv?.latestMessage?.createdAt || conv?.latestMessage?.timestamp || new Date(),
369
+ messageCount: conv.messageCount || 0,
370
+ unreadCount: 0,
371
+ isLastMessageMedia: isMedia || false,
372
+ lastMessageType: mediaType || null,
373
+ lastMessageFromMe: conv?.latestMessage?.from_me || false
374
+ };
375
+ });
376
+
377
+ res.status(200).json({
378
+ success: true,
379
+ conversations: processedConversations,
380
+ searchInfo: {
381
+ query,
382
+ results: processedConversations.length,
383
+ searchTime
384
+ }
385
+ });
386
+
387
+ } catch (error) {
388
+ console.error('Error searching conversations:', error);
389
+ res.status(500).json({
390
+ success: false,
391
+ error: error.message || 'Failed to search conversations'
392
+ });
393
+ }
394
+ };
395
+
396
+ const getConversationsByNameController = async (req, res) => {
397
+ try {
398
+ const conversations = await Message.aggregate([
399
+ { $match: { from_me: false, is_group: false } },
400
+ { $sort: { timestamp: -1 } },
401
+ { $group: {
402
+ _id: '$numero',
403
+ name: { $first: '$nombre_whatsapp' },
404
+ latestMessage: { $first: '$$ROOT' },
405
+ messageCount: { $sum: 1 }
406
+ }},
407
+ { $sort: { 'latestMessage.timestamp': -1 } }
408
+ ]);
409
+
410
+ res.status(200).json({
411
+ success: true,
412
+ conversations: conversations.map(conv => ({
413
+ phoneNumber: conv._id,
414
+ name: conv.name,
415
+ lastMessage: conv.latestMessage.body,
416
+ lastMessageTime: conv.latestMessage.timestamp,
417
+ messageCount: conv.messageCount
418
+ }))
419
+ });
420
+ } catch (error) {
421
+ console.error('Error fetching conversations by name:', error);
422
+ res.status(500).json({
423
+ success: false,
424
+ error: error.message || 'Failed to fetch conversations by name'
425
+ });
426
+ }
427
+ };
428
+
429
+ const getNewMessagesController = async (req, res) => {
430
+ try {
431
+ const { phoneNumber } = req.params;
432
+ const { after, limit = 20 } = req.query;
433
+
434
+ if (!after) {
435
+ return res.status(400).json({
436
+ success: false,
437
+ error: 'The after parameter is required'
438
+ });
439
+ }
440
+
441
+ const lastMessage = await Message.findById(after).lean();
442
+
443
+ if (!lastMessage) {
444
+ return res.status(404).json({
445
+ success: false,
446
+ error: 'Reference message not found'
447
+ });
448
+ }
449
+
450
+ const query = {
451
+ numero: phoneNumber,
452
+ is_group: false,
453
+ createdAt: { $gt: lastMessage.createdAt }
454
+ };
455
+
456
+ const messages = await Message.find(query)
457
+ .sort({ createdAt: 1 })
458
+ .limit(parseInt(limit))
459
+ .lean();
460
+
461
+ res.status(200).json({
462
+ success: true,
463
+ phoneNumber,
464
+ messages: messages
465
+ });
466
+ } catch (error) {
467
+ console.error(`Error fetching new messages for ${req.params.phoneNumber}:`, error);
468
+ res.status(500).json({
469
+ success: false,
470
+ error: error.message || 'Failed to fetch new messages'
471
+ });
472
+ }
473
+ };
474
+
475
+ const markMessagesAsReadController = async (req, res) => {
476
+ console.log('Starting markMessagesAsReadController at', new Date().toISOString());
477
+ try {
478
+ const { phoneNumber } = req.params;
479
+
480
+ if (!phoneNumber) {
481
+ return res.status(400).json({
482
+ success: false,
483
+ error: 'Phone number is required'
484
+ });
485
+ }
486
+
487
+ console.log('Marking messages as read for phone number:', phoneNumber);
488
+ const result = await Message.updateMany(
489
+ {
490
+ numero: phoneNumber,
491
+ from_me: false,
492
+ $or: [
493
+ { read: false },
494
+ { read: { $exists: false } }
495
+ ]
496
+ },
497
+ { $set: { read: true } }
498
+ );
499
+
500
+ res.status(200).json({
501
+ success: true,
502
+ message: `${result.nModified || result.modifiedCount} messages marked as read`,
503
+ modifiedCount: result.nModified || result.modifiedCount
504
+ });
505
+ } catch (error) {
506
+ console.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, error);
507
+ console.error('Error stack:', error.stack);
508
+ res.status(500).json({
509
+ success: false,
510
+ error: error.message || 'Failed to mark messages as read'
511
+ });
512
+ }
513
+ };
514
+
515
+ const sendTemplateToNewNumberController = async (req, res) => {
516
+ console.log('Starting sendTemplateToNewNumberController at', new Date().toISOString());
517
+ try {
518
+ const { phoneNumber, templateId, variables } = req.body;
519
+
520
+ if (!phoneNumber) {
521
+ return res.status(400).json({
522
+ success: false,
523
+ error: 'Phone number is required'
524
+ });
525
+ }
526
+
527
+ if (!templateId) {
528
+ return res.status(400).json({
529
+ success: false,
530
+ error: 'Template ID is required'
531
+ });
532
+ }
533
+
534
+ // Format phone number for WhatsApp if needed
535
+ const formattedPhoneNumber = phoneNumber.startsWith('whatsapp:')
536
+ ? phoneNumber
537
+ : `whatsapp:${phoneNumber}`;
538
+
539
+ // Log template details for debugging
540
+ console.log('Sending template to new number with details:', {
541
+ phoneNumber: formattedPhoneNumber,
542
+ templateId,
543
+ hasVariables: variables && Object.keys(variables).length > 0,
544
+ variableKeys: variables ? Object.keys(variables) : []
545
+ });
546
+
547
+ const messageData = {
548
+ code: formattedPhoneNumber,
549
+ contentSid: templateId
550
+ };
551
+
552
+ if (variables && Object.keys(variables).length > 0) {
553
+ messageData.variables = variables;
554
+ console.log('Template variables:', JSON.stringify(variables));
555
+ }
556
+
557
+ const message = await sendMessage(messageData);
558
+
559
+ res.status(200).json({
560
+ success: true,
561
+ message: 'Template sent successfully',
562
+ messageId: message.sid
563
+ });
564
+ } catch (error) {
565
+ console.error('Error sending template to new number:', error);
566
+ res.status(500).json({
567
+ success: false,
568
+ error: error.message || 'Failed to send template to new number'
569
+ });
570
+ }
571
+ };
572
+
573
+ module.exports = {
574
+ getConversationController,
575
+ getConversationMessagesController,
576
+ getConversationReplyController,
577
+ getConversationsByNameController,
578
+ getNewMessagesController,
579
+ markMessagesAsReadController,
580
+ searchConversationsController,
581
+ sendTemplateToNewNumberController
582
+ };
@@ -0,0 +1,105 @@
1
+ // Optional AWS config - will be undefined if not available
2
+ let downloadFileFromS3, s3;
3
+ try {
4
+ downloadFileFromS3 = require('../config/awsConfig')?.downloadFileFromS3;
5
+ s3 = require('../config/awsConfig')?.s3;
6
+ } catch (e) {
7
+ // AWS config not available
8
+ }
9
+ const bucketName = process.env.AWS_S3_BUCKET_NAME;
10
+
11
+
12
+ const getMediaController = async (req, res) => {
13
+ try {
14
+ const { key } = req.params;
15
+
16
+ console.log(`[MediaController] Received request for media file with key: ${key}`);
17
+ console.log(`[MediaController] Original URL path: ${req.originalUrl}`);
18
+
19
+ if (!key || key.trim() === '') {
20
+ console.error('[MediaController] Invalid key provided:', key);
21
+ return res.status(400).json({
22
+ success: false,
23
+ error: 'Invalid media key provided'
24
+ });
25
+ }
26
+
27
+ let mediaKey = key;
28
+
29
+ console.log(`[MediaController] Final S3 key to fetch: ${mediaKey}`);
30
+
31
+ const fileData = await downloadFileFromS3(bucketName, mediaKey);
32
+
33
+ if (!fileData || !fileData.Body) {
34
+ console.error(`[MediaController] Media not found in S3: ${key}`);
35
+
36
+ try {
37
+ const prefix = key.split('/')[0];
38
+ console.log(`[MediaController] Checking S3 for objects with prefix: ${prefix}/`);
39
+
40
+ s3.listObjectsV2({
41
+ Bucket: bucketName,
42
+ Prefix: prefix + '/',
43
+ MaxKeys: 10
44
+ }).promise()
45
+ .then(listData => {
46
+ if (listData.Contents && listData.Contents.length > 0) {
47
+ console.log(`[MediaController] Found ${listData.Contents.length} objects with similar prefix:`);
48
+ listData.Contents.forEach(item => {
49
+ console.log(`[MediaController] - ${item.Key} (${item.Size} bytes)`);
50
+ if (item.Key.includes(key.split('/').pop().substring(0, 10))) {
51
+ console.log(`[MediaController] !!! POTENTIAL MATCH: ${item.Key}`);
52
+ }
53
+ });
54
+ } else {
55
+ console.log(`[MediaController] No objects found with prefix: ${prefix}/`);
56
+ }
57
+ })
58
+ .catch(listErr => {
59
+ console.error(`[MediaController] Error listing objects: ${listErr.message}`);
60
+ });
61
+ } catch (listErr) {
62
+ console.error(`[MediaController] Error setting up bucket listing: ${listErr.message}`);
63
+ }
64
+
65
+ return res.status(404).json({
66
+ success: false,
67
+ error: 'Media not found in S3',
68
+ key: key,
69
+ bucket: bucketName,
70
+ timestamp: new Date().toISOString()
71
+ });
72
+ }
73
+
74
+ let contentType = fileData.ContentType || 'application/octet-stream';
75
+
76
+ if (contentType === 'application/octet-stream') {
77
+ const fileExtension = key.split('.').pop().toLowerCase();
78
+
79
+ if (['jpg', 'jpeg', 'png', 'webp'].includes(fileExtension)) {
80
+ contentType = `image/${fileExtension === 'jpg' ? 'jpeg' : fileExtension}`;
81
+ } else if (fileExtension === 'pdf') {
82
+ contentType = 'application/pdf';
83
+ } else if (['mp3', 'wav', 'ogg', 'm4a'].includes(fileExtension)) {
84
+ contentType = `audio/${fileExtension}`;
85
+ } else if (['mp4', 'webm', '3gp'].includes(fileExtension)) {
86
+ contentType = `video/${fileExtension}`;
87
+ }
88
+ }
89
+
90
+ res.setHeader('Cache-Control', 'public, max-age=86400');
91
+ res.setHeader('Content-Type', contentType);
92
+
93
+ res.send(fileData.Body);
94
+ } catch (error) {
95
+ console.error('Error serving media:', error);
96
+ res.status(500).json({
97
+ success: false,
98
+ error: 'Failed to retrieve media'
99
+ });
100
+ }
101
+ };
102
+
103
+ module.exports = {
104
+ getMediaController
105
+ };