Spaces:
Running
Running
| /** | |
| * ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β Alumni Feedback System β Advanced Google Apps Script v3.5 β | |
| * β PRO-GRADE | 100% CSV SYNC | Error-Resilient | Self-Auth β | |
| * ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * | |
| * SCRIPT PROPERTIES (Project Settings β Script properties) β must match Hugging Face / backend: | |
| * SECRET_KEY β same as server env APPS_SCRIPT_SECRET (default datalens2026 if unset) | |
| * WEBHOOK_SECRET β same as server env WEBHOOK_SECRET (default webhook-secret-key if unset) | |
| * WEBHOOK_URL β optional; used only by hourly heartbeat (not per-form webhooks) | |
| * | |
| * SETUP: Deploy β New deployment β Web app β Execute as: Me β Who has access: Anyone (or your choice). | |
| * Run RUN_ME_TO_AUTHORIZE once after changes so Triggers + UrlFetch to your Space are allowed. | |
| * Windows "Run as administrator" does not affect this script; authorization is your Google account. | |
| */ | |
| const CONFIG = { | |
| SECRET_KEY: PropertiesService.getScriptProperties().getProperty("SECRET_KEY") || "datalens2026", | |
| WEBHOOK_SECRET: PropertiesService.getScriptProperties().getProperty("WEBHOOK_SECRET") || "webhook-secret-key", | |
| VERSION: "v3.5-Final-Sync", | |
| DEPARTMENT_OPTIONS: [ | |
| "School of Education & Humanities", | |
| "School of Engineering", | |
| "School of Engineering: Computer Science & Technology", | |
| "School of Engineering: Electronics and Communication", | |
| "School of Engineering: Mechanical Engineering", | |
| "School of Law", | |
| "School of Management & Commerce" | |
| ] | |
| }; | |
| /** | |
| * MANUAL PERMISSION REPAIR: | |
| * If you get "Server Error", run this function once manually in the editor! | |
| */ | |
| function RUN_ME_TO_AUTHORIZE() { | |
| Logger.log("Permission Check: Verifying Forms, Properties, and Triggers..."); | |
| try { | |
| const props = PropertiesService.getScriptProperties(); | |
| props.setProperty("AUTH_CHECK", new Date().toISOString()); | |
| ScriptApp.newTrigger('onHeartbeatTrigger').timeBased().everyHours(1).create(); | |
| Logger.log("β AUTHORIZATION SUCCESSFUL. Heartbeat established."); | |
| } catch (e) { | |
| Logger.log("β οΈ INFO: " + e.message); | |
| Logger.log("If asked for permissions, click REVIEW PERMISSIONS -> Select Account -> Advanced -> Allow."); | |
| } | |
| } | |
| /** | |
| * TRIGGER CLEANUP: | |
| * Run this if you get the "Too many triggers" error! | |
| * It will delete all existing form triggers to make room for new ones. | |
| */ | |
| function CLEANUP_ALL_TRIGGERS() { | |
| Logger.log("Deleting all triggers to fix 'Too many triggers' error..."); | |
| const triggers = ScriptApp.getProjectTriggers(); | |
| triggers.forEach(t => ScriptApp.deleteTrigger(t)); | |
| Logger.log("β All triggers deleted. You can now generate a new form."); | |
| } | |
| // βββ ENTRY POINTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function doPost(e) { | |
| try { | |
| if (!e || !e.postData || !e.postData.contents) { | |
| return _json(false, "Invalid Request: No payload received.", null, 400); | |
| } | |
| const payload = JSON.parse(e.postData.contents); | |
| if (!payload || payload.secret !== CONFIG.SECRET_KEY) { | |
| return _json(false, "Unauthorized Access: Secret key mismatch.", null, 401); | |
| } | |
| const action = (payload.action || "").toLowerCase(); | |
| switch (action) { | |
| case "create_form": return _handleCreateForm(payload); | |
| case "get_responses": return _handleGetResponses(payload); | |
| case "close_form": return _handleCloseForm(payload); | |
| case "diagnose": return _handleDiagnose(payload); | |
| case "ping": return _json(true, "Connectivity Active", { version: CONFIG.VERSION }); | |
| default: return _json(false, "Unknown Action: " + action, null, 404); | |
| } | |
| } catch (err) { | |
| // Return the actual error in the 'data' field so backend can log it | |
| return _json(false, "Internal Execution Error", err.toString(), 500); | |
| } | |
| } | |
| // βββ ACTION HANDLERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Returns a full diagnostic report of the script's state | |
| */ | |
| function _handleDiagnose(payload) { | |
| const props = PropertiesService.getScriptProperties().getProperties(); | |
| const triggers = ScriptApp.getProjectTriggers(); | |
| const diagnosticData = { | |
| version: CONFIG.VERSION, | |
| properties_count: Object.keys(props).length, | |
| trigger_count: triggers.length, | |
| timezone: Session.getScriptTimeZone(), | |
| user_email: Session.getEffectiveUser().getEmail(), | |
| webhook_url_configured: !!props["WEBHOOK_URL"], | |
| auth_check: props["AUTH_CHECK"] || "Never verified" | |
| }; | |
| return _json(true, "Diagnostic Data Retrieved", diagnosticData); | |
| } | |
| function _handleCreateForm(payload) { | |
| // β‘ MANUAL RUN DETECTION | |
| if (!payload || typeof payload !== 'object') { | |
| Logger.log("β οΈ NOTICE: You clicked 'Run' in the editor. This only works from the Dashboard."); | |
| return _json(false, "Manual execution ignored.", null, 400); | |
| } | |
| const speaker = payload.speaker_name; | |
| const date = payload.venue_date; | |
| const eventId = payload.event_id; | |
| const webhookUrl = payload.webhook_url; | |
| if (!speaker || !date) return _json(false, "Data Error: Speaker and date are required.", null, 400); | |
| // Exact matches for CSV synchronization | |
| const form = FormApp.create(`Student Feedback: ${speaker}`); | |
| form.setDescription(`Session: ${date} | Speaker: ${speaker}\nJoin us in providing feedback for continuous improvement.`); | |
| form.setCollectEmail(false); | |
| form.setAllowResponseEdits(false); | |
| // Step 1: Student Identity (Matches CSV Headers) | |
| form.addSectionHeaderItem().setTitle("Step 1: Your Information"); | |
| form.addTextItem().setTitle("Name of Student").setRequired(true); | |
| form.addMultipleChoiceItem().setTitle("Department").setChoiceValues(CONFIG.DEPARTMENT_OPTIONS).setRequired(true); | |
| const rollItem = form.addTextItem().setTitle("Roll No.").setRequired(true); | |
| const rollPattern = "^2[Kk]\\d{2}[A-Za-z]{3,12}\\d{5}$"; | |
| rollItem.setHelpText("Format: 2K + 2-digit batch year + programme code + 5 digits (e.g. 2K25EDUN01013, 2K24ECUN03021). No spaces."); | |
| rollItem.setValidation( | |
| FormApp.createTextValidation() | |
| .requireTextMatchesPattern(rollPattern) | |
| .setHelpText("Use your official roll number format, e.g. 2K25EDUN01013.") | |
| .build() | |
| ); | |
| // Step 2: Session Value (Matches CSV Headers) | |
| form.addPageBreakItem().setTitle("Step 2: Session Value"); | |
| form.addMultipleChoiceItem() | |
| .setTitle("Did the session help you gain a better understanding of industry trends or career paths?") | |
| .setChoiceValues(["Yes, significantly", "To some extent", "Not really"]) | |
| .setRequired(true); | |
| form.addScaleItem() | |
| .setTitle("How would you rate the session overall? \n(1 β Poor | 2 β Fair | 3 β Good | 4 β Very Good | 5 β Excellent)") | |
| .setBounds(1, 5) | |
| .setLabels("1 β", "5 β") | |
| .setRequired(true); | |
| // Step 3: Detailed Insights (Matches CSV Headers) | |
| form.addPageBreakItem().setTitle("Step 3: Insights & Suggestions"); | |
| form.addParagraphTextItem().setTitle("What aspect of the session did you find most valuable?").setRequired(true); | |
| form.addParagraphTextItem().setTitle("What improvements or suggestions would you recommend for future alumni sessions?").setRequired(false); | |
| form.addParagraphTextItem().setTitle("Any specific topics or areas youβd like future alumni speakers to cover?").setRequired(false); | |
| const formId = form.getId(); | |
| PropertiesService.getScriptProperties().setProperty(`config_${formId}`, JSON.stringify({ | |
| webhook_url: webhookUrl, | |
| event_id: eventId, | |
| speaker_name: speaker, | |
| venue_date: date | |
| })); | |
| // β‘ AUTOMATIC TRIGGER PRUNING (Personal accounts are limited to 20) | |
| const allTriggers = ScriptApp.getProjectTriggers(); | |
| if (allTriggers.length > 15) { | |
| Logger.log("β οΈ Pruning old triggers to make room (Auto-Cleanup)..."); | |
| for (let i = 0; i < 5; i++) { | |
| if (allTriggers[i]) try { ScriptApp.deleteTrigger(allTriggers[i]); } catch(f) {} | |
| } | |
| } | |
| // 1: Trigger for INSTANT webhooks on submission | |
| ScriptApp.newTrigger('onFormSubmitTrigger').forForm(form).onFormSubmit().create(); | |
| // 2: Trigger for STRICT 24-hour closure | |
| const timeTrigger = ScriptApp.newTrigger('onAutoCloseFormTrigger').timeBased().after(24 * 60 * 60 * 1000).create(); | |
| // Save mapping so the time trigger knows WHICH form to close | |
| PropertiesService.getScriptProperties().setProperty(`close_${timeTrigger.getUniqueId()}`, formId); | |
| console.log(`[SUCCESS] Form created for ${speaker} (ID: ${formId})`); | |
| return _json(true, "Form Generated Successfully", { | |
| form_id: formId, | |
| form_url: form.getPublishedUrl() | |
| }, 201); | |
| } | |
| // βββ TRIGGER LOGIC βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function onFormSubmitTrigger(e) { | |
| if (!e || !e.source) return; | |
| let formId = ""; | |
| try { | |
| formId = e.source.getId(); | |
| const config = JSON.parse(PropertiesService.getScriptProperties().getProperty(`config_${formId}`) || "{}"); | |
| if (!config.webhook_url) return; | |
| const answers = {}; | |
| e.response.getItemResponses().forEach(ir => { | |
| const q = ir.getItem().getTitle(); | |
| const a = ir.getResponse(); | |
| // Precision Header Mapping (Matches CSV perfectly) | |
| if (q.includes("Name of Student")) answers.name_of_student = a; | |
| else if (q.includes("Department")) answers.department_original = a; | |
| else if (q.includes("Roll No.")) answers.roll_no_original = a; | |
| else if (q.includes("industry trends")) answers.session_help_understanding = a; | |
| else if (q.includes("rate the session")) answers.session_rating = a; | |
| else if (q.includes("most valuable")) answers.aspect_most_valuable = a; | |
| else if (q.includes("improvements")) answers.improvements_suggestions = a; | |
| else if (q.includes("future alumni speakers to cover")) answers.future_topics = a; | |
| }); | |
| const webhookPayload = JSON.stringify({ | |
| form_id: formId, | |
| event_id: config.event_id, | |
| timestamp: new Date().toISOString(), | |
| responses: { | |
| ...answers, | |
| alumni_speaker_name: config.speaker_name, | |
| date_of_lecture: config.venue_date | |
| } | |
| }); | |
| const resp = UrlFetchApp.fetch(config.webhook_url, { | |
| method: "post", | |
| contentType: "application/json", | |
| headers: { "Authorization": "Bearer " + CONFIG.WEBHOOK_SECRET }, | |
| payload: webhookPayload, | |
| muteHttpExceptions: true | |
| }); | |
| const code = resp.getResponseCode(); | |
| const text = resp.getContentText() || ""; | |
| const preview = text.length > 500 ? text.substring(0, 500) + "β¦" : text; | |
| PropertiesService.getScriptProperties().setProperty( | |
| "LAST_WEBHOOK_SYNC", | |
| JSON.stringify({ | |
| at: new Date().toISOString(), | |
| http_status: code, | |
| body_preview: preview, | |
| form_id: formId, | |
| webhook_host: (function () { | |
| try { | |
| return config.webhook_url.split("/")[2] || config.webhook_url; | |
| } catch (e) { | |
| return ""; | |
| } | |
| })() | |
| }) | |
| ); | |
| if (code < 200 || code >= 300) { | |
| console.error("Webhook HTTP " + code + ": " + preview); | |
| } else { | |
| console.log("Webhook OK HTTP " + code + " for form " + formId); | |
| } | |
| } catch (err) { | |
| console.error("Advanced Webhook Failure:", err.toString()); | |
| try { | |
| PropertiesService.getScriptProperties().setProperty( | |
| "LAST_WEBHOOK_SYNC", | |
| JSON.stringify({ | |
| at: new Date().toISOString(), | |
| http_status: 0, | |
| error: err.toString(), | |
| form_id: formId || "unknown" | |
| }) | |
| ); | |
| } catch (ignore) {} | |
| } | |
| } | |
| function onAutoCloseFormTrigger(e) { | |
| // This runs exactly 24 hours after creation | |
| if (!e || !e.triggerUid) return; | |
| const triggerId = e.triggerUid; | |
| const formId = PropertiesService.getScriptProperties().getProperty(`close_${triggerId}`); | |
| if (formId) { | |
| try { | |
| const form = FormApp.openById(formId); | |
| form.setAcceptingResponses(false); | |
| form.setCustomClosedFormMessage("Sorry this form is closed, reach your mentor"); | |
| PropertiesService.getScriptProperties().deleteProperty(`close_${triggerId}`); | |
| } catch(err) { | |
| console.error("Failed to auto-close form:", err); | |
| } | |
| } | |
| // Cleanup the used trigger | |
| const triggers = ScriptApp.getProjectTriggers(); | |
| for (let i = 0; i < triggers.length; i++) { | |
| if (triggers[i].getUniqueId() === triggerId) { | |
| ScriptApp.deleteTrigger(triggers[i]); | |
| break; | |
| } | |
| } | |
| } | |
| // βββ DATA RETRIEVAL & CLOSURE ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function _handleGetResponses(payload) { | |
| // β‘ MANUAL RUN DETECTION | |
| if (!payload || typeof payload !== 'object' || !payload.form_id) { | |
| Logger.log("β οΈ NOTICE: You clicked 'Run' in the editor. This only works from the Dashboard."); | |
| return _json(false, "Manual execution ignored.", null, 400); | |
| } | |
| const formId = payload.form_id; | |
| try { | |
| const form = FormApp.openById(formId); | |
| if (!form) throw new Error("Could not access Form."); | |
| const results = form.getResponses().map(resp => { | |
| const answers = { timestamp: resp.getTimestamp().toISOString() }; | |
| resp.getItemResponses().forEach(ir => { | |
| const q = ir.getItem().getTitle(); | |
| const a = ir.getResponse(); | |
| if (q.indexOf("Name of Student") > -1) answers.name_of_student = a; | |
| else if (q.indexOf("Department") > -1) answers.department_original = a; | |
| else if (q.indexOf("Roll No.") > -1) answers.roll_no_original = a; | |
| else if (q.indexOf("industry trends") > -1) answers.session_help_understanding = a; | |
| else if (q.indexOf("rate the session") > -1) answers.session_rating = a; | |
| else if (q.indexOf("most valuable") > -1) answers.aspect_most_valuable = a; | |
| else if (q.indexOf("improvements") > -1) answers.improvements_suggestions = a; | |
| else if (q.indexOf("future alumni speakers to cover") > -1) answers.future_topics = a; | |
| }); | |
| return answers; | |
| }); | |
| return _json(true, "Responses Extracted", results); | |
| } catch (e) { | |
| return _json(false, "Extraction Failure", e.toString(), 404); | |
| } | |
| } | |
| function _handleCloseForm(payload) { | |
| if (!payload || typeof payload !== 'object' || !payload.form_id) { | |
| return _json(false, "Missing form_id in payload.", null, 400); | |
| } | |
| const formId = payload.form_id; | |
| try { | |
| const form = FormApp.openById(formId); | |
| if (!form) throw new Error("Could not access Form."); | |
| // STRICT CLOSURE | |
| form.setAcceptingResponses(false); | |
| form.setCustomClosedFormMessage("Sorry this form is closed, reach your mentor"); | |
| return _json(true, "Form Strictly Closed on Google Servers", { form_id: formId }); | |
| } catch (e) { | |
| return _json(false, "Closure Failure", e.toString(), 500); | |
| } | |
| } | |
| // βββ JSON HELPER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function _json(success, message, data, code = 200) { | |
| const output = { success, message, data, v: CONFIG.VERSION, timestamp: new Date().toISOString() }; | |
| return ContentService.createTextOutput(JSON.stringify(output)).setMimeType(ContentService.MimeType.JSON); | |
| } | |
| /** | |
| * Sends a periodic heartbeat to the backend to verify connectivity | |
| */ | |
| function onHeartbeatTrigger() { | |
| const props = PropertiesService.getScriptProperties(); | |
| const webhookUrl = props.getProperty("WEBHOOK_URL"); | |
| const secret = props.getProperty("WEBHOOK_SECRET") || "webhook-secret-key"; | |
| if (!webhookUrl) { | |
| Logger.log("Heartbeat skipped: WEBHOOK_URL not set."); | |
| return; | |
| } | |
| try { | |
| const payload = { | |
| action: "heartbeat", | |
| timestamp: new Date().toISOString(), | |
| form_id: "HEARTBEAT" | |
| }; | |
| UrlFetchApp.fetch(webhookUrl, { | |
| method: "post", | |
| contentType: "application/json", | |
| headers: { "Authorization": "Bearer " + secret }, | |
| payload: JSON.stringify(payload), | |
| muteHttpExceptions: true | |
| }); | |
| Logger.log("Heartbeat sent to: " + webhookUrl); | |
| } catch (e) { | |
| Logger.log("Heartbeat failed: " + e.toString()); | |
| } | |
| } | |
| /** | |
| * ONE-TIME SETUP (Project Settings script properties are read-only when you have 50+ properties): | |
| * 1. Set the SAME WEBHOOK_SECRET in Hugging Face Space β Secrets. | |
| * 2. In the Apps Script editor, select this function β Run β allow permissions. | |
| * 3. Check Executions / Logs for "Saved WEBHOOK_SECRET and SECRET_KEY". | |
| * 4. Submit a test form; LAST_WEBHOOK_SYNC should show http_status 200, not 401. | |
| * 5. Remove the literal strings below (or delete this whole function) after success β do not leave secrets in source long-term. | |
| */ | |
| function ONE_TIME_SET_SECRETS() { | |
| const p = PropertiesService.getScriptProperties(); | |
| p.setProperty("WEBHOOK_SECRET", "DL_wh_9fK2mPq7vNx4Rt8sLw3"); | |
| p.setProperty("SECRET_KEY", "datalens2026"); | |
| Logger.log("Saved WEBHOOK_SECRET and SECRET_KEY (must match HF WEBHOOK_SECRET and APPS_SCRIPT_SECRET)."); | |
| } | |