From 7fd07c322b92d05a4bcc070ef0b5ec6f71700d16 Mon Sep 17 00:00:00 2001 From: Isaac Godin Date: Thu, 15 Jan 2026 09:44:36 -0500 Subject: [PATCH] New Ticket System will need to decide if its better or worse than old one before pushing to main --- src/main.js | 55 ---------- src/modules/sdp-api.js | 231 ----------------------------------------- src/test.js | 152 +++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 286 deletions(-) delete mode 100644 src/main.js delete mode 100644 src/modules/sdp-api.js create mode 100644 src/test.js diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 62492a8..0000000 --- a/src/main.js +++ /dev/null @@ -1,55 +0,0 @@ -/* SDP Request Watcher - * Monitors requests in Zoho's SDP Cloud platform to play a sound alert - * when a request is closed. - * - * Developed for MCCS ITS by Skylar Grant - */ - -const dotenv = require('dotenv'); -dotenv.config({ quiet: true }); -const { CLIENT_ID, CLIENT_SECRET } = process.env; -const { Client, ListInfoBuilder, SearchCriteriaBuilder } = require('./modules/sdp-api'); -const client = new Client(CLIENT_ID, CLIENT_SECRET); -const technicianNamesArray = process.env.TECHNICIAN_NAMES.split(','); -const statusNamesArray = process.env.STATUS_NAMES.split(','); - -async function authInitLoop() { - await client.init(); - console.log(`Please visit ${client.verificationUrl} and enter the code: ${client.userCode}`); - - // Poll for authorization - const pollInterval = setInterval(async () => { - try { - /** @type {boolean} */ - const authorized = await client.poll(); - if (authorized) { - clearInterval(pollInterval); - console.log('Authorization successful! Access token obtained.'); - } else { - console.log('Still waiting for user authorization...'); - } - } catch (error) { - clearInterval(pollInterval); - console.error('Error during polling:', error.message); - } - }, client.pollInterval + 500); -} - -async function pullRequests() { - try { - const listInfo = new ListInfoBuilder() - .setRowCount(10); - - const searchCriteria = new SearchCriteriaBuilder() - .addTechnicians(technicianNamesArray, "is") - .addStatuses(statusNamesArray, "is", "and"); - - listInfo.data.search_criteria = searchCriteria.data; - } catch (error) { - console.error('Error pulling requests:', error.message); - } -} - -authInitLoop().catch(err => { - console.error('Error in authInitLoop execution:', err.message); -}); \ No newline at end of file diff --git a/src/modules/sdp-api.js b/src/modules/sdp-api.js deleted file mode 100644 index 13d2582..0000000 --- a/src/modules/sdp-api.js +++ /dev/null @@ -1,231 +0,0 @@ -/* SDP-API Tool - * Client for interacting with the Zoho SDP Cloud API. - * - * Developed for MCCS ITS by Skylar Grant - */ -const axios = require('axios'); - -// Template strings for requests -const strings = { - tokenPrefix: 'Authorization: Zoho-oauthtoken ', - initiation: { - endpoint: 'https://accounts.zoho.com/oauth/v3/device/code', - params: { - grant_type: 'device_request', - scope: 'SDPOnDemand.requests.READ', - access_type: 'offline' - } - }, - polling: { - endpoint: 'https://accounts.zoho.com/oauth/v3/device/token', - params: { - grant_type: 'device_token' - } - } -} - -/** SDP API Client - * Handles OAuth device authorization flow for Zoho's SDP Cloud API. - * @class Client - * @constructor - * @param {string} clientId - The Client ID for the Zoho application. - * @param {string} clientSecret - The Client Secret for the Zoho application. - * @returns {Client} An instance of the SDP API Client. - */ -class Client { - constructor(clientId, clientSecret) { - if (!clientId || !clientSecret) { - throw new Error('Client ID and Client Secret are required to create an SDP API Client.'); - } - this.clientId = clientId; - this.clientSecret = clientSecret; - - // Set up placeholders for tokens and codes - this.deviceCode = null; - this.accessToken = null; - this.refreshToken = null; - this.userCode = null; - this.verificationUrl = null; - - // Create copies of the request parameter templates - this.initParams = JSON.parse(JSON.stringify(strings.initiation.params)); - this.initParams.client_id = this.clientId; - - this.pollParams = JSON.parse(JSON.stringify(strings.polling.params)); - this.pollParams.client_id = this.clientId; - this.pollParams.client_secret = this.clientSecret; - - return this; - } - - async init() { - const urlParams = new URLSearchParams(this.initParams).toString(); - const fullUrl = `${strings.initiation.endpoint}?${urlParams}`; - const initRes = await axios.post(fullUrl); - - if (initRes.status !== 200) { - throw new Error(`Error during initiation request: ${initRes.status} - ${initRes.statusText}`); - } - - this.deviceCode = initRes.data.device_code; - this.userCode = initRes.data.user_code; - this.verificationUrl = initRes.data.verification_uri_complete; - this.pollInterval = initRes.data.interval; - - return this; - } - - async poll() { - if (!this.deviceCode) { - throw new Error('Device code is not set. Please run the initiation request first.'); - } - this.pollParams.code = this.deviceCode; - const pollParams = new URLSearchParams(this.pollParams).toString(); - const fullPollUrl = `${strings.polling.endpoint}?${pollParams}`; - const pollRes = await axios.post(fullPollUrl); - if (pollRes.status !== 200) { - throw new Error(`Error during polling request: ${pollRes.status} - ${pollRes.statusText}`); - } - - if (pollRes.data.access_token) { - this.accessToken = pollRes.data.access_token; - this.refreshToken = pollRes.data.refresh_token; - return true; - } else { - return false; - } - } -} - -/** Search Criteria Builder - * Class to help build search criteria for SDP API requests. - * @class SearchCriteriaBuilder - * @constructor - * @returns {SearchCriteriaBuilder} An instance of the Search Criteria Builder. - */ -class SearchCriteriaBuilder { - constructor() { - this.data = new Array(); - return this; - } - - /** Add technician names to the search criteria. - * @param {Array} technicianNamesArray Array of technician names - * @param {String} condition Condition to use for filtering, "is" or "is not" (Default: "is") - * @param {String} operator Operator to use for filtering, "and" or "or" (Optional if first/only criterion) - * @returns {SearchCriteriaBuilder} - */ - addTechnicians(technicianNamesArray, condition, operator) { - if (!Array.isArray(technicianNamesArray) || technicianNamesArray.length === 0) { - throw new Error('Technician names must be a non-empty array.'); - } - condition = condition || 'is'; - - const newCriterion = { - field: 'technician.name', - condition: condition, - values: technicianNamesArray - }; - - if (operator) { - newCriterion.logical_operator = operator; - } - - this.data.push(newCriterion); - return this; - } - - /** Add status names to the search criteria. - * @param {Array} statusesArray Array of status names - * @param {String} condition Condition to use for filtering, "is" or "is not" (Default: "is not") - * @param {String} operator Operator to use for filtering, "and" or "or" (Optional if first/only criterion) - * @returns {SearchCriteriaBuilder} - */ - addStatuses(statusesArray, condition, operator) { - if (!Array.isArray(statusesArray) || statusesArray.length === 0) { - throw new Error('Statuses must be a non-empty array.'); - } - condition = condition || 'is not'; - - const newCriterion = { - field: 'status.internal_name', - condition: condition, - values: statusesArray - }; - if (operator) { - newCriterion.logical_operator = operator; - } - this.data.push(newCriterion); - return this; - } -} - -/** List Info Builder - * Class to help build list info for SDP API requests. - * @class ListInfoBuilder - * @constructor - * @returns {ListInfoBuilder} An instance of the List Info Builder. - */ -class ListInfoBuilder { - constructor() { - this.data = { - row_count: 100, - start_index: 0, - sort_field: 'last_updated_time', - sort_order: 'desc', - get_total_count: true - }; - return this; - } - - setRowCount(count) { - if (typeof count !== 'number' || count <= 0) { - throw new Error('Row count must be a positive number.'); - } - this.data.row_count = count; - return this; - } - - setStartIndex(index) { - if (typeof index !== 'number' || index < 0) { - throw new Error('Start index must be a non-negative number.'); - } - this.data.start_index = index; - return this; - } - - setSortField(field) { - if (typeof field !== 'string' || field.trim() === '') { - throw new Error('Sort field must be a non-empty string.'); - } - this.data.sort_field = field; - return this; - } - - setSortOrder(order) { - if (order !== 'asc' && order !== 'desc') { - throw new Error('Sort order must be either "asc" or "desc".'); - } - this.data.sort_order = order; - return this; - } - - enableTotalCount(enable) { - this.data.get_total_count = Boolean(enable); - return this; - } - - setSearchCriteria(criteriaArray) { - if (!Array.isArray(criteriaArray)) { - throw new Error('Search criteria must be an array.'); - } - this.data.search_criteria = criteriaArray; - return this; - } -} - -module.exports = { - Client, - SearchCriteriaBuilder, - ListInfoBuilder -} \ No newline at end of file diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000..2885144 --- /dev/null +++ b/src/test.js @@ -0,0 +1,152 @@ +//I barely understand this code, so don't ask me to explain it. +// Or do just know it will be very basic explanations. + +require('dotenv').config({ quiet: true }); +const axios = require('axios'); + +const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES } = process.env; + +if (!CLIENT_ID || !CLIENT_SECRET) { + console.error('Client id and secret missing from .env'); + process.exit(1); +} + +const technicians = TECHNICIAN_NAMES ? TECHNICIAN_NAMES.split(',') : []; +const statuses = STATUS_NAMES ? STATUS_NAMES.split(',') : []; + +let accessToken = null; + +async function getDeviceCode() { + const params = new URLSearchParams({ + client_id: CLIENT_ID, + scope: 'SDPOnDemand.requests.READ', + grant_type: 'device_request', + access_type: 'offline' + }); + + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/code', params); + return res.data; +} + +async function pollForToken(deviceCode, interval) { + const pollParams = new URLSearchParams({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + code: deviceCode, + grant_type: 'device_token' + }); + + return new Promise((resolve, reject) => { + const pollInterval = setInterval(async () => { + try { + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/token', pollParams); + if (res.data.access_token) { + clearInterval(pollInterval); + accessToken = res.data.access_token; + console.log('Authorization successful'); + resolve(); + } + } catch (err) { + + } + }, (interval + 500)); + }); +} + +async function fetchClosedTickets() { + if (!accessToken) return; + + try { + const tenMinutesAgo = Date.now() - (10 * 60 * 1000); + + const listInfo = { + list_info: { + row_count: 50, + sort_field: "completed_time", + sort_order: "desc", + fields_required: ["display_id", "subject", "status", "completed_time", "technician"], + search_criteria: [ + { + field: "status.name", + condition: "is", + value: "Closed" + }, + { + field: "completed_time", + condition: "greater_than", + value: tenMinutesAgo.toString(), + logical_operator: "and" + } + ] + } + }; + + const response = await axios.get( + 'https://ithelp.mainecc.edu/api/v3/requests', + { + params: { input_data: JSON.stringify(listInfo) }, + headers: { + 'Authorization': `Zoho-oauthtoken ${accessToken}`, + 'Accept': 'application/vnd.manageengine.sdp.v3+json' + } + } + ); + + let tickets = response.data?.requests || []; + + const trimmedTechnicians = technicians.map(t => t.trim()); + + tickets = tickets.filter(ticket => { + if (!ticket.completed_time){return false;} + const completedTime = typeof ticket.completed_time === 'object' + ? ticket.completed_time.value + : ticket.completed_time; + if (completedTime <= tenMinutesAgo){return false;} + + const technicianName = ticket.technician ? ticket.technician.name : null; + if (!technicianName){return false;} + return trimmedTechnicians.includes(technicianName); + }); + + console.log(`\nCheck ran at ${new Date().toLocaleTimeString()}`); + + + + + if (tickets.length === 0) { + console.log("No tickets closed in the last 10 minutes."); + } else { + console.log(`Found ${tickets.length} tickets closed since ${new Date(tenMinutesAgo).toLocaleTimeString()}:`); + console.log('---------------------------------------------------------'); + + tickets.forEach((ticket) => { + const ticketId = ticket.display_id; + const subject = ticket.subject; + const tech = ticket.technician ? ticket.technician.name : 'Unassigned'; + const closedAt = ticket.completed_time ? ticket.completed_time.display_value : 'N/A'; + + console.log(`ID: ${ticketId} - ${subject}`); + console.log(`Technician: ${tech}`); + console.log(`Closed At: ${closedAt}`); + console.log('---------------------------------------------------------'); + }); + } + + } catch (err) { + console.error('Error: ', err.response?.data || err.message); + } +} + + +async function main() { + const deviceData = await getDeviceCode(); + console.log('Open this URL and authorize:', deviceData.verification_uri_complete); + console.log('Code: ', deviceData.user_code); + + await pollForToken(deviceData.device_code, deviceData.interval); + + setInterval(fetchClosedTickets, 600000); //runs every 10 mins + fetchClosedTickets(); +} + +main().catch(err => console.error(err));