fix: support published key (2PACX-) format for Google Sheet CSV, add HTML response detection
This commit is contained in:
@@ -2,7 +2,6 @@ using Disco.Plugins.ServiceTracker.Models;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -35,32 +34,62 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
{
|
{
|
||||||
var cacheAge = DateTime.Now - File.GetLastWriteTime(_cachePath);
|
var cacheAge = DateTime.Now - File.GetLastWriteTime(_cachePath);
|
||||||
if (cacheAge.TotalMinutes < _config.RefreshMinutes)
|
if (cacheAge.TotalMinutes < _config.RefreshMinutes)
|
||||||
{
|
|
||||||
csvData = File.ReadAllText(_cachePath);
|
csvData = File.ReadAllText(_cachePath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch fresh if no cache
|
|
||||||
if (csvData == null)
|
if (csvData == null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var url = string.Format(
|
// Build URL based on ID format
|
||||||
"https://docs.google.com/spreadsheets/d/{0}/export?format=csv&gid={1}",
|
string url;
|
||||||
_config.SpreadsheetId, _config.GId);
|
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())
|
using (var client = new WebClient())
|
||||||
{
|
{
|
||||||
client.Encoding = Encoding.UTF8;
|
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);
|
csvData = client.DownloadString(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache it
|
// Validate we got actual CSV, not an HTML error page
|
||||||
File.WriteAllText(_cachePath, csvData);
|
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)
|
catch (WebException ex)
|
||||||
{
|
{
|
||||||
// Try cached data as fallback
|
|
||||||
if (File.Exists(_cachePath))
|
if (File.Exists(_cachePath))
|
||||||
{
|
{
|
||||||
csvData = File.ReadAllText(_cachePath);
|
csvData = File.ReadAllText(_cachePath);
|
||||||
@@ -68,7 +97,14 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.Error = "Could not fetch Google Sheet. Ensure it is published to web (File > Share > Publish to web > CSV). Error: " + ex.Message;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,10 +115,11 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse CSV
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.Tickets = ParseCsv(csvData);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -103,6 +140,14 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
var fields = ParseCsvLine(lines[i]);
|
var fields = ParseCsvLine(lines[i]);
|
||||||
if (fields.Count == 0) continue;
|
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
|
var ticket = new ExternalTicket
|
||||||
{
|
{
|
||||||
InternalId = nextId++,
|
InternalId = nextId++,
|
||||||
@@ -117,14 +162,13 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
AssignedTo = SafeGet(fields, _config.ColAssignedTo)
|
AssignedTo = SafeGet(fields, _config.ColAssignedTo)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate stable ID from key fields
|
|
||||||
ticket.ExternalId = GenerateStableId(ticket);
|
ticket.ExternalId = GenerateStableId(ticket);
|
||||||
|
|
||||||
// Determine if open
|
|
||||||
var status = (ticket.RawStatus ?? "").ToLower().Trim();
|
var status = (ticket.RawStatus ?? "").ToLower().Trim();
|
||||||
ticket.IsOpen = status != "closed" && status != "resolved" && status != "completed" && status != "done";
|
ticket.IsOpen = status != "closed" && status != "resolved" && status != "completed" && status != "done";
|
||||||
|
|
||||||
if (ticket.IsOpen && !string.IsNullOrWhiteSpace(ticket.IssueDescription))
|
// 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);
|
tickets.Add(ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,31 +205,16 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
var fields = new List<string>();
|
var fields = new List<string>();
|
||||||
bool inQuotes = false;
|
bool inQuotes = false;
|
||||||
var current = new StringBuilder();
|
var current = new StringBuilder();
|
||||||
|
|
||||||
for (int i = 0; i < line.Length; i++)
|
for (int i = 0; i < line.Length; i++)
|
||||||
{
|
{
|
||||||
char c = line[i];
|
char c = line[i];
|
||||||
if (c == '"')
|
if (c == '"')
|
||||||
{
|
{
|
||||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') { current.Append('"'); i++; }
|
||||||
{
|
else inQuotes = !inQuotes;
|
||||||
current.Append('"');
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c == ',' && !inQuotes)
|
|
||||||
{
|
|
||||||
fields.Add(current.ToString());
|
|
||||||
current.Clear();
|
|
||||||
}
|
|
||||||
else if (c != '\r')
|
|
||||||
{
|
|
||||||
current.Append(c);
|
|
||||||
}
|
}
|
||||||
|
else if (c == ',' && !inQuotes) { fields.Add(current.ToString()); current.Clear(); }
|
||||||
|
else if (c != '\r') current.Append(c);
|
||||||
}
|
}
|
||||||
fields.Add(current.ToString());
|
fields.Add(current.ToString());
|
||||||
return fields;
|
return fields;
|
||||||
@@ -197,10 +226,6 @@ namespace Disco.Plugins.ServiceTracker.Services
|
|||||||
public List<ExternalTicket> Tickets { get; set; }
|
public List<ExternalTicket> Tickets { get; set; }
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
public string Warning { get; set; }
|
public string Warning { get; set; }
|
||||||
|
public GoogleSheetResult() { Tickets = new List<ExternalTicket>(); }
|
||||||
public GoogleSheetResult()
|
|
||||||
{
|
|
||||||
Tickets = new List<ExternalTicket>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user