using Disco.Plugins.ServiceTracker.Models; using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; namespace Disco.Plugins.ServiceTracker.Services { public class GoogleSheetService { private readonly GoogleSheetConfig _config; private readonly string _cachePath; public GoogleSheetService(GoogleSheetConfig config, string dataDirectory) { _config = config; _cachePath = Path.Combine(dataDirectory, "sheet_cache.csv"); } public GoogleSheetResult FetchTickets() { var result = new GoogleSheetResult(); if (!_config.Enabled || string.IsNullOrEmpty(_config.SpreadsheetId)) { result.Error = "Google Sheet integration is disabled."; return result; } string csvData = null; // Check cache if (File.Exists(_cachePath)) { var cacheAge = DateTime.Now - File.GetLastWriteTime(_cachePath); if (cacheAge.TotalMinutes < _config.RefreshMinutes) csvData = File.ReadAllText(_cachePath); } if (csvData == null) { try { // Build URL based on ID format string url; var id = _config.SpreadsheetId.Trim(); if (id.StartsWith("2PACX", StringComparison.OrdinalIgnoreCase)) { // Published key format: /d/e/{publishedKey}/pub?output=csv&gid={gid} url = string.Format( "https://docs.google.com/spreadsheets/d/e/{0}/pub?output=csv&gid={1}", id, _config.GId); } else { // Original spreadsheet ID format: /d/{id}/export?format=csv&gid={gid} url = string.Format( "https://docs.google.com/spreadsheets/d/{0}/export?format=csv&gid={1}", id, _config.GId); } using (var client = new WebClient()) { client.Encoding = Encoding.UTF8; // Some Google responses require a user-agent client.Headers.Add("User-Agent", "Mozilla/5.0 DiscoServiceTracker/1.0"); csvData = client.DownloadString(url); } // Validate we got actual CSV, not an HTML error page if (csvData != null && csvData.TrimStart().StartsWith(" Share > Publish to web > select the tab > choose 'Comma-separated values (.csv)' > Publish."; return result; } } else { File.WriteAllText(_cachePath, csvData); } } catch (WebException ex) { if (File.Exists(_cachePath)) { csvData = File.ReadAllText(_cachePath); result.Warning = "Using cached data. Fetch error: " + ex.Message; } else { result.Error = "Could not fetch Google Sheet. Error: " + ex.Message + "\n\nTroubleshooting:" + "\n1. Go to your Google Sheet" + "\n2. File > Share > Publish to web" + "\n3. Select the correct tab" + "\n4. Choose 'Comma-separated values (.csv)'" + "\n5. Click Publish" + "\n6. In Config, set Spreadsheet ID to the published key (starts with 2PACX-...)"; return result; } } catch (Exception ex) { result.Error = "Sheet fetch error: " + ex.Message; return result; } } try { result.Tickets = ParseCsv(csvData); if (result.Tickets.Count == 0 && string.IsNullOrEmpty(result.Warning)) result.Warning = "Sheet fetched OK but no open tickets found. Check column indices and status values."; } catch (Exception ex) { result.Error = "CSV parse error: " + ex.Message; } return result; } private List ParseCsv(string csvData) { var tickets = new List(); var lines = csvData.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); int nextId = 100001; for (int i = _config.HeaderRows; i < lines.Length; i++) { var fields = ParseCsvLine(lines[i]); if (fields.Count == 0) continue; // Skip completely empty rows bool allEmpty = true; for (int j = 0; j < fields.Count; j++) { if (!string.IsNullOrWhiteSpace(fields[j])) { allEmpty = false; break; } } if (allEmpty) continue; var ticket = new ExternalTicket { InternalId = nextId++, Source = "ntt", Timestamp = SafeGetDate(fields, _config.ColTimestamp), RequesterEmail = SafeGet(fields, _config.ColEmail), DeviceName = SafeGet(fields, _config.ColDeviceName), Location = SafeGet(fields, _config.ColLocation), IssueDescription = SafeGet(fields, _config.ColIssue), RawPriority = SafeGet(fields, _config.ColPriority), RawStatus = SafeGet(fields, _config.ColStatus), AssignedTo = SafeGet(fields, _config.ColAssignedTo) }; ticket.ExternalId = GenerateStableId(ticket); var status = (ticket.RawStatus ?? "").ToLower().Trim(); ticket.IsOpen = status != "closed" && status != "resolved" && status != "completed" && status != "done"; // Need at least an issue description or device name to be a valid ticket if (ticket.IsOpen && (!string.IsNullOrWhiteSpace(ticket.IssueDescription) || !string.IsNullOrWhiteSpace(ticket.DeviceName))) tickets.Add(ticket); } return tickets; } private string GenerateStableId(ExternalTicket t) { var key = (t.Timestamp.ToString("yyyyMMddHHmm") + "|" + (t.RequesterEmail ?? "") + "|" + (t.DeviceName ?? "")).ToLower(); int hash = 0; for (int i = 0; i < key.Length; i++) hash = ((hash << 5) - hash) + key[i]; return "NTT" + Math.Abs(hash).ToString().PadLeft(6, '0').Substring(0, 6); } private string SafeGet(List fields, int index) { if (index < 0 || index >= fields.Count) return null; var val = fields[index].Trim(); return string.IsNullOrEmpty(val) ? null : val; } private DateTime SafeGetDate(List fields, int index) { var val = SafeGet(fields, index); if (val == null) return DateTime.Now; DateTime dt; if (DateTime.TryParse(val, out dt)) return dt; return DateTime.Now; } private List ParseCsvLine(string line) { var fields = new List(); bool inQuotes = false; var current = new StringBuilder(); for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') { if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') { current.Append('"'); i++; } else inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { fields.Add(current.ToString()); current.Clear(); } else if (c != '\r') current.Append(c); } fields.Add(current.ToString()); return fields; } } public class GoogleSheetResult { public List Tickets { get; set; } public string Error { get; set; } public string Warning { get; set; } public GoogleSheetResult() { Tickets = new List(); } } }