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; }
63 internal string NumberFormatPattern {
get;
set; }
64 internal object FormatValue {
get;
private set; }
65 internal int? InsertPosition {
get;
private set; }
66 internal string InsertSeparator {
get;
private set; }
67 internal TriggerScheduleType TriggerSchedule {
get;
private set; }
68 internal string AppsScriptProjectId {
get;
private set; }
69 internal string ScriptName {
get;
private set; }
70 internal string FunctionName {
get;
private set; }
71 internal ScriptAccessType ScriptAccess {
get;
private set; } = ScriptAccessType.ME;
72 internal ExecuteAsType ExecuteAs {
get;
private set; } = ExecuteAsType.USER_DEPLOYING;
73 internal string DeploymentDescription {
get;
private set; } =
"Web app deployment for script";
74 internal int VersionNumber {
get;
private set; } = 1;
76 internal IRESTClient SheetsRESTClient {
get;
private set; }
77 internal IRESTClient ScriptsRESTClient {
get;
private set; }
79 internal GoogleCloud _googleCloud {
get;
private set; }
81 internal GoogleSheets()
84 SheetsRESTClient =
GPAL.
RESTClient.WithAPIBase(googleSheetsConfig.SheetsApiBase).ToGPALObject();
85 ScriptsRESTClient =
GPAL.
RESTClient.WithAPIBase(googleSheetsConfig.ScriptsApiBase).ToGPALObject();
89 private void InitializeMode()
91 bool hasAccessToken = !
string.IsNullOrEmpty(((Credentials)Credentials)?.AccessToken);
93 if (Credentials !=
null)
95 if (
false == hasAccessToken)
97 Credentials.FetchAccessToken(out
string accessToken);
100 if (
false ==
string.IsNullOrEmpty(((Credentials)Credentials)?.AccessToken))
102 var testResponse =
GPAL.
RESTClient.WithAPIBase(
"https://oauth2.googleapis.com")
103 .WithEndpoint(
"/tokeninfo")
104 .WithParameters(
new { access_token = ((Credentials)Credentials)?.AccessToken })
106 if (testResponse !=
null && !testResponse.Contains(
"error"))
108 googleSheetsConfig.UseAppsScript =
false;
113 googleSheetsConfig.UseAppsScript =
true;
116 private string ResolveSheetName(
object sheetNameOrId)
118 if (sheetNameOrId ==
null)
120 return SheetName ??
"Sheet1";
123 if (sheetNameOrId is
string name)
125 return string.IsNullOrEmpty(name) ? SheetName ??
"Sheet1" : name;
127 else if (sheetNameOrId is
int id)
129 var spreadsheetResponseJson = SheetsRESTClient
130 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
131 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
134 if (spreadsheetResponseJson !=
null)
138 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
139 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.SheetId ==
id);
140 if (targetSheet !=
null)
142 return targetSheet.Properties.Title;
148 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Sheet with ID [{id}] not found",
this, GPALObjectType.GoogleSheets);
153 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
158 private object ResolveSheetIdOrName(
object sheetNameOrId)
160 if (sheetNameOrId ==
null)
162 return SheetName ??
"Sheet1";
165 if (sheetNameOrId is
string name)
167 return string.IsNullOrEmpty(name) ? SheetName ??
"Sheet1" : name;
169 else if (sheetNameOrId is
int id)
175 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
181 private bool ReadDataFromRange()
183 if (
string.IsNullOrEmpty(SpreadsheetId))
185 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
189 if (
string.IsNullOrEmpty(SheetName))
191 SheetName =
"Sheet1";
192 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
195 if (
string.IsNullOrEmpty(ReadRange))
197 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Range not set for reading data",
this, GPALObjectType.GoogleSheets);
201 if (googleSheetsConfig.UseAppsScript)
203 var response = CallWebApp(
"ReadRange",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, range = ReadRange });
204 object successObj =
null;
205 object dataObj =
null;
206 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"data", out dataObj))
210 var dataElement = JsonSerializer.Deserialize<JsonElement>(dataObj.ToString());
211 ReadData = dataElement.EnumerateArray()
212 .Select(row => row.EnumerateArray().Select(cell => cell.GetString()).ToList())
213 .ToList() as IGPALGrid<string>;
218 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse Apps Script read data",
this, GPALObjectType.GoogleSheets, ex);
224 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to read range via Apps Script",
this, GPALObjectType.GoogleSheets);
230 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, ReadRange);
231 var responseJson = SheetsRESTClient
232 .WithEndpoint(endpoint)
233 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
236 if (responseJson !=
null)
240 var valueRange = JsonSerializer.Deserialize<ValueRange>(responseJson);
241 ReadData = valueRange.values?.Select(row => row.Select(cell => cell?.ToString()).ToList()).ToList() as IGPALGrid<string>;
246 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse API read data",
this, GPALObjectType.GoogleSheets, ex);
252 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to read range via API",
this, GPALObjectType.GoogleSheets);
258 private dynamic CallWebApp(
string action,
object parameters)
260 if (
string.IsNullOrEmpty(googleSheetsConfig.WebAppUrl))
266 var payload =
new { action, parameters };
267 var responseJson = SheetsRESTClient
268 .WithEndpoint(googleSheetsConfig.WebAppUrl)
269 .WithParameters(payload)
270 .WithHttpMethod(
"POST")
271 .WithHeader(
"Content-Type",
"application/json")
274 if (responseJson ==
null)
276 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to call web app for action: [{action}]",
this, GPALObjectType.GoogleSheets);
282 return JsonSerializer.Deserialize<Dictionary<string, object>>(responseJson);
286 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to parse web app response",
this, GPALObjectType.GoogleSheets, ex);
293 if (
string.IsNullOrEmpty(SpreadsheetId))
295 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
300 if (ReadData ==
null)
302 if (!
string.IsNullOrEmpty(ReadRange))
304 if (!ReadDataFromRange())
312 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"No data to save; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
318 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Saving [{ReadData.Rows}] rows of sheet [{SheetName}] to grid.",
this, GPALObjectType.GoogleSheets);
327 if (
string.IsNullOrEmpty(cell))
329 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
333 if (
string.IsNullOrEmpty(SpreadsheetId))
335 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
339 if (
string.IsNullOrEmpty(SheetName))
341 SheetName =
"Sheet1";
342 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
345 if (ReadData ==
null)
347 if (!
string.IsNullOrEmpty(ReadRange))
349 if (!ReadDataFromRange())
356 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to append; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
362 if (googleSheetsConfig.UseAppsScript)
364 var response = CallWebApp(
"AppendToCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator });
365 object successObj =
null;
366 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
368 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to cell via Apps Script",
this, GPALObjectType.GoogleSheets);
373 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
374 var readResponseJson = SheetsRESTClient
375 .WithEndpoint(endpoint)
376 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
379 string currentContent =
"";
380 if (readResponseJson !=
null)
384 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
385 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
390 string appendData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
391 string newContent = currentContent + appendData;
393 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
394 var writeResponse = SheetsRESTClient
395 .WithEndpoint(writeEndpoint)
396 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
397 .WithHttpMethod(
"PUT")
398 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
401 if (writeResponse ==
null)
403 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to cell",
this, GPALObjectType.GoogleSheets);
412 if (
string.IsNullOrEmpty(cell))
414 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
418 if (
string.IsNullOrEmpty(SpreadsheetId))
420 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
424 if (
string.IsNullOrEmpty(SheetName))
426 SheetName =
"Sheet1";
427 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
430 if (ReadData ==
null)
432 if (!
string.IsNullOrEmpty(ReadRange))
434 if (!ReadDataFromRange())
441 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to prepend; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
446 if (googleSheetsConfig.UseAppsScript)
448 var response = CallWebApp(
"PrependToCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator });
449 object successObj =
null;
450 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
452 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to prepend to cell via Apps Script",
this, GPALObjectType.GoogleSheets);
457 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
458 var readResponseJson = SheetsRESTClient
459 .WithEndpoint(endpoint)
460 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
463 string currentContent =
"";
464 if (readResponseJson !=
null)
468 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
469 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
474 string prependData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
475 string newContent = prependData + currentContent;
477 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
478 var writeResponse = SheetsRESTClient
479 .WithEndpoint(writeEndpoint)
480 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
481 .WithHttpMethod(
"PUT")
482 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
485 if (writeResponse ==
null)
487 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to prepend to cell",
this, GPALObjectType.GoogleSheets);
496 if (
string.IsNullOrEmpty(cell))
498 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Cell reference cannot be empty",
this, GPALObjectType.GoogleSheets);
502 if (
string.IsNullOrEmpty(SpreadsheetId))
504 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
508 if (
string.IsNullOrEmpty(SheetName))
510 SheetName =
"Sheet1";
511 GPAL.
PublishSimpleEvent(GPALEventType.INFO,
"Using default sheet name 'Sheet1'",
this, GPALObjectType.GoogleSheets);
514 if (ReadData ==
null)
516 if (!
string.IsNullOrEmpty(ReadRange))
518 if (!ReadDataFromRange())
525 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to insert; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
530 if (googleSheetsConfig.UseAppsScript)
532 var response = CallWebApp(
"InsertAtCell",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, cell, data = ReadData, separator = InsertSeparator, position = InsertPosition });
533 object successObj =
null;
534 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
536 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to insert at cell via Apps Script",
this, GPALObjectType.GoogleSheets);
541 string endpoint =
string.Format(googleSheetsConfig.ReadRangeEndpoint, SpreadsheetId, SheetName, cell);
542 var readResponseJson = SheetsRESTClient
543 .WithEndpoint(endpoint)
544 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
547 string currentContent =
"";
548 if (readResponseJson !=
null)
552 var valueRange = JsonSerializer.Deserialize<ValueRange>(readResponseJson);
553 currentContent = valueRange.values?.FirstOrDefault()?.FirstOrDefault()?.ToString() ??
"";
558 string insertData =
string.Join(InsertSeparator ??
"", ReadData?.SelectMany(row => row) ?? Enumerable.Empty<
string>());
559 int pos = Math.Min(InsertPosition ?? 0, currentContent.Length);
560 string newContent = currentContent.Substring(0, pos) + insertData + currentContent.Substring(pos);
562 var writeEndpoint = endpoint + googleSheetsConfig.ValueInputOption;
563 var writeResponse = SheetsRESTClient
564 .WithEndpoint(writeEndpoint)
565 .WithParameters(
new { values =
new List<List<string>> {
new List<string> { newContent } } })
566 .WithHttpMethod(
"PUT")
567 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
570 if (writeResponse ==
null)
572 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to insert at cell",
this, GPALObjectType.GoogleSheets);
581 if (
string.IsNullOrEmpty(writeRange))
587 WriteRange = writeRange;
594 if (
string.IsNullOrEmpty(SpreadsheetId))
596 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
600 string targetSheet = ResolveSheetName(sheetNameOrId);
601 if (
string.IsNullOrEmpty(targetSheet))
606 if (ReadData ==
null)
608 if (!
string.IsNullOrEmpty(ReadRange))
610 if (!ReadDataFromRange())
617 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"No data to write; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
622 if (googleSheetsConfig.UseAppsScript)
624 var response = CallWebApp(
"WriteRange",
new { spreadsheetId = SpreadsheetId, sheetName = targetSheet, range = WriteRange, data = ReadData });
625 object successObj =
null;
626 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
628 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to write range via Apps Script",
this, GPALObjectType.GoogleSheets);
633 string endpoint =
string.Format(googleSheetsConfig.WriteRangeEndpoint, SpreadsheetId, targetSheet, WriteRange) + googleSheetsConfig.ValueInputOption;
634 var response = SheetsRESTClient
635 .WithEndpoint(endpoint)
636 .WithParameters(
new { values = ReadData })
637 .WithHttpMethod(
"PUT")
638 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
641 if (response ==
null)
652 object targetSheetOrId = ResolveSheetIdOrName(sheetNameOrId);
653 if (targetSheetOrId ==
null)
658 if (
string.IsNullOrEmpty(SpreadsheetId))
660 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID not set",
this, GPALObjectType.GoogleSheets);
664 if (ReadData ==
null)
666 if (!
string.IsNullOrEmpty(ReadRange))
668 if (!ReadDataFromRange())
681 if (googleSheetsConfig.UseAppsScript)
683 var response = CallWebApp(
"AppendToSheet",
new { spreadsheetId = SpreadsheetId, sheetNameOrId = targetSheetOrId, data = ReadData });
684 object successObj =
null;
685 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
687 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to sheet via Apps Script",
this, GPALObjectType.GoogleSheets);
692 string targetSheetName = ResolveSheetName(sheetNameOrId);
693 if (
string.IsNullOrEmpty(targetSheetName))
697 string endpoint =
string.Format(googleSheetsConfig.AppendRangeEndpoint, SpreadsheetId, targetSheetName,
"A1") +
"?valueInputOption=RAW&insertDataOption=INSERT_ROWS";
698 var response = SheetsRESTClient
699 .WithEndpoint(endpoint)
700 .WithParameters(
new { values = ReadData })
701 .WithHttpMethod(
"POST")
702 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
705 if (response ==
null)
707 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to append to sheet",
this, GPALObjectType.GoogleSheets);
724 private const string GatewayScriptResource =
"GenerallyPositive.Browser.gcode.js";
726 private string ReadGatewayScript()
728 string retVal =
null;
730 using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GatewayScriptResource))
734 string msg = $
"[{GatewayScriptResource}] is not in this assembly, so there is no gateway script to deploy";
741 using (StreamReader reader =
new StreamReader(stream))
742 retVal = reader.ReadToEnd();
748 private void SetupAndDeployScript(
string spreadsheetIdOrTitle)
750 string cloudProjectId = SetupCloudProject($
"GPAL_Sheets_{Guid.NewGuid().ToString().Substring(0, 8)}");
751 if (
string.IsNullOrEmpty(cloudProjectId))
756 string scriptTitle = $
"GPAL_Script_{spreadsheetIdOrTitle}";
757 string scriptId = GetOrCreateScriptProject(cloudProjectId, scriptTitle);
758 if (
string.IsNullOrEmpty(scriptId))
764 var deploymentResponse = ScriptsRESTClient
765 .WithEndpoint($
"/v1/projects/{scriptId}/deployments")
766 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
769 if (deploymentResponse !=
null)
773 var deploymentData = JsonSerializer.Deserialize<Dictionary<string, object>>(deploymentResponse);
774 object deploymentsObj =
null;
775 if (deploymentData.TryGetValue(
"deployments", out deploymentsObj))
777 var deployments = JsonSerializer.Deserialize<JsonElement>(deploymentsObj.ToString());
778 foreach (var deployment
in deployments.EnumerateArray())
780 if (deployment.TryGetProperty(
"entryPoints", out var entryPoints))
782 foreach (var entry
in entryPoints.EnumerateArray())
784 if (entry.TryGetProperty(
"webApp", out var webApp) && webApp.TryGetProperty(
"url", out var url))
786 googleSheetsConfig.WebAppUrl = url.GetString();
787 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Found existing web app at: [{googleSheetsConfig.WebAppUrl}]",
this, GPALObjectType.GoogleSheets);
788 AppsScriptProjectId = scriptId;
800 string scriptContent = ReadGatewayScript();
802 this.WithProjectId(scriptId)
803 .WithScriptName(
"main")
804 .WithScriptAccess(ScriptAccessType.ME)
805 .WithExecuteAs(ExecuteAsType.USER_DEPLOYING)
806 .UploadScript(scriptContent)
807 .DeployScriptAsWebApp(out
string webAppUrl);
809 if (!
string.IsNullOrEmpty(webAppUrl))
811 googleSheetsConfig.WebAppUrl = webAppUrl;
812 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Apps Script deployed at: [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
828 if (
true ==
string.IsNullOrWhiteSpace(webAppUrl))
830 string msg =
"No web app url was supplied, so there is no gateway to drive sheets through";
837 googleSheetsConfig.WebAppUrl = webAppUrl;
841 googleSheetsConfig.UseAppsScript =
true;
843 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Sheets run through the web app at [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
858 if (
string.IsNullOrEmpty(spreadsheetIdOrTitle))
860 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Spreadsheet ID or title cannot be empty",
this, GPALObjectType.GoogleSheets);
866 if (googleSheetsConfig.UseAppsScript)
868 if (
string.IsNullOrEmpty(googleSheetsConfig.WebAppUrl))
870 SetupAndDeployScript(spreadsheetIdOrTitle);
873 var response = CallWebApp(
"WithSpreadsheet",
new { spreadsheetIdOrTitle });
874 object successObj =
null;
876 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"spreadsheetId", out idObj))
878 SpreadsheetId = idObj.ToString();
882 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to open or create spreadsheet [{spreadsheetIdOrTitle}] via Apps Script",
this, GPALObjectType.GoogleSheets);
887 var responseJson = SheetsRESTClient
888 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{spreadsheetIdOrTitle}")
889 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
892 if (!
string.IsNullOrEmpty(responseJson))
894 SpreadsheetId = spreadsheetIdOrTitle;
898 var createRequest =
new { properties =
new { title = spreadsheetIdOrTitle } };
899 var createResponseJson = SheetsRESTClient
900 .WithEndpoint(
"/v4/spreadsheets")
901 .WithParameters(createRequest)
902 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
903 .WithHttpMethod(
"POST")
906 if (createResponseJson !=
null)
908 var createResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(createResponseJson);
909 if (!
string.IsNullOrEmpty(createResponse?.SpreadsheetId))
911 SpreadsheetId = createResponse.SpreadsheetId;
915 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create spreadsheet [{spreadsheetIdOrTitle}]",
this, GPALObjectType.GoogleSheets);
920 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create spreadsheet [{spreadsheetIdOrTitle}]",
this, GPALObjectType.GoogleSheets);
928 public IAllowSheetOperations CreateSheet(
string sheetName)
930 if (
string.IsNullOrEmpty(sheetName))
932 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
936 if (googleSheetsConfig.UseAppsScript)
938 var response = CallWebApp(
"CreateSheet",
new { spreadsheetId = SpreadsheetId, sheetName });
939 object successObj =
null;
940 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj)
942 SheetName = sheetName;
946 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create sheet via Apps Script",
this, GPALObjectType.GoogleSheets);
953 requests =
new[] {
new { addSheet =
new { properties =
new { title = sheetName } } } }
955 var response = SheetsRESTClient
956 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
957 .WithParameters(request)
958 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
961 if (response ==
null)
963 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create sheet",
this, GPALObjectType.GoogleSheets);
966 SheetName = sheetName;
972 public IAllowSheetSelection SetSheetName(
string newSheetName)
974 if (
string.IsNullOrEmpty(newSheetName))
976 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Title cannot be empty",
this, GPALObjectType.GoogleSheets);
980 if (googleSheetsConfig.UseAppsScript)
982 var response = CallWebApp(
"WithTitle",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, title = newSheetName });
983 object successObj =
null;
984 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
986 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title via Apps Script",
this, GPALObjectType.GoogleSheets);
993 requests =
new[] {
new { updateSheetProperties =
new { properties =
new { title = newSheetName }, fields = googleSheetsConfig.TitleField } } }
995 var response = SheetsRESTClient
996 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
997 .WithParameters(request)
998 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1001 if (response ==
null)
1003 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title",
this, GPALObjectType.GoogleSheets);
1009 public IAllowSheetSelection SetSpreadsheetTitle(
string title)
1011 if (
string.IsNullOrEmpty(title))
1013 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Title cannot be empty",
this, GPALObjectType.GoogleSheets);
1017 if (googleSheetsConfig.UseAppsScript)
1019 var response = CallWebApp(
"WithTitle",
new { spreadsheetId = SpreadsheetId, sheetName = SheetName, title });
1020 object successObj =
null;
1021 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1023 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title via Apps Script",
this, GPALObjectType.GoogleSheets);
1030 requests =
new[] {
new { updateSpreadsheetProperties =
new { properties =
new { title }, fields = googleSheetsConfig.TitleField } } }
1032 var response = SheetsRESTClient
1033 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1034 .WithParameters(request)
1035 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1038 if (response ==
null)
1040 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to set sheet title",
this, GPALObjectType.GoogleSheets);
1047 public IAllowInDataOperations WithSheet(
object sheetNameOrId)
1049 if (sheetNameOrId ==
null)
1051 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name or ID cannot be null",
this, GPALObjectType.GoogleSheets);
1055 string sheetName =
null;
1056 int? sheetId =
null;
1058 if (sheetNameOrId is
string name)
1060 if (
string.IsNullOrEmpty(name))
1062 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
1067 else if (sheetNameOrId is
int id)
1073 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
1077 if (googleSheetsConfig.UseAppsScript)
1079 var action = sheetId.HasValue ?
"WithSheetById" :
"WithSheet";
1080 var response = CallWebApp(action,
new { spreadsheetId = SpreadsheetId, sheetName, sheetId });
1081 object successObj =
null;
1082 object nameObj =
null;
1083 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj)
1085 if (response.TryGetValue(
"sheetName", out nameObj))
1087 SheetName = nameObj.ToString();
1091 SheetName = sheetName;
1096 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1097 $
"Failed to activate sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")} via Apps Script",
1098 this, GPALObjectType.GoogleSheets);
1103 var spreadsheetResponseJson = SheetsRESTClient
1104 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1105 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1108 SpreadsheetResponse spreadsheetResponse =
null;
1109 if (spreadsheetResponseJson !=
null)
1113 spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1118 if (spreadsheetResponse ==
null || spreadsheetResponse.Sheets ==
null)
1120 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve spreadsheet info",
this, GPALObjectType.GoogleSheets);
1124 var targetSheet = sheetId.HasValue
1125 ? spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.SheetId == sheetId.Value)
1126 : spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1128 if (targetSheet ==
null)
1130 if (sheetId.HasValue)
1132 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Sheet with ID [{sheetId}] not found",
this, GPALObjectType.GoogleSheets);
1137 CreateSheet(sheetName);
1138 targetSheet = spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1139 if (targetSheet ==
null)
1141 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to create sheet [{sheetName}]",
this, GPALObjectType.GoogleSheets);
1147 int targetSheetId = targetSheet.Properties.SheetId;
1148 SheetName = targetSheet.Properties.Title;
1150 var activateRequest =
new
1156 updateSheetProperties =
new
1158 properties =
new { sheetId = targetSheetId, hidden =
false },
1159 fields = googleSheetsConfig.HiddenField
1165 var activateResponse = SheetsRESTClient
1166 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1167 .WithParameters(activateRequest)
1168 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1171 if (activateResponse ==
null)
1173 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to activate sheet",
this, GPALObjectType.GoogleSheets);
1180 public IAllowDataOperations WithReadRange(
string readRange)
1182 if (
string.IsNullOrEmpty(readRange))
1184 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Range cannot be empty",
this, GPALObjectType.GoogleSheets);
1188 ReadRange = readRange;
1192 public IAllowDataOperations WithData(IGPALGrid<string> data)
1198 public IAllowSheetSelection DeleteSheet(
object sheetNameOrId)
1200 if (sheetNameOrId ==
null)
1202 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name or ID cannot be null",
this, GPALObjectType.GoogleSheets);
1206 string sheetName =
null;
1207 int? sheetId =
null;
1209 if (sheetNameOrId is
string name)
1211 if (
string.IsNullOrEmpty(name))
1213 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Sheet name cannot be empty",
this, GPALObjectType.GoogleSheets);
1218 else if (sheetNameOrId is
int id)
1224 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Invalid sheet name or ID type",
this, GPALObjectType.GoogleSheets);
1228 if (googleSheetsConfig.UseAppsScript)
1230 var action = sheetId.HasValue ?
"DeleteSheetById" :
"DeleteSheet";
1231 var response = CallWebApp(action,
new { spreadsheetId = SpreadsheetId, sheetName, sheetId });
1232 object successObj =
null;
1233 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1235 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1236 $
"Failed to delete sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")} via Apps Script",
1237 this, GPALObjectType.GoogleSheets);
1242 var spreadsheetResponseJson = SheetsRESTClient
1243 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1244 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1247 SpreadsheetResponse spreadsheetResponse =
null;
1248 if (spreadsheetResponseJson !=
null)
1252 spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1257 if (spreadsheetResponse ==
null || spreadsheetResponse.Sheets ==
null)
1259 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve spreadsheet info",
this, GPALObjectType.GoogleSheets);
1263 var targetSheet = sheetId.HasValue
1264 ? spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.SheetId == sheetId.Value)
1265 : spreadsheetResponse.Sheets.FirstOrDefault(s => s.Properties.Title.Equals(sheetName, StringComparison.OrdinalIgnoreCase));
1267 if (targetSheet ==
null)
1269 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1270 $
"{(sheetId.HasValue ? $"Sheet with ID {sheetId}
" : $"Sheet
'{sheetName}'")} not found",
1271 this, GPALObjectType.GoogleSheets);
1277 requests =
new[] {
new { deleteSheet =
new { sheetId = targetSheet.Properties.SheetId } } }
1279 var response = SheetsRESTClient
1280 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1281 .WithParameters(request)
1282 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1285 if (response ==
null)
1287 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1288 $
"Failed to delete sheet {(sheetId.HasValue ? $"with ID {sheetId}
" : $"named {sheetName}
")}",
1289 this, GPALObjectType.GoogleSheets);
1296 public IAllowSheetSelection ListSheets(out IEnumerable<(
string name,
int sheetId)> sheets)
1298 if (googleSheetsConfig.UseAppsScript)
1300 var response = CallWebApp(
"ListSheets",
new { spreadsheetId = SpreadsheetId });
1301 object successObj =
null;
1302 object sheetsObj =
null;
1303 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"sheets", out sheetsObj))
1307 var sheetsElement = JsonSerializer.Deserialize<JsonElement>(sheetsObj.ToString());
1308 sheets = sheetsElement.EnumerateArray().Select(s => (s.GetProperty(
"name").GetString(), s.GetProperty(
"sheetId").GetInt32()));
1313 sheets = Enumerable.Empty<(string, int)>();
1314 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse sheets list via Apps Script",
this, GPALObjectType.GoogleSheets);
1320 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to list sheets via Apps Script",
this, GPALObjectType.GoogleSheets);
1321 sheets = Enumerable.Empty<(string, int)>();
1327 var responseJson = SheetsRESTClient
1328 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1329 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1332 if (responseJson ==
null)
1334 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to list sheets",
this, GPALObjectType.GoogleSheets);
1335 sheets = Enumerable.Empty<(string, int)>();
1341 var response = JsonSerializer.Deserialize<SpreadsheetResponse>(responseJson);
1342 if (response?.Sheets !=
null)
1344 sheets = response.Sheets.Select(s => (s.Properties.Title, s.Properties.SheetId));
1348 sheets = Enumerable.Empty<(string, int)>();
1353 sheets = Enumerable.Empty<(string, int)>();
1354 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse sheets list",
this, GPALObjectType.GoogleSheets);
1361 public IAllowInsertSettings WithInsertPosition(
int position)
1363 InsertPosition = position;
1367 public IAllowInsertSettings WithInsertSeparator(
string separator)
1369 InsertSeparator = separator;
1373 public IAllowDataOperations CalculateSum(out
string result)
1375 if (ReadData ==
null && !
string.IsNullOrEmpty(ReadRange))
1377 ReadDataFromRange();
1380 if (googleSheetsConfig.UseAppsScript)
1382 var response = CallWebApp(
"CalculateSum",
new { data = ReadData });
1383 object successObj =
null;
1384 object resultObj =
null;
1385 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"result", out resultObj))
1387 result = resultObj.ToString();
1391 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to calculate sum via Apps Script",
this, GPALObjectType.GoogleSheets);
1397 result = ReadData?.SelectMany(row => row)
1398 .Where(v =>
double.TryParse(v, out _))
1399 .Sum(v =>
double.Parse(v))
1406 public IAllowDataOperations CalculateCount(out
string result)
1408 if (ReadData ==
null && !
string.IsNullOrEmpty(ReadRange))
1410 ReadDataFromRange();
1413 if (googleSheetsConfig.UseAppsScript)
1415 var response = CallWebApp(
"CalculateCount",
new { data = ReadData });
1416 object successObj =
null;
1417 object resultObj =
null;
1418 if (response !=
null && response.TryGetValue(
"success", out successObj) && successObj is
bool && (
bool)successObj && response.TryGetValue(
"result", out resultObj))
1420 result = resultObj.ToString();
1424 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to calculate count via Apps Script",
this, GPALObjectType.GoogleSheets);
1430 result = ReadData?.SelectMany(row => row).Count().ToString() ??
"0";
1436 public IAllowFormatSettings WithFormatType(FormatType formatType)
1438 OurFormatType = formatType;
1442 public IAllowFormatRange WithFormatValue(
object formatValue)
1444 FormatValue = formatValue;
1448 public IAllowFormatRange WithAlignmentFormatValue(HorizontalAlignmentType alignment)
1450 FormatValue = alignment;
1454 public IAllowFormatRange WithAlignmentFormatValue(VerticalAlignmentType alignment)
1456 FormatValue = alignment;
1460 public IAllowFormatRange WithNumberFormatValue(NumberFormatType numberFormat)
1462 NumberFormatType = numberFormat;
1466 public IAllowFormatRange WithTextRotationFormatValue(TextRotationType rotation)
1468 FormatValue = rotation;
1472 public IAllowFormatRange WithColorFormatValue(System.Drawing.Color color)
1474 FormatValue = color;
1478 public IAllowInDataOperations FormatRange(
string range)
1480 if (
string.IsNullOrEmpty(range))
1482 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Range cannot be empty",
this, GPALObjectType.GoogleSheets);
1486 if (OurFormatType ==
null)
1488 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Format type not set",
this, GPALObjectType.GoogleSheets);
1492 if (googleSheetsConfig.UseAppsScript)
1494 var response = CallWebApp(
"FormatRange",
new
1496 spreadsheetId = SpreadsheetId,
1497 sheetName = SheetName,
1499 formatType = OurFormatType.ToString(),
1500 formatValue = FormatValue?.ToString(),
1501 numberFormatType = NumberFormatType?.ToString(),
1502 numberFormatPattern = NumberFormatPattern
1504 object successObj =
null;
1505 if (response ==
null || !response.TryGetValue(
"success", out successObj) || !(successObj is
bool) || !(
bool)successObj)
1507 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to format range via Apps Script",
this, GPALObjectType.GoogleSheets);
1512 var (targetSheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex) = ParseRange(range);
1513 var formatRequest =
new
1521 range =
new { sheetId = targetSheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex },
1522 cell = GetCellFormat(),
1523 fields = GetFieldsForFormatType(OurFormatType.Value)
1528 var response = SheetsRESTClient
1529 .WithEndpoint(
string.Format(googleSheetsConfig.FormatRangeEndpoint, SpreadsheetId))
1530 .WithParameters(formatRequest)
1531 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1534 if (response ==
null)
1536 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to format range",
this, GPALObjectType.GoogleSheets);
1543 private object GetCellFormat()
1545 if (FormatValue ==
null)
1547 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Format value not set for [{OurFormatType}]",
this, GPALObjectType.GoogleSheets);
1548 return new { userEnteredFormat =
new { } };
1551 switch (OurFormatType)
1553 case FormatType.Bold:
1554 if (FormatValue is
bool bold)
1555 return new { userEnteredFormat =
new { textFormat =
new { bold } } };
1557 case FormatType.Italic:
1558 if (FormatValue is
bool italic)
1559 return new { userEnteredFormat =
new { textFormat =
new { italic } } };
1561 case FormatType.Underline:
1562 if (FormatValue is
bool underline)
1563 return new { userEnteredFormat =
new { textFormat =
new { underline } } };
1565 case FormatType.Strikethrough:
1566 if (FormatValue is
bool strikethrough)
1567 return new { userEnteredFormat =
new { textFormat =
new { strikethrough } } };
1569 case FormatType.FontSize:
1570 if (FormatValue is
int fontSize)
1571 return new { userEnteredFormat =
new { textFormat =
new { fontSize } } };
1573 case FormatType.FontFamily:
1574 if (FormatValue is
string fontFamily)
1575 return new { userEnteredFormat =
new { textFormat =
new { fontFamily } } };
1577 case FormatType.ForegroundColor:
1578 if (FormatValue is System.Drawing.Color color)
1579 return new { userEnteredFormat =
new { textFormat =
new { foregroundColor =
new { red = color.R / 255.0, green = color.G / 255.0, blue = color.B / 255.0 } } } };
1581 case FormatType.CellBackgroundColor:
1582 if (FormatValue is System.Drawing.Color bgColor)
1583 return new { userEnteredFormat =
new { backgroundColor =
new { red = bgColor.R / 255.0, green = bgColor.G / 255.0, blue = bgColor.B / 255.0 } } };
1585 case FormatType.NumberFormat:
1586 if (NumberFormatType !=
null)
1589 userEnteredFormat =
new
1593 type = NumberFormatType.Value.ToString().ToUpper(),
1594 pattern =
string.IsNullOrEmpty(NumberFormatPattern) ? null : NumberFormatPattern
1599 case FormatType.HorizontalAlignment:
1600 if (FormatValue is HorizontalAlignmentType hAlign)
1601 return new { userEnteredFormat =
new { horizontalAlignment = hAlign.ToString().ToUpper() } };
1603 case FormatType.VerticalAlignment:
1604 if (FormatValue is VerticalAlignmentType vAlign)
1605 return new { userEnteredFormat =
new { verticalAlignment = vAlign.ToString().ToUpper() } };
1607 case FormatType.TextRotation:
1608 if (FormatValue is TextRotationType rotation)
1610 object textRotation = rotation
switch
1612 TextRotationType.TiltUp30 =>
new { degrees = 30 },
1613 TextRotationType.TiltUp45 =>
new { degrees = 45 },
1614 TextRotationType.TiltDown30 =>
new { degrees = -30 },
1615 TextRotationType.TiltDown45 =>
new { degrees = -45 },
1616 TextRotationType.UpsideDown =>
new { degrees = -180 },
1617 TextRotationType.Vertical =>
new { vertical =
true },
1618 _ =>
new { degrees = 0 }
1620 return new { userEnteredFormat =
new { textFormat =
new { textRotation } } };
1625 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Invalid format value for [{OurFormatType}]",
this, GPALObjectType.GoogleSheets);
1626 return new { userEnteredFormat =
new { } };
1629 private string GetFieldsForFormatType(FormatType formatType)
1634 case FormatType.Bold:
1635 case FormatType.Italic:
1636 case FormatType.Underline:
1637 case FormatType.Strikethrough:
1638 case FormatType.FontSize:
1639 case FormatType.FontFamily:
1640 case FormatType.ForegroundColor:
1641 subfield =
"textFormat";
1643 case FormatType.CellBackgroundColor:
1644 subfield =
"backgroundColor";
1646 case FormatType.NumberFormat:
1647 subfield =
"numberFormat";
1649 case FormatType.HorizontalAlignment:
1650 subfield =
"horizontalAlignment";
1652 case FormatType.VerticalAlignment:
1653 subfield =
"verticalAlignment";
1655 case FormatType.TextRotation:
1656 subfield =
"textFormat.textRotation";
1663 return string.IsNullOrEmpty(subfield) ? googleSheetsConfig.UserEnteredFormatField : $
"{googleSheetsConfig.UserEnteredFormatField}.{subfield}";
1666 private (
int sheetId,
int? startRowIndex,
int? endRowIndex,
int? startColumnIndex,
int? endColumnIndex) ParseRange(
string range)
1669 string sheetNameFromRange =
null;
1670 string rangePart = range;
1672 if (range.Contains(
"!"))
1674 var parts = range.Split(
'!');
1675 sheetNameFromRange = parts[0].Trim(
'\'');
1676 rangePart = parts[1];
1679 if (!
string.IsNullOrEmpty(sheetNameFromRange))
1681 var spreadsheetResponseJson = SheetsRESTClient
1682 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1683 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1686 if (spreadsheetResponseJson !=
null)
1690 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1691 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.Title.Equals(sheetNameFromRange, StringComparison.OrdinalIgnoreCase));
1692 if (targetSheet !=
null)
1694 sheetId = targetSheet.Properties.SheetId;
1700 else if (!
string.IsNullOrEmpty(SheetName))
1703 var spreadsheetResponseJson = SheetsRESTClient
1704 .WithEndpoint($
"{googleSheetsConfig.SpreadsheetEndPoint}{SpreadsheetId}")
1705 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1708 if (spreadsheetResponseJson !=
null)
1712 var spreadsheetResponse = JsonSerializer.Deserialize<SpreadsheetResponse>(spreadsheetResponseJson);
1713 var targetSheet = spreadsheetResponse?.Sheets?.FirstOrDefault(s => s.Properties.Title.Equals(SheetName, StringComparison.OrdinalIgnoreCase));
1714 if (targetSheet !=
null)
1716 sheetId = targetSheet.Properties.SheetId;
1724 var match = System.Text.RegularExpressions.Regex.Match(rangePart,
@"^([A-Z]+)?(\d+)?(?::([A-Z]+)?(\d+)?)?$");
1727 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Invalid range format: [{range}]",
this, GPALObjectType.GoogleSheets);
1728 return (sheetId,
null,
null,
null,
null);
1731 string startCol = match.Groups[1].Value;
1732 string startRowStr = match.Groups[2].Value;
1733 string endCol = match.Groups[3].Value;
1734 string endRowStr = match.Groups[4].Value;
1736 if (
string.IsNullOrEmpty(startRowStr))
1738 startRowIndex =
null;
1742 startRowIndex =
int.Parse(startRowStr) - 1;
1746 if (
string.IsNullOrEmpty(endRowStr))
1752 endRowIndex =
int.Parse(endRowStr);
1755 int? startColumnIndex;
1756 if (
string.IsNullOrEmpty(startCol))
1758 startColumnIndex =
null;
1762 startColumnIndex = ColumnToIndex(startCol);
1765 int? endColumnIndex;
1766 if (
string.IsNullOrEmpty(endCol))
1768 endColumnIndex =
null;
1772 endColumnIndex = ColumnToIndex(endCol) + 1;
1775 return (sheetId, startRowIndex, endRowIndex, startColumnIndex, endColumnIndex);
1778 private int ColumnToIndex(
string column)
1781 foreach (
char c
in column.ToUpper())
1783 index = index * 26 + (c -
'A' + 1);
1788 public IAllowTriggerSettings WithTriggerFunction(
string functionName)
1790 if (
string.IsNullOrEmpty(functionName))
1792 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Trigger function name is empty",
this, GPALObjectType.GoogleSheets);
1794 FunctionName = functionName;
1798 public IAllowTriggerSettings WithTriggerSchedule(TriggerScheduleType schedule)
1800 TriggerSchedule = schedule;
1804 public IAllowDataOperations CreateTrigger(GoogleTriggerType triggerType)
1806 if (
string.IsNullOrEmpty(AppsScriptProjectId))
1808 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
1812 if (
string.IsNullOrEmpty(FunctionName))
1814 if (
string.IsNullOrEmpty(ScriptName))
1816 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Trigger function name not set",
this, GPALObjectType.GoogleSheets);
1819 FunctionName = ScriptName;
1820 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"Using script name [{ScriptName}] as trigger function name",
this, GPALObjectType.GoogleSheets);
1823 var triggerData =
new Dictionary<string, object> { {
"functionName", FunctionName } };
1824 if (triggerType == GoogleTriggerType.TimeBased && TriggerSchedule != TriggerScheduleType.None)
1826 triggerData[
"timeBased"] =
new { type = TriggerSchedule.ToString().ToUpper() };
1829 var response = ScriptsRESTClient
1830 .WithEndpoint(
string.Format(googleSheetsConfig.TriggerEndpoint, AppsScriptProjectId))
1831 .WithParameters(triggerData)
1832 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1835 if (response ==
null)
1837 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create trigger",
this, GPALObjectType.GoogleSheets);
1843 public string SetupCloudProject(
string projectName)
1845 if (
string.IsNullOrEmpty(projectName))
1847 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Cloud project name cannot be empty",
this, GPALObjectType.GoogleSheets);
1851 if (Credentials ==
null)
1853 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
1857 _googleCloud =
new GoogleCloud(Credentials);
1858 string projectId = _googleCloud.CreateProject(projectName);
1859 if (
string.IsNullOrEmpty(projectId))
1861 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create or retrieve cloud project",
this, GPALObjectType.GoogleSheets);
1865 bool sheetsEnabled = _googleCloud.EnableApi(projectId, GoogleApi.Sheets);
1866 bool scriptsEnabled = _googleCloud.EnableApi(projectId, GoogleApi.Scripts);
1868 if (!sheetsEnabled || !scriptsEnabled)
1870 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to enable Sheets or Scripts API",
this, GPALObjectType.GoogleSheets);
1877 public string GetOrCreateScriptProject(
string cloudProjectId,
string scriptTitle)
1879 if (
string.IsNullOrEmpty(cloudProjectId) ||
string.IsNullOrEmpty(scriptTitle))
1881 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Cloud project ID or script title cannot be empty",
this, GPALObjectType.GoogleSheets);
1885 if (Credentials ==
null)
1887 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
1891 var listResponse = ScriptsRESTClient
1892 .WithEndpoint(
"/v1/projects")
1893 .WithParameters(
new { pageSize = 50 })
1894 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1897 if (listResponse !=
null)
1901 var listData = JsonSerializer.Deserialize<Dictionary<string, object>>(listResponse);
1902 object projectsObj =
null;
1903 if (listData.TryGetValue(
"projects", out projectsObj))
1905 var projects = JsonSerializer.Deserialize<JsonElement>(projectsObj.ToString());
1906 foreach (var project
in projects.EnumerateArray())
1908 if (project.TryGetProperty(
"title", out var title) && title.GetString().Equals(scriptTitle) &&
1909 project.TryGetProperty(
"scriptId", out var projectId))
1911 return projectId.GetString();
1919 var scriptPayload =
new { title = scriptTitle, parentId = cloudProjectId };
1920 var createResponse = ScriptsRESTClient
1921 .WithEndpoint(
"/v1/projects")
1922 .WithParameters(scriptPayload)
1923 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
1926 if (createResponse ==
null)
1928 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create Apps Script project",
this, GPALObjectType.GoogleSheets);
1934 var scriptData = JsonSerializer.Deserialize<Dictionary<string, object>>(createResponse);
1935 object idObj =
null;
1936 if (scriptData.TryGetValue(
"scriptId", out idObj))
1938 return idObj.ToString();
1943 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No Apps Script project ID returned",
this, GPALObjectType.GoogleSheets);
1947 public IAllowScriptOperations WithProjectId(
string projectId)
1949 if (
string.IsNullOrEmpty(projectId))
1951 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Apps Script project ID is empty",
this, GPALObjectType.GoogleSheets);
1953 AppsScriptProjectId = projectId;
1957 public IAllowScriptOperations WithScriptName(
string scriptName)
1959 if (
string.IsNullOrEmpty(scriptName))
1961 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Script name is empty",
this, GPALObjectType.GoogleSheets);
1963 ScriptName = scriptName;
1967 public IAllowScriptOperations WithScriptAccess(ScriptAccessType access)
1969 ScriptAccess = access;
1973 public IAllowScriptOperations WithExecuteAs(ExecuteAsType executeAs)
1975 ExecuteAs = executeAs;
1979 public IAllowScriptOperations WithDeploymentDescription(
string description)
1981 if (
string.IsNullOrEmpty(description))
1983 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Deployment description is empty; using default",
this, GPALObjectType.GoogleSheets);
1984 DeploymentDescription =
"Web app deployment for script";
1988 DeploymentDescription = description;
1993 public IAllowScriptOperations WithVersionNumber(
int versionNumber)
1995 if (versionNumber < 1)
1997 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"Version number must be positive; using default 1",
this, GPALObjectType.GoogleSheets);
2002 VersionNumber = versionNumber;
2007 public IAllowDeployWebApp UploadScript(
string scriptContent)
2009 if (
string.IsNullOrEmpty(scriptContent))
2011 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Script content cannot be empty",
this, GPALObjectType.GoogleSheets);
2015 if (
string.IsNullOrEmpty(AppsScriptProjectId))
2017 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
2021 if (Credentials ==
null)
2023 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
2027 if (
string.IsNullOrEmpty(ScriptName))
2029 ScriptName =
"main";
2030 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Using default script name 'main'",
this, GPALObjectType.GoogleSheets);
2033 var response = ScriptsRESTClient
2034 .WithEndpoint(
string.Format(googleSheetsConfig.ScriptContentEndpoint, AppsScriptProjectId))
2037 files =
new[] {
new { name = ScriptName, type = googleSheetsConfig.ScriptFileType, source = scriptContent } }
2039 .WithHttpMethod(
"PUT")
2040 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2043 if (response ==
null)
2045 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to upload script",
this, GPALObjectType.GoogleSheets);
2049 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Script uploaded to project [{AppsScriptProjectId}]",
this, GPALObjectType.GoogleSheets);
2055 public IAllowDeployWebApp UploadScript(GPALFile scriptFile)
2057 if (scriptFile ==
null)
2059 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Script file is null",
this, GPALObjectType.GoogleSheets);
2063 string scriptContent =
"";
2066 scriptContent = File.ReadAllText(scriptFile.
Filename);
2068 catch (Exception ex)
2070 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to read script file",
this, GPALObjectType.GoogleSheets, ex);
2074 return UploadScript(scriptContent);
2077 public IAllowDataOperations DeployScriptAsWebApp(out
string webAppUrl)
2080 if (
string.IsNullOrEmpty(AppsScriptProjectId))
2082 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Apps Script project ID not set",
this, GPALObjectType.GoogleSheets);
2086 if (Credentials ==
null)
2088 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Credentials not set",
this, GPALObjectType.GoogleSheets);
2092 if (ExecuteAs == ExecuteAsType.USER_ACCESSING && ScriptAccess != ScriptAccessType.ANYONE && ScriptAccess != ScriptAccessType.DOMAIN)
2094 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"ExecuteAs 'USER_ACCESSING' requires access 'ANYONE' or 'DOMAIN'; using 'USER_DEPLOYING'",
this, GPALObjectType.GoogleSheets);
2095 ExecuteAs = ExecuteAsType.USER_DEPLOYING;
2099 var versionPayload =
new { description =
"Version for deployment" };
2100 var versionResponse = ScriptsRESTClient
2101 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/versions")
2102 .WithParameters(versionPayload)
2103 .WithHttpMethod(
"POST")
2104 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2107 if (versionResponse ==
null)
2109 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to create script version",
this, GPALObjectType.GoogleSheets);
2115 var versionData = JsonSerializer.Deserialize<Dictionary<string, object>>(versionResponse);
2116 object versionObj =
null;
2117 if (versionData.TryGetValue(
"versionNumber", out versionObj))
2119 VersionNumber = Convert.ToInt32(versionObj);
2123 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No version number returned",
this, GPALObjectType.GoogleSheets);
2129 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to parse version response",
this, GPALObjectType.GoogleSheets);
2133 string accessString = ScriptAccess
switch
2135 ScriptAccessType.ME =>
"ME",
2136 ScriptAccessType.ANYONE =>
"ANYONE",
2137 ScriptAccessType.ANYONE_ANONYMOUS =>
"ANYONE_ANONYMOUS",
2138 ScriptAccessType.DOMAIN =>
"DOMAIN",
2142 string executeAsString = ExecuteAs
switch
2144 ExecuteAsType.USER_ACCESSING =>
"USER_ACCESSING",
2145 ExecuteAsType.USER_DEPLOYING =>
"USER_DEPLOYING",
2146 _ =>
"USER_DEPLOYING"
2149 var deploymentPayload =
new
2151 versionNumber = VersionNumber,
2152 manifestFileName =
"appsscript",
2153 description = DeploymentDescription
2156 var deployResponse = ScriptsRESTClient
2157 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/deployments")
2158 .WithParameters(deploymentPayload)
2159 .WithHttpMethod(
"POST")
2160 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2163 if (deployResponse ==
null)
2165 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to deploy script",
this, GPALObjectType.GoogleSheets);
2171 var deployData = JsonSerializer.Deserialize<Dictionary<string, object>>(deployResponse);
2172 object deploymentIdObj =
null;
2173 if (!deployData.TryGetValue(
"deploymentId", out deploymentIdObj))
2175 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No deployment ID returned",
this, GPALObjectType.GoogleSheets);
2179 string deploymentId = deploymentIdObj.ToString();
2180 var updatePayload =
new
2182 deploymentConfig =
new
2184 scriptId = AppsScriptProjectId,
2185 versionNumber = VersionNumber,
2186 manifestFileName =
"appsscript",
2187 description = DeploymentDescription,
2188 webApp =
new { executeAs = executeAsString, whoHasAccess = accessString }
2192 var updateResponse = ScriptsRESTClient
2193 .WithEndpoint($
"/v1/projects/{AppsScriptProjectId}/deployments/{deploymentId}")
2194 .WithParameters(updatePayload)
2195 .WithHttpMethod(
"PUT")
2196 .WithHeader(googleSheetsConfig.AuthorizationHeader, $
"{googleSheetsConfig.Bearer} {((Credentials)Credentials)?.AccessToken ?? ""}")
2199 if (updateResponse ==
null)
2201 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to update deployment",
this, GPALObjectType.GoogleSheets);
2205 var updateData = JsonSerializer.Deserialize<Dictionary<string, object>>(updateResponse);
2206 object entryPointsObj =
null;
2207 if (updateData.TryGetValue(
"entryPoints", out entryPointsObj))
2209 var entryPoints = JsonSerializer.Deserialize<JsonElement>(entryPointsObj.ToString());
2210 foreach (var entry
in entryPoints.EnumerateArray())
2212 if (entry.TryGetProperty(
"webApp", out var webApp) && webApp.TryGetProperty(
"url", out var url))
2214 webAppUrl = url.GetString();
2215 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Web app deployed at: [{webAppUrl}]",
this, GPALObjectType.GoogleSheets);
2221 catch (Exception ex)
2223 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Deployment error",
this, GPALObjectType.GoogleSheets, ex);
2226 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Failed to retrieve web app URL",
this, GPALObjectType.GoogleSheets);
2230 public IAllowSheetSelection SaveTo(GPALFile gPALFile)
2232 if (gPALFile ==
null)
2234 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"GPAL file is null",
this, GPALObjectType.GoogleSheets);
2238 if (ReadData ==
null)
2240 if (!
string.IsNullOrEmpty(ReadRange))
2242 if (!ReadDataFromRange())
2249 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"No data to save; ReadData is null and no range specified",
this, GPALObjectType.GoogleSheets);
2254 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Saving [{ReadData.Rows}] rows of sheet [{SheetName}] to [{gPALFile.Filename}].",
this, GPALObjectType.GoogleSheets);
2258 using (var writer =
new StreamWriter(gPALFile.
Filename))
2260 foreach (var row
in ReadData)
2262 writer.WriteLine(
string.Join(
",", row));
2266 catch (Exception ex)
2268 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to save to file",
this, GPALObjectType.GoogleSheets, ex);
2274 public IGoogleSheets ToGPALObject()