Files
disco-service-tracker-plugin/Services/GoogleSheetService.cs
T

232 lines
9.2 KiB
C#

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("<!DOCTYPE", StringComparison.OrdinalIgnoreCase))
{
if (File.Exists(_cachePath))
{
csvData = File.ReadAllText(_cachePath);
result.Warning = "Got HTML instead of CSV. Using cached data. Check Publish to Web settings (must publish as CSV).";
}
else
{
result.Error = "Google returned HTML instead of CSV. Ensure the sheet is published to web. "
+ "Go to File > 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<ExternalTicket> ParseCsv(string csvData)
{
var tickets = new List<ExternalTicket>();
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<string> 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<string> 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<string> ParseCsvLine(string line)
{
var fields = new List<string>();
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<ExternalTicket> Tickets { get; set; }
public string Error { get; set; }
public string Warning { get; set; }
public GoogleSheetResult() { Tickets = new List<ExternalTicket>(); }
}
}