servchan/tools/appleTV/appleTV.js
jmcqueen c2e98d105e Initial commit: ServChan Webex bot for ServiceChannel work orders.
ServiceChannel webhook processor, proposal approval cards, attachment auto-post,
CollabSupport commands, and Docker deployment configuration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 15:12:08 -04:00

345 lines
No EOL
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import fs from 'fs';
import csv from 'csv-parser';
import path from 'path';
import axios from 'axios';
import { match } from 'assert';
var merakiNetworks = [];
var config = JSON.parse(fs.readFileSync('./config/config.json'));
// Assume you have these functions from your previous code
// e.g., findMerakiNetworkId(storeNum) → returns network ID
// getMerakiClients(networkId) → returns array of clients with description, vlan, switchport, status, etc.
async function processCSVAndEnrichMeraki(csvFilePath) {
const results = [];
// Step 1: Parse the CSV
await new Promise((resolve, reject) => {
fs.createReadStream(csvFilePath)
.pipe(csv())
.on('data', (row) => results.push(row))
.on('end', resolve)
.on('error', reject);
});
// Step 2: Process each row
const enrichedData = [];
for (const row of results) {
const friendlyName = row.device_friendly_name?.trim() || ''; // Column 2
if (!friendlyName) continue;
// Extract store number: e.g., "US003897MSCAERIE" → "003897"
const storeMatch = friendlyName.match(/(\d{6})/); // Pull 6 digits
const storeNum = storeMatch ? storeMatch[0] : null;
if (!storeNum) {
console.log(`Skipping row with invalid friendlyName: ${friendlyName}`);
continue;
}
try {
// Step 3: Find Meraki Network ID
const networkId = await findMerakiNetworkId(storeNum);
if (!networkId) {
console.log(`No Meraki network found for store: ${storeNum}`);
enrichedData.push({ ...row, meraki: null });
continue;
}
// Step 4: Get Meraki clients for the network
const clients = await getMerakiClients(networkId);
// Step 5: Find matching client by description === friendlyName (case-insensitive)
const matchingClient = clients.find(client =>
client.description?.toUpperCase().trim() === friendlyName.toUpperCase()
);
if (!matchingClient) {
console.log(`No matching Meraki client for device: ${friendlyName} in store ${storeNum}`);
enrichedData.push({ ...row, meraki: null });
continue;
} else {
console.log(matchingClient)
}
// Step 6: Extract relevant Meraki info (device, port, vlan, port status)
const merakiInfo = {
deviceName: matchingClient.recentDeviceName || 'N/A',
deviceSerial: matchingClient.recentDeviceSerial || 'N/A',
switchport: matchingClient.switchport || 'N/A',
vlan: matchingClient.vlan || 'N/A',
status: matchingClient.status || 'Unknown',
connection: matchingClient.recentDeviceConnection || 'N/A',
lastSeen: matchingClient.lastSeen || 'N/A',
ip: matchingClient.ip || 'N/A',
mac: matchingClient.mac || 'N/A'
// Add more fields as needed, e.g., portStatus if available from another API
};
// Note: If "port status" requires another Meraki call (e.g., get switch ports), add it here:
// const ports = await getMerakiSwitchPorts(matchingClient.recentDeviceSerial);
// Then find the port matching switchport and get its status (enabled, link, etc.)
enrichedData.push({ ...row, meraki: merakiInfo });
} catch (error) {
console.error(`Error processing store ${storeNum}, device ${friendlyName}:`, error.message);
enrichedData.push({ ...row, meraki: null, error: error.message });
}
}
// Step 7: Return or save the enriched data (e.g., as JSON)
return enrichedData;
}
async function getMerakiClients(merakiNetwork) {
if (merakiNetwork) {
try {
let allClients = [];
let nextUrl = `https://api.meraki.com/api/v1/networks/${merakiNetwork}/clients?perPage=5000&timespan=2592000`; // Start with high perPage + 24h window
while (nextUrl) {
const response = await axios.get(nextUrl, {
headers: {
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
'Content-Type': 'application/json'
}
});
const pageClients = await response.data;
allClients = allClients.concat(pageClients);
//console.log(`Fetched ${pageClients.length} clients from this page (total so far: ${allClients.length})`);
// Check Link header for next page
const linkHeader = await response.headers.link;
if (!linkHeader) {
nextUrl = null;
break;
}
// Parse the 'next' link from Link header (format: <url>; rel="next", ...)
const nextMatch = await linkHeader.match(/<([^>]+)>;\s*rel="next"/);
nextUrl = nextMatch ? nextMatch[1] : null;
}
return allClients;
} catch (error) {
logger(`getMerakiClients(${merakiNetwork})`, `Error: ${error}`)
return null;
}
} else {
return [];
}
}
async function findMerakiNetworkId(storeNum) {
var startTime = new Date().getTime();
var networkSearchTerm = Number(storeNum).toString().padStart(5, "0");
try {
// Filter networks by partial name match (case-insensitive)
const matchingNetworks = merakiNetworks.filter(net =>
(net.name || '').toLowerCase().includes(networkSearchTerm.toLowerCase())
);
if (matchingNetworks.length === 0) {
return null;
}
if (matchingNetworks.length > 1) {
console.log(`Multiple networks match '${networkSearchTerm}':`);
matchingNetworks.forEach(net => console.log(`- ${net.name} (ID: ${net.id})`));
console.log('Using the first match...');
}
return matchingNetworks[0].id;
} catch (error) {
console.error(`Error finding Meraki Network: ${error}`);
return null;
}
}
async function cacheMerakiNetworks() {
try {
let allNetworks = [];
let nextUrl = `https://api.meraki.com/api/v1/organizations/${config.auth.meraki.orgId}/networks?perPage=1000`;
while (nextUrl) {
const response = await axios.get(nextUrl, {
headers: {
'X-Cisco-Meraki-API-Key': config.auth.meraki.apiKey,
'Content-Type': 'application/json'
}
});
const pageNetworks = response.data;
allNetworks = allNetworks.concat(pageNetworks);
//logger(`cacheMerakiNetworks()`, `Fetched ${pageNetworks.length} networks (total so far: ${allNetworks.length})`);
// Log raw header
//logger(`cacheMerakiNetworks()`, `Link header raw: ${response.headers.link || '(none)'}`);
// Robust next URL extraction
const linkHeader = response.headers.link;
let foundNext = null;
if (linkHeader) {
//logger(`cacheMerakiNetworks()`, `Full Link header (raw): ${linkHeader}`);
const parts = linkHeader.split(',');
//logger(`cacheMerakiNetworks()`, `Split into ${parts.length} parts`);
for (const part of parts) {
const trimmed = part.trim();
//logger(`cacheMerakiNetworks()`, `Examining part: "${trimmed}"`);
// Forgiving checks: lower case, no quotes required, partial match
const lowerTrimmed = trimmed.toLowerCase();
if (lowerTrimmed.includes('rel=next') || lowerTrimmed.includes('rel="next"') || lowerTrimmed.includes("rel='next'")) {
//logger(`cacheMerakiNetworks()`, `→ Detected rel=next in: "${trimmed}"`);
const urlMatch = trimmed.match(/<([^>]+)>/);
if (urlMatch && urlMatch[1]) {
foundNext = urlMatch[1].trim(); // extra trim just in case
//logger(`cacheMerakiNetworks()`, `→ Extracted next URL: ${foundNext}`);
break;
} else {
//logger(`cacheMerakiNetworks()`, `→ URL match failed on that part`);
}
}
}
}
nextUrl = foundNext;
if (!nextUrl) {
//logger(`cacheMerakiNetworks()`, `No next page detected ending loop`);
} else {
//logger(`cacheMerakiNetworks()`, `Advancing to next URL: ${nextUrl}`);
}
}
merakiNetworks = allNetworks;
logger(`cacheMerakiNetworks()`, `Cached ${merakiNetworks.length} networks. (${new Date().getTime() - startTime}ms)`);
} catch (error) {
console.error('Meraki Networks API error:', error.message);
if (error.response) {
console.error('Status:', error.response.status);
console.error('Data:', error.response.data);
}
}
}
async function rebootWorkspaceOneDevice(deviceId) {
const baseUrl = `https://${awHost}/api`; // e.g., as123.awmdm.com
const headers = {
'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or use API key method
'aw-tenant-code': tenantCode,
'Accept': 'application/json',
'Content-Type': 'application/json'
};
try {
const response = await axios.post(
`${baseUrl}/mdm/devices/${deviceId}/commands`,
{ Command: 'RebootDevice' }, // or 'RestartDevice' for iOS/tvOS
{ headers }
);
console.log('Reboot command sent successfully:', response.data);
return response.data;
} catch (error) {
console.error('Error sending reboot:');
console.error(error.response?.data || error.message);
throw error;
}
}
/*
async function updateSwitchPort(networkId, serial, portId, settings) {
const apiKey = 'YOUR_MERAKI_API_KEY_HERE';
const baseUrl = 'https://api.meraki.com/api/v1';
try {
const response = await axios.put(
`${baseUrl}/networks/${networkId}/devices/${serial}/switch/ports/${portId}`,
settings,
{
headers: {
'X-Cisco-Meraki-API-Key': apiKey,
'Content-Type': 'application/json'
}
}
);
console.log('Success:', response.data);
return response.data;
} catch (error) {
console.error('Error updating port:');
console.error(error.response?.data || error.message);
throw error;
}
}
// Usage examples
updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', false); // Disable port 8
// updateSwitchPort('L_123456789012345678', 'Q3LU-ABCDE-12345', '8', true); // Enable port 8*/
function logger(functionName, message) {
var d = new Date();
var year = d.getFullYear();
var month = (d.getMonth() + 1).toString().padStart(2, "0");
var day = d.getDate().toString().padStart(2, "0");
let logFile = path.join(`./logs/${year}${month}${day}.log`);
console.log(d.toLocaleString() + " " + functionName + ": " + message);
fs.appendFileSync(logFile, d.toLocaleString() + " " + functionName + ": " + message + "\n");
}
async function sendMdmQuery(deviceId, apiKey, tenantCode, awHost) {
const baseUrl = `https://${awHost}/api`; // e.g., as1991.awmdm.com
const headers = {
'Authorization': `Basic ${Buffer.from('your_username:your_password').toString('base64')}`, // or API key method
'aw-tenant-code': tenantCode,
'Accept': 'application/json',
'Content-Type': 'application/json'
};
try {
const response = await axios.post(
`${baseUrl}/mdm/devices/${deviceId}/commands`,
{ Command: 'QueryDevice' },
{ headers }
);
console.log('Query command sent successfully:', response.data);
return response.data;
} catch (error) {
console.error('Error sending MDM Query:');
if (error.response) {
console.error('Status:', error.response.status);
console.error('Response:', error.response.data);
} else {
console.error(error.message);
}
throw error;
}
}
// Usage example
const csvFile = 'appleTV.csv';
await cacheMerakiNetworks();
processCSVAndEnrichMeraki(csvFile)
.then(enriched => {
//console.log('Enriched Data:', JSON.stringify(enriched, null, 2));
// Optional: save to file
fs.writeFileSync('enriched-meraki.csv.json', JSON.stringify(enriched, null, 2));
})
.catch(err => console.error('Fatal error:', err));