56 internal string SpreadsheetId {
get;
private set; }
57 internal string SheetName {
get;
private set; }
58 internal string ReadRange {
get;
private set; }
59 internal string WriteRange {
get;
private set; }
60 internal IGPALGrid<string> ReadData {
get;
private set; }
61 internal FormatType? OurFormatType {
get;
private set; }
62 internal NumberFormatType? NumberFormatType {
get;
set; }
65 internal object FormatValue {
get;
private set; }
66 internal int? InsertPosition {
get;
private set; }
67 internal string InsertSeparator {
get;
private set; }
68 internal TriggerScheduleType TriggerSchedule {
get;
private set; }
69 internal string AppsScriptProjectId {
get;
private set; }
70 internal string ScriptName {
get;
private set; }
71 internal string FunctionName {
get;
private set; }
72 internal ScriptAccessType ScriptAccess {
get;
private set; } = ScriptAccessType.ME;
73 internal ExecuteAsType ExecuteAs {
get;
private set; } = ExecuteAsType.USER_DEPLOYING;
74 internal string DeploymentDescription {
get;
private set; } =
"Web app deployment for script";
75 internal int VersionNumber {
get;
private set; } = 1;
77 internal IRESTClient SheetsRESTClient {
get;
private set; }
78 internal IRESTClient ScriptsRESTClient {
get;
private set; }
80 internal GoogleCloud _googleCloud {
get;
private set; }
82 internal GoogleSheets()
85 SheetsRESTClient =
GPAL.
RESTClient.WithAPIBase(googleSheetsConfig.SheetsApiBase).ToGPALObject();
86 ScriptsRESTClient =
GPAL.
RESTClient.WithAPIBase(googleSheetsConfig.ScriptsApiBase).ToGPALObject();
90 private void InitializeMode()
92 bool hasAccessToken = !
string.IsNullOrEmpty(((Credentials)Credentials)?.AccessToken);
94 if (Credentials !=
null)
96 if (
false == hasAccessToken)
98 Credentials.FetchAccessToken(out
string accessToken);
101 if (
false ==
string.IsNullOrEmpty(((Credentials)Credentials)?.AccessToken))
103 var testResponse =
GPAL.
RESTClient.WithAPIBase(
"https://oauth2.googleapis.com")
104 .WithEndpoint(
"/tokeninfo")
105 .WithParameters(
new { access_token = ((Credentials)Credentials)?.AccessToken })
107 if (testResponse !=
null && !testResponse.Contains(
"error"))
109 googleSheetsConfig.UseAppsScript =
false;
114 googleSheetsConfig.UseAppsScript =
true;
117 private string ResolveSheetName(
object sheetNameOrId)
119 if (sheetNameOrId ==
null)
121 return SheetName ??
"Sheet1";
124 if (sheetNameOrId is
string name)
126 return string.IsNullOrEmpty(name) ? SheetName ??
"Sheet1" : name;
128 else if (sheetNameOrId is
int id)
130 var spreadsheetResponseJson = SheetsRESTClient
131 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
132 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
135 if (spreadsheetResponseJson !=
null)
139 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
140 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.SheetId ==
id);
141 if (targetSheet !=
null)
143 return targetSheet.Properties.Title;
149 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Sheet with ID [{id}] not found",
this, GPALObjectType.GoogleSheets);
154 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
159 private object ResolveSheetIdOrName(
object sheetNameOrId)
161 if (sheetNameOrId ==
null)
163 return SheetName ??
"Sheet1";
166 if (sheetNameOrId is
string name)
168 return string.IsNullOrEmpty(name) ? SheetName ??
"Sheet1" : name;
170 else if (sheetNameOrId is
int id)
176 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
182 private bool ReadDataFromRange()
184 if (
string.IsNullOrEmpty(SpreadsheetId))
186 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
190 if (
string.IsNullOrEmpty(SheetName))
192 SheetName =
"Sheet1";
193 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
196 if (
string.IsNullOrEmpty(ReadRange))
198 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Range not set for reading data",
this, GPALObjectType.GoogleSheets);
202 if (googleSheetsConfig.UseAppsScript)
204 var response = CallWebApp(
"ReadRange",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, range = ReadRange });
205 object successObj =
null;
206 object dataObj =
null;
207 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"data", out dataObj))
211 var dataElement = JsonSerializer.Deserialize<JsonElement>(dataObj.ToString());
212 var appsScriptGrid =
GPAL.GridForType<
string>();
213 foreach (var row
in dataElement.EnumerateArray())
214 appsScriptGrid.AddRow(row.EnumerateArray().Select(cell => JsonValueKind.String == cell.ValueKind ? cell.GetString() : cell.ToString()).ToList());
215 ReadData = appsScriptGrid;
220 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse Apps Script read data",
this, GPALObjectType.GoogleSheets, ex);
226 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to read range via Apps Script",
this, GPALObjectType.GoogleSheets);
232 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, ReadRange);
233 var responseJson = SheetsRESTClient
234 .WithEndpoint(endpoint)
235 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
238 if (responseJson !=
null)
242 var valueRange = JsonSerializer.Deserialize<ValueRange>(responseJson);
243 var restGrid =
GPAL.GridForType<
string>();
244 foreach (var row
in valueRange?.values ??
new List<List<object>>())
245 restGrid.AddRow(row.Select(cell => cell?.ToString()).ToList());
251 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse API read data",
this, GPALObjectType.GoogleSheets, ex);
257 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to read range via API",
this, GPALObjectType.GoogleSheets);
263 private dynamic CallWebApp(
string action,
object parameters)
265 if (
string.IsNullOrEmpty(googleSheetsConfig.WebAppUrl))
271 var payload =
new { action, parameters };
272 var responseJson = SheetsRESTClient
273 .WithEndpoint(googleSheetsConfig.WebAppUrl)
274 .WithParameters(payload)
275 .WithHttpMethod(
"POST")
276 .WithHeader(
"Content-Type",
"application/json")
279 if (responseJson ==
null)
281 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to call web app for action: [{action}]",
this, GPALObjectType.GoogleSheets);
287 return JsonSerializer.Deserialize<Dictionary<string, object>>(responseJson);
291 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse web app response",
this, GPALObjectType.GoogleSheets, ex);
298 if (
string.IsNullOrEmpty(SpreadsheetId))
300 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
305 if (ReadData ==
null)
307 if (!
string.IsNullOrEmpty(ReadRange))
309 if (!ReadDataFromRange())
317 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No data to save; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
323 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Saving [{ReadData.Rows}] rows of sheet [{SheetName}] to grid.",
this, GPALObjectType.GoogleSheets);
332 if (
string.IsNullOrEmpty(cell))
334 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
338 if (
string.IsNullOrEmpty(SpreadsheetId))
340 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
344 if (
string.IsNullOrEmpty(SheetName))
346 SheetName =
"Sheet1";
347 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
350 if (ReadData ==
null)
352 if (!
string.IsNullOrEmpty(ReadRange))
354 if (!ReadDataFromRange())
361 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to append; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
367 if (googleSheetsConfig.UseAppsScript)
369 var response = CallWebApp(
"AppendToCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator });
370 object successObj =
null;
371 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
373 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to cell via Apps Script",
this, GPALObjectType.GoogleSheets);
378 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
379 var readResponseJson = SheetsRESTClient
380 .WithEndpoint(endpoint)
381 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
384 string currentContent =
"";
385 if (readResponseJson !=
null)
389 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
390 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
395 string appendData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
396 string newContent = currentContent + appendData;
398 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
399 var writeResponse = SheetsRESTClient
400 .WithEndpoint(writeEndpoint)
401 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
402 .WithHttpMethod(
"PUT")
403 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
406 if (writeResponse ==
null)
408 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to cell",
this, GPALObjectType.GoogleSheets);
417 if (
string.IsNullOrEmpty(cell))
419 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
423 if (
string.IsNullOrEmpty(SpreadsheetId))
425 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
429 if (
string.IsNullOrEmpty(SheetName))
431 SheetName =
"Sheet1";
432 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
435 if (ReadData ==
null)
437 if (!
string.IsNullOrEmpty(ReadRange))
439 if (!ReadDataFromRange())
446 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to prepend; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
451 if (googleSheetsConfig.UseAppsScript)
453 var response = CallWebApp(
"PrependToCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator });
454 object successObj =
null;
455 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
457 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to prepend to cell via Apps Script",
this, GPALObjectType.GoogleSheets);
462 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
463 var readResponseJson = SheetsRESTClient
464 .WithEndpoint(endpoint)
465 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
468 string currentContent =
"";
469 if (readResponseJson !=
null)
473 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
474 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
479 string prependData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
480 string newContent = prependData + currentContent;
482 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
483 var writeResponse = SheetsRESTClient
484 .WithEndpoint(writeEndpoint)
485 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
486 .WithHttpMethod(
"PUT")
487 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
490 if (writeResponse ==
null)
492 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to prepend to cell",
this, GPALObjectType.GoogleSheets);
501 if (
string.IsNullOrEmpty(cell))
503 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
507 if (
string.IsNullOrEmpty(SpreadsheetId))
509 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
513 if (
string.IsNullOrEmpty(SheetName))
515 SheetName =
"Sheet1";
516 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
519 if (ReadData ==
null)
521 if (!
string.IsNullOrEmpty(ReadRange))
523 if (!ReadDataFromRange())
530 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to insert; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
535 if (googleSheetsConfig.UseAppsScript)
537 var response = CallWebApp(
"InsertAtCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator, position = InsertPosition });
538 object successObj =
null;
539 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
541 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to insert at cell via Apps Script",
this, GPALObjectType.GoogleSheets);
546 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
547 var readResponseJson = SheetsRESTClient
548 .WithEndpoint(endpoint)
549 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
552 string currentContent =
"";
553 if (readResponseJson !=
null)
557 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
558 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
563 string insertData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
564 int pos = Math.Min(InsertPosition ?? 0, currentContent.Length);
565 string newContent = currentContent.Substring(0, pos) + insertData + currentContent.Substring(pos);
567 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
568 var writeResponse = SheetsRESTClient
569 .WithEndpoint(writeEndpoint)
570 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
571 .WithHttpMethod(
"PUT")
572 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
575 if (writeResponse ==
null)
577 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to insert at cell",
this, GPALObjectType.GoogleSheets);
586 if (
string.IsNullOrEmpty(writeRange))
592 WriteRange = writeRange;
599 if (
string.IsNullOrEmpty(SpreadsheetId))
601 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
605 string targetSheet = ResolveSheetName(sheetNameOrId);
606 if (
string.IsNullOrEmpty(targetSheet))
611 if (ReadData ==
null)
613 if (!
string.IsNullOrEmpty(ReadRange))
615 if (!ReadDataFromRange())
622 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to write; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
627 if (googleSheetsConfig.UseAppsScript)
629 var response = CallWebApp(
"WriteRange",
new { spreadsheetId = SpreadsheetId, sheetName = targetSheet, range = WriteRange, data = ReadData });
630 object successObj =
null;
631 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
633 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to write range via Apps Script",
this, GPALObjectType.GoogleSheets);
638 string endpoint =
string.Format(googleSheetsConfig.WriteRangeEndpoint, SpreadsheetId, targetSheet, WriteRange) + googleSheetsConfig.ValueInputOption;
639 var response = SheetsRESTClient
640 .WithEndpoint(endpoint)
641 .WithParameters(
new { values = ReadData })
642 .WithHttpMethod(
"PUT")
643 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
646 if (response ==
null)
657 object targetSheetOrId = ResolveSheetIdOrName(sheetNameOrId);
658 if (targetSheetOrId ==
null)
663 if (
string.IsNullOrEmpty(SpreadsheetId))
665 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
669 if (ReadData ==
null)
671 if (!
string.IsNullOrEmpty(ReadRange))
673 if (!ReadDataFromRange())
686 if (googleSheetsConfig.UseAppsScript)
688 var response = CallWebApp(
"AppendToSheet",
new { spreadsheetId = SpreadsheetId, sheetNameOrId = targetSheetOrId, data = ReadData });
689 object successObj =
null;
690 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
692 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to sheet via Apps Script",
this, GPALObjectType.GoogleSheets);
697 string targetSheetName = ResolveSheetName(sheetNameOrId);
698 if (
string.IsNullOrEmpty(targetSheetName))
702 string endpoint =
string.Format(googleSheetsConfig.AppendRangeEndpoint, SpreadsheetId, targetSheetName,
"A1") +
"?valueInputOption=RAW&insertDataOption=INSERT_ROWS";
703 var response = SheetsRESTClient
704 .WithEndpoint(endpoint)
705 .WithParameters(
new { values = ReadData })
706 .WithHttpMethod(
"POST")
707 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
710 if (response ==
null)
712 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to sheet",
this, GPALObjectType.GoogleSheets);
729 private const string GatewayScriptResource =
"GenerallyPositive.Browser.gcode.js";
731 private string ReadGatewayScript()
733 string retVal =
null;
735 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GatewayScriptResource))
739 string msg = $
"[{GatewayScriptResource}] is not in this assembly, so there is no gateway script to deploy";
746 using (StreamReader reader =
new StreamReader(stream))
747 retVal = reader.ReadToEnd();
753 private void SetupAndDeployScript(
string spreadsheetIdOrTitle)
755 string cloudProjectId = SetupCloudProject($
"GPAL_Sheets_{Guid.NewGuid().ToString().Substring(0, 8)}");
756 if (
string.IsNullOrEmpty(cloudProjectId))
761 string scriptTitle = $
"GPAL_Script_{spreadsheetIdOrTitle}";
762 string scriptId = GetOrCreateScriptProject(cloudProjectId, scriptTitle);
763 if (
string.IsNullOrEmpty(scriptId))
769 var deploymentResponse = ScriptsRESTClient
770 .WithEndpoint($
"/v1/projects/{scriptId}/deployments")
771 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
774 if (deploymentResponse !=
null)
778 var deploymentData = JsonSerializer.Deserialize<Dictionary<string, object>>(deploymentResponse);
779 object deploymentsObj =
null;
780 if (deploymentData.TryGetValue(
"deployments", out deploymentsObj))
782 var deployments = JsonSerializer.Deserialize<JsonElement>(deploymentsObj.ToString());
783 foreach (var deployment
in deployments.EnumerateArray())
785 if (deployment.TryGetProperty(
"entryPoints", out var entryPoints))
787 foreach (var entry
in entryPoints.EnumerateArray())
789 if (entry.TryGetProperty(
"webApp", out var webApp) && webApp.TryGetProperty(
"url", out var url))
791 googleSheetsConfig.WebAppUrl = url.GetString();
792 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Found existing web app at: [{googleSheetsConfig.WebAppUrl}]",
this, GPALObjectType.GoogleSheets);
793 AppsScriptProjectId = scriptId;
805 string scriptContent = ReadGatewayScript();
807 this.WithProjectId(scriptId)
808 .WithScriptName(
"main")
809 .WithScriptAccess(ScriptAccessType.ME)
810 .WithExecuteAs(ExecuteAsType.USER_DEPLOYING)
811 .UploadScript(scriptContent)
812 .DeployScriptAsWebApp(out
string webAppUrl);
814 if (!
string.IsNullOrEmpty(webAppUrl))
816 googleSheetsConfig.WebAppUrl = webAppUrl;
817 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Apps Script deployed at: [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
833 if (
true ==
string.IsNullOrWhiteSpace(webAppUrl))
835 string msg =
"No web app url was supplied, so there is no gateway to drive sheets through";
842 googleSheetsConfig.WebAppUrl = webAppUrl;
846 googleSheetsConfig.UseAppsScript =
true;
848 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Sheets run through the web app at [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
863 if (
string.IsNullOrEmpty(spreadsheetIdOrTitle))
865 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID or title cannot be empty",
this, GPALObjectType.GoogleSheets);
871 if (googleSheetsConfig.UseAppsScript)
873 if (
string.IsNullOrEmpty(googleSheetsConfig.WebAppUrl))
875 SetupAndDeployScript(spreadsheetIdOrTitle);
878 var response = CallWebApp(
"WithSpreadsheet",
new { spreadsheetIdOrTitle });
879 object successObj =
null;
881 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"spreadsheetId", out idObj))
883 SpreadsheetId = idObj.ToString();
887 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to open or create spreadsheet [{spreadsheetIdOrTitle}] via Apps Script",
this, GPALObjectType.GoogleSheets);
892 var responseJson = SheetsRESTClient
893 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{spreadsheetIdOrTitle}")
894 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
897 if (!
string.IsNullOrEmpty(responseJson))
899 SpreadsheetId = spreadsheetIdOrTitle;
903 var createRequest =
new { properties =
new { title = spreadsheetIdOrTitle } };
904 var createResponseJson = SheetsRESTClient
905 .WithEndpoint(
"/v4/spreadsheets")
906 .WithParameters(createRequest)
907 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
908 .WithHttpMethod(
"POST")
911 if (createResponseJson !=
null)
913 var createResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(createResponseJson);
914 if (!
string.IsNullOrEmpty(createResponse?.SpreadsheetId))
916 SpreadsheetId = createResponse.SpreadsheetId;
920 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create spreadsheet [{spreadsheetIdOrTitle}]",
this, GPALObjectType.GoogleSheets);
925 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create spreadsheet [{spreadsheetIdOrTitle}]",
this, GPALObjectType.GoogleSheets);
933 public IAllowSheetOperations CreateSheet(
string sheetName)
935 if (
string.IsNullOrEmpty(sheetName))
937 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
941 if (googleSheetsConfig.UseAppsScript)
943 var response = CallWebApp(
"CreateSheet",
new { spreadsheetId = SpreadsheetId, sheetName });
944 object successObj =
null;
945 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj)
947 SheetName = sheetName;
951 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create sheet via Apps Script",
this, GPALObjectType.GoogleSheets);
958 requests =
new[] {
new { addSheet =
new { properties =
new { title = sheetName } } } }
960 var response = SheetsRESTClient
961 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
962 .WithParameters(request)
963 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
966 if (response ==
null)
968 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create sheet",
this, GPALObjectType.GoogleSheets);
971 SheetName = sheetName;
977 public IAllowSheetSelection SetSheetName(
string newSheetName)
979 if (
string.IsNullOrEmpty(newSheetName))
981 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Title cannot be empty",
this, GPALObjectType.GoogleSheets);
985 if (googleSheetsConfig.UseAppsScript)
987 var response = CallWebApp(
"WithTitle",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, title = newSheetName });
988 object successObj =
null;
989 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
991 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title via Apps Script",
this, GPALObjectType.GoogleSheets);
998 requests =
new[] {
new { updateSheetProperties =
new { properties =
new { title = newSheetName }, fields = googleSheetsConfig.TitleField } } }
1000 var response = SheetsRESTClient
1001 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1002 .WithParameters(request)
1003 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1006 if (response ==
null)
1008 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title",
this, GPALObjectType.GoogleSheets);
1014 public IAllowSheetSelection SetSpreadsheetTitle(
string title)
1016 if (
string.IsNullOrEmpty(title))
1018 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Title cannot be empty",
this, GPALObjectType.GoogleSheets);
1022 if (googleSheetsConfig.UseAppsScript)
1024 var response = CallWebApp(
"WithTitle",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, title });
1025 object successObj =
null;
1026 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1028 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title via Apps Script",
this, GPALObjectType.GoogleSheets);
1035 requests =
new[] {
new { updateSpreadsheetProperties =
new { properties =
new { title }, fields = googleSheetsConfig.TitleField } } }
1037 var response = SheetsRESTClient
1038 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1039 .WithParameters(request)
1040 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1043 if (response ==
null)
1045 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title",
this, GPALObjectType.GoogleSheets);
1052 public IAllowInDataOperations WithSheet(
object sheetNameOrId)
1054 if (sheetNameOrId ==
null)
1056 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name or ID cannot be null",
this, GPALObjectType.GoogleSheets);
1060 string sheetName =
null;
1061 int? sheetId =
null;
1063 if (sheetNameOrId is
string name)
1065 if (
string.IsNullOrEmpty(name))
1067 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
1072 else if (sheetNameOrId is
int id)
1078 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
1082 if (googleSheetsConfig.UseAppsScript)
1084 var action = sheetId.HasValue ?
"WithSheetById" :
"WithSheet";
1085 var response = CallWebApp(action,
new { spreadsheetId = SpreadsheetId, sheetName, sheetId });
1086 object successObj =
null;
1087 object nameObj =
null;
1088 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj)
1090 if (response.TryGetValue(
"sheetName", out nameObj))
1092 SheetName = nameObj.ToString();
1096 SheetName = sheetName;
1101 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1102 $
"Failed to activate sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")} via Apps Script",
1103 this, GPALObjectType.GoogleSheets);
1108 var spreadsheetResponseJson = SheetsRESTClient
1109 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1110 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1113 SpreadsheetResponse spreadsheetResponse =
null;
1114 if (spreadsheetResponseJson !=
null)
1118 spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1123 if (spreadsheetResponse ==
null || spreadsheetResponse.Sheets ==
null)
1125 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve spreadsheet info",
this, GPALObjectType.GoogleSheets);
1129 var targetSheet = sheetId.HasValue
1130 ? spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.SheetId == sheetId.Value)
1131 : spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1133 if (targetSheet ==
null)
1135 if (sheetId.HasValue)
1137 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Sheet with ID [{sheetId}] not found",
this, GPALObjectType.GoogleSheets);
1142 CreateSheet(sheetName);
1143 targetSheet = spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1144 if (targetSheet ==
null)
1146 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create sheet [{sheetName}]",
this, GPALObjectType.GoogleSheets);
1152 int targetSheetId = targetSheet.Properties.SheetId;
1153 SheetName = targetSheet.Properties.Title;
1155 var activateRequest =
new
1161 updateSheetProperties =
new
1163 properties =
new { sheetId = targetSheetId, hidden =
false },
1164 fields = googleSheetsConfig.HiddenField
1170 var activateResponse = SheetsRESTClient
1171 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1172 .WithParameters(activateRequest)
1173 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1176 if (activateResponse ==
null)
1178 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to activate sheet",
this, GPALObjectType.GoogleSheets);
1185 public IAllowDataOperations WithReadRange(
string readRange)
1187 if (
string.IsNullOrEmpty(readRange))
1189 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Range cannot be empty",
this, GPALObjectType.GoogleSheets);
1193 ReadRange = readRange;
1197 public IAllowDataOperations WithData(IGPALGrid<string> data)
1203 public IAllowSheetSelection DeleteSheet(
object sheetNameOrId)
1205 if (sheetNameOrId ==
null)
1207 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name or ID cannot be null",
this, GPALObjectType.GoogleSheets);
1211 string sheetName =
null;
1212 int? sheetId =
null;
1214 if (sheetNameOrId is
string name)
1216 if (
string.IsNullOrEmpty(name))
1218 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
1223 else if (sheetNameOrId is
int id)
1229 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
1233 if (googleSheetsConfig.UseAppsScript)
1235 var action = sheetId.HasValue ?
"DeleteSheetById" :
"DeleteSheet";
1236 var response = CallWebApp(action,
new { spreadsheetId = SpreadsheetId, sheetName, sheetId });
1237 object successObj =
null;
1238 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1240 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1241 $
"Failed to delete sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")} via Apps Script",
1242 this, GPALObjectType.GoogleSheets);
1247 var spreadsheetResponseJson = SheetsRESTClient
1248 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1249 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1252 SpreadsheetResponse spreadsheetResponse =
null;
1253 if (spreadsheetResponseJson !=
null)
1257 spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1262 if (spreadsheetResponse ==
null || spreadsheetResponse.Sheets ==
null)
1264 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve spreadsheet info",
this, GPALObjectType.GoogleSheets);
1268 var targetSheet = sheetId.HasValue
1269 ? spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.SheetId == sheetId.Value)
1270 : spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1272 if (targetSheet ==
null)
1274 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1275 $
"{(sheetId.HasValue ? $"Sheet with ID {sheetId}
" : $"Sheet
'{sheetName}'")} not found",
1276 this, GPALObjectType.GoogleSheets);
1282 requests =
new[] {
new { deleteSheet =
new { sheetId = targetSheet.Properties.SheetId } } }
1284 var response = SheetsRESTClient
1285 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1286 .WithParameters(request)
1287 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1290 if (response ==
null)
1292 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1293 $
"Failed to delete sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")}",
1294 this, GPALObjectType.GoogleSheets);
1301 public IAllowSheetSelection ListSheets(out IEnumerable<(
string name,
int sheetId)> sheets)
1303 if (googleSheetsConfig.UseAppsScript)
1305 var response = CallWebApp(
"ListSheets",
new { spreadsheetId = SpreadsheetId });
1306 object successObj =
null;
1307 object sheetsObj =
null;
1308 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"sheets", out sheetsObj))
1312 var sheetsElement = JsonSerializer.Deserialize<JsonElement>(sheetsObj.ToString());
1313 sheets = sheetsElement.EnumerateArray().Select(s => (s.GetProperty(
"name").GetString(), s.GetProperty(
"sheetId").GetInt32()));
1318 sheets = Enumerable.Empty<(string, int)>();
1319 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse sheets list via Apps Script",
this, GPALObjectType.GoogleSheets);
1325 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to list sheets via Apps Script",
this, GPALObjectType.GoogleSheets);
1326 sheets = Enumerable.Empty<(string, int)>();
1332 var responseJson = SheetsRESTClient
1333 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1334 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1337 if (responseJson ==
null)
1339 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to list sheets",
this, GPALObjectType.GoogleSheets);
1340 sheets = Enumerable.Empty<(string, int)>();
1346 var response = JsonSerializer.Deserialize<SpreadsheetResponse>(responseJson);
1347 if (response?.Sheets !=
null)
1349 sheets = response.Sheets.Select(s => (s.Properties.Title, s.Properties.SheetId));
1353 sheets = Enumerable.Empty<(string, int)>();
1358 sheets = Enumerable.Empty<(string, int)>();
1359 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse sheets list",
this, GPALObjectType.GoogleSheets);
1366 public IAllowInsertSettings WithInsertPosition(
int position)
1368 InsertPosition = position;
1372 public IAllowInsertSettings WithInsertSeparator(
string separator)
1374 InsertSeparator = separator;
1378 public IAllowDataOperations CalculateSum(out
string result)
1380 if (ReadData ==
null && !
string.IsNullOrEmpty(ReadRange))
1382 ReadDataFromRange();
1385 if (googleSheetsConfig.UseAppsScript)
1387 var response = CallWebApp(
"CalculateSum",
new { data = ReadData });
1388 object successObj =
null;
1389 object resultObj =
null;
1390 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"result", out resultObj))
1392 result = resultObj.ToString();
1396 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to calculate sum via Apps Script",
this, GPALObjectType.GoogleSheets);
1402 result = ReadData?.SelectMany(row => row)
1403 .Where(v =>
double.TryParse(v, out _))
1404 .Sum(v =>
double.Parse(v))
1411 public IAllowDataOperations CalculateCount(out
string result)
1413 if (ReadData ==
null && !
string.IsNullOrEmpty(ReadRange))
1415 ReadDataFromRange();
1418 if (googleSheetsConfig.UseAppsScript)
1420 var response = CallWebApp(
"CalculateCount",
new { data = ReadData });
1421 object successObj =
null;
1422 object resultObj =
null;
1423 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"result", out resultObj))
1425 result = resultObj.ToString();
1429 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to calculate count via Apps Script",
this, GPALObjectType.GoogleSheets);
1435 result = ReadData?.SelectMany(row => row).Count().ToString() ??
"0";
1441 public IAllowFormatSettings WithFormatType(FormatType formatType)
1443 OurFormatType = formatType;
1447 public IAllowFormatRange WithFormatValue(
object formatValue)
1449 FormatValue = formatValue;
1453 public IAllowFormatRange WithAlignmentFormatValue(HorizontalAlignmentType alignment)
1455 FormatValue = alignment;
1459 public IAllowFormatRange WithAlignmentFormatValue(VerticalAlignmentType alignment)
1461 FormatValue = alignment;
1465 public IAllowFormatRange WithNumberFormatValue(NumberFormatType numberFormat)
1467 NumberFormatType = numberFormat;
1471 public IAllowFormatRange WithTextRotationFormatValue(TextRotationType rotation)
1473 FormatValue = rotation;
1477 public IAllowFormatRange WithColorFormatValue(System.Drawing.Color color)
1479 FormatValue = color;
1483 public IAllowInDataOperations FormatRange(
string range)
1485 if (
string.IsNullOrEmpty(range))
1487 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Range cannot be empty",
this, GPALObjectType.GoogleSheets);
1491 if (OurFormatType ==
null)
1493 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Format type not set",
this, GPALObjectType.GoogleSheets);
1497 if (googleSheetsConfig.UseAppsScript)
1499 var response = CallWebApp(
"FormatRange",
new
1501 spreadsheetId = SpreadsheetId,
1502 sheetName = SheetName,
1504 formatType = OurFormatType.ToString(),
1505 formatValue = FormatValue?.ToString(),
1506 numberFormatType = NumberFormatType?.ToString(),
1510 object successObj =
null;
1511 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1513 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to format range via Apps Script",
this, GPALObjectType.GoogleSheets);
1518 var (targetSheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex) = ParseRange(range);
1519 var formatRequest =
new
1527 range =
new { sheetId = targetSheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex },
1528 cell = GetCellFormat(),
1529 fields = GetFieldsForFormatType(OurFormatType.Value)
1534 var response = SheetsRESTClient
1535 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1536 .WithParameters(formatRequest)
1537 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1540 if (response ==
null)
1542 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to format range",
this, GPALObjectType.GoogleSheets);
1549 private object GetCellFormat()
1551 if (FormatValue ==
null)
1553 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Format value not set for [{OurFormatType}]",
this, GPALObjectType.GoogleSheets);
1554 return new { userEnteredFormat =
new { } };
1557 switch (OurFormatType)
1559 case FormatType.Bold:
1560 if (FormatValue is
bool bold)
1561 return new { userEnteredFormat =
new { textFormat =
new { bold } } };
1563 case FormatType.Italic:
1564 if (FormatValue is
bool italic)
1565 return new { userEnteredFormat =
new { textFormat =
new { italic } } };
1567 case FormatType.Underline:
1568 if (FormatValue is
bool underline)
1569 return new { userEnteredFormat =
new { textFormat =
new { underline } } };
1571 case FormatType.Strikethrough:
1572 if (FormatValue is
bool strikethrough)
1573 return new { userEnteredFormat =
new { textFormat =
new { strikethrough } } };
1575 case FormatType.FontSize:
1576 if (FormatValue is
int fontSize)
1577 return new { userEnteredFormat =
new { textFormat =
new { fontSize } } };
1579 case FormatType.FontFamily:
1580 if (FormatValue is
string fontFamily)
1581 return new { userEnteredFormat =
new { textFormat =
new { fontFamily } } };
1583 case FormatType.ForegroundColor:
1584 if (FormatValue is System.Drawing.Color color)
1585 return new { userEnteredFormat =
new { textFormat =
new { foregroundColor =
new { red = color.R / 255.0, green = color.G / 255.0, blue = color.B / 255.0 } } } };
1587 case FormatType.CellBackgroundColor:
1588 if (FormatValue is System.Drawing.Color bgColor)
1589 return new { userEnteredFormat =
new { backgroundColor =
new { red = bgColor.R / 255.0, green = bgColor.G / 255.0, blue = bgColor.B / 255.0 } } };
1591 case FormatType.NumberFormat:
1592 if (NumberFormatType !=
null)
1595 userEnteredFormat =
new
1599 type = NumberFormatType.Value.ToString().ToUpper(),
1601 pattern = (string)
null
1606 case FormatType.HorizontalAlignment:
1607 if (FormatValue is HorizontalAlignmentType hAlign)
1608 return new { userEnteredFormat =
new { horizontalAlignment = hAlign.ToString().ToUpper() } };
1610 case FormatType.VerticalAlignment:
1611 if (FormatValue is VerticalAlignmentType vAlign)
1612 return new { userEnteredFormat =
new { verticalAlignment = vAlign.ToString().ToUpper() } };
1614 case FormatType.TextRotation:
1615 if (FormatValue is TextRotationType rotation)
1617 object textRotation = rotation
switch
1619 TextRotationType.TiltUp30 =>
new { degrees = 30 },
1620 TextRotationType.TiltUp45 =>
new { degrees = 45 },
1621 TextRotationType.TiltDown30 =>
new { degrees = -30 },
1622 TextRotationType.TiltDown45 =>
new { degrees = -45 },
1623 TextRotationType.UpsideDown =>
new { degrees = -180 },
1624 TextRotationType.Vertical =>
new { vertical =
true },
1625 _ =>
new { degrees = 0 }
1627 return new { userEnteredFormat =
new { textFormat =
new { textRotation } } };
1632 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Invalid format value for [{OurFormatType}]",
this, GPALObjectType.GoogleSheets);
1633 return new { userEnteredFormat =
new { } };
1636 private string GetFieldsForFormatType(FormatType formatType)
1641 case FormatType.Bold:
1642 case FormatType.Italic:
1643 case FormatType.Underline:
1644 case FormatType.Strikethrough:
1645 case FormatType.FontSize:
1646 case FormatType.FontFamily:
1647 case FormatType.ForegroundColor:
1648 subfield =
"textFormat";
1650 case FormatType.CellBackgroundColor:
1651 subfield =
"backgroundColor";
1653 case FormatType.NumberFormat:
1654 subfield =
"numberFormat";
1656 case FormatType.HorizontalAlignment:
1657 subfield =
"horizontalAlignment";
1659 case FormatType.VerticalAlignment:
1660 subfield =
"verticalAlignment";
1662 case FormatType.TextRotation:
1663 subfield =
"textFormat.textRotation";
1670 return string.IsNullOrEmpty(subfield) ? googleSheetsConfig.UserEnteredFormatField : $
"{googleSheetsConfig.UserEnteredFormatField}.{subfield}";
1673 private (
int sheetId,
int? startRowIndex,
int? endRowIndex,
int? startColumnIndex,
int? endColumnIndex) ParseRange(
string range)
1676 string sheetNameFromRange =
null;
1677 string rangePart = range;
1679 if (range.Contains(
"!"))
1681 var parts = range.Split(
'!');
1682 sheetNameFromRange = parts[0].Trim(
'\'');
1683 rangePart = parts[1];
1686 if (!
string.IsNullOrEmpty(sheetNameFromRange))
1688 var spreadsheetResponseJson = SheetsRESTClient
1689 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1690 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1693 if (spreadsheetResponseJson !=
null)
1697 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1698 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.Title.Equals(sheetNameFromRange, StringComparison.OrdinalIgnoreCase));
1699 if (targetSheet !=
null)
1701 sheetId = targetSheet.Properties.SheetId;
1707 else if (!
string.IsNullOrEmpty(SheetName))
1710 var spreadsheetResponseJson = SheetsRESTClient
1711 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1712 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1715 if (spreadsheetResponseJson !=
null)
1719 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1720 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.Title.Equals(SheetName, StringComparison.OrdinalIgnoreCase));
1721 if (targetSheet !=
null)
1723 sheetId = targetSheet.Properties.SheetId;
1731 var match = System.Text.RegularExpressions.Regex.Match(rangePart,
@"^([A-Z]+)?(\d+)?(?::([A-Z]+)?(\d+)?)?$");
1734 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Invalid range format: [{range}]",
this, GPALObjectType.GoogleSheets);
1735 return (sheetId,
null,
null,
null,
null);
1738 string startCol = match.Groups[1].Value;
1739 string startRowStr = match.Groups[2].Value;
1740 string endCol = match.Groups[3].Value;
1741 string endRowStr = match.Groups[4].Value;
1743 if (
string.IsNullOrEmpty(startRowStr))
1745 startRowIndex =
null;
1749 startRowIndex =
int.Parse(startRowStr) - 1;
1753 if (
string.IsNullOrEmpty(endRowStr))
1759 endRowIndex =
int.Parse(endRowStr);
1762 int? startColumnIndex;
1763 if (
string.IsNullOrEmpty(startCol))
1765 startColumnIndex =
null;
1769 startColumnIndex = ColumnToIndex(startCol);
1772 int? endColumnIndex;
1773 if (
string.IsNullOrEmpty(endCol))
1775 endColumnIndex =
null;
1779 endColumnIndex = ColumnToIndex(endCol) + 1;
1782 return (sheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex);
1785 private int ColumnToIndex(
string column)
1788 foreach (
char c
in column.ToUpper())
1790 index = index * 26 + (c -
'A' + 1);
1795 public IAllowTriggerSettings WithTriggerFunction(
string functionName)
1797 if (
string.IsNullOrEmpty(functionName))
1799 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Trigger function name is empty",
this, GPALObjectType.GoogleSheets);
1801 FunctionName = functionName;
1805 public IAllowTriggerSettings WithTriggerSchedule(TriggerScheduleType schedule)
1807 TriggerSchedule = schedule;
1811 public IAllowDataOperations CreateTrigger(GoogleTriggerType triggerType)
1813 if (
string.IsNullOrEmpty(AppsScriptProjectId))
1815 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
1819 if (
string.IsNullOrEmpty(FunctionName))
1821 if (
string.IsNullOrEmpty(ScriptName))
1823 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Trigger function name not set",
this, GPALObjectType.GoogleSheets);
1826 FunctionName = ScriptName;
1827 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Using script name [{ScriptName}] as trigger function name",
this, GPALObjectType.GoogleSheets);
1830 var triggerData =
new Dictionary<string, object> { {
"functionName", FunctionName } };
1831 if (triggerType == GoogleTriggerType.TimeBased && TriggerSchedule != TriggerScheduleType.None)
1833 triggerData[
"timeBased"] =
new { type = TriggerSchedule.ToString().ToUpper() };
1836 var response = ScriptsRESTClient
1837 .WithEndpoint(
string.Format(googleSheetsConfig.TriggerEndpoint, AppsScriptProjectId))
1838 .WithParameters(triggerData)
1839 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1842 if (response ==
null)
1844 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create trigger",
this, GPALObjectType.GoogleSheets);
1850 public string SetupCloudProject(
string projectName)
1852 if (
string.IsNullOrEmpty(projectName))
1854 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Cloud project name cannot be empty",
this, GPALObjectType.GoogleSheets);
1858 if (Credentials ==
null)
1860 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
1864 _googleCloud =
new GoogleCloud(Credentials);
1865 string projectId = _googleCloud.CreateProject(projectName);
1866 if (
string.IsNullOrEmpty(projectId))
1868 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create or retrieve cloud project",
this, GPALObjectType.GoogleSheets);
1872 bool sheetsEnabled = _googleCloud.EnableApi(projectId, GoogleApi.Sheets);
1873 bool scriptsEnabled = _googleCloud.EnableApi(projectId, GoogleApi.Scripts);
1875 if (!sheetsEnabled || !scriptsEnabled)
1877 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to enable Sheets or Scripts API",
this, GPALObjectType.GoogleSheets);
1884 public string GetOrCreateScriptProject(
string cloudProjectId,
string scriptTitle)
1886 if (
string.IsNullOrEmpty(cloudProjectId) ||
string.IsNullOrEmpty(scriptTitle))
1888 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Cloud project ID or script title cannot be empty",
this, GPALObjectType.GoogleSheets);
1892 if (Credentials ==
null)
1894 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
1898 var listResponse = ScriptsRESTClient
1899 .WithEndpoint(
"/v1/projects")
1900 .WithParameters(
new { pageSize = 50 })
1901 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1904 if (listResponse !=
null)
1908 var listData = JsonSerializer.Deserialize<Dictionary<string, object>>(listResponse);
1909 object projectsObj =
null;
1910 if (listData.TryGetValue(
"projects", out projectsObj))
1912 var projects = JsonSerializer.Deserialize<JsonElement>(projectsObj.ToString());
1913 foreach (var project
in projects.EnumerateArray())
1915 if (project.TryGetProperty(
"title", out var title) && title.GetString().Equals(scriptTitle) &&
1916 project.TryGetProperty(
"scriptId", out var projectId))
1918 return projectId.GetString();
1926 var scriptPayload =
new { title = scriptTitle, parentId = cloudProjectId };
1927 var createResponse = ScriptsRESTClient
1928 .WithEndpoint(
"/v1/projects")
1929 .WithParameters(scriptPayload)
1930 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1933 if (createResponse ==
null)
1935 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create Apps Script project",
this, GPALObjectType.GoogleSheets);
1941 var scriptData = JsonSerializer.Deserialize<Dictionary<string, object>>(createResponse);
1942 object idObj =
null;
1943 if (scriptData.TryGetValue(
"scriptId", out idObj))
1945 return idObj.ToString();
1950 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No Apps Script project ID returned",
this, GPALObjectType.GoogleSheets);
1954 public IAllowScriptOperations WithProjectId(
string projectId)
1956 if (
string.IsNullOrEmpty(projectId))
1958 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Apps Script project ID is empty",
this, GPALObjectType.GoogleSheets);
1960 AppsScriptProjectId = projectId;
1964 public IAllowScriptOperations WithScriptName(
string scriptName)
1966 if (
string.IsNullOrEmpty(scriptName))
1968 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Script name is empty",
this, GPALObjectType.GoogleSheets);
1970 ScriptName = scriptName;
1974 public IAllowScriptOperations WithScriptAccess(ScriptAccessType access)
1976 ScriptAccess = access;
1980 public IAllowScriptOperations WithExecuteAs(ExecuteAsType executeAs)
1982 ExecuteAs = executeAs;
1986 public IAllowScriptOperations WithDeploymentDescription(
string description)
1988 if (
string.IsNullOrEmpty(description))
1990 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Deployment description is empty; using default",
this, GPALObjectType.GoogleSheets);
1991 DeploymentDescription =
"Web app deployment for script";
1995 DeploymentDescription = description;
2016 public IAllowDeployWebApp UploadScript(
string scriptContent)
2018 if (
string.IsNullOrEmpty(scriptContent))
2020 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Script content cannot be empty",
this, GPALObjectType.GoogleSheets);
2024 if (
string.IsNullOrEmpty(AppsScriptProjectId))
2026 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
2030 if (Credentials ==
null)
2032 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
2036 if (
string.IsNullOrEmpty(ScriptName))
2038 ScriptName =
"main";
2039 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Using default script name 'main'",
this, GPALObjectType.GoogleSheets);
2042 var response = ScriptsRESTClient
2043 .WithEndpoint(
string.Format(googleSheetsConfig.ScriptContentEndpoint, AppsScriptProjectId))
2046 files =
new[] {
new { name = ScriptName, type = googleSheetsConfig.ScriptFileType, source = scriptContent } }
2048 .WithHttpMethod(
"PUT")
2049 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2052 if (response ==
null)
2054 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to upload script",
this, GPALObjectType.GoogleSheets);
2058 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Script uploaded to project [{AppsScriptProjectId}]",
this, GPALObjectType.GoogleSheets);
2064 public IAllowDeployWebApp UploadScript(GPALFile scriptFile)
2066 if (scriptFile ==
null)
2068 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Script file is null",
this, GPALObjectType.GoogleSheets);
2072 string scriptContent =
"";
2075 scriptContent = File.ReadAllText(scriptFile.
Filename);
2077 catch (Exception ex)
2079 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to read script file",
this, GPALObjectType.GoogleSheets, ex);
2083 return UploadScript(scriptContent);
2086 public IAllowDataOperations DeployScriptAsWebApp(out
string webAppUrl)
2089 if (
string.IsNullOrEmpty(AppsScriptProjectId))
2091 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
2095 if (Credentials ==
null)
2097 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
2101 if (ExecuteAs == ExecuteAsType.USER_ACCESSING && ScriptAccess != ScriptAccessType.ANYONE && ScriptAccess != ScriptAccessType.DOMAIN)
2103 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"ExecuteAs 'USER_ACCESSING' requires access 'ANYONE' or 'DOMAIN'; using 'USER_DEPLOYING'",
this, GPALObjectType.GoogleSheets);
2104 ExecuteAs = ExecuteAsType.USER_DEPLOYING;
2108 var versionPayload =
new { description =
"Version for deployment" };
2109 var versionResponse = ScriptsRESTClient
2110 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/versions")
2111 .WithParameters(versionPayload)
2112 .WithHttpMethod(
"POST")
2113 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2116 if (versionResponse ==
null)
2118 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create script version",
this, GPALObjectType.GoogleSheets);
2124 var versionData = JsonSerializer.Deserialize<Dictionary<string, object>>(versionResponse);
2125 object versionObj =
null;
2126 if (versionData.TryGetValue(
"versionNumber", out versionObj))
2128 VersionNumber = Convert.ToInt32(versionObj);
2132 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No version number returned",
this, GPALObjectType.GoogleSheets);
2138 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse version response",
this, GPALObjectType.GoogleSheets);
2142 string accessString = ScriptAccess
switch
2144 ScriptAccessType.ME =>
"ME",
2145 ScriptAccessType.ANYONE =>
"ANYONE",
2146 ScriptAccessType.ANYONE_ANONYMOUS =>
"ANYONE_ANONYMOUS",
2147 ScriptAccessType.DOMAIN =>
"DOMAIN",
2151 string executeAsString = ExecuteAs
switch
2153 ExecuteAsType.USER_ACCESSING =>
"USER_ACCESSING",
2154 ExecuteAsType.USER_DEPLOYING =>
"USER_DEPLOYING",
2155 _ =>
"USER_DEPLOYING"
2158 var deploymentPayload =
new
2160 versionNumber = VersionNumber,
2161 manifestFileName =
"appsscript",
2162 description = DeploymentDescription
2165 var deployResponse = ScriptsRESTClient
2166 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/deployments")
2167 .WithParameters(deploymentPayload)
2168 .WithHttpMethod(
"POST")
2169 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2172 if (deployResponse ==
null)
2174 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to deploy script",
this, GPALObjectType.GoogleSheets);
2180 var deployData = JsonSerializer.Deserialize<Dictionary<string, object>>(deployResponse);
2181 object deploymentIdObj =
null;
2182 if (!deployData.TryGetValue(
"deploymentId", out deploymentIdObj))
2184 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No deployment ID returned",
this, GPALObjectType.GoogleSheets);
2188 string deploymentId = deploymentIdObj.ToString();
2189 var updatePayload =
new
2191 deploymentConfig =
new
2193 scriptId = AppsScriptProjectId,
2194 versionNumber = VersionNumber,
2195 manifestFileName =
"appsscript",
2196 description = DeploymentDescription,
2197 webApp =
new { executeAs = executeAsString, whoHasAccess = accessString }
2201 var updateResponse = ScriptsRESTClient
2202 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/deployments/{deploymentId}")
2203 .WithParameters(updatePayload)
2204 .WithHttpMethod(
"PUT")
2205 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2208 if (updateResponse ==
null)
2210 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to update deployment",
this, GPALObjectType.GoogleSheets);
2214 var updateData = JsonSerializer.Deserialize<Dictionary<string, object>>(updateResponse);
2215 object entryPointsObj =
null;
2216 if (updateData.TryGetValue(
"entryPoints", out entryPointsObj))
2218 var entryPoints = JsonSerializer.Deserialize<JsonElement>(entryPointsObj.ToString());
2219 foreach (var entry
in entryPoints.EnumerateArray())
2221 if (entry.TryGetProperty(
"webApp", out var webApp) && webApp.TryGetProperty(
"url", out var url))
2223 webAppUrl = url.GetString();
2224 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Web app deployed at: [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
2230 catch (Exception ex)
2232 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Deployment error",
this, GPALObjectType.GoogleSheets, ex);
2235 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve web app URL",
this, GPALObjectType.GoogleSheets);
2239 public IAllowSheetSelection SaveTo(GPALFile gPALFile)
2241 if (gPALFile ==
null)
2243 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"GPAL file is null",
this, GPALObjectType.GoogleSheets);
2247 if (ReadData ==
null)
2249 if (!
string.IsNullOrEmpty(ReadRange))
2251 if (!ReadDataFromRange())
2258 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No data to save; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
2263 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Saving [{ReadData.Rows}] rows of sheet [{SheetName}] to [{gPALFile.Filename}].",
this, GPALObjectType.GoogleSheets);
2267 using (var writer =
new StreamWriter(gPALFile.
Filename))
2269 foreach (var row
in ReadData)
2271 writer.WriteLine(
string.Join(
",", row));
2275 catch (Exception ex)
2277 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to save to file",
this, GPALObjectType.GoogleSheets, ex);
2283 public IGoogleSheets ToGPALObject()