[
  {
    "path": ".gemini/GEMINI.md",
    "content": "# Overview\n\nThis codebase is part of the Google Workspace GitHub organization, https://github.com/googleworkspace.\n\n## Style Guide\n\nUse open source best practices for code style and formatting with a preference for Google's style guides.\n\n## Tools\n\n- Verify against Google Workspace documentation with the `workspace-developer` MCP server tools.\n- Use `gh` for GitHub interactions.\n"
  },
  {
    "path": ".gemini/config.yaml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# Config for the Gemini Pull Request Review Bot.\n# https://github.com/marketplace/gemini-code-assist\nhave_fun: false\ncode_review:\n  disable: false\n  comment_severity_threshold: \"HIGH\"\n  max_review_comments: -1\n  pull_request_opened:\n    help: false\n    summary: true\n    code_review: true\nignore_patterns: []\n"
  },
  {
    "path": ".gemini/settings.json",
    "content": "{\n  \"mcpServers\": {\n    \"workspace-developer\": {\n      \"httpUrl\": \"https://workspace-developer.goog/mcp\",\n      \"trust\": true\n    }\n  },\n  \"tools\": {\n    \"allowed\": [\n      \"run_shell_command(pnpm install)\",\n      \"run_shell_command(pnpm format)\",\n      \"run_shell_command(pnpm lint)\",\n      \"run_shell_command(pnpm check)\",\n      \"run_shell_command(pnpm test)\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Copyright 2022 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners\n\n.github/ @googleworkspace/workspace-devrel-dpe\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "# Summary\n\nTODO\n\n## Expected Behavior\n\nSample URL:\nDescription:\n\n## Actual Behavior\n\n\n## Steps to Reproduce the Problem\n\n1.\n1.\n1.\n"
  },
  {
    "path": ".github/linters/.htmlhintrc",
    "content": "{\n  \"tagname-lowercase\": true,\n  \"attr-lowercase\": true,\n  \"attr-value-double-quotes\": true,\n  \"attr-value-not-empty\": false,\n  \"attr-no-duplication\": true,\n  \"doctype-first\": false,\n  \"tag-pair\": true,\n  \"tag-self-close\": false,\n  \"spec-char-escape\": false,\n  \"id-unique\": true,\n  \"src-not-empty\": true,\n  \"title-require\": false,\n  \"alt-require\": true,\n  \"doctype-html5\": true,\n  \"id-class-value\": false,\n  \"style-disabled\": false,\n  \"inline-style-disabled\": false,\n  \"inline-script-disabled\": false,\n  \"space-tab-mixed-disabled\": \"space\",\n  \"id-class-ad-disabled\": false,\n  \"href-abs-or-rel\": false,\n  \"attr-unsafe-chars\": true,\n  \"head-script-disabled\": false\n}\n"
  },
  {
    "path": ".github/linters/.yaml-lint.yml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n---\n###########################################\n# These are the rules used for            #\n# linting all the yaml files in the stack #\n# NOTE:                                   #\n# You can disable line with:              #\n# # yamllint disable-line                 #\n###########################################\nrules:\n  braces:\n    level: warning\n    min-spaces-inside: 0\n    max-spaces-inside: 0\n    min-spaces-inside-empty: 1\n    max-spaces-inside-empty: 5\n  brackets:\n    level: warning\n    min-spaces-inside: 0\n    max-spaces-inside: 0\n    min-spaces-inside-empty: 1\n    max-spaces-inside-empty: 5\n  colons:\n    level: warning\n    max-spaces-before: 0\n    max-spaces-after: 1\n  commas:\n    level: warning\n    max-spaces-before: 0\n    min-spaces-after: 1\n    max-spaces-after: 1\n  comments: disable\n  comments-indentation: disable\n  document-end: disable\n  document-start:\n    level: warning\n    present: true\n  empty-lines:\n    level: warning\n    max: 2\n    max-start: 0\n    max-end: 0\n  hyphens:\n    level: warning\n    max-spaces-after: 1\n  indentation:\n    level: warning\n    spaces: consistent\n    indent-sequences: true\n    check-multi-line-strings: false\n  key-duplicates: enable\n  line-length:\n    level: warning\n    max: 120\n    allow-non-breakable-words: true\n    allow-non-breakable-inline-mappings: true\n  new-line-at-end-of-file: disable\n  new-lines:\n    type: unix\n  trailing-spaces: disable"
  },
  {
    "path": ".github/linters/sun_checks.xml",
    "content": "<?xml version=\"1.0\"?>\n<!--\n Copyright 2022 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n      http://www.apache.org/licenses/LICENSE-2.0\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    Checkstyle configuration that checks the Google coding conventions from Google Java Style\n    that can be found at https://google.github.io/styleguide/javaguide.html\n    Checkstyle is very configurable. Be sure to read the documentation at\n    http://checkstyle.org (or in your downloaded distribution).\n    To completely disable a check, just comment it out or delete it from the file.\n    To suppress certain violations please review suppression filters.\n    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.\n -->\n\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n\n    <property name=\"severity\" value=\"warning\"/>\n\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n    <!-- Excludes all 'module-info.java' files              -->\n    <!-- See https://checkstyle.org/config_filefilters.html -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\"/>\n    </module>\n    <!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n                  default=\"checkstyle-suppressions.xml\"/>\n        <property name=\"optional\" value=\"true\"/>\n    </module>\n\n    <!-- Checks for whitespace                               -->\n    <!-- See http://checkstyle.org/config_whitespace.html -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <module name=\"LineLength\">\n        <property name=\"fileExtensions\" value=\"java\"/>\n        <property name=\"max\" value=\"100\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <module name=\"OuterTypeFilename\"/>\n        <module name=\"IllegalTokenText\">\n            <property name=\"tokens\" value=\"STRING_LITERAL, CHAR_LITERAL\"/>\n            <property name=\"format\"\n                      value=\"\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\"\n                      value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\"/>\n        </module>\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n        <module name=\"AvoidStarImport\"/>\n        <module name=\"OneTopLevelClass\"/>\n        <module name=\"NoLineWrap\">\n            <property name=\"tokens\" value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT\"/>\n        </module>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\"\n                      value=\"LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE\"/>\n        </module>\n        <module name=\"LeftCurly\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,\n                    INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,\n                    LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,\n                    OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_DO\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"option\" value=\"alone\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,\n                    INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"SuppressionXpathSingleFilter\">\n            <!-- suppresion is required till https://github.com/checkstyle/checkstyle/issues/7541 -->\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"query\" value=\"//RCURLY[parent::SLIST[count(./*)=1]\n                                     or preceding-sibling::*[last()][self::LCURLY]]\"/>\n        </module>\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,\n                    LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE\"/>\n        </module>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\"/>\n            <property name=\"allowEmptyLambdas\" value=\"true\"/>\n            <property name=\"allowEmptyMethods\" value=\"true\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLoops\" value=\"true\"/>\n            <property name=\"ignoreEnhancedForColon\" value=\"false\"/>\n            <property name=\"tokens\"\n                      value=\"ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,\n                    BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,\n                    LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,\n                    LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,\n                    LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,\n                    NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,\n                    SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND\"/>\n            <message key=\"ws.notFollowed\"\n                     value=\"WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"WhitespaceAround: ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"MultipleVariableDeclarations\"/>\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"FallThrough\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"tokens\"\n                      value=\"PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,\n                    COMPACT_CTOR_DEF\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapDot\"/>\n            <property name=\"tokens\" value=\"DOT\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapComma\"/>\n            <property name=\"tokens\" value=\"COMMA\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->\n            <property name=\"id\" value=\"SeparatorWrapEllipsis\"/>\n            <property name=\"tokens\" value=\"ELLIPSIS\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->\n            <property name=\"id\" value=\"SeparatorWrapArrayDeclarator\"/>\n            <property name=\"tokens\" value=\"ARRAY_DECLARATOR\"/>\n            <property name=\"option\" value=\"EOL\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapMethodRef\"/>\n            <property name=\"tokens\" value=\"METHOD_REF\"/>\n            <property name=\"option\" value=\"nl\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"TypeName\">\n            <property name=\"tokens\" value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                    ANNOTATION_DEF, RECORD_DEF\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Member name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LambdaParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Lambda parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"CatchParameterName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Catch parameter name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Local variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"PatternVariableName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Pattern variable name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"ClassTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Class type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"RecordComponentName\">\n            <property name=\"format\" value=\"^[a-z]([a-z0-9][a-zA-Z0-9]*)?$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Record component name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"RecordTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Record type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"MethodTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"InterfaceTypeParameterName\">\n            <property name=\"format\" value=\"(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Interface type name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"NoFinalizer\"/>\n        <module name=\"GenericWhitespace\">\n            <message key=\"ws.followed\"\n                     value=\"GenericWhitespace ''{0}'' is followed by whitespace.\"/>\n            <message key=\"ws.preceded\"\n                     value=\"GenericWhitespace ''{0}'' is preceded with whitespace.\"/>\n            <message key=\"ws.illegalFollow\"\n                     value=\"GenericWhitespace ''{0}'' should followed by whitespace.\"/>\n            <message key=\"ws.notPreceded\"\n                     value=\"GenericWhitespace ''{0}'' is not preceded with whitespace.\"/>\n        </module>\n        <module name=\"Indentation\">\n            <property name=\"basicOffset\" value=\"2\"/>\n            <property name=\"braceAdjustment\" value=\"2\"/>\n            <property name=\"caseIndent\" value=\"2\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"4\"/>\n            <property name=\"arrayInitIndent\" value=\"2\"/>\n        </module>\n        <module name=\"AbbreviationAsWordInName\">\n            <property name=\"ignoreFinal\" value=\"false\"/>\n            <property name=\"allowedAbbreviationLength\" value=\"0\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,\n                    PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,\n                    RECORD_COMPONENT_DEF\"/>\n        </module>\n        <module name=\"NoWhitespaceBeforeCaseDefaultColon\"/>\n        <module name=\"OverloadMethodsDeclarationOrder\"/>\n        <module name=\"VariableDeclarationUsageDistance\"/>\n        <module name=\"CustomImportOrder\">\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n            <property name=\"separateLineBetweenGroups\" value=\"true\"/>\n            <property name=\"customImportOrderRules\" value=\"STATIC###THIRD_PARTY_PACKAGE\"/>\n            <property name=\"tokens\" value=\"IMPORT, STATIC_IMPORT, PACKAGE_DEF\"/>\n        </module>\n        <module name=\"MethodParamPad\">\n            <property name=\"tokens\"\n                      value=\"CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,\n                    SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF\"/>\n        </module>\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, POST_INC, POST_DEC, DOT,\n                    LABELED_STAT, METHOD_REF\"/>\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n        </module>\n        <module name=\"ParenPad\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,\n                    EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,\n                    LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,\n                    METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,\n                    RECORD_DEF\"/>\n        </module>\n        <module name=\"OperatorWrap\">\n            <property name=\"option\" value=\"NL\"/>\n            <property name=\"tokens\"\n                      value=\"BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,\n                    LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,\n                    TYPE_EXTENSION_AND \"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationMostCases\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,\n                      RECORD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"AnnotationLocation\">\n            <property name=\"id\" value=\"AnnotationLocationVariables\"/>\n            <property name=\"tokens\" value=\"VARIABLE_DEF\"/>\n            <property name=\"allowSamelineMultipleAnnotations\" value=\"true\"/>\n        </module>\n        <module name=\"NonEmptyAtclauseDescription\"/>\n        <module name=\"InvalidJavadocPosition\"/>\n        <module name=\"JavadocTagContinuationIndentation\"/>\n        <module name=\"SummaryJavadoc\">\n            <property name=\"forbiddenSummaryFragments\"\n                      value=\"^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )\"/>\n        </module>\n        <module name=\"JavadocParagraph\"/>\n        <module name=\"RequireEmptyLineBeforeBlockTagGroup\"/>\n        <module name=\"AtclauseOrder\">\n            <property name=\"tagOrder\" value=\"@param, @return, @throws, @deprecated\"/>\n            <property name=\"target\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF\"/>\n        </module>\n        <module name=\"JavadocMethod\">\n            <property name=\"accessModifiers\" value=\"public\"/>\n            <property name=\"allowMissingParamTags\" value=\"true\"/>\n            <property name=\"allowMissingReturnTag\" value=\"true\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"MissingJavadocMethod\">\n            <property name=\"scope\" value=\"public\"/>\n            <property name=\"minLineCount\" value=\"2\"/>\n            <property name=\"allowedAnnotations\" value=\"Override, Test\"/>\n            <property name=\"tokens\" value=\"METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,\n                                   COMPACT_CTOR_DEF\"/>\n        </module>\n        <module name=\"MissingJavadocType\">\n            <property name=\"scope\" value=\"protected\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, INTERFACE_DEF, ENUM_DEF,\n                      RECORD_DEF, ANNOTATION_DEF\"/>\n            <property name=\"excludeScope\" value=\"nothing\"/>\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-z0-9][a-zA-Z0-9_]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}''.\"/>\n        </module>\n        <module name=\"SingleLineJavadoc\"/>\n        <module name=\"EmptyCatchBlock\">\n            <property name=\"exceptionVariableName\" value=\"expected\"/>\n        </module>\n        <module name=\"CommentsIndentation\">\n            <property name=\"tokens\" value=\"SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN\"/>\n        </module>\n        <!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->\n        <module name=\"SuppressionXpathFilter\">\n            <property name=\"file\" value=\"${org.checkstyle.google.suppressionxpathfilter.config}\"\n                      default=\"checkstyle-xpath-suppressions.xml\"/>\n            <property name=\"optional\" value=\"true\"/>\n        </module>\n    </module>\n</module>"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "# Description\n\nPlease include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.\n\nFixes # (issue)\n\n## Is it been tested?\n- [ ] Development testing done\n\n## Checklist\n\n- [ ] My code follows the style guidelines of this project\n- [ ] I have performed a self-review of my own code\n- [ ] I have performed a peer-reviewed with team member(s)\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] I have made corresponding changes to the documentation\n- [ ] My changes generate no new warnings\n- [ ] Any dependent changes have been merged and published in downstream modules\n"
  },
  {
    "path": ".github/scripts/biome-gs.ts",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { exec } from \"node:child_process\";\nimport { readdirSync, renameSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\n\nconst execAsync = promisify(exec);\n\nasync function findGsFiles(\n  dir: string,\n  fileList: string[] = [],\n): Promise<string[]> {\n  const files = readdirSync(dir);\n  for (const file of files) {\n    const filePath = join(dir, file);\n    if (\n      file === \"node_modules\" ||\n      file === \".git\" ||\n      file === \"dist\" ||\n      file === \"target\" ||\n      file === \"pkg\"\n    ) {\n      continue;\n    }\n    const stat = statSync(filePath);\n    if (stat.isDirectory()) {\n      await findGsFiles(filePath, fileList);\n    } else if (file.endsWith(\".gs\") && file !== \"moment.gs\") {\n      fileList.push(filePath);\n    }\n  }\n  return fileList;\n}\n\nasync function main() {\n  const command = process.argv[2]; // 'lint' or 'format'\n  if (command !== \"lint\" && command !== \"format\") {\n    console.error(\"Usage: tsx biome-gs.ts [lint|format]\");\n    process.exit(1);\n  }\n\n  const rootDir = resolve(\".\");\n  const gsFiles = await findGsFiles(rootDir);\n  const renamedFiles: { oldPath: string; newPath: string }[] = [];\n\n  const restoreFiles = () => {\n    for (const { oldPath, newPath } of renamedFiles) {\n      try {\n        renameSync(newPath, oldPath);\n      } catch (e) {\n        console.error(`Failed to restore ${newPath} to ${oldPath}:`, e);\n      }\n    }\n    renamedFiles.length = 0;\n  };\n\n  process.on(\"SIGINT\", () => {\n    restoreFiles();\n    process.exit(1);\n  });\n  process.on(\"SIGTERM\", () => {\n    restoreFiles();\n    process.exit(1);\n  });\n  process.on(\"exit\", restoreFiles);\n\n  try {\n    // 1. Rename .gs to .gs.js\n    for (const gsFile of gsFiles) {\n      const jsFile = `${gsFile}.js`;\n      renameSync(gsFile, jsFile);\n      renamedFiles.push({ oldPath: gsFile, newPath: jsFile });\n    }\n\n    // 2. Run Biome\n    const biomeArgs = command === \"format\" ? \"check --write .\" : \"check .\";\n    console.log(`Running biome ${biomeArgs}...`);\n    try {\n      const { stdout, stderr } = await execAsync(\n        `pnpm exec biome ${biomeArgs}`,\n        { cwd: rootDir },\n      );\n      if (stdout) console.log(stdout.replace(/\\.gs\\.js/g, \".gs\"));\n      if (stderr) console.error(stderr.replace(/\\.gs\\.js/g, \".gs\"));\n    } catch (e: unknown) {\n      const err = e as { stdout?: string; stderr?: string };\n      if (err.stdout) console.log(err.stdout.replace(/\\.gs\\.js/g, \".gs\"));\n      if (err.stderr) console.error(err.stderr.replace(/\\.gs\\.js/g, \".gs\"));\n      // Don't exit yet, we need to restore files\n    }\n  } catch (err) {\n    console.error(\"An error occurred:\", err);\n  } finally {\n    restoreFiles();\n    // Remove listeners to avoid double-running or issues on exit\n    process.removeAllListeners(\"exit\");\n    process.removeAllListeners(\"SIGINT\");\n    process.removeAllListeners(\"SIGTERM\");\n  }\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": ".github/scripts/check-gs.ts",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/// <reference types=\"node\" />\n\nimport { exec } from \"node:child_process\";\nimport {\n  copyFileSync,\n  existsSync,\n  mkdirSync,\n  readdirSync,\n  rmSync,\n  statSync,\n  writeFileSync,\n} from \"node:fs\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { promisify } from \"node:util\";\n\nconst execAsync = promisify(exec);\nconst TEMP_ROOT = \".tsc_check\";\n\ninterface Project {\n  files: string[];\n  name: string;\n  path: string;\n}\n\ninterface CheckResult {\n  name: string;\n  success: boolean;\n  output: string;\n}\n\n// Helper to recursively find all files with a specific extension\nfunction findFiles(\n  dir: string,\n  extension: string,\n  fileList: string[] = [],\n): string[] {\n  const files = readdirSync(dir);\n  for (const file of files) {\n    if (file.endsWith(\".js\")) continue;\n    const filePath = join(dir, file);\n    const stat = statSync(filePath);\n    if (stat.isDirectory()) {\n      if (file !== \"node_modules\" && file !== \".git\" && file !== TEMP_ROOT) {\n        findFiles(filePath, extension, fileList);\n      }\n    } else if (file.endsWith(extension)) {\n      fileList.push(filePath);\n    }\n  }\n  return fileList;\n}\n\n// Find all directories containing appsscript.json\nfunction findProjectRoots(rootDir: string): string[] {\n  return findFiles(rootDir, \"appsscript.json\").map((f) => dirname(f));\n}\n\nfunction createProjects(\n  rootDir: string,\n  projectRoots: string[],\n  allGsFiles: string[],\n): Project[] {\n  // Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json).\n  const projectGroups = new Map<string, string[]>();\n\n  // Holds \"orphan\" files that do not belong to any defined Apps Script project (no appsscript.json found).\n  const looseGroups = new Map<string, string[]>();\n\n  // Initialize project groups\n  for (const p of projectRoots) {\n    projectGroups.set(p, []);\n  }\n\n  for (const file of allGsFiles) {\n    let assigned = false;\n    let currentDir = dirname(file);\n\n    while (currentDir.startsWith(rootDir) && currentDir !== rootDir) {\n      if (projectGroups.has(currentDir)) {\n        projectGroups.get(currentDir)?.push(file);\n        assigned = true;\n        break;\n      }\n      currentDir = dirname(currentDir);\n    }\n\n    if (!assigned) {\n      const dir = dirname(file);\n      if (!looseGroups.has(dir)) {\n        looseGroups.set(dir, []);\n      }\n      looseGroups.get(dir)?.push(file);\n    }\n  }\n\n  const projects: Project[] = [];\n  projectGroups.forEach((files, dir) => {\n    if (files.length > 0) {\n      projects.push({\n        files,\n        name: `Project: ${relative(rootDir, dir)}`,\n        path: relative(rootDir, dir),\n      });\n    }\n  });\n  looseGroups.forEach((files, dir) => {\n    if (files.length > 0) {\n      projects.push({\n        files,\n        name: `Loose Project: ${relative(rootDir, dir)}`,\n        path: relative(rootDir, dir),\n      });\n    }\n  });\n\n  return projects;\n}\n\nasync function checkProject(\n  project: Project,\n  rootDir: string,\n): Promise<CheckResult> {\n  const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, \"_\");\n  const projectTempDir = join(TEMP_ROOT, projectNameSafe);\n\n  // Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't)\n  mkdirSync(projectTempDir, { recursive: true });\n\n  for (const file of project.files) {\n    const fileRelPath = relative(rootDir, file);\n    const destPath = join(projectTempDir, fileRelPath.replace(/\\.gs$/, \".js\"));\n    const destDir = dirname(destPath);\n    mkdirSync(destDir, { recursive: true });\n    copyFileSync(file, destPath);\n  }\n\n  const tsConfig = {\n    extends: \"../../tsconfig.json\",\n    compilerOptions: {\n      noEmit: true,\n      allowJs: true,\n      checkJs: true,\n      typeRoots: [resolve(rootDir, \"node_modules/@types\")],\n    },\n    include: [\"**/*.js\"],\n  };\n\n  writeFileSync(\n    join(projectTempDir, \"tsconfig.json\"),\n    JSON.stringify(tsConfig, null, 2),\n  );\n\n  try {\n    await execAsync(`tsc -p \\\"${projectTempDir}\\\"`, { cwd: rootDir });\n    return { name: project.name, success: true, output: \"\" };\n  } catch (e) {\n    const err = e as { stdout?: string; stderr?: string };\n    const rawOutput = (err.stdout ?? \"\") + (err.stderr || \"\");\n\n    const rewritten = rawOutput\n      .split(\"\\n\")\n      .map((line: string) => {\n        if (line.includes(projectTempDir)) {\n          let newLine = line.split(projectTempDir + sep).pop();\n          if (!newLine) {\n            return line;\n          }\n          newLine = newLine.replace(/\\.js(:|\\()/g, \".gs$1\");\n          return newLine;\n        }\n        return line;\n      })\n      .join(\"\\n\");\n\n    return { name: project.name, success: false, output: rewritten };\n  }\n}\n\nasync function main() {\n  try {\n    const rootDir = resolve(\".\");\n    const args = process.argv.slice(2);\n    const searchArg = args.find((arg) => arg !== \"--\");\n\n    // 1. Discovery\n    const projectRoots = findProjectRoots(rootDir);\n    const allGsFiles = findFiles(rootDir, \".gs\");\n\n    // 2. Grouping\n    const projects = createProjects(rootDir, projectRoots, allGsFiles);\n\n    // 3. Filtering\n    const projectsToCheck = projects.filter((p) => {\n      return !searchArg || p.path.startsWith(searchArg);\n    });\n\n    if (projectsToCheck.length === 0) {\n      console.log(\"No projects found matching the search path.\");\n      return;\n    }\n\n    // 4. Setup\n    if (existsSync(TEMP_ROOT)) {\n      rmSync(TEMP_ROOT, { recursive: true, force: true });\n    }\n    mkdirSync(TEMP_ROOT);\n\n    console.log(`Checking ${projectsToCheck.length} projects in parallel...`);\n\n    // 5. Parallel Execution\n    const results = await Promise.all(\n      projectsToCheck.map((p) => checkProject(p, rootDir)),\n    );\n\n    // 6. Reporting\n    let hasError = false;\n    for (const result of results) {\n      if (!result.success) {\n        hasError = true;\n        console.log(`\\n--- Failed: ${result.name} ---`);\n        console.log(result.output);\n      }\n    }\n\n    if (hasError) {\n      console.error(\"\\nOne or more checks failed.\");\n      process.exit(1);\n    } else {\n      console.log(\"\\nAll checks passed.\");\n    }\n  } catch (err) {\n    console.error(\"Unexpected error:\", err);\n    process.exit(1);\n  } finally {\n    if (existsSync(TEMP_ROOT)) {\n      rmSync(TEMP_ROOT, { recursive: true, force: true });\n    }\n  }\n}\n\nmain();\n"
  },
  {
    "path": ".github/scripts/clasp_push.sh",
    "content": "#! /bin/bash\n# Copyright 2020 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nexport LC_ALL=C.UTF-8\nexport LANG=C.UTF-8\n\nfunction contains_changes() {\n  [[ \"${*:2}\" = \"\" ]] && return 0\n  for f in \"${@:2}\"; do\n    case $(realpath \"$f\")/ in \n      $(realpath \"$1\")/*) return 0;;\n    esac\n  done\n  return 1\n}\n\nchanged_files=$(echo \"${@:1}\" | xargs realpath | xargs -I {} dirname {}| sort -u | uniq)\ndirs=()\n\nIFS=$'\\n' read -r -d '' -a dirs < <( find . -name '.clasp.json' -exec dirname '{}' \\; | sort -u | xargs realpath )\n\nexit_code=0\n\nfor dir in \"${dirs[@]}\"; do\n  pushd \"${dir}\" > /dev/null || exit\n  contains_changes \"$dir\" \"${changed_files[@]}\" || continue\n  echo \"Publishing ${dir}\"\n  clasp push -f\n  status=$?\n  if [ $status -ne 0 ]; then\n    exit_code=$status\n  fi\n  popd > /dev/null || exit\ndone\n\nif [ $exit_code -ne 0 ]; then\n  echo \"Script push failed.\"\nfi\n\nexit $exit_code"
  },
  {
    "path": ".github/snippet-bot.yml",
    "content": "# Copyright 2022 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n"
  },
  {
    "path": ".github/sync-repo-settings.yaml",
    "content": "# Copyright 2022 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# .github/sync-repo-settings.yaml\n# See https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings for app options.\nrebaseMergeAllowed: true\nsquashMergeAllowed: true\nmergeCommitAllowed: false\ndeleteBranchOnMerge: true\nbranchProtectionRules:\n  - pattern: main\n    isAdminEnforced: false\n    requiresStrictStatusChecks: false\n    requiredStatusCheckContexts:\n      # .github/workflows/test.yml with a job called \"test\"\n      - \"test\"\n      # .github/workflows/lint.yml with a job called \"lint\"\n      - \"lint\"\n      # Google bots below\n      - \"cla/google\"\n      - \"snippet-bot check\"\n      - \"header-check\"\n      - \"conventionalcommits.org\"\n    requiredApprovingReviewCount: 1\n    requiresCodeOwnerReviews: true\npermissionRules:\n  - team: workspace-devrel-dpe\n    permission: admin\n  - team: workspace-devrel\n    permission: push\n"
  },
  {
    "path": ".github/workflows/automation.yml",
    "content": "# Copyright 2022 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n---\nname: Automation\non: [ push, pull_request, workflow_dispatch ]\njobs:\n  dependabot:\n    runs-on: ubuntu-latest\n    if: ${{ github.actor == 'dependabot[bot]' &&  github.event_name == 'pull_request' }}\n    env:\n      PR_URL: ${{github.event.pull_request.html_url}}\n      GITHUB_TOKEN: ${{secrets.GOOGLEWORKSPACE_BOT_TOKEN}}\n    steps:\n      - name: approve\n        run: gh pr review --approve \"$PR_URL\"\n      - name: merge\n        run: gh pr merge --auto --squash --delete-branch \"$PR_URL\"\n  default-branch-migration:\n    # this job helps with migrating the default branch to main\n    # it pushes main to master if master exists and main is the default branch\n    # it pushes master to main if master is the default branch\n    runs-on: ubuntu-latest\n    if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }}\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n          # required otherwise GitHub blocks infinite loops in pushes originating in an action\n          token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}\n      - name: Set env\n        run: |\n          # set DEFAULT BRANCH\n          echo \"DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')\" >> \"$GITHUB_ENV\";\n\n          # set HAS_MASTER_BRANCH\n          if [ -n \"$(git ls-remote --heads origin master)\" ]; then\n            echo \"HAS_MASTER_BRANCH=true\" >> \"$GITHUB_ENV\"\n          else\n            echo \"HAS_MASTER_BRANCH=false\" >> \"$GITHUB_ENV\"\n          fi\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: configure git\n        run: |\n          git config --global user.name 'googleworkspace-bot'\n          git config --global user.email 'googleworkspace-bot@google.com'\n      - if: ${{ env.DEFAULT_BRANCH == 'main' && env.HAS_MASTER_BRANCH == 'true' }}\n        name: Update master branch from main\n        run: |\n          git checkout -B master\n          git reset --hard origin/main\n          git push origin master\n      - if: ${{ env.DEFAULT_BRANCH == 'master'}}\n        name: Update main branch from master\n        run: |\n          git checkout -B main\n          git reset --hard origin/master\n          git push origin main\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "# Copyright 2021 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n---\nname: Lint\non:\n  push:\n    branches:\n      - main\n  pull_request:\njobs:\n  lint:\n    concurrency:\n      group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6\n      - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0\n      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5\n        with:\n          cache: \"pnpm\"\n      - run: pnpm i\n      - run: pnpm lint\n"
  },
  {
    "path": ".github/workflows/publish.yaml",
    "content": "# Copyright 2021 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n---\nname: Publish Apps Script\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\njobs:\n  publish:\n    concurrency:\n      group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6\n      - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0\n      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5\n        with:\n          cache: \"pnpm\"\n      - run: pnpm i\n      - name: Get changed files\n        id: changed-files\n        uses: tj-actions/changed-files@v23.1\n      - name: Write test credentials\n        run: |\n          echo \"${CLASP_CREDENTIALS}\" > \"${HOME}/.clasprc.json\"\n        env:\n          CLASP_CREDENTIALS: ${{secrets.CLASP_CREDENTIALS}}\n      - run: pnpm install -g @google/clasp\n      - run: ./.github/scripts/clasp_push.sh ${{ steps.changed-files.outputs.all_changed_files }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "# Copyright 2022 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nname: Test\non:\n  push:\n    branches:\n      - main\n  pull_request:\njobs:\n  test:\n    # temporarily use matrix until all are passing\n    strategy:\n      fail-fast: false\n      matrix:\n        folder:\n          - adminSDK\n          - advanced\n          - ai\n          - calendar\n          - chat\n          - classroom\n          - data-studio\n          - docs\n          - drive\n          - forms\n          - forms-api\n          - gmail\n          - gmail-sentiment-analysis\n          - mashups\n          - people\n          - picker\n          - service\n          - sheets\n          - slides\n          - solutions\n          - tasks\n          - templates\n          - triggers\n          - ui\n          - utils\n    concurrency:\n      group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.head_ref || github.ref }}-${{ matrix.folder }}\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6\n      - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0\n      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5\n        with:\n          cache: \"pnpm\"\n      - run: pnpm i\n      - run: pnpm check ${{ matrix.folder }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\n.gradle\n**/dist\n**/node_modules\n**/target\n**/.tsc_check"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"google-workspace.google-workspace-developer-tools\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"files.associations\": {\n    \"*.gs\": \"javascript\"\n  }\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to become a contributor and submit your own code\n\n## Contributor License Agreements\n\nWe'd love to accept your sample apps and patches! Before we can take them, we\nhave to jump a couple of legal hurdles.\n\nPlease fill out either the individual or corporate Contributor License Agreement\n(CLA).\n\n* If you are an individual writing original source code and you're sure you\n  own the intellectual property, then you'll need to sign an\n  [individual CLA](https://developers.google.com/open-source/cla/individual).\n* If you work for a company that wants to allow you to contribute your work,\n  then you'll need to sign a\n  [corporate CLA](https://developers.google.com/open-source/cla/corporate).\n\nFollow either of the two links above to access the appropriate CLA and\ninstructions for how to sign and return it. Once we receive it, we'll be able to\naccept your pull requests.\n\n## Contributing A Patch\n\n1. Submit an issue describing your proposed change to the repository in question.\n1. The repository owner will respond to your issue promptly.\n1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above).\n1. Fork the desired repository, develop and test your code changes.\n1. Ensure that your code adheres to the existing style in the sample to which you are contributing.\n1. Ensure that your code has an appropriate set of unit tests which all pass.\n1. Run `pnpm check` to ensure there are no type errors or syntax issues in your `.gs` files.\n1. Submit a pull request!\n\n## Style\n\nSamples in this repository follow the [JavaScript Semi-Standard\nStyle](https://github.com/Flet/semistandard).\n"
  },
  {
    "path": "GEMINI.md",
    "content": "# Apps Script Sample Development Guide\n\nThis guide outlines best practices for developing Google Apps Script projects, focusing on type safety and modern JavaScript features.\n\n## Important\n\n* For new sample directories, ensure the top-level folder is included in the [`test.yaml`](.github/workflows/test.yaml) GitHub workflow's matrix configuration.\n* Do not move or delete snippet tags: `[END apps_script_... ]` or `[END apps_script_... ]`.\n* Keep code within snippet tags self-contained. Avoid depending on helper functions defined outside the snippet tags if the snippet is intended to be copied and pasted.\n* Avoid function name collisions (e.g., multiple `onOpen` or `main` functions) by placing separate samples in their own directories or files. Do not append suffixes like `_2`, `_3` to function names. For variables, replace collisions with a more descriptive name.\n\n## Tools\n\nLint and format code using [Biome](https://biomejs.dev/).\n\n```bash\npnpm lint\npnpm format\n```\n\n## Apps Script Code Best Practices\n\nApps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone.\n\n### `let` and `const`\nUse `let` and `const` instead of `var` for block-scoped variables.\n\n*   **`const`**: Use for values that should not be reassigned.\n*   **`let`**: Use for values that will change.\n\n```javascript\nconst PI = 3.14;\nlet count = 0;\n\nif (true) {\n  let local = \"I exist only in this block\";\n}\n// local is not accessible here\n```\n\n### Arrow Functions\nUse arrow functions for concise function expressions, especially for callbacks.\n\n```javascript\nconst numbers = [1, 2, 3];\nconst squares = numbers.map(x => x * x); // [1, 4, 9]\n```\n\n### Destructuring\nUnpack values from arrays or properties from objects into distinct variables.\n\n```javascript\nconst user = { name: \"Alice\", age: 30 };\nconst { name, age } = user;\n\nconst coords = [10, 20];\nconst [x, y] = coords;\n```\n\n### Template Literals\nUse template literals for string interpolation and multi-line strings.\n\n```javascript\nconst name = \"World\";\nconst greeting = `Hello, ${name}!`;\n\nconst multiLine = `\n  This is a\n  multi-line string.\n`;\n```\n\n### Default Parameters\nSpecify default values for function parameters.\n\n```javascript\nfunction greet(name = \"Guest\") {\n  console.log(`Hello, ${name}!`);\n}\n\ngreet(); // \"Hello, Guest!\"\n```\n\n### Prefer `for...of` for Iteration\nWhile `forEach` is convenient, `for...of` loops generally offer better performance and more control (e.g., `break`, `continue`) in Apps Script, especially when dealing with large arrays.\n\n```javascript\nconst numbers = [1, 2, 3];\n\n// Using forEach (less performant for large arrays)\nnumbers.forEach(num => {\n  console.log(num);\n});\n\n// Using for...of (preferred)\nfor (const num of numbers) {\n  console.log(num);\n}\n```\n\n## Apps Script V8 Runtime\n\nIt's important to understand that the Apps Script V8 runtime is\nnot a standard Node.js or browser environment. This can lead to compatibility\nissues when incorporating third-party libraries or adapting code examples\nfrom other JavaScript environments.\n\n### Unavailable APIs\n\nThe following standard JavaScript APIs are **NOT** available in the\nApps Script V8 runtime:\n\n*   **Timers**: `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`\n*   **Streams**: `ReadableStream`, `WritableStream`, `TextEncoder`,\n    `TextDecoder`\n*   **Web APIs**: `fetch`, `FormData`, `File`, `Blob`, `URL`, `URLSearchParams`,\n    `DOMException`, `atob`, `btoa`\n*   **Crypto**: `crypto`, `SubtleCrypto`\n*   **Global Objects**: `window`, `navigator`, `performance`, `process`\n    (Node.js)\n\nInstead of the unavailable APIs, you can use the following\nApps Script APIs as alternatives:\n\n*   **Timers**: Use\n    [`Utilities.sleep(milliseconds)`](https://developers.google.com/apps-script/reference/utilities/utilities#sleepmilliseconds)\n    for synchronous pauses. Asynchronous timers are not supported.\n*   **Fetch**: Use [`UrlFetchApp.fetch(url,\n    params)`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app) to make HTTP(S)\n    requests.\n*   **atob**: Use\n    [`Utilities.base64Decode()`](https://developers.google.com/apps-script/reference/utilities/utilities#base64decodeencoded)\n    to decode Base64-encoded strings.\n*   **btoa**: Use\n    [`Utilities.base64Encode()`](https://developers.google.com/apps-script/reference/utilities/utilities#base64encodedata)\n    to encode strings in Base64.\n*   **Crypto**: Use [`Utilities`](https://developers.google.com/apps-script/reference/utilities/utilities)\n    for cryptographic functions like\n    [`computeDigest()`](https://developers.google.com/apps-script/reference/utilities/utilities#computedigestalgorithm,-value),\n    [`computeHmacSha256Signature()`](https://developers.google.com/apps-script/reference/utilities/utilities#computehmacsha256signaturevalue,-key),\n    and\n    [`computeRsaSha256Signature()`](https://developers.google.com/apps-script/reference/utilities/utilities#computersasha256signaturevalue,-key).\n\nFor some APIs, other workarounds might exist. For example, you might be able to\nuse a polyfill for `TextEncoder`.\n\n### Asynchronous Limitations\n\nThe V8 runtime supports `async` and `await` syntax and the `Promise` object.\nHowever, the Apps Script runtime environment is fundamentally\nsynchronous.\n\n*   **Microtasks (Supported)**: The runtime processes the microtask queue (where\n    `Promise.then()` callbacks and `await` resolutions occur) after the current\n    call stack clears.\n*   **Macrotasks (Not Supported)**: Apps Script does not have a\n    standard event loop for macrotasks. Functions like `setTimeout()` and\n    `setInterval()` are not available.\n*   **WebAssembly Exception**: The WebAssembly API is the only built-in\n    feature that operates in a non-blocking manner within the runtime, allowing\n    for specific asynchronous compilation patterns (WebAssembly.instantiate).\n\nAll I/O operations, such as\n[`UrlFetchApp.fetch()`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app), are\nblocking. To achieve parallel network requests, use\n[`UrlFetchApp.fetchAll()`](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchallrequests).\n\n### Class Limitations\n\nThe V8 runtime has specific limitations regarding modern ES6+ class features:\n\n*   **Private Fields**: Private class fields (for example, `#field`) are not\n    supported and cause parsing errors. Consider using closures or `WeakMap` for\n    true encapsulation.\n*   **Static Fields**: Direct static field declarations within the class body\n    (for example, `static count = 0;`) are not supported. Assign static\n    properties to the class after its definition (for example, `MyClass.count =\n    0;`).\n\n### Module Limitations\n\n*   **ES6 Modules**: The V8 runtime does not support ES6 modules (`import` /\n    `export`). To use libraries, you must either use the [\n    Apps Script library mechanism](https://developers.google.com/apps-script/guides/libraries)\n    or bundle your code and its dependencies into a single script file. ([Issue\n    Tracker](https://issuetracker.google.com/issues/134627726))\n*   **File Execution Order**: All script files in your project are executed in a\n    global scope. It's best to avoid top-level code with side effects and ensure\n    functions and classes are defined before being used across files. Explicitly\n    order your files in the editor if dependencies exist between them.\n\n## Type Checking with JSDoc\n\nThis project uses a type checker to validate `.gs` files for errors. Since `.gs` files are technically JavaScript, we use JSDoc comments to provide type information. This ensures your code is type-safe and well-documented.\n\n### Running Checks\n\nYou can run the type checker from the root of the repository.\n\n**Check all projects:**\n```bash\npnpm run check\n```\n\n**Check a specific path:**\nTo check only projects within a specific directory (e.g., `solutions/automations`), pass the path as an argument:\n```bash\npnpm run check solutions/automations\n```\n\n### Core Concepts\n\n#### 1. Basic Types\nUse `@param` and `@return` to define function inputs and outputs.\n\n```javascript\n/**\n * Adds two numbers.\n * @param {number} a The first number.\n * @param {number} b The second number.\n * @return {number} The sum.\n */\nfunction add(a, b) {\n  return a + b;\n}\n```\n\n#### 2. Apps Script Types\nYou can reference global Apps Script types directly.\n\n```javascript\n/**\n * Gets the active sheet name.\n * @return {string} The name of the sheet.\n */\nfunction getSheetName() {\n  // Types like SpreadsheetApp, Sheet, Range are available globally\n  const sheet = SpreadsheetApp.getActiveSheet();\n  return sheet.getName();\n}\n```\n\n#### 3. Optional Parameters\nUse `[]` or `=` to denote optional parameters.\n\n```javascript\n/**\n * @param {string} name The name.\n * @param {number=} age Optional age.\n */\nfunction greet(name, age) {\n  if (age) { ... }\n}\n```\n\n### Advanced Patterns\n\n#### 1. Custom Objects (@typedef)\nFor complex objects, define a type using `@typedef`.\n\n```javascript\n/**\n * @typedef {Object} UserConfig\n * @property {string} username The user's name.\n * @property {boolean} isAdmin Whether the user is an admin.\n * @property {number} [retryCount] Optional retry attempts.\n */\n\n/**\n * Processes a user configuration.\n * @param {UserConfig} config The configuration object.\n */\nfunction processUser(config) {\n  console.log(config.username);\n}\n```\n\n#### 2. Type Casting\nSometimes the type checker cannot infer the type correctly. Use inline `@type` to cast.\n\n```javascript\nconst data = JSON.parse(jsonString);\n\n/** @type {UserConfig} */\nconst config = data;\n```\n\n#### 3. Arrays and Generics\nSpecify array contents clearly.\n\n```javascript\n/**\n * @param {string[]} names An array of strings.\n * @return {Array<number>} An array of numbers.\n */\nfunction lengths(names) {\n  return names.map(n => n.length);\n}\n```\n\n#### 4. Handling `null` and `undefined`\nBe explicit if a value can be null.\n\n```javascript\n/**\n * @param {string|null} id The ID, or null if not found.\n */\nfunction find(id) { ... }\n```\n\n### Common Issues & Fixes\n\n- **TypeScript**: DO NOT REFERENCE GoogleAppsScript in JSDocs. Instead use a locally defined type definition and link to the appropriate reference documenation page if possible.\n- **\"Property 'x' does not exist on type 'Object'\"**: This usually means you are accessing a property on a generic object. Define a `@typedef` for that object structure.\n- **Implicit 'any'**: If you see \"Parameter 'x' implicitly has an 'any' type\", it means you forgot a JSDoc `@param` tag. Add it to fix the error.\n- **Advanced Services**: To fix errors with these globals, check for existence. This helps TypeScript narrow the type and prevents runtime errors if the service is not enabled.\n\n   ```js\n   if (!AdminDirectory) {\n     console.log('AdminDirectory Advanced Service must be enabled.');\n     return;\n   }\n   ```\n\n- **Optional Properties**: Use optional chaining (`?.`) when accessing properties that might be undefined in API responses. This is often the case when when using `fields` to limit the response.\n\n   ```js\n   // Safe access\n   console.log(user.name?.fullName);\n   ```\n\n- **Error Handling**: Avoid wrapping code in `try/catch` blocks if you are only logging the error message. Let the runtime handle the error reporting for cleaner sample code.\n\n   ```js\n   // Avoid this\n   try {\n     AdminDirectory.Users.list();\n   } catch (err) {\n     console.log(err.message);\n   }\n\n   // Prefer this\n   AdminDirectory.Users.list();\n   ```"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Google Apps Script Samples\n\nVarious sample code and projects for the Google Apps Script platform, a JavaScript platform in the cloud.\n\nLearn more at [developers.google.com](https://developers.google.com/apps-script).\n\n## Google APIs\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/admin_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### AdminSDK\n- [Manage domains and apps](adminSDK)\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/google_cloud_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Advanced Services\n- [Access Google APIs via Advanced Google services](advanced/)\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/calendar_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Calendar\n- [List upcoming events](calendar/quickstart)\n- [Create a vacation calendar](solutions/automations/vacation-calendar/Code.js)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/classroom_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Classroom\n- [Manage Google Classroom](classroom/quickstart)\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/data_studio_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Data Studio\n- [Build a connector](data-studio/build.gs)\n- [Authentication and Authorization](data-studio/auth.gs)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/docs_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Docs\n- [Cursor inspector add-on](docs/cursorInspector)\n- [Translate add-on](docs/translate)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/drive_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Drive\n- [Manage Google Drive files and folders](drive/quickstart)\n- [View Google Drive activity](drive/activity)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/forms_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Forms\n- [Notification add-on](forms)\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/gmail_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Gmail\n- [Sending email](gmail/sendingEmails)\n- [Mailmerge: Merge a template email with content](gmail/mailmerge)\n\n<img\nsrc=\"https://www.gstatic.com/images/icons/material/system/2x/people_black_48dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### People\n- [Listing Connections](people/quickstart)\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/sheets_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Sheets\n- [Managing Responses for Google Forms](sheets)\n- [Menus and Custom Functions](sheets)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/slides_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Slides\n- [Translate Slides Add-on](slides/translate)\n- [Progress Bars add-on](slides/progress)\n\n<img\nsrc=\"https://www.gstatic.com/images/branding/product/2x/tasks_96dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Tasks\n- [List Tasks](tasks/quickstart)\n- [Simple Tasks Web App](tasks/simpleTasks)\n\n<img\nsrc=\"https://www.gstatic.com/images/icons/material/system/2x/code_grey600_48dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Templates\n- Build off a working framework for new Apps Script projects.\n<br><br>\n\n<img\nsrc=\"https://www.gstatic.com/images/icons/material/system/2x/alarm_grey600_48dp.png\"\nalign=\"left\"\nwidth=\"96px\"/>\n### Triggers\n- Call an Apps Script function such as `onOpen`, `onEdit`, or `onInstall` in an add-on\n- Create a [time-driven trigger](https://developers.google.com/apps-script/guides/triggers/installable#time_driven_triggers)\n\n## Codelabs\n\nCodelab tutorials combine detailed explanation, coding exercises, and documented best practices to help engineers get up to speed with key Google technologies. Here's a list of Apps Script codelabs:\n\n- [Apps Script Intro](http://g.co/codelabs/apps-script-intro)\n- [Apps Script CLI – clasp](http://g.co/codelabs/clasp)\n- [BigQuery + Sheets + Slides](http://g.co/codelabs/bigquery-sheets-slides)\n- [Docs Add-on + Cloud Natural Language API](http://g.co/codelabs/nlp-docs)\n- [Gmail Add-ons](http://g.co/codelabs/gmail-add-ons)\n- [Google Chat Apps](https://developers.google.com/codelabs/chat-apps-script)\n\n## Clone using the `clasp` command-line tool\n\nLearn how to clone, pull, and push Apps Script projects on the command-line\nusing [clasp](https://developers.google.com/apps-script/guides/clasp).\n\n## Lint\n\nRun ESLint over this whole repository with:\n\n```shell\npnpm lint\n```\n\nThis command will fix simple errors.\n\n## Type Checking\n\nRun the TypeScript-based check over the repository with:\n\n```shell\npnpm check\n```\n\nThis command validates `.gs` files by temporarily converting them to `.js` and running `tsc`. It checks for syntax errors and type issues using JSDoc annotations.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Report a security issue\n\nTo report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use\n[https://g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here on\nGitHub (including using GitHub Security Advisory). The Google Security Team will\nrespond within 5 working days of your report on [https://g.co/vulnz](https://g.co/vulnz).\n"
  },
  {
    "path": "adminSDK/directory/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START admin_sdk_directory_quickstart]\n/**\n * Lists users in a Google Workspace domain.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list\n */\nfunction listUsers() {\n  const optionalArgs = {\n    customer: \"my_customer\",\n    maxResults: 10,\n    orderBy: \"email\",\n  };\n  if (!AdminDirectory || !AdminDirectory.Users) {\n    throw new Error(\"Enable the AdminDirectory Advanced Service.\");\n  }\n  const response = AdminDirectory.Users.list(optionalArgs);\n  const users = response.users;\n  if (!users || users.length === 0) {\n    console.log(\"No users found.\");\n    return;\n  }\n  // Print the list of user's full name and email\n  console.log(\"Users:\");\n  for (const user of users) {\n    if (user.primaryEmail) {\n      if (user.name?.fullName) {\n        console.log(\"%s (%s)\", user.primaryEmail, user.name.fullName);\n      } else {\n        console.log(\"%s\", user.primaryEmail);\n      }\n    }\n  }\n}\n// [END admin_sdk_directory_quickstart]\n"
  },
  {
    "path": "adminSDK/reports/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START admin_sdk_reports_quickstart]\n/**\n * List login events for a Google Workspace domain.\n * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list\n */\nfunction listLogins() {\n  const userKey = \"all\";\n  const applicationName = \"login\";\n  const optionalArgs = {\n    maxResults: 10,\n  };\n  if (!AdminReports || !AdminReports.Activities) {\n    throw new Error(\"Enable the AdminReports Advanced Service.\");\n  }\n  const response = AdminReports.Activities.list(\n    userKey,\n    applicationName,\n    optionalArgs,\n  );\n  const activities = response.items;\n  if (!activities || activities.length === 0) {\n    console.log(\"No logins found.\");\n    return;\n  }\n  // Print login events\n  console.log(\"Logins:\");\n  for (const activity of activities) {\n    if (\n      activity.id?.time &&\n      activity.actor?.email &&\n      activity.events?.[0]?.name\n    ) {\n      console.log(\n        \"%s: %s (%s)\",\n        activity.id.time,\n        activity.actor.email,\n        activity.events[0].name,\n      );\n    }\n  }\n}\n// [END admin_sdk_reports_quickstart]\n"
  },
  {
    "path": "adminSDK/reseller/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START admin_sdk_reseller_quickstart]\n/**\n * List Admin SDK reseller.\n * @see https://developers.google.com/admin-sdk/reseller/reference/rest/v1/subscriptions/list\n */\nfunction listSubscriptions() {\n  const optionalArgs = {\n    maxResults: 10,\n  };\n  if (!AdminReseller || !AdminReseller.Subscriptions) {\n    throw new Error(\"Enable the AdminReseller Advanced Service.\");\n  }\n  const response = AdminReseller.Subscriptions.list(optionalArgs);\n  const subscriptions = response.subscriptions;\n  if (!subscriptions || subscriptions.length === 0) {\n    console.log(\"No subscriptions found.\");\n    return;\n  }\n  console.log(\"Subscriptions:\");\n  for (const subscription of subscriptions) {\n    if (subscription.customerId && subscription.skuId) {\n      if (subscription.plan?.planName) {\n        console.log(\n          \"%s (%s, %s)\",\n          subscription.customerId,\n          subscription.skuId,\n          subscription.plan.planName,\n        );\n      } else {\n        console.log(\"%s (%s)\", subscription.customerId, subscription.skuId);\n      }\n    }\n  }\n}\n// [END admin_sdk_reseller_quickstart]\n"
  },
  {
    "path": "advanced/README.md",
    "content": "# Advanced Services Samples\n\nThis directory contains samples for using Apps Script Advanced Services.\n\n> Note: These services must be [enabled](https://developers.google.com/apps-script/guides/services/advanced#enabling_advanced_services) before running these samples.\n\nLearn more at [developers.google.com](https://developers.google.com/apps-script/guides/services/advanced).\n"
  },
  {
    "path": "advanced/adminSDK.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_admin_sdk_list_all_users]\n/**\n * Lists all the users in a domain sorted by first name.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list\n */\nfunction listAllUsers() {\n  let pageToken;\n  let page;\n  do {\n    page = AdminDirectory.Users.list({\n      domain: \"example.com\",\n      orderBy: \"givenName\",\n      maxResults: 100,\n      pageToken: pageToken,\n    });\n    const users = page.users;\n    if (!users) {\n      console.log(\"No users found.\");\n      return;\n    }\n    // Print the user's full name and email.\n    for (const user of users) {\n      console.log(\"%s (%s)\", user.name.fullName, user.primaryEmail);\n    }\n    pageToken = page.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_admin_sdk_list_all_users]\n\n// [START apps_script_admin_sdk_get_users]\n/**\n * Get a user by their email address and logs all of their data as a JSON string.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get\n */\nfunction getUser() {\n  // TODO (developer) - Replace userEmail value with yours\n  const userEmail = \"liz@example.com\";\n  try {\n    const user = AdminDirectory.Users.get(userEmail);\n    console.log(\"User data:\\n %s\", JSON.stringify(user, null, 2));\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_get_users]\n\n// [START apps_script_admin_sdk_add_user]\n/**\n * Adds a new user to the domain, including only the required information. For\n * the full list of user fields, see the API's reference documentation:\n * @see https://developers.google.com/admin-sdk/directory/v1/reference/users/insert\n */\nfunction addUser() {\n  let user = {\n    // TODO (developer) - Replace primaryEmail value with yours\n    primaryEmail: \"liz@example.com\",\n    name: {\n      givenName: \"Elizabeth\",\n      familyName: \"Smith\",\n    },\n    // Generate a random password string.\n    password: Math.random().toString(36),\n  };\n  try {\n    user = AdminDirectory.Users.insert(user);\n    console.log(\"User %s created with ID %s.\", user.primaryEmail, user.id);\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_add_user]\n\n// [START apps_script_admin_sdk_create_alias]\n/**\n * Creates an alias (nickname) for a user.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users.aliases/insert\n */\nfunction createAlias() {\n  // TODO (developer) - Replace userEmail value with yours\n  const userEmail = \"liz@example.com\";\n  let alias = {\n    alias: \"chica@example.com\",\n  };\n  try {\n    alias = AdminDirectory.Users.Aliases.insert(alias, userEmail);\n    console.log(\"Created alias %s for user %s.\", alias.alias, userEmail);\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_create_alias]\n\n// [START apps_script_admin_sdk_list_all_groups]\n/**\n * Lists all the groups in the domain.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list\n */\nfunction listAllGroups() {\n  let pageToken;\n  let page;\n  do {\n    page = AdminDirectory.Groups.list({\n      domain: \"example.com\",\n      maxResults: 100,\n      pageToken: pageToken,\n    });\n    const groups = page.groups;\n    if (!groups) {\n      console.log(\"No groups found.\");\n      return;\n    }\n    // Print group name and email.\n    for (const group of groups) {\n      console.log(\"%s (%s)\", group.name, group.email);\n    }\n    pageToken = page.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_admin_sdk_list_all_groups]\n\n// [START apps_script_admin_sdk_add_group_member]\n/**\n * Adds a user to an existing group in the domain.\n * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert\n */\nfunction addGroupMember() {\n  // TODO (developer) - Replace userEmail value with yours\n  const userEmail = \"liz@example.com\";\n  // TODO (developer) - Replace groupEmail value with yours\n  const groupEmail = \"bookclub@example.com\";\n  const member = {\n    email: userEmail,\n    role: \"MEMBER\",\n  };\n  try {\n    AdminDirectory.Members.insert(member, groupEmail);\n    console.log(\n      \"User %s added as a member of group %s.\",\n      userEmail,\n      groupEmail,\n    );\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_add_group_member]\n\n// [START apps_script_admin_sdk_migrate]\n/**\n * Gets three RFC822 formatted messages from the each of the latest three\n * threads in the user's Gmail inbox, creates a blob from the email content\n * (including attachments), and inserts it in a Google Group in the domain.\n */\nfunction migrateMessages() {\n  // TODO (developer) - Replace groupId value with yours\n  const groupId = \"exampleGroup@example.com\";\n  const messagesToMigrate = getRecentMessagesContent();\n  for (const messageContent of messagesToMigrate) {\n    const contentBlob = Utilities.newBlob(messageContent, \"message/rfc822\");\n    AdminGroupsMigration.Archive.insert(groupId, contentBlob);\n  }\n}\n\n/**\n * Gets a list of recent messages' content from the user's Gmail account.\n * By default, fetches 3 messages from the latest 3 threads.\n *\n * @return {Array} the messages' content.\n */\nfunction getRecentMessagesContent() {\n  const NUM_THREADS = 3;\n  const NUM_MESSAGES = 3;\n  const threads = GmailApp.getInboxThreads(0, NUM_THREADS);\n  const messages = GmailApp.getMessagesForThreads(threads);\n  const messagesContent = [];\n  for (let i = 0; i < messages.length; i++) {\n    for (let j = 0; j < NUM_MESSAGES; j++) {\n      const message = messages[i][j];\n      if (message) {\n        messagesContent.push(message.getRawContent());\n      }\n    }\n  }\n  return messagesContent;\n}\n// [END apps_script_admin_sdk_migrate]\n\n// [START apps_script_admin_sdk_get_group_setting]\n/**\n * Gets a group's settings and logs them to the console.\n */\nfunction getGroupSettings() {\n  // TODO (developer) - Replace groupId value with yours\n  const groupId = \"exampleGroup@example.com\";\n  try {\n    const group = AdminGroupsSettings.Groups.get(groupId);\n    console.log(JSON.stringify(group, null, 2));\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_get_group_setting]\n\n// [START apps_script_admin_sdk_update_group_setting]\n/**\n * Updates group's settings. Here, the description is modified, but various\n * other settings can be changed in the same way.\n * @see https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups/patch\n */\nfunction updateGroupSettings() {\n  const groupId = \"exampleGroup@example.com\";\n  try {\n    const group = AdminGroupsSettings.newGroups();\n    group.description = \"Newly changed group description\";\n    AdminGroupsSettings.Groups.patch(group, groupId);\n  } catch (err) {\n    // TODO (developer)- Handle exception from the API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_admin_sdk_update_group_setting]\n\n// [START apps_script_admin_sdk_get_license_assignments]\n/**\n * Logs the license assignments, including the product ID and the sku ID, for\n * the users in the domain. Notice the use of page tokens to access the full\n * list of results.\n */\nfunction getLicenseAssignments() {\n  const productId = \"Google-Apps\";\n  const customerId = \"example.com\";\n  let assignments = [];\n  let pageToken = null;\n  do {\n    const response = AdminLicenseManager.LicenseAssignments.listForProduct(\n      productId,\n      customerId,\n      {\n        maxResults: 500,\n        pageToken: pageToken,\n      },\n    );\n    assignments = assignments.concat(response.items);\n    pageToken = response.nextPageToken;\n  } while (pageToken);\n  // Print the productId and skuId\n  for (const assignment of assignments) {\n    console.log(\n      \"userId: %s, productId: %s, skuId: %s\",\n      assignment.userId,\n      assignment.productId,\n      assignment.skuId,\n    );\n  }\n}\n// [END apps_script_admin_sdk_get_license_assignments]\n\n// [START apps_script_admin_sdk_insert_license_assignment]\n/**\n * Insert a license assignment for a user, for a given product ID and sku ID\n * combination.\n * For more details follow the link\n * https://developers.google.com/admin-sdk/licensing/reference/rest/v1/licenseAssignments/insert\n */\nfunction insertLicenseAssignment() {\n  const productId = \"Google-Apps\";\n  const skuId = \"Google-Vault\";\n  const userId = \"marty@hoverboard.net\";\n  try {\n    const results = AdminLicenseManager.LicenseAssignments.insert(\n      { userId: userId },\n      productId,\n      skuId,\n    );\n    console.log(results);\n  } catch (e) {\n    // TODO (developer) - Handle exception.\n    console.log(\"Failed with an error %s \", e.message);\n  }\n}\n// [END apps_script_admin_sdk_insert_license_assignment]\n\n// [START apps_script_admin_sdk_generate_login_activity_report]\n/**\n * Generates a login activity report for the last week as a spreadsheet. The\n * report includes the time, user, and login result.\n * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list\n */\nfunction generateLoginActivityReport() {\n  const now = new Date();\n  const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\n  const startTime = oneWeekAgo.toISOString();\n  const endTime = now.toISOString();\n\n  const rows = [];\n  let pageToken;\n  let page;\n  do {\n    page = AdminReports.Activities.list(\"all\", \"login\", {\n      startTime: startTime,\n      endTime: endTime,\n      maxResults: 500,\n      pageToken: pageToken,\n    });\n    const items = page.items;\n    if (items) {\n      for (const item of items) {\n        const row = [\n          new Date(item.id.time),\n          item.actor.email,\n          item.events[0].name,\n        ];\n        rows.push(row);\n      }\n    }\n    pageToken = page.nextPageToken;\n  } while (pageToken);\n\n  if (rows.length === 0) {\n    console.log(\"No results returned.\");\n    return;\n  }\n  const spreadsheet = SpreadsheetApp.create(\"Google Workspace Login Report\");\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  const headers = [\"Time\", \"User\", \"Login Result\"];\n  sheet.appendRow(headers);\n\n  // Append the results.\n  sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);\n\n  console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n}\n// [END apps_script_admin_sdk_generate_login_activity_report]\n\n// [START apps_script_admin_sdk_generate_user_usage_report]\n/**\n * Generates a user usage report for this day last week as a spreadsheet. The\n * report includes the date, user, last login time, number of emails received,\n * and number of drive files created.\n * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/userUsageReport/get\n */\nfunction generateUserUsageReport() {\n  const today = new Date();\n  const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);\n  const timezone = Session.getScriptTimeZone();\n  const date = Utilities.formatDate(oneWeekAgo, timezone, \"yyyy-MM-dd\");\n\n  const parameters = [\n    \"accounts:last_login_time\",\n    \"gmail:num_emails_received\",\n    \"drive:num_items_created\",\n  ];\n  const rows = [];\n  let pageToken;\n  let page;\n  do {\n    page = AdminReports.UserUsageReport.get(\"all\", date, {\n      parameters: parameters.join(\",\"),\n      maxResults: 500,\n      pageToken: pageToken,\n    });\n    if (page.warnings) {\n      for (const warning of page.warnings) {\n        console.log(warning.message);\n      }\n    }\n    const reports = page.usageReports;\n    if (reports) {\n      for (const report of reports) {\n        const parameterValues = getParameterValues(report.parameters);\n        const row = [\n          report.date,\n          report.entity.userEmail,\n          parameterValues[\"accounts:last_login_time\"],\n          parameterValues[\"gmail:num_emails_received\"],\n          parameterValues[\"drive:num_items_created\"],\n        ];\n        rows.push(row);\n      }\n    }\n    pageToken = page.nextPageToken;\n  } while (pageToken);\n\n  if (rows.length === 0) {\n    console.log(\"No results returned.\");\n    return;\n  }\n  const spreadsheet = SpreadsheetApp.create(\n    \"Google Workspace User Usage Report\",\n  );\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  const headers = [\n    \"Date\",\n    \"User\",\n    \"Last Login\",\n    \"Num Emails Received\",\n    \"Num Drive Files Created\",\n  ];\n  sheet.appendRow(headers);\n\n  // Append the results.\n  sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);\n\n  console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n}\n\n/**\n * Gets a map of parameter names to values from an array of parameter objects.\n * @param {Array} parameters An array of parameter objects.\n * @return {Object} A map from parameter names to their values.\n */\nfunction getParameterValues(parameters) {\n  return parameters.reduce((result, parameter) => {\n    const name = parameter.name;\n    let value;\n    if (parameter.intValue !== undefined) {\n      value = parameter.intValue;\n    } else if (parameter.stringValue !== undefined) {\n      value = parameter.stringValue;\n    } else if (parameter.datetimeValue !== undefined) {\n      value = new Date(parameter.datetimeValue);\n    } else if (parameter.boolValue !== undefined) {\n      value = parameter.boolValue;\n    }\n    result[name] = value;\n    return result;\n  }, {});\n}\n// [END apps_script_admin_sdk_generate_user_usage_report]\n\n// [START apps_script_admin_sdk_get_subscriptions]\n/**\n * Logs the list of subscriptions, including the customer ID, date created, plan\n * name, and the sku ID. Notice the use of page tokens to access the full list\n * of results.\n * @see https://developers.google.com/admin-sdk/reseller/reference/rest/v1/subscriptions/list\n */\nfunction getSubscriptions() {\n  let result;\n  let pageToken;\n  do {\n    result = AdminReseller.Subscriptions.list({\n      pageToken: pageToken,\n    });\n    for (const sub of result.subscriptions) {\n      const creationDate = new Date();\n      creationDate.setUTCSeconds(sub.creationTime);\n      console.log(\n        \"customer ID: %s, date created: %s, plan name: %s, sku id: %s\",\n        sub.customerId,\n        creationDate.toDateString(),\n        sub.plan.planName,\n        sub.skuId,\n      );\n    }\n    pageToken = result.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_admin_sdk_get_subscriptions]\n"
  },
  {
    "path": "advanced/adsense.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_adsense_list_accounts]\n/**\n * Lists available AdSense accounts.\n */\nfunction listAccounts() {\n  let pageToken;\n  do {\n    const response = AdSense.Accounts.list({ pageToken: pageToken });\n    if (!response.accounts) {\n      console.log(\"No accounts found.\");\n      return;\n    }\n    for (const account of response.accounts) {\n      console.log(\n        'Found account with resource name \"%s\" and display name \"%s\".',\n        account.name,\n        account.displayName,\n      );\n    }\n    pageToken = response.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_adsense_list_accounts]\n\n// [START apps_script_adsense_list_ad_clients]\n/**\n * Logs available Ad clients for an account.\n *\n * @param {string} accountName The resource name of the account that owns the\n *     collection of ad clients.\n */\nfunction listAdClients(accountName) {\n  let pageToken;\n  do {\n    const response = AdSense.Accounts.Adclients.list(accountName, {\n      pageToken: pageToken,\n    });\n    if (!response.adClients) {\n      console.log(\"No ad clients found for this account.\");\n      return;\n    }\n    for (const adClient of response.adClients) {\n      console.log(\n        'Found ad client for product \"%s\" with resource name \"%s\".',\n        adClient.productCode,\n        adClient.name,\n      );\n      console.log(\n        \"Reporting dimension ID: %s\",\n        adClient.reportingDimensionId ?? \"None\",\n      );\n    }\n    pageToken = response.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_adsense_list_ad_clients]\n\n// [START apps_script_adsense_list_ad_units]\n/**\n * Lists ad units.\n * @param {string} adClientName The resource name of the ad client that owns the collection\n *     of ad units.\n */\nfunction listAdUnits(adClientName) {\n  let pageToken;\n  do {\n    const response = AdSense.Accounts.Adclients.Adunits.list(adClientName, {\n      pageSize: 50,\n      pageToken: pageToken,\n    });\n    if (!response.adUnits) {\n      console.log(\"No ad units found for this ad client.\");\n      return;\n    }\n    for (const adUnit of response.adUnits) {\n      console.log(\n        'Found ad unit with resource name \"%s\" and display name \"%s\".',\n        adUnit.name,\n        adUnit.displayName,\n      );\n    }\n\n    pageToken = response.nextPageToken;\n  } while (pageToken);\n}\n// [END apps_script_adsense_list_ad_units]\n\n// [START apps_script_adsense_generate_report]\n/**\n * Generates a spreadsheet report for a specific ad client in an account.\n * @param {string} accountName The resource name of the account.\n * @param {string} adClientReportingDimensionId The reporting dimension ID\n *     of the ad client.\n */\nfunction generateReport(accountName, adClientReportingDimensionId) {\n  // Prepare report.\n  const today = new Date();\n  const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);\n\n  const report = AdSense.Accounts.Reports.generate(accountName, {\n    // Specify the desired ad client using a filter.\n    filters: [\n      `AD_CLIENT_ID==${escapeFilterParameter(adClientReportingDimensionId)}`,\n    ],\n    metrics: [\n      \"PAGE_VIEWS\",\n      \"AD_REQUESTS\",\n      \"AD_REQUESTS_COVERAGE\",\n      \"CLICKS\",\n      \"AD_REQUESTS_CTR\",\n      \"COST_PER_CLICK\",\n      \"AD_REQUESTS_RPM\",\n      \"ESTIMATED_EARNINGS\",\n    ],\n    dimensions: [\"DATE\"],\n    ...dateToJson(\"startDate\", oneWeekAgo),\n    ...dateToJson(\"endDate\", today),\n    // Sort by ascending date.\n    orderBy: [\"+DATE\"],\n  });\n\n  if (!report.rows) {\n    console.log(\"No rows returned.\");\n    return;\n  }\n  const spreadsheet = SpreadsheetApp.create(\"AdSense Report\");\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  sheet.appendRow(report.headers.map((header) => header.name));\n\n  // Append the results.\n  sheet\n    .getRange(2, 1, report.rows.length, report.headers.length)\n    .setValues(report.rows.map((row) => row.cells.map((cell) => cell.value)));\n\n  console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n}\n\n/**\n * Escape special characters for a parameter being used in a filter.\n * @param {string} parameter The parameter to be escaped.\n * @return {string} The escaped parameter.\n */\nfunction escapeFilterParameter(parameter) {\n  return parameter.replace(\"\\\\\", \"\\\\\\\\\").replace(\",\", \"\\\\,\");\n}\n\n/**\n * Returns the JSON representation of a Date object (as a google.type.Date).\n *\n * @param {string} paramName the name of the date parameter\n * @param {Date} value the date\n * @return {object} formatted date\n */\nfunction dateToJson(paramName, value) {\n  return {\n    [`${paramName}.year`]: value.getFullYear(),\n    [`${paramName}.month`]: value.getMonth() + 1,\n    [`${paramName}.day`]: value.getDate(),\n  };\n}\n\n// [END apps_script_adsense_generate_report]\n"
  },
  {
    "path": "advanced/analytics.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_analytics_accounts]\n/**\n * Lists Analytics accounts.\n */\nfunction listAccounts() {\n  try {\n    const accounts = Analytics.Management.Accounts.list();\n    if (!accounts.items || !accounts.items.length) {\n      console.log(\"No accounts found.\");\n      return;\n    }\n\n    for (let i = 0; i < accounts.items.length; i++) {\n      const account = accounts.items[i];\n      console.log('Account: name \"%s\", id \"%s\".', account.name, account.id);\n\n      // List web properties in the account.\n      listWebProperties(account.id);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Lists web properites for an Analytics account.\n * @param  {string} accountId The account ID.\n */\nfunction listWebProperties(accountId) {\n  try {\n    const webProperties = Analytics.Management.Webproperties.list(accountId);\n    if (!webProperties.items || !webProperties.items.length) {\n      console.log(\"\\tNo web properties found.\");\n      return;\n    }\n    for (let i = 0; i < webProperties.items.length; i++) {\n      const webProperty = webProperties.items[i];\n      console.log(\n        '\\tWeb Property: name \"%s\", id \"%s\".',\n        webProperty.name,\n        webProperty.id,\n      );\n\n      // List profiles in the web property.\n      listProfiles(accountId, webProperty.id);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Logs a list of Analytics accounts profiles.\n * @param  {string} accountId     The Analytics account ID\n * @param  {string} webPropertyId The web property ID\n */\nfunction listProfiles(accountId, webPropertyId) {\n  // Note: If you experience \"Quota Error: User Rate Limit Exceeded\" errors\n  // due to the number of accounts or profiles you have, you may be able to\n  // avoid it by adding a Utilities.sleep(1000) statement here.\n  try {\n    const profiles = Analytics.Management.Profiles.list(\n      accountId,\n      webPropertyId,\n    );\n\n    if (!profiles.items || !profiles.items.length) {\n      console.log(\"\\t\\tNo web properties found.\");\n      return;\n    }\n    for (let i = 0; i < profiles.items.length; i++) {\n      const profile = profiles.items[i];\n      console.log('\\t\\tProfile: name \"%s\", id \"%s\".', profile.name, profile.id);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_analytics_accounts]\n\n// [START apps_script_analytics_reports]\n/**\n * Runs a report of an Analytics profile ID. Creates a sheet with the report.\n * @param  {string} profileId The profile ID.\n */\nfunction runReport(profileId) {\n  const today = new Date();\n  const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);\n\n  const startDate = Utilities.formatDate(\n    oneWeekAgo,\n    Session.getScriptTimeZone(),\n    \"yyyy-MM-dd\",\n  );\n  const endDate = Utilities.formatDate(\n    today,\n    Session.getScriptTimeZone(),\n    \"yyyy-MM-dd\",\n  );\n\n  const tableId = `ga:${profileId}`;\n  const metric = \"ga:visits\";\n  const options = {\n    dimensions: \"ga:source,ga:keyword\",\n    sort: \"-ga:visits,ga:source\",\n    filters: \"ga:medium==organic\",\n    \"max-results\": 25,\n  };\n  const report = Analytics.Data.Ga.get(\n    tableId,\n    startDate,\n    endDate,\n    metric,\n    options,\n  );\n\n  if (!report.rows) {\n    console.log(\"No rows returned.\");\n    return;\n  }\n\n  const spreadsheet = SpreadsheetApp.create(\"Google Analytics Report\");\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  const headers = report.columnHeaders.map((columnHeader) => {\n    return columnHeader.name;\n  });\n  sheet.appendRow(headers);\n\n  // Append the results.\n  sheet\n    .getRange(2, 1, report.rows.length, headers.length)\n    .setValues(report.rows);\n\n  console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n}\n// [END apps_script_analytics_reports]\n"
  },
  {
    "path": "advanced/analyticsAdmin.gs",
    "content": "/**\n * Copyright 2021 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_analyticsadmin]\n/**\n * Logs the Google Analytics accounts accessible by the current user.\n */\nfunction listAccounts() {\n  try {\n    accounts = AnalyticsAdmin.Accounts.list();\n    if (!accounts.items || !accounts.items.length) {\n      console.log(\"No accounts found.\");\n      return;\n    }\n\n    for (let i = 0; i < accounts.items.length; i++) {\n      const account = accounts.items[i];\n      console.log(\n        'Account: name \"%s\", displayName \"%s\".',\n        account.name,\n        account.displayName,\n      );\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_analyticsadmin]\n"
  },
  {
    "path": "advanced/analyticsData.gs",
    "content": "/**\n * Copyright 2021 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_analyticsdata]\n/**\n * Runs a report of a Google Analytics 4 property ID. Creates a sheet with the\n * report.\n */\nfunction runReport() {\n  /**\n   * TODO(developer): Uncomment this variable and replace with your\n   *   Google Analytics 4 property ID before running the sample.\n   */\n  const propertyId = \"YOUR-GA4-PROPERTY-ID\";\n\n  try {\n    const metric = AnalyticsData.newMetric();\n    metric.name = \"activeUsers\";\n\n    const dimension = AnalyticsData.newDimension();\n    dimension.name = \"city\";\n\n    const dateRange = AnalyticsData.newDateRange();\n    dateRange.startDate = \"2020-03-31\";\n    dateRange.endDate = \"today\";\n\n    const request = AnalyticsData.newRunReportRequest();\n    request.dimensions = [dimension];\n    request.metrics = [metric];\n    request.dateRanges = dateRange;\n\n    const report = AnalyticsData.Properties.runReport(\n      request,\n      `properties/${propertyId}`,\n    );\n    if (!report.rows) {\n      console.log(\"No rows returned.\");\n      return;\n    }\n\n    const spreadsheet = SpreadsheetApp.create(\"Google Analytics Report\");\n    const sheet = spreadsheet.getActiveSheet();\n\n    // Append the headers.\n    const dimensionHeaders = report.dimensionHeaders.map((dimensionHeader) => {\n      return dimensionHeader.name;\n    });\n    const metricHeaders = report.metricHeaders.map((metricHeader) => {\n      return metricHeader.name;\n    });\n    const headers = [...dimensionHeaders, ...metricHeaders];\n\n    sheet.appendRow(headers);\n\n    // Append the results.\n    const rows = report.rows.map((row) => {\n      const dimensionValues = row.dimensionValues.map((dimensionValue) => {\n        return dimensionValue.value;\n      });\n      const metricValues = row.metricValues.map((metricValues) => {\n        return metricValues.value;\n      });\n      return [...dimensionValues, ...metricValues];\n    });\n\n    sheet.getRange(2, 1, report.rows.length, headers.length).setValues(rows);\n\n    console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_analyticsdata]\n"
  },
  {
    "path": "advanced/bigquery.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_bigquery_run_query]\n/**\n * Runs a BigQuery query and logs the results in a spreadsheet.\n */\nfunction runQuery() {\n  // Replace this value with the project ID listed in the Google\n  // Cloud Platform project.\n  const projectId = \"XXXXXXXX\";\n\n  const request = {\n    // TODO (developer) - Replace query with yours\n    query:\n      \"SELECT refresh_date AS Day, term AS Top_Term, rank \" +\n      \"FROM `bigquery-public-data.google_trends.top_terms` \" +\n      \"WHERE rank = 1 \" +\n      \"AND refresh_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 2 WEEK) \" +\n      \"GROUP BY Day, Top_Term, rank \" +\n      \"ORDER BY Day DESC;\",\n    useLegacySql: false,\n  };\n  let queryResults = BigQuery.Jobs.query(request, projectId);\n  const jobId = queryResults.jobReference.jobId;\n\n  // Check on status of the Query Job.\n  let sleepTimeMs = 500;\n  while (!queryResults.jobComplete) {\n    Utilities.sleep(sleepTimeMs);\n    sleepTimeMs *= 2;\n    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);\n  }\n\n  // Get all the rows of results.\n  let rows = queryResults.rows;\n  while (queryResults.pageToken) {\n    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {\n      pageToken: queryResults.pageToken,\n    });\n    rows = rows.concat(queryResults.rows);\n  }\n\n  if (!rows) {\n    console.log(\"No rows returned.\");\n    return;\n  }\n  const spreadsheet = SpreadsheetApp.create(\"BigQuery Results\");\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  const headers = queryResults.schema.fields.map((field) => field.name);\n  sheet.appendRow(headers);\n\n  // Append the results.\n  const data = new Array(rows.length);\n  for (let i = 0; i < rows.length; i++) {\n    const cols = rows[i].f;\n    data[i] = new Array(cols.length);\n    for (let j = 0; j < cols.length; j++) {\n      data[i][j] = cols[j].v;\n    }\n  }\n  sheet.getRange(2, 1, rows.length, headers.length).setValues(data);\n\n  console.log(\"Results spreadsheet created: %s\", spreadsheet.getUrl());\n}\n// [END apps_script_bigquery_run_query]\n\n// [START apps_script_bigquery_load_csv]\n/**\n * Loads a CSV into BigQuery\n */\nfunction loadCsv() {\n  // Replace this value with the project ID listed in the Google\n  // Cloud Platform project.\n  const projectId = \"XXXXXXXX\";\n  // Create a dataset in the BigQuery UI (https://bigquery.cloud.google.com)\n  // and enter its ID below.\n  const datasetId = \"YYYYYYYY\";\n  // Sample CSV file of Google Trends data conforming to the schema below.\n  // https://docs.google.com/file/d/0BwzA1Orbvy5WMXFLaTR1Z1p2UDg/edit\n  const csvFileId = \"0BwzA1Orbvy5WMXFLaTR1Z1p2UDg\";\n\n  // Create the table.\n  const tableId = `pets_${new Date().getTime()}`;\n  let table = {\n    tableReference: {\n      projectId: projectId,\n      datasetId: datasetId,\n      tableId: tableId,\n    },\n    schema: {\n      fields: [\n        { name: \"week\", type: \"STRING\" },\n        { name: \"cat\", type: \"INTEGER\" },\n        { name: \"dog\", type: \"INTEGER\" },\n        { name: \"bird\", type: \"INTEGER\" },\n      ],\n    },\n  };\n  try {\n    table = BigQuery.Tables.insert(table, projectId, datasetId);\n    console.log(\"Table created: %s\", table.id);\n  } catch (err) {\n    console.log(\"unable to create table\");\n  }\n  // Load CSV data from Drive and convert to the correct format for upload.\n  const file = DriveApp.getFileById(csvFileId);\n  const data = file.getBlob().setContentType(\"application/octet-stream\");\n\n  // Create the data upload job.\n  const job = {\n    configuration: {\n      load: {\n        destinationTable: {\n          projectId: projectId,\n          datasetId: datasetId,\n          tableId: tableId,\n        },\n        skipLeadingRows: 1,\n      },\n    },\n  };\n  try {\n    const jobResult = BigQuery.Jobs.insert(job, projectId, data);\n    console.log(`Load job started. Status: ${jobResult.status.state}`);\n  } catch (err) {\n    console.log(\"unable to insert job\");\n  }\n}\n// [END apps_script_bigquery_load_csv]\n"
  },
  {
    "path": "advanced/calendar.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START calendar_list_calendars]\n/**\n * Lists the calendars shown in the user's calendar list.\n * @see https://developers.google.com/calendar/api/v3/reference/calendarList/list\n */\nfunction listCalendars() {\n  let calendars;\n  let pageToken;\n  do {\n    calendars = Calendar.CalendarList.list({\n      maxResults: 100,\n      pageToken: pageToken,\n    });\n    if (!calendars.items || calendars.items.length === 0) {\n      console.log(\"No calendars found.\");\n      return;\n    }\n    // Print the calendar id and calendar summary\n    for (const calendar of calendars.items) {\n      console.log(\"%s (ID: %s)\", calendar.summary, calendar.id);\n    }\n    pageToken = calendars.nextPageToken;\n  } while (pageToken);\n}\n// [END calendar_list_calendars]\n\n// [START calendar_create_event]\n/**\n * Creates an event in the user's default calendar.\n * @see https://developers.google.com/calendar/api/v3/reference/events/insert\n */\nfunction createEvent() {\n  const calendarId = \"primary\";\n  const start = getRelativeDate(1, 12);\n  const end = getRelativeDate(1, 13);\n  // event details for creating event.\n  let event = {\n    summary: \"Lunch Meeting\",\n    location: \"The Deli\",\n    description: \"To discuss our plans for the presentation next week.\",\n    start: {\n      dateTime: start.toISOString(),\n    },\n    end: {\n      dateTime: end.toISOString(),\n    },\n    attendees: [\n      { email: \"gduser1@workspacesample.dev\" },\n      { email: \"gduser2@workspacesample.dev\" },\n    ],\n    // Red background. Use Calendar.Colors.get() for the full list.\n    colorId: 11,\n  };\n  try {\n    // call method to insert/create new event in provided calandar\n    event = Calendar.Events.insert(event, calendarId);\n    console.log(`Event ID: ${event.id}`);\n  } catch (err) {\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n\n/**\n * Helper function to get a new Date object relative to the current date.\n * @param {number} daysOffset The number of days in the future for the new date.\n * @param {number} hour The hour of the day for the new date, in the time zone\n *     of the script.\n * @return {Date} The new date.\n */\nfunction getRelativeDate(daysOffset, hour) {\n  const date = new Date();\n  date.setDate(date.getDate() + daysOffset);\n  date.setHours(hour);\n  date.setMinutes(0);\n  date.setSeconds(0);\n  date.setMilliseconds(0);\n  return date;\n}\n// [END calendar_create_event]\n\n// [START calendar_list_events]\n/**\n * Lists the next 10 upcoming events in the user's default calendar.\n * @see https://developers.google.com/calendar/api/v3/reference/events/list\n */\nfunction listNext10Events() {\n  const calendarId = \"primary\";\n  const now = new Date();\n  const events = Calendar.Events.list(calendarId, {\n    timeMin: now.toISOString(),\n    singleEvents: true,\n    orderBy: \"startTime\",\n    maxResults: 10,\n  });\n  if (!events.items || events.items.length === 0) {\n    console.log(\"No events found.\");\n    return;\n  }\n  for (const event of events.items) {\n    if (event.start.date) {\n      // All-day event.\n      const start = new Date(event.start.date);\n      console.log(\"%s (%s)\", event.summary, start.toLocaleDateString());\n      continue;\n    }\n    const start = new Date(event.start.dateTime);\n    console.log(\"%s (%s)\", event.summary, start.toLocaleString());\n  }\n}\n// [END calendar_list_events]\n\n// [START calendar_log_synced_events]\n/**\n * Retrieve and log events from the given calendar that have been modified\n * since the last sync. If the sync token is missing or invalid, log all\n * events from up to a month ago (a full sync).\n *\n * @param {string} calendarId The ID of the calender to retrieve events from.\n * @param {boolean} fullSync If true, throw out any existing sync token and\n *        perform a full sync; if false, use the existing sync token if possible.\n */\nfunction logSyncedEvents(calendarId, fullSync) {\n  const properties = PropertiesService.getUserProperties();\n  const options = {\n    maxResults: 100,\n  };\n  const syncToken = properties.getProperty(\"syncToken\");\n  if (syncToken && !fullSync) {\n    options.syncToken = syncToken;\n  } else {\n    // Sync events up to thirty days in the past.\n    options.timeMin = getRelativeDate(-30, 0).toISOString();\n  }\n  // Retrieve events one page at a time.\n  let events;\n  let pageToken;\n  do {\n    try {\n      options.pageToken = pageToken;\n      events = Calendar.Events.list(calendarId, options);\n    } catch (e) {\n      // Check to see if the sync token was invalidated by the server;\n      // if so, perform a full sync instead.\n      if (\n        e.message === \"Sync token is no longer valid, a full sync is required.\"\n      ) {\n        properties.deleteProperty(\"syncToken\");\n        logSyncedEvents(calendarId, true);\n        return;\n      }\n      throw new Error(e.message);\n    }\n    if (events.items && events.items.length === 0) {\n      console.log(\"No events found.\");\n      return;\n    }\n    for (const event of events.items) {\n      if (event.status === \"cancelled\") {\n        console.log(\"Event id %s was cancelled.\", event.id);\n        return;\n      }\n      if (event.start.date) {\n        const start = new Date(event.start.date);\n        console.log(\"%s (%s)\", event.summary, start.toLocaleDateString());\n        return;\n      }\n      // Events that don't last all day; they have defined start times.\n      const start = new Date(event.start.dateTime);\n      console.log(\"%s (%s)\", event.summary, start.toLocaleString());\n    }\n    pageToken = events.nextPageToken;\n  } while (pageToken);\n  properties.setProperty(\"syncToken\", events.nextSyncToken);\n}\n// [END calendar_log_synced_events]\n\n// [START calendar_conditional_update]\n/**\n * Creates an event in the user's default calendar, waits 30 seconds, then\n * attempts to update the event's location, on the condition that the event\n * has not been changed since it was created.  If the event is changed during\n * the 30-second wait, then the subsequent update will throw a 'Precondition\n * Failed' error.\n *\n * The conditional update is accomplished by setting the 'If-Match' header\n * to the etag of the new event when it was created.\n */\nfunction conditionalUpdate() {\n  const calendarId = \"primary\";\n  const start = getRelativeDate(1, 12);\n  const end = getRelativeDate(1, 13);\n  let event = {\n    summary: \"Lunch Meeting\",\n    location: \"The Deli\",\n    description: \"To discuss our plans for the presentation next week.\",\n    start: {\n      dateTime: start.toISOString(),\n    },\n    end: {\n      dateTime: end.toISOString(),\n    },\n    attendees: [\n      { email: \"gduser1@workspacesample.dev\" },\n      { email: \"gduser2@workspacesample.dev\" },\n    ],\n    // Red background. Use Calendar.Colors.get() for the full list.\n    colorId: 11,\n  };\n  event = Calendar.Events.insert(event, calendarId);\n  console.log(`Event ID: ${event.getId()}`);\n  // Wait 30 seconds to see if the event has been updated outside this script.\n  Utilities.sleep(30 * 1000);\n  // Try to update the event, on the condition that the event state has not\n  // changed since the event was created.\n  event.location = \"The Coffee Shop\";\n  try {\n    event = Calendar.Events.update(\n      event,\n      calendarId,\n      event.id,\n      {},\n      { \"If-Match\": event.etag },\n    );\n    console.log(`Successfully updated event: ${event.id}`);\n  } catch (e) {\n    console.log(`Fetch threw an exception: ${e}`);\n  }\n}\n// [END calendar_conditional_update]\n\n// [START calendar_conditional_fetch]\n/**\n * Creates an event in the user's default calendar, then re-fetches the event\n * every second, on the condition that the event has changed since the last\n * fetch.\n *\n * The conditional fetch is accomplished by setting the 'If-None-Match' header\n * to the etag of the last known state of the event.\n */\nfunction conditionalFetch() {\n  const calendarId = \"primary\";\n  const start = getRelativeDate(1, 12);\n  const end = getRelativeDate(1, 13);\n  let event = {\n    summary: \"Lunch Meeting\",\n    location: \"The Deli\",\n    description: \"To discuss our plans for the presentation next week.\",\n    start: {\n      dateTime: start.toISOString(),\n    },\n    end: {\n      dateTime: end.toISOString(),\n    },\n    attendees: [\n      { email: \"gduser1@workspacesample.dev\" },\n      { email: \"gduser2@workspacesample.dev\" },\n    ],\n    // Red background. Use Calendar.Colors.get() for the full list.\n    colorId: 11,\n  };\n  try {\n    // insert event\n    event = Calendar.Events.insert(event, calendarId);\n    console.log(`Event ID: ${event.getId()}`);\n    // Re-fetch the event each second, but only get a result if it has changed.\n    for (let i = 0; i < 30; i++) {\n      Utilities.sleep(1000);\n      event = Calendar.Events.get(\n        calendarId,\n        event.id,\n        {},\n        { \"If-None-Match\": event.etag },\n      );\n      console.log(`New event description: ${event.start.dateTime}`);\n    }\n  } catch (e) {\n    console.log(`Fetch threw an exception: ${e}`);\n  }\n}\n// [END calendar_conditional_fetch]\n"
  },
  {
    "path": "advanced/chat.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START chat_post_message_with_user_credentials]\n/**\n * Posts a new message to the specified space on behalf of the user.\n * @param {string} spaceName The resource name of the space.\n */\nfunction postMessageWithUserCredentials(spaceName) {\n  try {\n    const message = { text: \"Hello world!\" };\n    Chat.Spaces.Messages.create(message, spaceName);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to create message with error %s\", err.message);\n  }\n}\n// [END chat_post_message_with_user_credentials]\n\n// [START chat_post_message_with_app_credentials]\n/**\n * Posts a new message to the specified space on behalf of the app.\n * @param {string} spaceName The resource name of the space.\n */\nfunction postMessageWithAppCredentials(spaceName) {\n  try {\n    // See https://developers.google.com/chat/api/guides/auth/service-accounts\n    // for details on how to obtain a service account OAuth token.\n    const appToken = getToken_();\n    const message = { text: \"Hello world!\" };\n    Chat.Spaces.Messages.create(\n      message,\n      spaceName,\n      {},\n      // Authenticate with the service account token.\n      { Authorization: `Bearer ${appToken}` },\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to create message with error %s\", err.message);\n  }\n}\n// [END chat_post_message_with_app_credentials]\n\n// [START chat_get_space]\n/**\n * Gets information about a Chat space.\n * @param {string} spaceName The resource name of the space.\n */\nfunction getSpace(spaceName) {\n  try {\n    const space = Chat.Spaces.get(spaceName);\n    console.log(\"Space display name: %s\", space.displayName);\n    console.log(\"Space type: %s\", space.spaceType);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to get space with error %s\", err.message);\n  }\n}\n// [END chat_get_space]\n\n// [START chat_create_space]\n/**\n * Creates a new Chat space.\n */\nfunction createSpace() {\n  try {\n    const space = { displayName: \"New Space\", spaceType: \"SPACE\" };\n    Chat.Spaces.create(space);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to create space with error %s\", err.message);\n  }\n}\n// [END chat_create_space]\n\n// [START chat_list_memberships]\n/**\n * Lists all the members of a Chat space.\n * @param {string} spaceName The resource name of the space.\n */\nfunction listMemberships(spaceName) {\n  let response;\n  let pageToken = null;\n  try {\n    do {\n      response = Chat.Spaces.Members.list(spaceName, {\n        pageSize: 10,\n        pageToken: pageToken,\n      });\n      if (!response.memberships || response.memberships.length === 0) {\n        pageToken = response.nextPageToken;\n        continue;\n      }\n      for (const membership of response.memberships) {\n        console.log(\n          \"Member: %s, Role: %s\",\n          membership.member.displayName,\n          membership.role,\n        );\n      }\n      pageToken = response.nextPageToken;\n    } while (pageToken);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END chat_list_memberships]\n"
  },
  {
    "path": "advanced/classroom.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_classroom_list_courses]\n/**\n * Lists 10 course names and IDs.\n */\nfunction listCourses() {\n  /**\n   * @see https://developers.google.com/classroom/reference/rest/v1/courses/list\n   */\n  const optionalArgs = {\n    pageSize: 10,\n    // Use other query parameters here if needed.\n  };\n  try {\n    const response = Classroom.Courses.list(optionalArgs);\n    const courses = response.courses;\n    if (!courses || courses.length === 0) {\n      console.log(\"No courses found.\");\n      return;\n    }\n    // Print the course names and IDs of the available courses.\n    for (const course in courses) {\n      console.log(\"%s (%s)\", courses[course].name, courses[course].id);\n    }\n  } catch (err) {\n    // TODO (developer)- Handle Courses.list() exception from Classroom API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_classroom_list_courses]\n"
  },
  {
    "path": "advanced/displayvideo.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_dv360_list_partners]\n/**\n * Logs all of the partners available in the account.\n */\nfunction listPartners() {\n  // Retrieve the list of available partners\n  try {\n    const partners = DisplayVideo.Partners.list();\n\n    if (partners.partners) {\n      // Print out the ID and name of each\n      for (let i = 0; i < partners.partners.length; i++) {\n        const partner = partners.partners[i];\n        console.log(\n          'Found partner with ID %s and name \"%s\".',\n          partner.partnerId,\n          partner.displayName,\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dv360_list_partners]\n\n// [START apps_script_dv360_list_active_campaigns]\n/**\n * Logs names and ID's of all active campaigns.\n * Note the use of paging tokens to retrieve the whole list.\n */\nfunction listActiveCampaigns() {\n  const advertiserId = \"1234567\"; // Replace with your advertiser ID.\n  let result;\n  let pageToken;\n  try {\n    do {\n      result = DisplayVideo.Advertisers.Campaigns.list(advertiserId, {\n        filter: 'entityStatus=\"ENTITY_STATUS_ACTIVE\"',\n        pageToken: pageToken,\n      });\n      if (result.campaigns) {\n        for (let i = 0; i < result.campaigns.length; i++) {\n          const campaign = result.campaigns[i];\n          console.log(\n            'Found campaign with ID %s and name \"%s\".',\n            campaign.campaignId,\n            campaign.displayName,\n          );\n        }\n      }\n      pageToken = result.nextPageToken;\n    } while (pageToken);\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dv360_list_active_campaigns]\n\n// [START apps_script_dv360_update_line_item_name]\n/**\n * Updates the display name of a line item\n */\nfunction updateLineItemName() {\n  const advertiserId = \"1234567\"; // Replace with your advertiser ID.\n  const lineItemId = \"123456789\"; //Replace with your line item ID.\n  const updateMask = \"displayName\";\n\n  const lineItemDef = {\n    displayName: \"New Line Item Name (updated from Apps Script!)\",\n  };\n\n  try {\n    const lineItem = DisplayVideo.Advertisers.LineItems.patch(\n      lineItemDef,\n      advertiserId,\n      lineItemId,\n      { updateMask: updateMask },\n    );\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dv360_update_line_item_name]\n"
  },
  {
    "path": "advanced/docs.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START docs_create_document]\n/**\n * Create a new document.\n * @see https://developers.google.com/docs/api/reference/rest/v1/documents/create\n * @return {string} documentId\n */\nfunction createDocument() {\n  // Create document with title\n  const document = Docs.Documents.create({ title: \"My New Document\" });\n  console.log(`Created document with ID: ${document.documentId}`);\n  return document.documentId;\n}\n// [END docs_create_document]\n\n// [START docs_find_and_replace_text]\n/**\n * Performs \"replace all\".\n * @param {string} documentId The document to perform the replace text operations on.\n * @param {Object} findTextToReplacementMap A map from the \"find text\" to the \"replace text\".\n * @return {Object} replies\n * @see https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate\n */\nfunction findAndReplace(documentId, findTextToReplacementMap) {\n  const requests = [];\n  for (const findText in findTextToReplacementMap) {\n    const replaceText = findTextToReplacementMap[findText];\n\n    // Replace all text across all tabs.\n    const replaceAllTextRequest = {\n      replaceAllText: {\n        containsText: {\n          text: findText,\n          matchCase: true,\n        },\n        replaceText: replaceText,\n      },\n    };\n\n    // Replace all text across specific tabs.\n    const _replaceAllTextWithTabsCriteria = {\n      replaceAllText: {\n        ...replaceAllTextRequest.replaceAllText,\n        tabsCriteria: {\n          tabIds: [TAB_ID_1, TAB_ID_2, TAB_ID_3],\n        },\n      },\n    };\n    requests.push(replaceAllTextRequest);\n  }\n  const response = Docs.Documents.batchUpdate(\n    { requests: requests },\n    documentId,\n  );\n  const replies = response.replies;\n  for (const [index] of replies.entries()) {\n    const numReplacements =\n      replies[index].replaceAllText.occurrencesChanged || 0;\n    console.log(\n      \"Request %s performed %s replacements.\",\n      index,\n      numReplacements,\n    );\n  }\n  return replies;\n}\n// [END docs_find_and_replace_text]\n\n// [START docs_insert_and_style_text]\n/**\n * Insert text at the beginning of the first tab in the document and then style\n * the inserted text.\n * @param {string} documentId The document the text is inserted into.\n * @param {string} text The text to insert into the document.\n * @return {Object} replies\n * @see https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate\n */\nfunction insertAndStyleText(documentId, text) {\n  const requests = [\n    {\n      insertText: {\n        location: {\n          index: 1,\n          // A tab can be specified using its ID. When omitted, the request is\n          // applied to the first tab.\n          // tabId: TAB_ID\n        },\n        text: text,\n      },\n    },\n    {\n      updateTextStyle: {\n        range: {\n          startIndex: 1,\n          endIndex: text.length + 1,\n        },\n        textStyle: {\n          fontSize: {\n            magnitude: 12,\n            unit: \"PT\",\n          },\n          weightedFontFamily: {\n            fontFamily: \"Calibri\",\n          },\n        },\n        fields: \"weightedFontFamily, fontSize\",\n      },\n    },\n  ];\n  const response = Docs.Documents.batchUpdate(\n    { requests: requests },\n    documentId,\n  );\n  return response.replies;\n}\n// [END docs_insert_and_style_text]\n\n// [START docs_read_first_paragraph]\n/**\n * Read the first paragraph of the first tab in a document.\n * @param {string} documentId The ID of the document to read.\n * @return {Object} paragraphText\n * @see https://developers.google.com/docs/api/reference/rest/v1/documents/get\n */\nfunction readFirstParagraph(documentId) {\n  // Get the document using document ID\n  const document = Docs.Documents.get(documentId, {\n    includeTabsContent: true,\n  });\n  const firstTab = document.tabs[0];\n  const bodyElements = firstTab.documentTab.body.content;\n  for (let i = 0; i < bodyElements.length; i++) {\n    const structuralElement = bodyElements[i];\n    // Print the first paragraph text present in document\n    if (structuralElement.paragraph) {\n      const paragraphElements = structuralElement.paragraph.elements;\n      let paragraphText = \"\";\n\n      for (let j = 0; j < paragraphElements.length; j++) {\n        const paragraphElement = paragraphElements[j];\n        if (paragraphElement.textRun !== null) {\n          paragraphText += paragraphElement.textRun.content;\n        }\n      }\n      console.log(paragraphText);\n      return paragraphText;\n    }\n  }\n}\n// [END docs_read_first_paragraph]\n"
  },
  {
    "path": "advanced/doubleclick.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_doubleclick_list_user_profiles]\n/**\n * Logs all of the user profiles available in the account.\n */\nfunction listUserProfiles() {\n  // Retrieve the list of available user profiles\n  try {\n    const profiles = DoubleClickCampaigns.UserProfiles.list();\n\n    if (profiles.items) {\n      // Print out the user ID and name of each\n      for (let i = 0; i < profiles.items.length; i++) {\n        const profile = profiles.items[i];\n        console.log(\n          'Found profile with ID %s and name \"%s\".',\n          profile.profileId,\n          profile.userName,\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_doubleclick_list_user_profiles]\n\n// [START apps_script_doubleclick_list_active_campaigns]\n/**\n * Logs names and ID's of all active campaigns.\n * Note the use of paging tokens to retrieve the whole list.\n */\nfunction listActiveCampaigns() {\n  const profileId = \"1234567\"; // Replace with your profile ID.\n  const fields = \"nextPageToken,campaigns(id,name)\";\n  let result;\n  let pageToken;\n  try {\n    do {\n      result = DoubleClickCampaigns.Campaigns.list(profileId, {\n        archived: false,\n        fields: fields,\n        pageToken: pageToken,\n      });\n      if (result.campaigns) {\n        for (let i = 0; i < result.campaigns.length; i++) {\n          const campaign = result.campaigns[i];\n          console.log(\n            'Found campaign with ID %s and name \"%s\".',\n            campaign.id,\n            campaign.name,\n          );\n        }\n      }\n      pageToken = result.nextPageToken;\n    } while (pageToken);\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_doubleclick_list_active_campaigns]\n\n// [START apps_script_doubleclick_create_advertiser_and_campaign]\n/**\n * Creates a new advertiser, and creates a new campaign with that advertiser.\n * The campaign is set to last for one month.\n */\nfunction createAdvertiserAndCampaign() {\n  const profileId = \"1234567\"; // Replace with your profile ID.\n\n  const advertiser = {\n    name: \"Example Advertiser\",\n    status: \"APPROVED\",\n  };\n\n  try {\n    const advertiserId = DoubleClickCampaigns.Advertisers.insert(\n      advertiser,\n      profileId,\n    ).id;\n\n    const landingPage = {\n      advertiserId: advertiserId,\n      archived: false,\n      name: \"Example landing page\",\n      url: \"https://www.google.com\",\n    };\n    const landingPageId = DoubleClickCampaigns.AdvertiserLandingPages.insert(\n      landingPage,\n      profileId,\n    ).id;\n\n    const campaignStart = new Date();\n    // End campaign after 1 month.\n    const campaignEnd = new Date();\n    campaignEnd.setMonth(campaignEnd.getMonth() + 1);\n\n    const campaign = {\n      advertiserId: advertiserId,\n      defaultLandingPageId: landingPageId,\n      name: \"Example campaign\",\n      startDate: Utilities.formatDate(campaignStart, \"GMT\", \"yyyy-MM-dd\"),\n      endDate: Utilities.formatDate(campaignEnd, \"GMT\", \"yyyy-MM-dd\"),\n    };\n    DoubleClickCampaigns.Campaigns.insert(campaign, profileId);\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_doubleclick_create_advertiser_and_campaign]\n"
  },
  {
    "path": "advanced/doubleclickbidmanager.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_dcbm_list_queries]\n/**\n * Logs all of the queries available in the account.\n */\nfunction listQueries() {\n  // Retrieve the list of available queries\n  try {\n    const queries = DoubleClickBidManager.Queries.list();\n\n    if (queries.queries) {\n      // Print out the ID and name of each\n      for (let i = 0; i < queries.queries.length; i++) {\n        const query = queries.queries[i];\n        console.log(\n          'Found query with ID %s and name \"%s\".',\n          query.queryId,\n          query.metadata.title,\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dcbm_list_queries]\n\n// [START apps_script_dcbm_create_and_run_query]\n/**\n * Create and run a new DBM Query\n */\nfunction createAndRunQuery() {\n  let result;\n  let execution;\n  //We leave the default date range blank for the report run to\n  //use the value defined during query creation\n  const defaultDateRange = {};\n  const partnerId = \"1234567\"; //Replace with your Partner ID\n  const query = {\n    metadata: {\n      title: \"Apps Script Example Report\",\n      dataRange: {\n        range: \"YEAR_TO_DATE\",\n      },\n      format: \"CSV\",\n    },\n    params: {\n      type: \"STANDARD\",\n      groupBys: [\n        \"FILTER_PARTNER\",\n        \"FILTER_PARTNER_NAME\",\n        \"FILTER_ADVERTISER\",\n        \"FILTER_ADVERTISER_NAME\",\n      ],\n      filters: [{ type: \"FILTER_PARTNER\", value: partnerId }],\n      metrics: [\"METRIC_IMPRESSIONS\"],\n    },\n    schedule: {\n      frequency: \"ONE_TIME\",\n    },\n  };\n\n  try {\n    result = DoubleClickBidManager.Queries.create(query);\n    if (result.queryId) {\n      console.log(\n        'Created query with ID %s and name \"%s\".',\n        result.queryId,\n        result.metadata.title,\n      );\n      execution = DoubleClickBidManager.Queries.run(\n        defaultDateRange,\n        result.queryId,\n      );\n      if (execution.key) {\n        console.log(\n          'Created query report with query ID %s and report ID \"%s\".',\n          execution.key.queryId,\n          execution.key.reportId,\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(e);\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dcbm_create_and_run_query]\n\n// [START apps_script_dcbm_fetch_report]\n/**\n * Fetches a report file\n */\nfunction fetchReport() {\n  const queryId = \"1234567\"; // Replace with your query ID.\n  const orderBy = \"key.reportId desc\";\n\n  try {\n    const report = DoubleClickBidManager.Queries.Reports.list(queryId, {\n      orderBy: orderBy,\n    });\n    if (report.reports) {\n      const firstReport = report.reports[0];\n      if (firstReport.metadata.status.state === \"DONE\") {\n        const reportFile = UrlFetchApp.fetch(\n          firstReport.metadata.googleCloudStoragePath,\n        );\n        console.log(\"Printing report content to log...\");\n        console.log(reportFile.getContentText());\n      } else {\n        console.log(\n          \"Report status is %s, and is not available for download\",\n          firstReport.metadata.status.state,\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(e);\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_dcbm_fetch_report]\n"
  },
  {
    "path": "advanced/drive.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START drive_upload_file]\n/**\n * Uploads a new file to the user's Drive.\n */\nfunction uploadFile() {\n  try {\n    // Makes a request to fetch a URL.\n    const image = UrlFetchApp.fetch(\"http://goo.gl/nd7zjB\").getBlob();\n    let file = {\n      name: \"google_logo.png\",\n      mimeType: \"image/png\",\n    };\n    // Create a file in the user's Drive.\n    file = Drive.Files.create(file, image, { fields: \"id,size\" });\n    console.log(\"ID: %s, File size (bytes): %s\", file.id, file.size);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to upload file with error %s\", err.message);\n  }\n}\n// [END drive_upload_file]\n\n// [START drive_list_root_folders]\n/**\n * Lists the top-level folders in the user's Drive.\n */\nfunction listRootFolders() {\n  const query =\n    '\"root\" in parents and trashed = false and ' +\n    'mimeType = \"application/vnd.google-apps.folder\"';\n  let folders;\n  let pageToken = null;\n  do {\n    try {\n      folders = Drive.Files.list({\n        q: query,\n        pageSize: 100,\n        pageToken: pageToken,\n      });\n      if (!folders.files || folders.files.length === 0) {\n        console.log(\"All folders found.\");\n        return;\n      }\n      for (let i = 0; i < folders.files.length; i++) {\n        const folder = folders.files[i];\n        console.log(\"%s (ID: %s)\", folder.name, folder.id);\n      }\n      pageToken = folders.nextPageToken;\n    } catch (err) {\n      // TODO (developer) - Handle exception\n      console.log(\"Failed with error %s\", err.message);\n    }\n  } while (pageToken);\n}\n// [END drive_list_root_folders]\n\n// [START drive_add_custom_property]\n/**\n * Adds a custom app property to a file. Unlike Apps Script's DocumentProperties,\n * Drive's custom file properties can be accessed outside of Apps Script and\n * by other applications; however, appProperties are only visible to the script.\n * @param {string} fileId The ID of the file to add the app property to.\n */\nfunction addAppProperty(fileId) {\n  try {\n    let file = {\n      appProperties: {\n        department: \"Sales\",\n      },\n    };\n    // Updates a file to add an app property.\n    file = Drive.Files.update(file, fileId, null, {\n      fields: \"id,appProperties\",\n    });\n    console.log(\n      \"ID: %s, appProperties: %s\",\n      file.id,\n      JSON.stringify(file.appProperties, null, 2),\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END drive_add_custom_property]\n\n// [START drive_list_revisions]\n/**\n * Lists the revisions of a given file.\n * @param {string} fileId The ID of the file to list revisions for.\n */\nfunction listRevisions(fileId) {\n  let revisions;\n  let pageToken = null;\n  do {\n    try {\n      revisions = Drive.Revisions.list(fileId, {\n        fields: \"revisions(modifiedTime,size),nextPageToken\",\n      });\n      if (!revisions.revisions || revisions.revisions.length === 0) {\n        console.log(\"All revisions found.\");\n        return;\n      }\n      for (let i = 0; i < revisions.revisions.length; i++) {\n        const revision = revisions.revisions[i];\n        const date = new Date(revision.modifiedTime);\n        console.log(\n          \"Date: %s, File size (bytes): %s\",\n          date.toLocaleString(),\n          revision.size,\n        );\n      }\n      pageToken = revisions.nextPageToken;\n    } catch (err) {\n      // TODO (developer) - Handle exception\n      console.log(\"Failed with error %s\", err.message);\n    }\n  } while (pageToken);\n}\n\n// [END drive_list_revisions]\n"
  },
  {
    "path": "advanced/driveActivity.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_drive_activity_get_users_activity]\n/**\n * Gets a file's activity and logs the list of\n * unique users that performed the activity.\n */\nfunction getUsersActivity() {\n  const fileId = \"YOUR_FILE_ID_HERE\";\n\n  let pageToken;\n  const users = {};\n  do {\n    const result = AppsActivity.Activities.list({\n      \"drive.fileId\": fileId,\n      source: \"drive.google.com\",\n      pageToken: pageToken,\n    });\n    const activities = result.activities;\n    for (let i = 0; i < activities.length; i++) {\n      const events = activities[i].singleEvents;\n      for (let j = 0; j < events.length; j++) {\n        const event = events[j];\n        users[event.user.name] = true;\n      }\n    }\n    pageToken = result.nextPageToken;\n  } while (pageToken);\n  console.log(Object.keys(users));\n}\n// [END apps_script_drive_activity_get_users_activity]\n"
  },
  {
    "path": "advanced/driveLabels.gs",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_drive_labels_list_labels]\n/**\n * List labels available to the user.\n */\nfunction listLabels() {\n  let pageToken = null;\n  let labels = [];\n  do {\n    try {\n      const response = DriveLabels.Labels.list({\n        publishedOnly: true,\n        pageToken: pageToken,\n      });\n      pageToken = response.nextPageToken;\n      labels = labels.concat(response.labels);\n    } catch (err) {\n      // TODO (developer) - Handle exception\n      console.log(\"Failed to list labels with error %s\", err.message);\n    }\n  } while (pageToken != null);\n\n  console.log(\"Found %d labels\", labels.length);\n}\n// [END apps_script_drive_labels_list_labels]\n\n// [START apps_script_drive_labels_get_label]\n/**\n * Get a label by name.\n * @param {string} labelName The label name.\n */\nfunction getLabel(labelName) {\n  try {\n    const label = DriveLabels.Labels.get(labelName, {\n      view: \"LABEL_VIEW_FULL\",\n    });\n    const title = label.properties.title;\n    const fieldsLength = label.fields.length;\n    console.log(\n      `Fetched label with title: '${title}' and ${fieldsLength} fields.`,\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to get label with error %s\", err.message);\n  }\n}\n// [END apps_script_drive_labels_get_label]\n\n// [START apps_script_drive_labels_list_labels_on_drive_item]\n/**\n * List Labels on a Drive Item\n * Fetches a Drive Item and prints all applied values along with their to their\n * human-readable names.\n *\n * @param {string} fileId The Drive File ID\n */\nfunction listLabelsOnDriveItem(fileId) {\n  try {\n    const appliedLabels = Drive.Files.listLabels(fileId);\n\n    console.log(\n      \"%d label(s) are applied to this file\",\n      appliedLabels.labels.length,\n    );\n\n    for (const appliedLabel of appliedLabels.labels) {\n      // Resource name of the label at the applied revision.\n      const labelName = `labels/${appliedLabel.id}@${appliedLabel.revisionId}`;\n\n      console.log(\"Fetching Label: %s\", labelName);\n      const label = DriveLabels.Labels.get(labelName, {\n        view: \"LABEL_VIEW_FULL\",\n      });\n\n      console.log(\"Label Title: %s\", label.properties.title);\n\n      for (const fieldId of Object.keys(appliedLabel.fields)) {\n        const fieldValue = appliedLabel.fields[fieldId];\n        const field = label.fields.find((f) => f.id === fieldId);\n\n        console.log(\n          `Field ID: ${field.id}, Display Name: ${field.properties.displayName}`,\n        );\n        switch (fieldValue.valueType) {\n          case \"text\":\n            console.log(\"Text: %s\", fieldValue.text[0]);\n            break;\n          case \"integer\":\n            console.log(\"Integer: %d\", fieldValue.integer[0]);\n            break;\n          case \"dateString\":\n            console.log(\"Date: %s\", fieldValue.dateString[0]);\n            break;\n          case \"user\": {\n            const user = fieldValue.user\n              .map((user) => {\n                return `${user.emailAddress}: ${user.displayName}`;\n              })\n              .join(\", \");\n            console.log(`User: ${user}`);\n            break;\n          }\n          case \"selection\": {\n            const choices = fieldValue.selection.map((choiceId) => {\n              return field.selectionOptions.choices.find(\n                (choice) => choice.id === choiceId,\n              );\n            });\n            const selection = choices\n              .map((choice) => {\n                return `${choice.id}: ${choice.properties.displayName}`;\n              })\n              .join(\", \");\n            console.log(`Selection: ${selection}`);\n            break;\n          }\n          default:\n            console.log(\"Unknown: %s\", fieldValue.valueType);\n            console.log(fieldValue.value);\n        }\n      }\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_drive_labels_list_labels_on_drive_item]\n"
  },
  {
    "path": "advanced/events.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START events_create_subscription]\n/**\n * Creates a subscription to receive events about a Google Workspace resource.\n * For a list of supported resources and event types, see the\n * [Google Workspace Events API Overview](https://developers.google.com/workspace/events#supported-events).\n * For additional information, see the\n * [subscriptions.create](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/create)\n * method reference.\n * @param {!string} targetResource The full resource name of the Google Workspace resource to subscribe to.\n * @param {!string|!Array<string>} eventTypes The types of events to receive about the resource.\n * @param {!string} pubsubTopic The resource name of the Pub/Sub topic that receives events from the subscription.\n */\nfunction createSubscription(targetResource, eventTypes, pubsubTopic) {\n  try {\n    const operation = WorkspaceEvents.Subscriptions.create({\n      targetResource: targetResource,\n      eventTypes: eventTypes,\n      notificationEndpoint: {\n        pubsubTopic: pubsubTopic,\n      },\n    });\n    console.log(operation);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to create subscription with error %s\", err.message);\n  }\n}\n// [END events_create_subscription]\n\n// [START events_list_subscriptions]\n/**\n * Lists subscriptions created by the calling app filtered by one or more event types and optionally by a target resource.\n * For additional information, see the\n * [subscriptions.list](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/list)\n * method reference.\n * @param {!string} filter The query filter.\n */\nfunction listSubscriptions(filter) {\n  try {\n    const response = WorkspaceEvents.Subscriptions.list({ filter });\n    console.log(response);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to list subscriptions with error %s\", err.message);\n  }\n}\n// [END events_list_subscriptions]\n\n// [START events_get_subscription]\n/**\n * Gets details about a subscription.\n * For additional information, see the\n * [subscriptions.get](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/get)\n * method reference.\n * @param {!string} name The resource name of the subscription.\n */\nfunction getSubscription(name) {\n  try {\n    const subscription = WorkspaceEvents.Subscriptions.get(name);\n    console.log(subscription);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to get subscription with error %s\", err.message);\n  }\n}\n// [END events_get_subscription]\n\n// [START events_patch_subscription]\n/**\n * Updates an existing subscription.\n * This can be used to renew a subscription that is about to expire.\n * For additional information, see the\n * [subscriptions.patch](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/patch)\n * method reference.\n * @param {!string} name The resource name of the subscription.\n */\nfunction patchSubscription(name) {\n  try {\n    const operation = WorkspaceEvents.Subscriptions.patch(\n      {\n        // Setting the TTL to 0 seconds extends the subscription to its maximum expiration time.\n        ttl: \"0s\",\n      },\n      name,\n    );\n    console.log(operation);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to update subscription with error %s\", err.message);\n  }\n}\n// [END events_patch_subscription]\n\n// [START events_reactivate_subscription]\n/**\n * Reactivates a suspended subscription.\n * Before reactivating, you must resolve any errors with the subscription.\n * For additional information, see the\n * [subscriptions.reactivate](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/reactivate)\n * method reference.\n * @param {!string} name The resource name of the subscription.\n */\nfunction reactivateSubscription(name) {\n  try {\n    const operation = WorkspaceEvents.Subscriptions.reactivate({}, name);\n    console.log(operation);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to reactivate subscription with error %s\", err.message);\n  }\n}\n// [END events_reactivate_subscription]\n\n// [START events_delete_subscription]\n/**\n * Deletes a subscription.\n * For additional information, see the\n * [subscriptions.delete](https://developers.google.com/workspace/events/reference/rest/v1/subscriptions/delete)\n * method reference.\n * @param {!string} name The resource name of the subscription.\n */\nfunction deleteSubscription(name) {\n  try {\n    const operation = WorkspaceEvents.Subscriptions.remove(name);\n    console.log(operation);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to delete subscription with error %s\", err.message);\n  }\n}\n// [END events_delete_subscription]\n\n// [START events_get_operation]\n/**\n * Gets details about an operation returned by one of the methods on the subscription\n * resource of the Google Workspace Events API.\n * For additional information, see the\n * [operations.get](https://developers.google.com/workspace/events/reference/rest/v1/operations/get)\n * method reference.\n * @param {!string} name The resource name of the operation.\n */\nfunction getOperation(name) {\n  try {\n    const operation = WorkspaceEvents.Operations.get(name);\n    console.log(operation);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to get operation with error %s\", err.message);\n  }\n}\n// [END events_get_operation]\n"
  },
  {
    "path": "advanced/gmail.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START gmail_label]\n/**\n * Lists the user's labels, including name, type,\n * ID and visibility information.\n */\nfunction listLabelInfo() {\n  try {\n    const response = Gmail.Users.Labels.list(\"me\");\n    for (let i = 0; i < response.labels.length; i++) {\n      const label = response.labels[i];\n      console.log(JSON.stringify(label));\n    }\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_label]\n\n// [START gmail_inbox_snippets]\n/**\n * Lists, for each thread in the user's Inbox, a\n * snippet associated with that thread.\n */\nfunction listInboxSnippets() {\n  try {\n    let pageToken;\n    do {\n      const threadList = Gmail.Users.Threads.list(\"me\", {\n        q: \"label:inbox\",\n        pageToken: pageToken,\n      });\n      if (threadList.threads && threadList.threads.length > 0) {\n        for (const thread of threadList.threads) {\n          console.log(`Snippet: ${thread.snippet}`);\n        }\n      }\n      pageToken = threadList.nextPageToken;\n    } while (pageToken);\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_inbox_snippets]\n\n// [START gmail_history]\n/**\n * Gets a history record ID associated with the most\n * recently sent message, then logs all the message IDs\n * that have changed since that message was sent.\n */\nfunction logRecentHistory() {\n  try {\n    // Get the history ID associated with the most recent\n    // sent message.\n    const sent = Gmail.Users.Threads.list(\"me\", {\n      q: \"label:sent\",\n      maxResults: 1,\n    });\n    if (!sent.threads || !sent.threads[0]) {\n      console.log(\"No sent threads found.\");\n      return;\n    }\n    const historyId = sent.threads[0].historyId;\n\n    // Log the ID of each message changed since the most\n    // recent message was sent.\n    let pageToken;\n    const changed = [];\n    do {\n      const recordList = Gmail.Users.History.list(\"me\", {\n        startHistoryId: historyId,\n        pageToken: pageToken,\n      });\n      const history = recordList.history;\n      if (history && history.length > 0) {\n        for (const record of history) {\n          for (const message of record.messages) {\n            if (changed.indexOf(message.id) === -1) {\n              changed.push(message.id);\n            }\n          }\n        }\n      }\n      pageToken = recordList.nextPageToken;\n    } while (pageToken);\n\n    for (const id of changed) {\n      console.log(\"Message Changed: %s\", id);\n    }\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_history]\n\n// [START gmail_raw]\n/**\n * Logs the raw message content for the most recent message in gmail.\n */\nfunction getRawMessage() {\n  try {\n    const messageId = Gmail.Users.Messages.list(\"me\").messages[0].id;\n    console.log(messageId);\n    const message = Gmail.Users.Messages.get(\"me\", messageId, {\n      format: \"raw\",\n    });\n\n    // Get raw content as base64url encoded string.\n    const encodedMessage = Utilities.base64Encode(message.raw);\n    console.log(encodedMessage);\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_raw]\n\n// [START gmail_list_messages]\n/**\n * Lists unread messages in the user's inbox using the advanced Gmail service.\n */\nfunction listMessages() {\n  // The special value 'me' indicates the authenticated user.\n  const userId = \"me\";\n\n  // Define optional parameters for the request.\n  const options = {\n    maxResults: 10, // Limit the number of messages returned.\n    q: \"is:unread\", // Search for unread messages.\n  };\n\n  try {\n    // Call the Gmail.Users.Messages.list method.\n    const response = Gmail.Users.Messages.list(userId, options);\n    const messages = response.messages;\n    console.log(\"Unread Messages:\");\n\n    for (const message of messages) {\n      console.log(`- Message ID: ${message.id}`);\n    }\n  } catch (err) {\n    // Log any errors to the Apps Script execution log.\n    console.log(`Failed with error: ${err.message}`);\n  }\n}\n// [END gmail_list_messages]\n"
  },
  {
    "path": "advanced/iot.gs",
    "content": "/**\n * Copyright 2019 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_iot_list_registries]\n/**\n * Lists the registries for the configured project and region.\n */\nfunction listRegistries() {\n  const projectId = \"your-project-id\";\n  const cloudRegion = \"us-central1\";\n  const parent = `projects/${projectId}/locations/${cloudRegion}`;\n\n  const response = CloudIoT.Projects.Locations.Registries.list(parent);\n  console.log(response);\n  if (response.deviceRegistries) {\n    for (const registry of response.deviceRegistries) {\n      console.log(registry.id);\n    }\n  }\n}\n// [END apps_script_iot_list_registries]\n\n// [START apps_script_iot_create_registry]\n/**\n * Creates a registry.\n */\nfunction createRegistry() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-registry-name\";\n  const projectId = \"your-project-id\";\n  const topic = \"your-pubsub-topic\";\n\n  const pubsubTopic = `projects/${projectId}/topics/${topic}`;\n\n  const registry = {\n    eventNotificationConfigs: [\n      {\n        // From - https://console.cloud.google.com/cloudpubsub\n        pubsubTopicName: pubsubTopic,\n      },\n    ],\n    id: name,\n  };\n  const parent = `projects/${projectId}/locations/${cloudRegion}`;\n\n  const response = CloudIoT.Projects.Locations.Registries.create(\n    registry,\n    parent,\n  );\n  console.log(`Created registry: ${response.id}`);\n}\n// [END apps_script_iot_create_registry]\n\n// [START apps_script_iot_get_registry]\n/**\n * Describes a registry.\n */\nfunction getRegistry() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-registry-name\";\n  const projectId = \"your-project-id\";\n\n  const parent = `projects/${projectId}/locations/${cloudRegion}`;\n  const registryName = `${parent}/registries/${name}`;\n\n  const response = CloudIoT.Projects.Locations.Registries.get(registryName);\n  console.log(`Retrieved registry: ${response.id}`);\n}\n// [END apps_script_iot_get_registry]\n\n// [START apps_script_iot_delete_registry]\n/**\n * Deletes a registry.\n */\nfunction deleteRegistry() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-registry-name\";\n  const projectId = \"your-project-id\";\n\n  const parent = `projects/${projectId}/locations/${cloudRegion}`;\n  const registryName = `${parent}/registries/${name}`;\n\n  const response = CloudIoT.Projects.Locations.Registries.remove(registryName);\n  // Successfully removed registry if exception was not thrown.\n  console.log(`Deleted registry: ${name}`);\n}\n// [END apps_script_iot_delete_registry]\n\n// [START apps_script_iot_list_devices]\n/**\n * Lists the devices in the given registry.\n */\nfunction listDevicesForRegistry() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-registry-name\";\n  const projectId = \"your-project-id\";\n\n  const parent = `projects/${projectId}/locations/${cloudRegion}`;\n  const registryName = `${parent}/registries/${name}`;\n\n  const response =\n    CloudIoT.Projects.Locations.Registries.Devices.list(registryName);\n\n  console.log(\"Registry contains the following devices: \");\n  if (response.devices) {\n    for (const device of response.devices) {\n      console.log(`\\t${device.id}`);\n    }\n  }\n}\n// [END apps_script_iot_list_devices]\n\n// [START apps_script_iot_create_unauth_device]\n/**\n * Creates a device without credentials.\n */\nfunction createDevice() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-device-name\";\n  const projectId = \"your-project-id\";\n  const registry = \"your-registry-name\";\n\n  console.log(`Creating device: ${name} in Registry: ${registry}`);\n  const parent = `projects/${projectId}/locations/${cloudRegion}/registries/${registry}`;\n\n  const device = {\n    id: name,\n    gatewayConfig: {\n      gatewayType: \"NON_GATEWAY\",\n      gatewayAuthMethod: \"ASSOCIATION_ONLY\",\n    },\n  };\n\n  const response = CloudIoT.Projects.Locations.Registries.Devices.create(\n    device,\n    parent,\n  );\n  console.log(`Created device:${response.name}`);\n}\n// [END apps_script_iot_create_unauth_device]\n\n// [START apps_script_iot_create_rsa_device]\n/**\n * Creates a device with RSA credentials.\n */\nfunction createRsaDevice() {\n  // Create the RSA public/private keypair with the following OpenSSL command:\n  //    openssl req -x509 -newkey rsa:2048 -days 3650 -keyout rsa_private.pem \\\n  //      -nodes -out rsa_cert.pem -subj \"/CN=unused\"\n  //\n  // **NOTE** Be sure to insert the newline charaters in the string constant.\n  const cert =\n    \"-----BEGIN CERTIFICATE-----\\n\" +\n    \"your-PUBLIC-certificate-b64-bytes\\n\" +\n    \"...\\n\" +\n    \"more-PUBLIC-certificate-b64-bytes==\\n\" +\n    \"-----END CERTIFICATE-----\\n\";\n\n  const cloudRegion = \"us-central1\";\n  const name = \"your-device-name\";\n  const projectId = \"your-project-id\";\n  const registry = \"your-registry-name\";\n\n  const parent = `projects/${projectId}/locations/${cloudRegion}/registries/${registry}`;\n  const device = {\n    id: name,\n    gatewayConfig: {\n      gatewayType: \"NON_GATEWAY\",\n      gatewayAuthMethod: \"ASSOCIATION_ONLY\",\n    },\n    credentials: [\n      {\n        publicKey: {\n          format: \"RSA_X509_PEM\",\n          key: cert,\n        },\n      },\n    ],\n  };\n\n  const response = CloudIoT.Projects.Locations.Registries.Devices.create(\n    device,\n    parent,\n  );\n  console.log(`Created device:${response.name}`);\n}\n// [END apps_script_iot_create_rsa_device]\n\n// [START apps_script_iot_delete_device]\n/**\n * Deletes a device from the given registry.\n */\nfunction deleteDevice() {\n  const cloudRegion = \"us-central1\";\n  const name = \"your-device-name\";\n  const projectId = \"your-project-id\";\n  const registry = \"your-registry-name\";\n\n  const parent = `projects/${projectId}/locations/${cloudRegion}/registries/${registry}`;\n  const deviceName = `${parent}/devices/${name}`;\n\n  const response =\n    CloudIoT.Projects.Locations.Registries.Devices.remove(deviceName);\n  // If no exception thrown, device was successfully removed\n  console.log(`Successfully deleted device: ${deviceName}`);\n}\n// [END apps_script_iot_delete_device]\n"
  },
  {
    "path": "advanced/people.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START people_get_connections]\n/**\n * Gets a list of people in the user's contacts.\n * @see https://developers.google.com/people/api/rest/v1/people.connections/list\n */\nfunction getConnections() {\n  try {\n    // Get the list of connections/contacts of user's profile\n    const people = People.People.Connections.list(\"people/me\", {\n      personFields: \"names,emailAddresses\",\n    });\n    // Print the connections/contacts\n    console.log(\"Connections: %s\", JSON.stringify(people, null, 2));\n  } catch (err) {\n    // TODO (developers) - Handle exception here\n    console.log(\"Failed to get the connection with an error %s\", err.message);\n  }\n}\n// [END people_get_connections]\n\n// [START people_get_self_profile]\n/**\n * Gets the own user's profile.\n * @see https://developers.google.com/people/api/rest/v1/people/getBatchGet\n */\nfunction getSelf() {\n  try {\n    // Get own user's profile using People.getBatchGet() method\n    const people = People.People.getBatchGet({\n      resourceNames: [\"people/me\"],\n      personFields: \"names,emailAddresses\",\n      // Use other query parameter here if needed\n    });\n    console.log(\"Myself: %s\", JSON.stringify(people, null, 2));\n  } catch (err) {\n    // TODO (developer) -Handle exception\n    console.log(\"Failed to get own profile with an error %s\", err.message);\n  }\n}\n// [END people_get_self_profile]\n\n// [START people_get_account]\n/**\n * Gets the person information for any Google Account.\n * @param {string} accountId The account ID.\n * @see https://developers.google.com/people/api/rest/v1/people/get\n */\nfunction getAccount(accountId) {\n  try {\n    // Get the Account details using account ID.\n    const people = People.People.get(`people/${accountId}`, {\n      personFields: \"names,emailAddresses\",\n    });\n    // Print the profile details of Account.\n    console.log(\"Public Profile: %s\", JSON.stringify(people, null, 2));\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to get account with an error %s\", err.message);\n  }\n}\n// [END people_get_account]\n\n// [START people_get_group]\n\n/**\n * Gets a contact group with the given name\n * @param {string} name The group name.\n * @see https://developers.google.com/people/api/rest/v1/contactGroups/list\n */\nfunction getContactGroup(name) {\n  try {\n    const people = People.ContactGroups.list();\n    // Finds the contact group for the person where the name matches.\n    const group = people.contactGroups.find((group) => group.name === name);\n    // Prints the contact group\n    console.log(\"Group: %s\", JSON.stringify(group, null, 2));\n  } catch (err) {\n    // TODO (developers) - Handle exception\n    console.log(\n      \"Failed to get the contact group with an error %s\",\n      err.message,\n    );\n  }\n}\n\n// [END people_get_group]\n\n// [START people_get_contact_by_email]\n\n/**\n * Gets a contact by the email address.\n * @param {string} email The email address.\n * @see https://developers.google.com/people/api/rest/v1/people.connections/list\n */\nfunction getContactByEmail(email) {\n  try {\n    // Gets the person with that email address by iterating over all contacts.\n    const people = People.People.Connections.list(\"people/me\", {\n      personFields: \"names,emailAddresses\",\n    });\n    const contact = people.connections.find((connection) => {\n      return connection.emailAddresses.some(\n        (emailAddress) => emailAddress.value === email,\n      );\n    });\n    // Prints the contact.\n    console.log(\"Contact: %s\", JSON.stringify(contact, null, 2));\n  } catch (err) {\n    // TODO (developers) - Handle exception\n    console.log(\"Failed to get the connection with an error %s\", err.message);\n  }\n}\n\n// [END people_get_contact_by_email]\n\n// [START people_get_full_name]\n/**\n * Gets the full name (given name and last name) of the contact as a string.\n * @see https://developers.google.com/people/api/rest/v1/people/get\n */\nfunction getFullName() {\n  try {\n    // Gets the person by specifying resource name/account ID\n    // in the first parameter of People.People.get.\n    // This example gets the person for the user running the script.\n    const people = People.People.get(\"people/me\", { personFields: \"names\" });\n    // Prints the full name (given name + family name)\n    console.log(`${people.names[0].givenName} ${people.names[0].familyName}`);\n  } catch (err) {\n    // TODO (developers) - Handle exception\n    console.log(\"Failed to get the connection with an error %s\", err.message);\n  }\n}\n\n// [END people_get_full_name]\n\n// [START people_get_phone_numbers]\n/**\n * Gets all the phone numbers for this contact.\n * @see https://developers.google.com/people/api/rest/v1/people/get\n */\nfunction getPhoneNumbers() {\n  try {\n    // Gets the person by specifying resource name/account ID\n    // in the first parameter of People.People.get.\n    // This example gets the person for the user running the script.\n    const people = People.People.get(\"people/me\", {\n      personFields: \"phoneNumbers\",\n    });\n    // Prints the phone numbers.\n    console.log(people.phoneNumbers);\n  } catch (err) {\n    // TODO (developers) - Handle exception\n    console.log(\"Failed to get the connection with an error %s\", err.message);\n  }\n}\n\n// [END people_get_phone_numbers]\n\n// [START people_get_single_phone_number]\n/**\n * Gets a phone number by type, such as work or home.\n * @see https://developers.google.com/people/api/rest/v1/people/get\n */\nfunction getPhone() {\n  try {\n    // Gets the person by specifying resource name/account ID\n    // in the first parameter of People.People.get.\n    // This example gets the person for the user running the script.\n    const people = People.People.get(\"people/me\", {\n      personFields: \"phoneNumbers\",\n    });\n    // Gets phone number by type, such as home or work.\n    const phoneNumber = people.phoneNumbers.find(\n      (phone) => phone.type === \"home\",\n    ).value;\n    // Prints the phone numbers.\n    console.log(phoneNumber);\n  } catch (err) {\n    // TODO (developers) - Handle exception\n    console.log(\"Failed to get the connection with an error %s\", err.message);\n  }\n}\n\n// [END people_get_single_phone_number]\n"
  },
  {
    "path": "advanced/sheets.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// TODO (developer)- Replace the spreadsheet ID and sheet ID with yours values.\nconst yourspreadsheetId = \"1YdrrmXSjpi4Tz-UuQ0eUKtdzQuvpzRLMoPEz3niTTVU\";\nconst yourpivotSourceDataSheetId = 635809130;\nconst yourdestinationSheetId = 83410180;\n// [START sheets_read_range]\n/**\n * Read a range (A1:D5) of data values. Logs the values.\n * @param {string} spreadsheetId The spreadsheet ID to read from.\n * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get\n */\nfunction readRange(spreadsheetId = yourspreadsheetId) {\n  try {\n    const response = Sheets.Spreadsheets.Values.get(\n      spreadsheetId,\n      \"Sheet1!A1:D5\",\n    );\n    if (response.values) {\n      console.log(response.values);\n      return;\n    }\n    console.log(\"Failed to get range of values from spreadsheet\");\n  } catch (e) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END sheets_read_range]\n\n// [START sheets_write_range]\n/**\n * Write to multiple, disjoint data ranges.\n * @param {string} spreadsheetId The spreadsheet ID to write to.\n * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate\n */\nfunction writeToMultipleRanges(spreadsheetId = yourspreadsheetId) {\n  // Specify some values to write to the sheet.\n  const columnAValues = [[\"Item\", \"Wheel\", \"Door\", \"Engine\"]];\n  const rowValues = [\n    [\"Cost\", \"Stocked\", \"Ship Date\"],\n    [\"$20.50\", \"4\", \"3/1/2016\"],\n  ];\n\n  const request = {\n    valueInputOption: \"USER_ENTERED\",\n    data: [\n      {\n        range: \"Sheet1!A1:A4\",\n        majorDimension: \"COLUMNS\",\n        values: columnAValues,\n      },\n      {\n        range: \"Sheet1!B1:D2\",\n        majorDimension: \"ROWS\",\n        values: rowValues,\n      },\n    ],\n  };\n  try {\n    const response = Sheets.Spreadsheets.Values.batchUpdate(\n      request,\n      spreadsheetId,\n    );\n    if (response) {\n      console.log(response);\n      return;\n    }\n    console.log(\"response null\");\n  } catch (e) {\n    // TODO (developer) - Handle  exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END sheets_write_range]\n\n// [START sheets_add_new_sheet]\n/**\n * Add a new sheet with some properties.\n * @param {string} spreadsheetId The spreadsheet ID.\n * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate\n */\nfunction addSheet(spreadsheetId = yourspreadsheetId) {\n  const requests = [\n    {\n      addSheet: {\n        properties: {\n          title: \"Deposits\",\n          gridProperties: {\n            rowCount: 20,\n            columnCount: 12,\n          },\n          tabColor: {\n            red: 1.0,\n            green: 0.3,\n            blue: 0.4,\n          },\n        },\n      },\n    },\n  ];\n  try {\n    const response = Sheets.Spreadsheets.batchUpdate(\n      { requests: requests },\n      spreadsheetId,\n    );\n    console.log(\n      `Created sheet with ID: ${response.replies[0].addSheet.properties.sheetId}`,\n    );\n  } catch (e) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END sheets_add_new_sheet]\n\n// [START sheets_add_pivot_table]\n/**\n * Add a pivot table.\n * @param {string} spreadsheetId The spreadsheet ID to add the pivot table to.\n * @param {string} pivotSourceDataSheetId The sheet ID to get the data from.\n * @param {string} destinationSheetId The sheet ID to add the pivot table to.\n * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate\n */\nfunction addPivotTable(\n  spreadsheetId = yourspreadsheetId,\n  pivotSourceDataSheetId = yourpivotSourceDataSheetId,\n  destinationSheetId = yourdestinationSheetId,\n) {\n  const requests = [\n    {\n      updateCells: {\n        rows: {\n          values: [\n            {\n              pivotTable: {\n                source: {\n                  sheetId: pivotSourceDataSheetId,\n                  startRowIndex: 0,\n                  startColumnIndex: 0,\n                  endRowIndex: 20,\n                  endColumnIndex: 7,\n                },\n                rows: [\n                  {\n                    sourceColumnOffset: 0,\n                    showTotals: true,\n                    sortOrder: \"ASCENDING\",\n                    valueBucket: {\n                      buckets: [\n                        {\n                          stringValue: \"West\",\n                        },\n                      ],\n                    },\n                  },\n                  {\n                    sourceColumnOffset: 1,\n                    showTotals: true,\n                    sortOrder: \"DESCENDING\",\n                    valueBucket: {},\n                  },\n                ],\n                columns: [\n                  {\n                    sourceColumnOffset: 4,\n                    sortOrder: \"ASCENDING\",\n                    showTotals: true,\n                    valueBucket: {},\n                  },\n                ],\n                values: [\n                  {\n                    summarizeFunction: \"SUM\",\n                    sourceColumnOffset: 3,\n                  },\n                ],\n                valueLayout: \"HORIZONTAL\",\n              },\n            },\n          ],\n        },\n        start: {\n          sheetId: destinationSheetId,\n          rowIndex: 49,\n          columnIndex: 0,\n        },\n        fields: \"pivotTable\",\n      },\n    },\n  ];\n  try {\n    const response = Sheets.Spreadsheets.batchUpdate(\n      { requests: requests },\n      spreadsheetId,\n    );\n    // The Pivot table will appear anchored to cell A50 of the destination sheet.\n  } catch (e) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END sheets_add_pivot_table]\n"
  },
  {
    "path": "advanced/shoppingContent.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_shopping_product_insert]\n/**\n * Inserts a product into the products list. Logs the API response.\n */\nfunction productInsert() {\n  const merchantId = 123456; // Replace this with your Merchant Center ID.\n  // Create a product resource and insert it\n  const productResource = {\n    offerId: \"book123\",\n    title: \"A Tale of Two Cities\",\n    description: \"A classic novel about the French Revolution\",\n    link: \"http://my-book-shop.com/tale-of-two-cities.html\",\n    imageLink: \"http://my-book-shop.com/tale-of-two-cities.jpg\",\n    contentLanguage: \"en\",\n    targetCountry: \"US\",\n    channel: \"online\",\n    availability: \"in stock\",\n    condition: \"new\",\n    googleProductCategory: \"Media > Books\",\n    productType: \"Media > Books\",\n    gtin: \"9780007350896\",\n    price: {\n      value: \"2.50\",\n      currency: \"USD\",\n    },\n    shipping: [\n      {\n        country: \"US\",\n        service: \"Standard shipping\",\n        price: {\n          value: \"0.99\",\n          currency: \"USD\",\n        },\n      },\n    ],\n    shippingWeight: {\n      value: \"2\",\n      unit: \"pounds\",\n    },\n  };\n\n  try {\n    response = ShoppingContent.Products.insert(productResource, merchantId);\n    // RESTful insert returns the JSON object as a response.\n    console.log(response);\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_shopping_product_insert]\n\n// [START apps_script_shopping_product_list]\n/**\n * Lists the products for a given merchant.\n */\nfunction productList() {\n  const merchantId = 123456; // Replace this with your Merchant Center ID.\n  let pageToken;\n  let pageNum = 1;\n  const maxResults = 10;\n  try {\n    do {\n      const products = ShoppingContent.Products.list(merchantId, {\n        pageToken: pageToken,\n        maxResults: maxResults,\n      });\n      console.log(`Page ${pageNum}`);\n      if (products.resources) {\n        for (let i = 0; i < products.resources.length; i++) {\n          console.log(`Item [${i}] ==> ${products.resources[i]}`);\n        }\n      } else {\n        console.log(`No more products in account ${merchantId}`);\n      }\n      pageToken = products.nextPageToken;\n      pageNum++;\n    } while (pageToken);\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_shopping_product_list]\n\n// [START apps_script_shopping_product_batch_insert]\n/**\n * Batch updates products. Logs the response.\n * @param  {object} productResource1 The first product resource.\n * @param  {object} productResource2 The second product resource.\n * @param  {object} productResource3 The third product resource.\n */\nfunction custombatch(productResource1, productResource2, productResource3) {\n  const merchantId = 123456; // Replace this with your Merchant Center ID.\n  custombatchResource = {\n    entries: [\n      {\n        batchId: 1,\n        merchantId: merchantId,\n        method: \"insert\",\n        productId: \"book124\",\n        product: productResource1,\n      },\n      {\n        batchId: 2,\n        merchantId: merchantId,\n        method: \"insert\",\n        productId: \"book125\",\n        product: productResource2,\n      },\n      {\n        batchId: 3,\n        merchantId: merchantId,\n        method: \"insert\",\n        productId: \"book126\",\n        product: productResource3,\n      },\n    ],\n  };\n  try {\n    const response = ShoppingContent.Products.custombatch(custombatchResource);\n    console.log(response);\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_shopping_product_batch_insert]\n\n// [START apps_script_shopping_account_info]\n/**\n * Updates content account tax information.\n * Logs the API response.\n */\nfunction updateAccountTax() {\n  // Replace this with your Merchant Center ID.\n  const merchantId = 123456;\n\n  // Replace this with the account that you are updating taxes for.\n  const accountId = 123456;\n\n  try {\n    const accounttax = ShoppingContent.Accounttax.get(merchantId, accountId);\n    console.log(accounttax);\n\n    const taxInfo = {\n      accountId: accountId,\n      rules: [\n        {\n          useGlobalRate: true,\n          locationId: 21135,\n          shippingTaxed: true,\n          country: \"US\",\n        },\n        {\n          ratePercent: 3,\n          locationId: 21136,\n          country: \"US\",\n        },\n        {\n          ratePercent: 2,\n          locationId: 21160,\n          shippingTaxed: true,\n          country: \"US\",\n        },\n      ],\n    };\n\n    console.log(\n      ShoppingContent.Accounttax.update(taxInfo, merchantId, accountId),\n    );\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_shopping_account_info]\n"
  },
  {
    "path": "advanced/slides.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_create_presentation]\n/**\n * Create a new presentation.\n * @return {string} presentation Id.\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/create\n */\nfunction createPresentation() {\n  try {\n    const presentation = Slides.Presentations.create({\n      title: \"MyNewPresentation\",\n    });\n    console.log(`Created presentation with ID: ${presentation.presentationId}`);\n    return presentation.presentationId;\n  } catch (e) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_create_presentation]\n\n// [START apps_script_slides_create_slide]\n/**\n * Create a new slide.\n * @param {string} presentationId The presentation to add the slide to.\n * @return {Object} slide\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate\n */\nfunction createSlide(presentationId) {\n  // You can specify the ID to use for the slide, as long as it's unique.\n  const pageId = Utilities.getUuid();\n\n  const requests = [\n    {\n      createSlide: {\n        objectId: pageId,\n        insertionIndex: 1,\n        slideLayoutReference: {\n          predefinedLayout: \"TITLE_AND_TWO_COLUMNS\",\n        },\n      },\n    },\n  ];\n  try {\n    const slide = Slides.Presentations.batchUpdate(\n      { requests: requests },\n      presentationId,\n    );\n    console.log(\n      `Created Slide with ID: ${slide.replies[0].createSlide.objectId}`,\n    );\n    return slide;\n  } catch (e) {\n    // TODO (developer) - Handle Exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_create_slide]\n\n// [START apps_script_slides_read_page]\n/**\n * Read page element IDs.\n * @param {string} presentationId The presentation to read from.\n * @param {string} pageId The page to read from.\n * @return {Object} response\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations.pages/get\n */\nfunction readPageElementIds(presentationId, pageId) {\n  // You can use a field mask to limit the data the API retrieves\n  // in a get request, or what fields are updated in an batchUpdate.\n  try {\n    const response = Slides.Presentations.Pages.get(presentationId, pageId, {\n      fields: \"pageElements.objectId\",\n    });\n    console.log(response);\n    return response;\n  } catch (e) {\n    // TODO (developer) - Handle Exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_read_page]\n\n// [START apps_script_slides_add_text_box]\n/**\n * Add a new text box with text to a page.\n * @param {string} presentationId The presentation ID.\n * @param {string} pageId The page ID.\n * @return {Object} response\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate\n */\nfunction addTextBox(presentationId, pageId) {\n  // You can specify the ID to use for elements you create,\n  // as long as the ID is unique.\n  const pageElementId = Utilities.getUuid();\n\n  const requests = [\n    {\n      createShape: {\n        objectId: pageElementId,\n        shapeType: \"TEXT_BOX\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            width: {\n              magnitude: 150,\n              unit: \"PT\",\n            },\n            height: {\n              magnitude: 50,\n              unit: \"PT\",\n            },\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 200,\n            translateY: 100,\n            unit: \"PT\",\n          },\n        },\n      },\n    },\n    {\n      insertText: {\n        objectId: pageElementId,\n        text: \"My Added Text Box\",\n        insertionIndex: 0,\n      },\n    },\n  ];\n  try {\n    const response = Slides.Presentations.batchUpdate(\n      { requests: requests },\n      presentationId,\n    );\n    console.log(\n      `Created Textbox with ID: ${response.replies[0].createShape.objectId}`,\n    );\n    return response;\n  } catch (e) {\n    // TODO (developer) - Handle Exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_add_text_box]\n\n// [START apps_script_slides_format_shape_text]\n/**\n * Format the text in a shape.\n * @param {string} presentationId The presentation ID.\n * @param {string} shapeId The shape ID.\n * @return {Object} replies\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate\n */\nfunction formatShapeText(presentationId, shapeId) {\n  const requests = [\n    {\n      updateTextStyle: {\n        objectId: shapeId,\n        fields: \"foregroundColor,bold,italic,fontFamily,fontSize,underline\",\n        style: {\n          foregroundColor: {\n            opaqueColor: {\n              themeColor: \"ACCENT5\",\n            },\n          },\n          bold: true,\n          italic: true,\n          underline: true,\n          fontFamily: \"Corsiva\",\n          fontSize: {\n            magnitude: 18,\n            unit: \"PT\",\n          },\n        },\n        textRange: {\n          type: \"ALL\",\n        },\n      },\n    },\n  ];\n  try {\n    const response = Slides.Presentations.batchUpdate(\n      { requests: requests },\n      presentationId,\n    );\n    return response.replies;\n  } catch (e) {\n    // TODO (developer) - Handle Exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_format_shape_text]\n\n// [START apps_script_slides_save_thumbnail]\n/**\n * Saves a thumbnail image of the current Google Slide presentation in Google Drive.\n * Logs the image URL.\n * @param {number} i The zero-based slide index. 0 is the first slide.\n * @example saveThumbnailImage(0)\n * @see https://developers.google.com/slides/api/reference/rest/v1/presentations.pages/getThumbnail\n */\nfunction saveThumbnailImage(i) {\n  try {\n    const presentation = SlidesApp.getActivePresentation();\n    // Get the thumbnail of specified page\n    const thumbnail = Slides.Presentations.Pages.getThumbnail(\n      presentation.getId(),\n      presentation.getSlides()[i].getObjectId(),\n    );\n    // fetch the  URL to the thumbnail image.\n    const response = UrlFetchApp.fetch(thumbnail.contentUrl);\n    const image = response.getBlob();\n    // Creates a file in the root of the user's Drive from a given Blob of arbitrary data.\n    const file = DriveApp.createFile(image);\n    console.log(file.getUrl());\n  } catch (e) {\n    // TODO (developer) - Handle Exception\n    console.log(\"Failed with error %s\", e.message);\n  }\n}\n// [END apps_script_slides_save_thumbnail]\n"
  },
  {
    "path": "advanced/tagManager.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_tag_manager_create_version]\n/**\n * Creates a container version for a particular account\n * with the input accountPath.\n * @param {string} accountPath The account path.\n * @return {string} The tag manager container version.\n */\nfunction createContainerVersion(accountPath) {\n  const date = new Date();\n  // Creates a container in the account, using the current timestamp to make\n  // sure the container is unique.\n  try {\n    const container = TagManager.Accounts.Containers.create(\n      {\n        name: `appscript tagmanager container ${date.getTime()}`,\n        usageContext: [\"WEB\"],\n      },\n      accountPath,\n    );\n    const containerPath = container.path;\n    // Creates a workspace in the container to track entity changes.\n    const workspace = TagManager.Accounts.Containers.Workspaces.create(\n      { name: \"appscript workspace\", description: \"appscript workspace\" },\n      containerPath,\n    );\n    const workspacePath = workspace.path;\n    // Creates a random value variable.\n    const variable = TagManager.Accounts.Containers.Workspaces.Variables.create(\n      { name: \"apps script variable\", type: \"r\" },\n      workspacePath,\n    );\n    // Creates a trigger that fires on any page view.\n    const trigger = TagManager.Accounts.Containers.Workspaces.Triggers.create(\n      { name: \"apps script trigger\", type: \"PAGEVIEW\" },\n      workspacePath,\n    );\n    // Creates a arbitary pixel that fires the tag on all page views.\n    const tag = TagManager.Accounts.Containers.Workspaces.Tags.create(\n      {\n        name: \"apps script tag\",\n        type: \"img\",\n        liveOnly: false,\n        parameter: [\n          { type: \"boolean\", key: \"useCacheBuster\", value: \"true\" },\n          {\n            type: \"template\",\n            key: \"cacheBusterQueryParam\",\n            value: \"gtmcb\",\n          },\n          { type: \"template\", key: \"url\", value: \"//example.com\" },\n        ],\n        firingTriggerId: [trigger.triggerId],\n      },\n      workspacePath,\n    );\n    // Creates a container version with the variabe, trigger, and tag.\n    const version = TagManager.Accounts.Containers.Workspaces.create_version(\n      { name: \"apps script version\" },\n      workspacePath,\n    ).containerVersion;\n    console.log(version);\n    return version;\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_tag_manager_create_version]\n\n// [START apps_script_tag_manager_publish_version]\n\n/**\n * Publishes a container version publically to the world and creates a quick\n * preview of the current container draft.\n * @param {object} version The container version.\n */\nfunction publishVersionAndQuickPreviewDraft(version) {\n  try {\n    const pathParts = version.path.split(\"/\");\n    const containerPath = pathParts.slice(0, 4).join(\"/\");\n    // Publish the input container version.\n    TagManager.Accounts.Containers.Versions.publish(version.path);\n    const workspace = TagManager.Accounts.Containers.Workspaces.create(\n      { name: \"appscript workspace\", description: \"appscript workspace\" },\n      containerPath,\n    );\n    const workspaceId = workspace.path;\n    // Quick previews the current container draft.\n    const quickPreview =\n      TagManager.Accounts.Containers.Workspaces.quick_preview(workspace.path);\n    console.log(quickPreview);\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_tag_manager_publish_version]\n\n// [START apps_script_tag_manager_create_user_environment]\n/**\n * Creates and reauthorizes a user environment in a container that points\n * to a container version passed in as an argument.\n * @param {object} version The container version object.\n */\nfunction createAndReauthorizeUserEnvironment(version) {\n  try {\n    // Creates a container version.\n    const pathParts = version.path.split(\"/\");\n    const containerPath = pathParts.slice(0, 4).join(\"/\");\n    // Creates a user environment that points to a container version.\n    const environment = TagManager.Accounts.Containers.Environments.create(\n      {\n        name: \"test_environment\",\n        type: \"user\",\n        containerVersionId: version.containerVersionId,\n      },\n      containerPath,\n    );\n    console.log(`Original user environment: ${environment}`);\n    // Reauthorizes the user environment that points to a container version.\n    TagManager.Accounts.Containers.Environments.reauthorize(\n      {},\n      environment.path,\n    );\n    console.log(`Reauthorized user environment: ${environment}`);\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_tag_manager_create_user_environment]\n\n// [START apps_script_tag_manager_log]\n/**\n * Logs all emails and container access permission within an account.\n * @param {string} accountPath The account path.\n */\nfunction logAllAccountUserPermissionsWithContainerAccess(accountPath) {\n  try {\n    const userPermissions =\n      TagManager.Accounts.User_permissions.list(accountPath).userPermission;\n    for (let i = 0; i < userPermissions.length; i++) {\n      const userPermission = userPermissions[i];\n      if (\"emailAddress\" in userPermission) {\n        const containerAccesses = userPermission.containerAccess;\n        for (let j = 0; j < containerAccesses.length; j++) {\n          const containerAccess = containerAccesses[j];\n          console.log(\n            `emailAddress:${userPermission.emailAddress} containerId:${containerAccess.containerId} containerAccess:${containerAccess.permission}`,\n          );\n        }\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exceptions\n    console.log(\"Failed with error: $s\", e.error);\n  }\n}\n// [END apps_script_tag_manager_log]\n"
  },
  {
    "path": "advanced/tasks.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START tasks_lists_task_lists]\n/**\n * Lists the titles and IDs of tasksList.\n * @see https://developers.google.com/tasks/reference/rest/v1/tasklists/list\n */\nfunction listTaskLists() {\n  try {\n    // Returns all the authenticated user's task lists.\n    const taskLists = Tasks.Tasklists.list();\n    // If taskLists are available then print all tasklists.\n    if (!taskLists.items) {\n      console.log(\"No task lists found.\");\n      return;\n    }\n    // Print the tasklist title and tasklist id.\n    for (let i = 0; i < taskLists.items.length; i++) {\n      const taskList = taskLists.items[i];\n      console.log(\n        'Task list with title \"%s\" and ID \"%s\" was found.',\n        taskList.title,\n        taskList.id,\n      );\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception from Task API\n    console.log(\"Failed with an error %s \", err.message);\n  }\n}\n// [END tasks_lists_task_lists]\n\n// [START tasks_list_tasks]\n/**\n * Lists task items for a provided tasklist ID.\n * @param  {string} taskListId The tasklist ID.\n * @see https://developers.google.com/tasks/reference/rest/v1/tasks/list\n */\nfunction listTasks(taskListId) {\n  try {\n    // List the task items of specified tasklist using taskList id.\n    const tasks = Tasks.Tasks.list(taskListId);\n    // If tasks are available then print all task of given tasklists.\n    if (!tasks.items) {\n      console.log(\"No tasks found.\");\n      return;\n    }\n    // Print the task title and task id of specified tasklist.\n    for (let i = 0; i < tasks.items.length; i++) {\n      const task = tasks.items[i];\n      console.log(\n        'Task with title \"%s\" and ID \"%s\" was found.',\n        task.title,\n        task.id,\n      );\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception from Task API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END tasks_list_tasks]\n\n// [START tasks_add_task]\n/**\n * Adds a task to a tasklist.\n * @param {string} taskListId The tasklist to add to.\n * @see https://developers.google.com/tasks/reference/rest/v1/tasks/insert\n */\nfunction addTask(taskListId) {\n  // Task details with title and notes for inserting new task\n  let task = {\n    title: \"Pick up dry cleaning\",\n    notes: \"Remember to get this done!\",\n  };\n  try {\n    // Call insert method with taskDetails and taskListId to insert Task to specified tasklist.\n    task = Tasks.Tasks.insert(task, taskListId);\n    // Print the Task ID of created task.\n    console.log('Task with ID \"%s\" was created.', task.id);\n  } catch (err) {\n    // TODO (developer) - Handle exception from Tasks.insert() of Task API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END tasks_add_task]\n"
  },
  {
    "path": "advanced/test_adminSDK.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listAllUsers function of adminSDK.gs\n */\nfunction itShouldListAllUsers() {\n  console.log(\"> itShouldListAllUsers\");\n  listAllUsers();\n}\n\n/**\n * Tests getUser function of adminSDK.gs\n */\nfunction itShouldGetUser() {\n  console.log(\"> itShouldGetUser\");\n  getUser();\n}\n\n/**\n * Tests addUser function of adminSDK.gs\n */\nfunction itShouldAddUser() {\n  console.log(\"> itShouldAddUser\");\n  addUser();\n}\n\n/**\n * Tests createAlias function of adminSDK.gs\n */\nfunction itShouldCreateAlias() {\n  console.log(\"> itShouldCreateAlias\");\n  createAlias();\n}\n\n/**\n * Tests listAllGroups function of adminSDK.gs\n */\nfunction itShouldListAllGroups() {\n  console.log(\"> itShouldListAllGroups\");\n  listAllGroups();\n}\n\n/**\n * Tests addGroupMember function of adminSDK.gs\n */\nfunction itShouldAddGroupMember() {\n  console.log(\"> itShouldAddGroupMember\");\n  addGroupMember();\n}\n\n/**\n * Tests migrateMessages function of adminSDK.gs\n */\nfunction itShouldMigrateMessages() {\n  console.log(\"> itShouldMigrateMessages\");\n  migrateMessages();\n}\n\n/**\n * Tests getGroupSettings function of adminSDK.gs\n */\nfunction itShouldGetGroupSettings() {\n  console.log(\"> itShouldGetGroupSettings\");\n  getGroupSettings();\n}\n\n/**\n * Tests updateGroupSettings function of adminSDK.gs\n */\nfunction itShouldUpdateGroupSettings() {\n  console.log(\"> itShouldUpdateGroupSettings\");\n  updateGroupSettings();\n}\n\n/**\n * Tests getLicenseAssignments function of adminSDK.gs\n */\nfunction itShouldGetLicenseAssignments() {\n  console.log(\"> itShouldGetLicenseAssignments\");\n  getLicenseAssignments();\n}\n\n/**\n * Tests insertLicenseAssignment function of adminSDK.gs\n */\nfunction itShouldInsertLicenseAssignment() {\n  console.log(\"> itShouldInsertLicenseAssignment\");\n  insertLicenseAssignment();\n}\n\n/**\n * Tests generateLoginActivityReport function of adminSDK.gs\n */\nfunction itShouldGenerateLoginActivityReport() {\n  console.log(\"> itShouldGenerateLoginActivityReport\");\n  generateLoginActivityReport();\n}\n\n/**\n * Tests generateUserUsageReport function of adminSDK.gs\n */\nfunction itShouldGenerateUserUsageReport() {\n  console.log(\"> itShouldGenerateUserUsageReport\");\n  generateUserUsageReport();\n}\n\n/**\n * Tests getSubscriptions function of adminSDK.gs\n */\nfunction itShouldGetSubscriptions() {\n  console.log(\"> itShouldGetSubscriptions\");\n  getSubscriptions();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListAllUsers();\n  itShouldGetUser();\n  itShouldAddUser();\n  itShouldCreateAlias();\n  itShouldListAllGroups();\n  itShouldAddGroupMember();\n  itShouldMigrateMessages();\n  itShouldGetGroupSettings();\n  itShouldUpdateGroupSettings();\n  itShouldGetLicenseAssignments();\n  itShouldInsertLicenseAssignment();\n  itShouldGenerateLoginActivityReport();\n  itShouldGenerateUserUsageReport();\n  itShouldGetSubscriptions();\n}\n"
  },
  {
    "path": "advanced/test_adsense.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Replace with correct values\nconst accountName = \"account name\";\nconst clientName = \"ad client name\";\n\n/**\n * Tests listAccounts function of adsense.gs\n */\nfunction itShouldListAccounts() {\n  console.log(\"> itShouldListAccounts\");\n  listAccounts();\n}\n\n/**\n * Tests listAdClients function of adsense.gs\n */\nfunction itShouldListAdClients() {\n  console.log(\"> itShouldListAdClients\");\n  listAdClients(accountName);\n}\n\n/**\n * Tests listAdUnits function of adsense.gs\n */\nfunction itShouldListAdUnits() {\n  console.log(\"> itShouldListAdUnits\");\n  listAdUnits(clientName);\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListAccounts();\n  itShouldListAdClients();\n  itShouldListAdUnits();\n}\n"
  },
  {
    "path": "advanced/test_analytics.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Replace with the required profileId\nconst profileId = \"abcd\";\n\n/**\n * Tests listAccounts function of analytics.gs\n */\nfunction itShouldListAccounts() {\n  console.log(\"> itShouldListAccounts\");\n  listAccounts();\n}\n\n/**\n * Tests runReport function of analytics.gs\n */\nfunction itShouldRunReport() {\n  console.log(\"> itShouldRunReport\");\n  runReport(profileId);\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListAccounts();\n  itShouldRunReport();\n}\n"
  },
  {
    "path": "advanced/test_bigquery.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests runQuery function of adminSDK.gs\n */\nfunction itShouldRunQuery() {\n  console.log(\"> itShouldRunQuery\");\n  runQuery();\n}\n\n/**\n * Tests loadCsv function of adminSDK.gs\n */\nfunction itShouldLoadCsv() {\n  console.log(\"> itShouldLoadCsv\");\n  loadCsv();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldRunQuery();\n  itShouldLoadCsv();\n}\n"
  },
  {
    "path": "advanced/test_calendar.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listCalendars function of calendar.gs\n */\nfunction itShouldListCalendars() {\n  console.log(\"> itShouldListCalendars\");\n  listCalendars();\n}\n\n/**\n * Tests createEvent function of calendars.gs\n */\nfunction itShouldCreateEvent() {\n  console.log(\"> itShouldCreateEvent\");\n  createEvent();\n}\n\n/**\n * Tests gerRelativeDate function of calendar.gs\n */\nfunction itShouldGetRelativeDate() {\n  console.log(\"> itShouldGetRelativeDate\");\n  console.log(`no offset: ${getRelativeDate(0, 0)}`);\n  console.log(`4 hour offset: ${getRelativeDate(0, 4)}`);\n  console.log(`1 day offset: ${getRelativeDate(1, 0)}`);\n  console.log(`1 day and 3 hour off set: ${getRelativeDate(1, 3)}`);\n}\n\n/**\n * Tests listNext10Events function of calendar.gs\n */\nfunction itShouldListNext10Events() {\n  console.log(\"> itShouldListNext10Events\");\n  listNext10Events();\n}\n\n/**\n * Tests logSyncedEvents function of calendar.gs\n */\nfunction itShouldLogSyncedEvents() {\n  console.log(\"> itShouldLogSyncedEvents\");\n  logSyncedEvents(\"primary\", true);\n  logSyncedEvents(\"primary\", false);\n}\n\n/**\n * Tests conditionalUpdate function of calendar.gs\n */\nfunction itShouldConditionalUpdate() {\n  console.log(\"> itShouldConditionalUpdate (takes 30 seconds)\");\n  conditionalUpdate();\n}\n\n/**\n * Tests conditionalFetch function of calendar.gs\n */\nfunction itShouldConditionalFetch() {\n  console.log(\"> itShouldConditionalFetch\");\n  conditionalFetch();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListCalendars();\n  itShouldCreateEvent();\n  itShouldGetRelativeDate();\n  itShouldListNext10Events();\n  itShouldLogSyncedEvents();\n  itShouldConditionalUpdate();\n  itShouldConditionalFetch();\n}\n"
  },
  {
    "path": "advanced/test_classroom.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listCourses function of classroom.gs\n */\nfunction itShouldListCourses() {\n  console.log(\"> itShouldListCourses\");\n  listCourses();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListCourses();\n}\n"
  },
  {
    "path": "advanced/test_displayvideo.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listPartners function of displayvideo.gs\n */\nfunction itShouldListPartners() {\n  console.log(\"> itShouldListPartners\");\n  listPartners();\n}\n\n/**\n * Tests listActiveCampaigns function of displayvideo.gs\n */\nfunction itShouldListActiveCampaigns() {\n  console.log(\"> itShouldListActiveCampaigns\");\n  listActiveCampaigns();\n}\n\n/**\n * Tests updateLineItemName function of displayvideo.gs\n */\nfunction itShouldUpdateLineItemName() {\n  console.log(\"> itShouldUpdateLineItemName\");\n  updateLineItemName();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListPartners();\n  itShouldListActiveCampaigns();\n  itShouldUpdateLineItemName();\n}\n"
  },
  {
    "path": "advanced/test_docs.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// TODO (developer) - Replace with your documentId\nconst documentId = \"1EaLpBfuo3bMUeP6_P34auuQroh3bCWi6hLDppY6J6us\";\n/**\n * A simple exists assertion check. Expects a value to exist. Errors if DNE.\n * @param {any} value A value that is expected to exist.\n */\nfunction expectToExist(value) {\n  if (!value) {\n    console.log(\"DNE\");\n    return;\n  }\n  console.log(\"TEST: Exists\");\n}\n\n/**\n * A simple exists assertion check for primatives (no nested objects).\n * Expects actual to equal expected. Logs the output.\n * @param {any} expected The actual value.\n * @param {any} actual  The expected value.\n */\nfunction expectToEqual(expected, actual) {\n  if (actual !== expected) {\n    console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n    return;\n  }\n  console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n}\n\n/**\n * Runs all tests.\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateDocument();\n  itShouldInsertTextWithStyle();\n  itShouldReplaceText();\n  itShouldReadFirstParagraph();\n}\n\n/**\n * Creates a presentation.\n */\nfunction itShouldCreateDocument() {\n  const documentId = createDocument();\n  expectToExist(documentId);\n  deleteFileOnCleanup(documentId);\n}\n\n/**\n * Insert text with style.\n */\nfunction itShouldInsertTextWithStyle() {\n  const documentId = createDocument();\n  expectToExist(documentId);\n  const text = \"This is the sample document\";\n  const replies = insertAndStyleText(documentId, text);\n  expectToEqual(2, replies.length);\n  deleteFileOnCleanup(documentId);\n}\n\n/**\n * Find and Replace the text.\n */\nfunction itShouldReplaceText() {\n  const documentId = createDocument();\n  expectToExist(documentId);\n  const text = \"This is the sample document\";\n  const response = insertAndStyleText(documentId, text);\n  expectToEqual(2, response.replies.length);\n  const findTextToReplacementMap = { sample: \"test\", document: \"Doc\" };\n  const replies = findAndReplace(documentId, findTextToReplacementMap);\n  expectToEqual(2, replies.length);\n  deleteFileOnCleanup(documentId);\n}\n\n/**\n * Read first paragraph\n */\nfunction itShouldReadFirstParagraph() {\n  const paragraphText = readFirstParagraph(documentId);\n  expectToExist(paragraphText);\n  expectToEqual(89, paragraphText.length);\n}\n/**\n * Delete the file\n * @param {string} id Document ID\n */\nfunction deleteFileOnCleanup(id) {\n  Drive.Files.remove(id);\n}\n"
  },
  {
    "path": "advanced/test_doubleclick.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listUserProfiles function of doubleclick.gs\n */\nfunction itShouldListUserProfiles() {\n  console.log(\"> itShouldListUserProfiles\");\n  listUserProfiles();\n}\n\n/**\n * Tests listActiveCampaigns function of doubleclick.gs\n */\nfunction itShouldListActiveCampaigns() {\n  console.log(\"> itShouldListActiveCampaigns\");\n  listActiveCampaigns();\n}\n\n/**\n * Tests createAdvertiserAndCampaign function of doubleclick.gs\n */\nfunction itShouldCreateAdvertiserAndCampaign() {\n  console.log(\"> itShouldCreateAdvertiserAndCampaign\");\n  createAdvertiserAndCampaign();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListUserProfiles();\n  itShouldListActiveCampaigns();\n  itShouldCreateAdvertiserAndCampaign();\n}\n"
  },
  {
    "path": "advanced/test_doubleclickbidmanager.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests listQueries function of doubleclickbidmanager.gs\n */\nfunction itShouldListQueries() {\n  console.log(\"> itShouldListQueries\");\n  listQueries();\n}\n\n/**\n * Tests createAndRunQuery function of doubleclickbidmanager.gs\n */\nfunction itShouldCreateAndRunQuery() {\n  console.log(\"> itShouldCreateAndRunQuery\");\n  createAndRunQuery();\n}\n\n/**\n * Tests fetchReport function of doubleclickbidmanager.gs\n */\nfunction itShouldFetchReport() {\n  console.log(\"> itShouldFetchReport\");\n  fetchReport();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListQueries();\n  itShouldCreateAndRunQuery();\n  itShouldFetchReport();\n}\n"
  },
  {
    "path": "advanced/test_drive.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Helper functions to help test drive.gs expectToExist(...)\n * @param {string} value\n * To test drive.gs please add drive services\n */\nfunction expectToExist(value) {\n  if (value) {\n    console.log(\"TEST: Exists\");\n  } else {\n    throw new Error(\"TEST: DNE\");\n  }\n}\n\n/**\n * Helper functions to help test drive.gs expectToEqual\n * @param {string} actual\n * @param {string} expected\n * To test drive.gs please add drive services\n */\nfunction expectToEqual(actual, expected) {\n  console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n  if (actual !== expected) {\n    console.log(\"TEST: actual: %s expected: %s\", actual, expected);\n  }\n}\n\n/**\n * Helper functions to help test drive.gs createFolder()\n *\n * To test drive.gs please add drive services\n */\nfunction createTestFolder() {\n  DriveApp.createFolder(\"test1\");\n  DriveApp.createFolder(\"test2\");\n}\n\n/**\n * Helper functions to help test drive.gs getFilesByName(...)\n *\n * To test drive.gs please add drive services\n */\nfunction fileCleanUp() {\n  DriveApp.getFilesByName(\"google_logo.png\").next().setTrashed(true);\n}\n\n/**\n * Helper functions folderCleanUp()\n *\n * To test getFoldersByName() please add drive services\n */\nfunction folderCleanUp() {\n  DriveApp.getFoldersByName(\"test1\").next().setTrashed(true);\n  DriveApp.getFoldersByName(\"test2\").next().setTrashed(true);\n}\n\n/**\n * drive.gs test functions below\n */\n\n/**\n * tests drive.gs uploadFile\n * @return {string} fileId The ID of the file\n */\nfunction checkUploadFile() {\n  uploadFile();\n  const fileId = DriveApp.getFilesByName(\"google_logo.png\").next().getId();\n  expectToExist(fileId);\n  return fileId;\n}\n\n/**\n * tests drive.gs listRootFolders\n */\nfunction checkListRootFolders() {\n  createTestFolder();\n\n  const folders = DriveApp.getFolders();\n  while (folders.hasNext()) {\n    const folder = folders.next();\n    console.log(`${folder.getName()} ${folder.getId()}`);\n  }\n  listRootFolders();\n  folderCleanUp();\n}\n\n/**\n * tests drive.gs addCustomProperty\n * @param {string} fileId The ID of the file\n */\nfunction checkAddCustomProperty(fileId) {\n  addCustomProperty(fileId);\n  expectToEqual(\n    Drive.Properties.get(fileId, \"department\", { visibility: \"PUBLIC\" }).value,\n    \"Sales\",\n  );\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  const fileId = checkUploadFile();\n  checkListRootFolders();\n  checkAddCustomProperty(fileId);\n  listRevisions(fileId);\n  fileCleanUp();\n}\n"
  },
  {
    "path": "advanced/test_gmail.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Run All functions of gmail.gs\n * Add gmail services to run\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> ltShouldListLabelInfo\");\n  listLabelInfo();\n  console.log(\"> ltShouldListInboxSnippets\");\n  listInboxSnippets();\n  console.log(\"> ltShouldLogRecentHistory\");\n  logRecentHistory();\n  console.log(\"> ltShouldGetRawMessage\");\n  getRawMessage();\n}\n"
  },
  {
    "path": "advanced/test_people.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Helper functions for sheets.gs testing\n *\n * to tests people.gs add people api services\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> itShouldGetConnections\");\n  getConnections();\n  console.log(\"> itShouldGetSelf\"); // Requires the scope userinfo.profile\n  getSelf();\n  console.log(\"> itShouldGetAccount\");\n  getAccount(\"me\");\n}\n"
  },
  {
    "path": "advanced/test_sheets.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Helper functions for sheets.gs testing\n * to tests sheets.gs add sheets services\n *\n * create test spreadsheets\n * @return {string} spreadsheet\n */\nfunction createTestSpreadsheet() {\n  const spreadsheet = SpreadsheetApp.create(\"Test Spreadsheet\");\n  for (let i = 0; i < 3; ++i) {\n    spreadsheet.appendRow([1, 2, 3]);\n  }\n  return spreadsheet.getId();\n}\n\n/**\n * populate the created spreadsheet with values\n * @param {string} spreadsheetId\n */\nfunction populateValues(spreadsheetId) {\n  const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest();\n  const repeatCellRequest = Sheets.newRepeatCellRequest();\n\n  const values = [];\n  for (let i = 0; i < 10; ++i) {\n    values[i] = [];\n    for (let j = 0; j < 10; ++j) {\n      values[i].push(\"Hello\");\n    }\n  }\n  const range = \"A1:J10\";\n  SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values);\n  SpreadsheetApp.flush();\n}\n\n/**\n * Functions to test sheets.gs below this line\n * tests readRange function of sheets.gs\n * @return {string} spreadsheet ID\n */\nfunction itShouldReadRange() {\n  console.log(\"> itShouldReadRange\");\n  spreadsheetId = createTestSpreadsheet();\n  populateValues(spreadsheetId);\n  readRange(spreadsheetId);\n  return spreadsheetId;\n}\n\n/**\n * tests the addPivotTable function of sheets.gs\n * @param {string} spreadsheetId\n */\nfunction itShouldAddPivotTable(spreadsheetId) {\n  console.log(\"> itShouldAddPivotTable\");\n  const spreadsheet = SpreadsheetApp.openById(spreadsheetId);\n  const sheets = spreadsheet.getSheets();\n  sheetId = sheets[0].getSheetId();\n  addPivotTable(spreadsheetId, sheetId, sheetId);\n  SpreadsheetApp.flush();\n  console.log(\"Created pivot table\");\n}\n\n/**\n * runs all the tests\n */\nfunction RUN_ALL_TEST() {\n  const spreadsheetId = itShouldReadRange();\n  console.log(\"> itShouldWriteToMultipleRanges\");\n  writeToMultipleRanges(spreadsheetId);\n  console.log(\"> itShouldAddSheet\");\n  addSheet(spreadsheetId);\n  itShouldAddPivotTable(spreadsheetId);\n}\n"
  },
  {
    "path": "advanced/test_shoppingContent.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Before running these tests replace the product resource variables\nconst productResource1 = {};\nconst productResource2 = {};\nconst productResource3 = {};\n\n/**\n * Tests productInsert function of shoppingContent.gs\n */\nfunction itShouldProductInsert() {\n  console.log(\"> itShouldPproductInsert\");\n  productInsert();\n}\n\n/**\n * Tests productList function of shoppingContent.gs\n */\nfunction itShouldProductList() {\n  console.log(\"> itShouldProductList\");\n  productList();\n}\n\n/**\n * Tests custombatch function of shoppingContent.gs\n */\nfunction itShouldCustombatch() {\n  console.log(\"> itShouldCustombatch\");\n  custombatch(productResource1, productResource2, productResource3);\n}\n\n/**\n * Tests updateAccountTax function of shoppingContent.gs\n */\nfunction itShouldUpdateAccountTax() {\n  console.log(\"> itShouldUpdateAccountTax\");\n  updateAccountTax();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldProductInsert();\n  itShouldProductList();\n  itShouldCustombatch();\n  itShouldUpdateAccountTax();\n}\n"
  },
  {
    "path": "advanced/test_slides.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A simple existance assertion. Logs if the value is falsy.\n * @param {object} value The value we expect to exist.\n */\nfunction expectToExist(value) {\n  if (!value) {\n    console.log(\"DNE\");\n    return;\n  }\n  console.log(\"TEST: Exists\");\n}\n\n/**\n * A simple equality assertion. Logs if there is a mismatch.\n * @param {object} expected The expected value.\n * @param {object} actual The actual value.\n */\nfunction expectToEqual(expected, actual) {\n  if (actual !== expected) {\n    console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n    return;\n  }\n  console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n}\n/**\n * Creates a presentation.\n * @param {string} presentationId The presentation ID.\n * @param {string} pageId The page ID.\n * @return {string} objectId\n */\nfunction addShape(presentationId, pageId) {\n  // Create a new square textbox, using the supplied element ID.\n  const elementId = \"MyTextBox_01\";\n  const pt350 = {\n    magnitude: 350,\n    unit: \"PT\",\n  };\n  const requests = [\n    {\n      createShape: {\n        objectId: elementId,\n        shapeType: \"ELLIPSE\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            height: pt350,\n            width: pt350,\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 350,\n            translateY: 100,\n            unit: \"PT\",\n          },\n        },\n      },\n    },\n\n    // Insert text into the box, using the supplied element ID.\n    {\n      insertText: {\n        objectId: elementId,\n        insertionIndex: 0,\n        text: \"Text Formatted!\",\n      },\n    },\n  ];\n\n  // Execute the request.\n  const createTextboxWithTextResponse = Slides.Presentations.batchUpdate(\n    {\n      requests: requests,\n    },\n    presentationId,\n  );\n  const createShapeResponse =\n    createTextboxWithTextResponse.replies[0].createShape;\n  console.log(\"Created textbox with ID: %s\", createShapeResponse.objectId);\n  // [END slides_create_textbox_with_text]\n  return createShapeResponse.objectId;\n}\n\n/**\n * Runs all tests.\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateAPresentation();\n  itShouldCreateASlide();\n  itShouldCreateATextboxWithText();\n  itShouldFormatShapes();\n  itShouldReadPage();\n}\n\n/**\n * Creates a presentation.\n */\nfunction itShouldCreateAPresentation() {\n  const presentationId = createPresentation();\n  expectToExist(presentationId);\n  deleteFileOnCleanup(presentationId);\n}\n\n/**\n * Creates a new slide.\n */\nfunction itShouldCreateASlide() {\n  console.log(\"> itShouldCreateASlide\");\n  const presentationId = createPresentation();\n  const slideId = createSlide(presentationId);\n  expectToExist(slideId);\n  deleteFileOnCleanup(presentationId);\n}\n\n/**\n * Creates a slide with text.\n */\nfunction itShouldCreateATextboxWithText() {\n  const presentationId = createPresentation();\n  const slide = createSlide(presentationId);\n  const pageId = slide.replies[0].createSlide.objectId;\n  const response = addTextBox(presentationId, pageId);\n  expectToEqual(2, response.replies.length);\n  const boxId = response.replies[0].createShape.objectId;\n  expectToExist(boxId);\n  deleteFileOnCleanup(presentationId);\n}\n\n/**\n * Test for Read Page.\n */\nfunction itShouldReadPage() {\n  const presentationId = createPresentation();\n  const slide = createSlide(presentationId);\n  const pageId = slide.replies[0].createSlide.objectId;\n  const response = readPageElementIds(presentationId, pageId);\n  expectToEqual(3, response.pageElements.length);\n  deleteFileOnCleanup(presentationId);\n}\n/**\n * Test for format shapes\n */\nfunction itShouldFormatShapes() {\n  const presentationId = createPresentation();\n  const slide = createSlide(presentationId);\n  const pageId = slide.replies[0].createSlide.objectId;\n  const shapeId = addShape(presentationId, pageId);\n  const replies = formatShapeText(presentationId, shapeId);\n  expectToExist(replies);\n  deleteFileOnCleanup(presentationId);\n}\n/**\n * Delete the file\n * @param {string} id presentationId\n */\nfunction deleteFileOnCleanup(id) {\n  Drive.Files.remove(id);\n}\n"
  },
  {
    "path": "advanced/test_tagManager.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// Before running tagManager tests create a test tagMAnager account\n// and replace the value below with its account path\nconst path = \"accounts/6007387289\";\n\n/**\n * Tests createContainerVersion function of tagManager.gs\n * @param {string} accountPath Tag manager account's path\n * @return {object} version The container version\n */\nfunction itShouldCreateContainerVersion(accountPath) {\n  console.log(\"> itShouldCreateContainerVersion\");\n  const version = createContainerVersion(accountPath);\n  return version;\n}\n\n/**\n * Tests publishVersionAndQuickPreviewDraft function of tagManager.gs\n * @param {object} version tag managers container version\n */\nfunction itShouldPublishVersionAndQuickPreviewDraft(version) {\n  console.log(\"> itShouldPublishVersionAndQuickPreviewDraft\");\n  publishVersionAndQuickPreviewDraft(version);\n}\n\n/**\n * Tests createAndReauthorizeUserEnvironment function of tagManager.gs\n * @param {object} version tag managers container version\n */\nfunction itShouldCreateAndReauthorizeUserEnvironment(version) {\n  console.log(\"> itShouldCreateAndReauthorizeUserEnvironment\");\n  createAndReauthorizeUserEnvironment(version);\n}\n\n/**\n * Tests logAllAccountUserPermissionsWithContainerAccess function of tagManager.gs\n * @param {string} accountPath Tag manager account's path\n */\nfunction itShouldLogAllAccountUserPermissionsWithContainerAccess(accountPath) {\n  console.log(\"> itShouldLogAllAccountUserPermissionsWithContainerAccess\");\n  logAllAccountUserPermissionsWithContainerAccess(accountPath);\n}\n/**\n * Runs all tests\n */\nfunction RUN_ALL_TESTS() {\n  const version = itShouldCreateContainerVersion(path);\n  itShouldPublishVersionAndQuickPreviewDraft(version);\n  itShouldCreateAndReauthorizeUserEnvironment(version);\n  itShouldLogAllAccountUserPermissionsWithContainerAccess(path);\n}\n"
  },
  {
    "path": "advanced/test_tasks.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Test functions for tasks.gs\n *\n * Add task API services to test\n */\n\n/**\n * tests listTaskLists of tasks.gs\n */\nfunction itShouldListTaskLists() {\n  console.log(\"> itShouldListTaskLists\");\n  listTaskLists();\n}\n\n/**\n * tests listTasks of tasks.gs\n */\nfunction itShouldListTasks() {\n  console.log(\"> itShouldListTasks\");\n  const taskId = Tasks.Tasklists.list().items[0].id;\n  listTasks(taskId);\n}\n\n/**\n * tests addTask of tasks.gs\n */\nfunction itShouldAddTask() {\n  console.log(\"> itShouldAddTask\");\n  const taskId = Tasks.Tasklists.list().items[0].id;\n  addTask(taskId);\n}\n\n/**\n * run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldListTaskLists();\n  itShouldListTasks();\n  itShouldAddTask();\n  itShouldListTasks();\n}\n"
  },
  {
    "path": "advanced/test_youtube.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> itShouldSearchByKeyword\");\n  searchByKeyword();\n  console.log(\"> itShouldRetrieveMyUploads\");\n  retrieveMyUploads();\n  console.log(\"> itShouldAddSubscription\");\n  addSubscription();\n  console.log(\"> itShouldCreateSlides\");\n  createSlides();\n}\n"
  },
  {
    "path": "advanced/test_youtubeAnalytics.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests createReport function of youtubeAnalytics.gs\n */\nfunction itShouldCreateReport() {\n  console.log(\"> itShouldCreateReport\");\n  createReport();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateReport();\n}\n"
  },
  {
    "path": "advanced/test_youtubeContentId.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests claimYourVideoWithMonetizePolicy function of youtubeContentId.gs\n */\nfunction itShouldClaimVideoWithMonetizePolicy() {\n  console.log(\"> itShouldClaimVideoWithMonetizePolicy\");\n  claimYourVideoWithMonetizePolicy();\n}\n\n/**\n * Tests updateAssetOwnership function of youtubeContentId.gs\n */\nfunction itShouldUpdateAssetOwnership() {\n  console.log(\"> itShouldUpdateAssetOwnership\");\n  updateAssetOwnership();\n}\n\n/**\n * Tests releaseClaim function of youtubeContentId.gs\n */\nfunction itShouldReleaseClaim() {\n  console.log(\"> itShouldReleaseClaim\");\n  releaseClaim();\n}\n\n/**\n * Run all tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldClaimVideoWithMonetizePolicy();\n  itShouldUpdateAssetOwnership();\n  itShouldReleaseClaim();\n}\n"
  },
  {
    "path": "advanced/youtube.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_youtube_search]\n/**\n * Searches for videos about dogs, then logs the video IDs and title.\n * Note that this sample limits the results to 25. To return more\n * results, pass additional parameters as shown in the YouTube Data API docs.\n * @see https://developers.google.com/youtube/v3/docs/search/list\n */\nfunction searchByKeyword() {\n  try {\n    const results = YouTube.Search.list(\"id,snippet\", {\n      q: \"dogs\",\n      maxResults: 25,\n    });\n    if (results === null) {\n      console.log(\"Unable to search videos\");\n      return;\n    }\n    for (const item of results.items) {\n      console.log(\"[%s] Title: %s\", item.id.videoId, item.snippet.title);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exceptions from Youtube API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END apps_script_youtube_search]\n\n// [START apps_script_youtube_uploads]\n/**\n * This function retrieves the user's uploaded videos by:\n * 1. Fetching the user's channel's.\n * 2. Fetching the user's \"uploads\" playlist.\n * 3. Iterating through this playlist and logs the video IDs and titles.\n * 4. If there is a next page of resuts, fetching it and returns to step 3.\n */\nfunction retrieveMyUploads() {\n  try {\n    // @see https://developers.google.com/youtube/v3/docs/channels/list\n    const results = YouTube.Channels.list(\"contentDetails\", {\n      mine: true,\n    });\n    if (!results || results.items.length === 0) {\n      console.log(\"No Channels found.\");\n      return;\n    }\n    for (let i = 0; i < results.items.length; i++) {\n      const item = results.items[i];\n      /** Get the channel ID - it's nested in contentDetails, as described in the\n       * Channel resource: https://developers.google.com/youtube/v3/docs/channels.\n       */\n      const playlistId = item.contentDetails.relatedPlaylists.uploads;\n      let nextPageToken = null;\n      do {\n        // @see: https://developers.google.com/youtube/v3/docs/playlistItems/list\n        const playlistResponse = YouTube.PlaylistItems.list(\"snippet\", {\n          playlistId: playlistId,\n          maxResults: 25,\n          pageToken: nextPageToken,\n        });\n        if (!playlistResponse || playlistResponse.items.length === 0) {\n          console.log(\"No Playlist found.\");\n          break;\n        }\n        for (let j = 0; j < playlistResponse.items.length; j++) {\n          const playlistItem = playlistResponse.items[j];\n          console.log(\n            \"[%s] Title: %s\",\n            playlistItem.snippet.resourceId.videoId,\n            playlistItem.snippet.title,\n          );\n        }\n        nextPageToken = playlistResponse.nextPageToken;\n      } while (nextPageToken);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with err %s\", err.message);\n  }\n}\n// [END apps_script_youtube_uploads]\n\n// [START apps_script_youtube_subscription]\n/**\n * This sample subscribes the user to the Google Developers channel on YouTube.\n * @see https://developers.google.com/youtube/v3/docs/subscriptions/insert\n */\nfunction addSubscription() {\n  // Replace this channel ID with the channel ID you want to subscribe to\n  const channelId = \"UC_x5XG1OV2P6uZZ5FSM9Ttw\";\n  const resource = {\n    snippet: {\n      resourceId: {\n        kind: \"youtube#channel\",\n        channelId: channelId,\n      },\n    },\n  };\n\n  try {\n    const response = YouTube.Subscriptions.insert(resource, \"snippet\");\n    console.log(\n      \"Added subscription for channel title : %s\",\n      response.snippet.title,\n    );\n  } catch (e) {\n    if (e.message.match(\"subscriptionDuplicate\")) {\n      console.log(\n        `Cannot subscribe; already subscribed to channel: ${channelId}`,\n      );\n    } else {\n      // TODO (developer) - Handle exception\n      console.log(`Error adding subscription: ${e.message}`);\n    }\n  }\n}\n// [END apps_script_youtube_subscription]\n\n// [START apps_script_youtube_slides]\n/**\n * Creates a slide presentation with 10 videos from the YouTube search `YOUTUBE_QUERY`.\n * The YouTube Advanced Service must be enabled before using this sample.\n */\nconst PRESENTATION_TITLE = \"San Francisco, CA\";\nconst YOUTUBE_QUERY = \"San Francisco, CA\";\n\n/**\n * Gets a list of YouTube videos.\n * @param {String} query - The query term to search for.\n * @return {object[]} A list of objects with YouTube video data.\n * @see https://developers.google.com/youtube/v3/docs/search/list\n */\nfunction getYouTubeVideosJSON(query) {\n  const youTubeResults = YouTube.Search.list(\"id,snippet\", {\n    q: query,\n    type: \"video\",\n    maxResults: 10,\n  });\n\n  return youTubeResults.items.map((item) => {\n    return {\n      url: `https://youtu.be/${item.id.videoId}`,\n      title: item.snippet.title,\n      thumbnailUrl: item.snippet.thumbnails.high.url,\n    };\n  });\n}\n\n/**\n * Creates a presentation where each slide features a YouTube video.\n * Logs out the URL of the presentation.\n */\nfunction createSlides() {\n  try {\n    const youTubeVideos = getYouTubeVideosJSON(YOUTUBE_QUERY);\n    const presentation = SlidesApp.create(PRESENTATION_TITLE);\n    presentation\n      .getSlides()[0]\n      .getPageElements()[0]\n      .asShape()\n      .getText()\n      .setText(PRESENTATION_TITLE);\n    if (!presentation) {\n      console.log(\"Unable to create presentation\");\n      return;\n    }\n    // Add slides with videos and log the presentation URL to the user.\n    for (const video of youTubeVideos) {\n      const slide = presentation.appendSlide();\n      slide.insertVideo(\n        video.url,\n        0,\n        0,\n        presentation.getPageWidth(),\n        presentation.getPageHeight(),\n      );\n    }\n    console.log(presentation.getUrl());\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_youtube_slides]\n"
  },
  {
    "path": "advanced/youtubeAnalytics.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_youtube_report]\n/**\n * Creates a spreadsheet containing daily view counts, watch-time metrics,\n * and new-subscriber counts for a channel's videos.\n */\nfunction createReport() {\n  // Retrieve info about the user's YouTube channel.\n  const channels = YouTube.Channels.list(\"id,contentDetails\", {\n    mine: true,\n  });\n  const channelId = channels.items[0].id;\n\n  // Retrieve analytics report for the channel.\n  const oneMonthInMillis = 1000 * 60 * 60 * 24 * 30;\n  const today = new Date();\n  const lastMonth = new Date(today.getTime() - oneMonthInMillis);\n\n  const metrics = [\n    \"views\",\n    \"estimatedMinutesWatched\",\n    \"averageViewDuration\",\n    \"subscribersGained\",\n  ];\n  const result = YouTubeAnalytics.Reports.query({\n    ids: `channel==${channelId}`,\n    startDate: formatDateString(lastMonth),\n    endDate: formatDateString(today),\n    metrics: metrics.join(\",\"),\n    dimensions: \"day\",\n    sort: \"day\",\n  });\n\n  if (!result.rows) {\n    console.log(\"No rows returned.\");\n    return;\n  }\n  const spreadsheet = SpreadsheetApp.create(\"YouTube Analytics Report\");\n  const sheet = spreadsheet.getActiveSheet();\n\n  // Append the headers.\n  const headers = result.columnHeaders.map((columnHeader) => {\n    return formatColumnName(columnHeader.name);\n  });\n  sheet.appendRow(headers);\n\n  // Append the results.\n  sheet\n    .getRange(2, 1, result.rows.length, headers.length)\n    .setValues(result.rows);\n\n  console.log(\"Report spreadsheet created: %s\", spreadsheet.getUrl());\n}\n\n/**\n * Converts a Date object into a YYYY-MM-DD string.\n * @param {Date} date The date to convert to a string.\n * @return {string} The formatted date.\n */\nfunction formatDateString(date) {\n  return Utilities.formatDate(date, Session.getScriptTimeZone(), \"yyyy-MM-dd\");\n}\n\n/**\n * Formats a column name into a more human-friendly name.\n * @param {string} columnName The unprocessed name of the column.\n * @return {string} The formatted column name.\n * @example \"averageViewPercentage\" becomes \"Average View Percentage\".\n */\nfunction formatColumnName(columnName) {\n  let name = columnName.replace(/([a-z])([A-Z])/g, \"$1 $2\");\n  name = name.slice(0, 1).toUpperCase() + name.slice(1);\n  return name;\n}\n// [END apps_script_youtube_report]\n"
  },
  {
    "path": "advanced/youtubeContentId.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_youtube_claim]\n/**\n * This function creates a partner-uploaded claim on a video with the specified\n * asset and policy rules.\n * @see https://developers.google.com/youtube/partner/docs/v1/claims/insert\n */\nfunction claimYourVideoWithMonetizePolicy() {\n  // The ID of the content owner that you are acting on behalf of.\n  const onBehalfOfContentOwner = \"replaceWithYourContentOwnerID\";\n  // A YouTube video ID to claim. In this example, the video must be uploaded\n  // to one of your onBehalfOfContentOwner's linked channels.\n  const videoId = \"replaceWithYourVideoID\";\n  const assetId = \"replaceWithYourAssetID\";\n  const claimToInsert = {\n    videoId: videoId,\n    assetId: assetId,\n    contentType: \"audiovisual\",\n    // Set the claim policy to monetize. You can also specify a policy ID here\n    // instead of policy rules.\n    // For details, please refer to the YouTube Content ID API Policies\n    // documentation:\n    // https://developers.google.com/youtube/partner/docs/v1/policies\n    policy: { rules: [{ action: \"monetize\" }] },\n  };\n  try {\n    const claimInserted = YouTubeContentId.Claims.insert(claimToInsert, {\n      onBehalfOfContentOwner: onBehalfOfContentOwner,\n    });\n    console.log(\"Claim created on video %s: %s\", videoId, claimInserted);\n  } catch (e) {\n    console.log(\n      \"Failed to create claim on video %s, error: %s\",\n      videoId,\n      e.message,\n    );\n  }\n}\n// [END apps_script_youtube_claim]\n\n// [START apps_script_youtube_update_asset_ownership]\n/**\n * This function updates your onBehalfOfContentOwner's ownership on an existing\n * asset.\n * @see https://developers.google.com/youtube/partner/docs/v1/ownership/update\n */\nfunction updateAssetOwnership() {\n  // The ID of the content owner that you are acting on behalf of.\n  const onBehalfOfContentOwner = \"replaceWithYourContentOwnerID\";\n  // Replace values with your asset id\n  const assetId = \"replaceWithYourAssetID\";\n  // The new ownership here would replace your existing ownership on the asset.\n  const myAssetOwnership = {\n    general: [\n      {\n        ratio: 100,\n        owner: onBehalfOfContentOwner,\n        type: \"include\",\n        territories: [\"US\", \"CA\"],\n      },\n    ],\n  };\n  try {\n    const updatedOwnership = YouTubeContentId.Ownership.update(\n      myAssetOwnership,\n      assetId,\n      { onBehalfOfContentOwner: onBehalfOfContentOwner },\n    );\n    console.log(\"Ownership updated on asset %s: %s\", assetId, updatedOwnership);\n  } catch (e) {\n    console.log(\n      \"Ownership update failed on asset %s, error: %s\",\n      assetId,\n      e.message,\n    );\n  }\n}\n// [END apps_script_youtube_update_asset_ownership]\n\n// [START apps_script_youtube_release_claim]\n/**\n * This function releases an existing claim your onBehalfOfContentOwner has\n * on a video.\n * @see https://developers.google.com/youtube/partner/docs/v1/claims/patch\n */\nfunction releaseClaim() {\n  // The ID of the content owner that you are acting on behalf of.\n  const onBehalfOfContentOwner = \"replaceWithYourContentOwnerID\";\n  // The ID of the claim to be released.\n  const claimId = \"replaceWithYourClaimID\";\n  // To release the claim, change the resource's status to inactive.\n  const claimToBeReleased = {\n    status: \"inactive\",\n  };\n  try {\n    const claimReleased = YouTubeContentId.Claims.patch(\n      claimToBeReleased,\n      claimId,\n      { onBehalfOfContentOwner: onBehalfOfContentOwner },\n    );\n    console.log(\"Claim %s was released: %s\", claimId, claimReleased);\n  } catch (e) {\n    console.log(\"Failed to release claim %s, error: %s\", claimId, e.message);\n  }\n}\n// [END apps_script_youtube_release_claim]\n"
  },
  {
    "path": "ai/autosummarize/README.md",
    "content": "# Editor Add-on: Sheets - AutoSummarize AI\n\n## Project Description\n\nGoogle Workspace Editor Add-on for Google Sheets that uses AI to create AI summaries in bulk for a listing of Google Docs and Slides files.\n\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n\n1. Create a Cloud Project\n   1. Enable the Vertex AI API\n   1. Create a Service Account and grant the role Service Account Token Creator Role\n   1. Create a private key with type JSON. This will download the JSON file for use in the next section.\n1. Open an Apps Script Project bound to a Google Sheets Spreadsheet.\n   1. Rename the script to `Autosummarize AI`.\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `model_id` as the property name and `gemini-pro-vision` as the value. \n   1. Add a Script Property. Enter `project_location` as the property name and `us-central1` as the value. \n   1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. \n1. Add `OAuth2 v43` Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.\n1. Enable the `Drive v3` advanced service.\n1. Add the project code to Apps Script\n\n\n## Usage\n\n1. Insert one or more links to any Google Doc or Slides files in a column.\n1. Select one or more of the links in the sheet.\n1. From the `Sheets` menu, select `Extensions > AutoSummarize AI > Open AutoSummarize AI`\n1. Click Get summaries button.\n"
  },
  {
    "path": "ai/autosummarize/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Denver\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"43\",\n        \"developmentMode\": false\n      }\n    ],\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"version\": \"v3\",\n        \"serviceId\": \"drive\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "ai/autosummarize/gemini.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nfunction scriptPropertyWithDefault(key, defaultValue = undefined) {\n  const scriptProperties = PropertiesService.getScriptProperties();\n  const value = scriptProperties.getProperty(key);\n  if (value) {\n    return value;\n  }\n  return defaultValue;\n}\n\nconst VERTEX_AI_LOCATION = scriptPropertyWithDefault(\n  \"project_location\",\n  \"us-central1\",\n);\nconst MODEL_ID = scriptPropertyWithDefault(\"model_id\", \"gemini-pro-vision\");\nconst SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault(\"service_account_key\");\n\n/**\n * Packages prompt and necessary settings, then sends a request to\n * Vertex API. Returns the response as an JSON object extracted from the\n * Vertex API response object.\n *\n * @param {string} prompt The prompt to senb to Vertex AI API.\n * @param {string} options.temperature The temperature setting set by user.\n * @param {string} options.maxOutputTokens The number of tokens to limit to the prompt.\n */\nfunction getAiSummary(parts, options = {}) {\n  const defaultOptions = {\n    temperature: 0.1,\n    maxOutputTokens: 8192,\n    topK: 1,\n    topP: 1,\n    stopSequences: [],\n  };\n  const request = {\n    contents: [\n      {\n        role: \"user\",\n        parts: parts,\n      },\n    ],\n    generationConfig: {\n      ...defaultOptions,\n      ...options,\n    },\n  };\n\n  const credentials = credentialsForVertexAI();\n\n  const fetchOptions = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${credentials.accessToken}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true,\n    payload: JSON.stringify(request),\n  };\n\n  const url =\n    `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` +\n    `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n  const response = UrlFetchApp.fetch(url, fetchOptions);\n\n  const responseCode = response.getResponseCode();\n  if (responseCode >= 400) {\n    throw new Error(`Unable to process file: Error code ${responseCode}`);\n  }\n\n  const responseText = response.getContentText();\n  const parsedResponse = JSON.parse(responseText);\n  if (parsedResponse.error) {\n    throw new Error(parsedResponse.error.message);\n  }\n  const text = parsedResponse.candidates[0].content.parts[0].text;\n  return text;\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n * Requires use of Service Account Key stored with project\n *\n * @return {!Object} Containing the Cloud Project Id and the access token.\n */\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/autosummarize/main.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Creates a menu entry in the Google Sheets Extensions menu when the document is opened.\n *\n * @param {object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  SpreadsheetApp.getUi()\n    .createAddonMenu()\n    .addItem(\"📄 Open AutoSummarize AI\", \"showSidebar\")\n    .addSeparator()\n    .addItem(\"❎ Quick summary\", \"doAutoSummarizeAI\")\n    .addItem(\"❌ Remove all summaries\", \"removeAllSummaries\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately. This method is only used by\n * the desktop add-on and is never called by the mobile version.\n *\n * @param {object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens sidebar in Sheets with AutoSummarize AI interface.\n */\nfunction showSidebar() {\n  const ui =\n    HtmlService.createHtmlOutputFromFile(\"sidebar\").setTitle(\n      \"AutoSummarize AI\",\n    );\n  SpreadsheetApp.getUi().showSidebar(ui);\n}\n\n/**\n * Deletes all of the AutoSummarize AI created sheets\n *  i.e. any sheets with prefix of 'AutoSummarize AI'\n */\nfunction removeAllSummaries() {\n  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();\n  const allSheets = spreadsheet.getSheets();\n\n  for (const sheet of allSheets) {\n    const sheetName = sheet.getName();\n    // Check if the sheet name starts with \"AutoSummarize AI\"\n    if (sheetName.startsWith(\"AutoSummarize AI\")) {\n      spreadsheet.deleteSheet(sheet);\n    }\n  }\n}\n\n/**\n * Wrapper function for add-on.\n */\nfunction doAutoSummarizeAI(\n  customPrompt1,\n  customPrompt2,\n  temperature = 0.1,\n  tokens = 2048,\n) {\n  // Get selected cell values.\n  console.log(\"Getting selection...\");\n  const selection = SpreadsheetApp.getSelection()\n    .getActiveRange()\n    .getRichTextValues()\n    .map((value) => {\n      if (value[0].getLinkUrl()) {\n        return value[0].getLinkUrl();\n      }\n      return value[0].getText();\n    });\n\n  // Get AI summary\n  const data = summarizeFiles(\n    selection,\n    customPrompt1,\n    customPrompt2,\n    temperature,\n    tokens,\n  );\n\n  // Add and format a new new sheet.\n  const now = new Date();\n  const nowFormatted = Utilities.formatDate(\n    now,\n    now.getTimezoneOffset().toString(),\n    \"MM/dd HH:mm\",\n  );\n  let sheetName = `AutoSummarize AI (${nowFormatted})`;\n  if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) {\n    sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`;\n  }\n  const aiSheet = SpreadsheetApp.getActiveSpreadsheet()\n    .insertSheet()\n    .setName(sheetName);\n  const aiSheetHeaderStyle = SpreadsheetApp.newTextStyle()\n    .setFontSize(12)\n    .setBold(true)\n    .setFontFamily(\"Google Sans\")\n    .setForegroundColor(\"#ffffff\")\n    .build();\n  const aiSheetValuesStyle = SpreadsheetApp.newTextStyle()\n    .setFontSize(10)\n    .setBold(false)\n    .setFontFamily(\"Google Sans\")\n    .setForegroundColor(\"#000000\")\n    .build();\n  aiSheet\n    .getRange(\"A1:E1\")\n    .setBackground(\"#434343\")\n    .setTextStyle(aiSheetHeaderStyle)\n    .setValues([\n      [\n        \"Link\",\n        \"Title\",\n        `Summary from Gemini AI [Temperature: ${temperature}]`,\n        `Custom Prompt #1: ${customPrompt1}`,\n        `Custom Prompt #2: ${customPrompt2}`,\n      ],\n    ])\n    .setWrap(true);\n  aiSheet.setColumnWidths(1, 1, 100);\n  aiSheet.setColumnWidths(2, 1, 300);\n  aiSheet.setColumnWidths(3, 3, 300);\n\n  // Copy results\n  aiSheet.getRange(`A2:E${data.length + 1}`).setValues(data);\n\n  aiSheet\n    .getRange(`A2:E${data.length + 1}`)\n    .setBackground(\"#ffffff\")\n    .setTextStyle(aiSheetValuesStyle)\n    .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP)\n    .setVerticalAlignment(\"top\");\n  aiSheet\n    .getRange(`C2:E${data.length + 1}`)\n    .setBackground(\"#efefef\")\n    .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP);\n\n  aiSheet.deleteColumns(8, 19);\n  aiSheet.deleteRows(\n    aiSheet.getLastRow() + 1,\n    aiSheet.getMaxRows() - aiSheet.getLastRow(),\n  );\n}\n"
  },
  {
    "path": "ai/autosummarize/sidebar.html",
    "content": "<!-- \nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n\n<!DOCTYPE html>\n<html>\n\n<head>\n  <base target=\"_top\">\n  <style>\n    <!--\n    div.container {\n      padding: 10px;\n      font-family: Google Sans, Roboto, Arial, Sans-serif;\n    }\n\n    h1 {\n      font-size: 1.4em;\n      margin-top: 0;\n    }\n\n    img.logo {\n      height: 35px;\n    }\n\n    div.note {\n      background: #fff2cc;\n      background-image: url(\"https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/error/default/24px.svg\");\n      background-position-x: 10px;\n      background-position-y: 10px;\n      background-repeat: no-repeat;\n      border-radius: 10px;\n      font-size: 0.9em;\n      padding: 10px 10px 10px 40px;\n      margin: 0 0 8px 0;\n    }\n\n    div.customPrompts>div {\n      margin-top: 5px;\n    }\n\n    div.customPrompts>div>label {\n      color: #8f8f8f;\n      font-size: 0.8em;\n      font-weight: bold;\n      margin-left: 5px;\n    }\n\n    div.customPrompts>div>textarea {\n      border: 1px #8f8f8f solid;\n      border-radius: 10px;\n      font-family: Google Sans, Roboto, Arial, Sans-serif;\n      height: 50px;\n      padding: 8px;\n      width: 92%;\n    }\n\n    li {\n      margin-top: 8px;\n      font-size: 0.9em;\n    }\n\n    div#status {\n      color: #aaa;\n      font-size: 0.9em;\n      font-style: italic;\n      margin: 20px auto 8px auto;\n      text-align: center;\n    }\n\n    div#wip {\n      width: 80px;\n      margin: 0 auto;\n      display: none;\n    }\n\n    div#wip>img {\n      height: 15px;\n    }\n\n    button {\n      -webkit-appearance: none;\n      margin-top: 10px;\n      border-width: 0px;\n      border-radius: 0.3em;\n      width: auto;\n      height: 36px;\n      padding: 0px 24px;\n      font-family: 'Google Sans';\n      font-size: 14px;\n      cursor: pointer;\n    }\n\n    button#run {\n      background-color: #1A73E8;\n      color: #fff;\n    }\n\n    button#help {\n      background-color: #fff;\n      border-color: #1A73E8;\n      color: #1A73E8;\n    }\n\n\n    button#settings {\n      background-color: #fff;\n      border-color: #1A73E8;\n      color: #302b2b;\n    }\n\n    button.disabledButton,\n    button.disabledButton:hover {\n      background-color: #ccc;\n      pointer-events: none;\n    }\n\n    button:hover {\n      background-color: #6ca6cd;\n      box-shadow: 0 2px 1px -1px rgba(26, 115, 232, 0.2), 0 1px 1px 0 rgba(26, 115, 232, 0.14), 0 1px 3px 0 rgba(26, 115, 232, 0.12);\n    }\n\n    .tooltip {\n      position: relative;\n      /* Needed for absolute positioning of tooltip */\n      display: inline-block;\n      /* Keep element on one line */\n    }\n\n    .tooltiptext {\n      visibility: hidden;\n      /* Hide the tooltip text by default */\n      background-color: #aad5ff;\n      color: black;\n      font-size: 14px;\n      text-align: left;\n      padding: 8px;\n      border-radius: 6px;\n      width: 255px;\n      line-height: 1.5;\n\n      position: absolute;\n      /* Position the tooltip absolutely */\n      z-index: 1;\n      /* Ensure tooltip appears above other content */\n      bottom: 125%;\n      /* Place below the main text */\n      left: 50%;\n      /* Center the tooltip */\n      margin-left: -150px;\n      /* Adjust centering if needed */\n    }\n\n    .tooltip:hover .tooltiptext {\n      visibility: visible;\n      /* Show the tooltip text on hover */\n    }\n    -->\n  </style>\n</head>\n\n<body>\n  <div class=\"container\">\n    <h1>\n      <img src=\"https://storage.cloud.google.com/demos-workspace-next24/AutoSummarizeLogo.png\" class=\"logo\" />\n        AutoSummarize AI\n    </h1>\n\n\n    <!-- Instructions -->\n\n    <p id=\"instructions\" onClick=\"divToggle('divInstructions', this, 'How to Use...')\"><small>&#9660</small>&nbsp;How to\n      Use...</p>\n\n    <div id=\"divInstructions\" class=\"customPrompts\" style=\"display: none;\">\n      <ol>\n        <li>In a column of this sheet, select up to 50 links to Google Slides or Google Docs files.</li>\n        <li>(Optional) Add your own <b>custom prompts</b> to get even more insights from each file.</li>\n        <li>Click <b>Get summaries</b>.</li>\n        <li>A new sheet is populated with summaries and custom prompt responses for selected links, authored by Gemini\n          AI.\n        </li>\n      </ol>\n      <div class=\"note\">\n        <b>Note:</b> Smart chips aren't supported just yet.\n        <p />\n        <div style=\"font-size: 0.8em;\">Use Data Extraction on smart chips (e.g. =A1.url) to access the underlying url\n        </div>\n      </div>\n      <p>\n        <hr>\n      <p>\n\n    </div>\n\n    <!-- Custom Prompts -->\n\n    <p id=\"custom\" onClick=\"divToggle('divCustom', this, 'Custom Prompts')\"><small>&#9650</small>&nbsp;Custom\n      Prompts </p>\n\n    <div id=\"divCustom\" class=\"customPrompts\" style=\"display: block;\">\n      <div>\n        <label>Prompt One [Optional]</label>\n        <textarea id=\"customPrompt1\" placeholder=\"e.g. What are the key takeaways for developers from this content?\"></textarea>\n      </div>\n      <div>\n        <label>Prompt Two [Optional]</label>\n        <textarea id=\"customPrompt2\" placeholder=\"e.g. Summarize the challenges discussed in this document.\"></textarea>\n      </div>\n      <p>\n        <hr>\n      <p>\n\n    </div>\n\n    <!-- Prompt Settings -->\n\n    <p id=\"settings\" onClick=\"divToggle('divSettings', this, 'Prompt Settings')\"><small>&#9660</small>&nbsp;Prompt\n      Settings</p>\n\n    <div id=\"divSettings\" style=\"display: none;\">\n      <p>Temperature&emsp;&emsp;&emsp;\n        <span class=\"tooltip\">&#x2753;<span class=\"tooltiptext\"><b>Temperature</b> controls the degree of randomness in token selection. <br><br><b>Lower temperatures</b> are good for prompts that expect a true or correct response, while <b>higher temperatures</b> can lead to more diverse or unexpected results. <br><br>With a <b>temperature of 0</b> the highest probability token is always selected.</span></span>\n      </p>\n      0 <input id = \"sliderTemperature\" type=\"range\" value=\"10\" min=\"1\" max=\"100\"  class=\"slider\" onchange=\"tempSync(this.id)\"> 1 &nbsp;&nbsp;&nbsp;\n      <input id = \"textTemperature\" type=\"text\" value=\"0.1\" min =\"0\" max= \"1\" class=\"temp\" size=\"2\" oninput=\"tempSync(this.id)\">\n\n      <p>Output token limit\n        <span class=\"tooltip\">&#x2753;<span class=\"tooltiptext\"><b>Output token limit</b> determines the maximum amount of text output from one prompt. A token is approximately four characters. <br><br>If you need a larger output token limit, contact your friendly developer to use the Gemini API .</span></span>\n      </p>\n      1 <input id = \"sliderTokens\" type=\"range\" min=\"1\" max=\"2048\" value=\"2048\" class=\"slider\" onchange=\"tokenSync(this.value)\"> 2048 &nbsp;\n      <input id = \"textTokens\" type=\"text\"  min =\"1\" max= \"2048\" value=\"2048\" class=\"temp\" size=\"4\" oninput=\"tokenSync(this.value)\">\n\n\n\n      <p>\n        <hr>\n      <p>\n    </div>\n\n\n    <!-- Footer  -->\n\n\n    <div>\n      <button id=\"run\">Get summaries</button>\n      <button id=\"help\" onClick=\"window.open('https://docs.google.com/document/d/1z5wjws0vHNN5evVxEw73tvHdLY2QYKV_W-mCzDZDi8M/')\">Help</button>\n\n      <div id=\"status\"></div>\n      <div id=\"wip\">\n        <img src=\"https://storage.cloud.google.com/demos-workspace-next24/AutoSummarizeProcess.gif\" />\n      </div>\n    </div>\n\n    <script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n    <script>\n      /**\n       * On document load, assign click handlers to each button and try to load the\n       * user's origin and destination language preferences if previously set.\n       */\n      $(function() {\n        $('#run').click(runScript);\n      });\n\n      /**\n       * Runs a server-side function to translate the user-selected text and update\n       * the sidebar UI with the resulting translation.\n       */\n      function runScript() {\n        $('#run').toggleClass(\"disabledButton\");\n        $('#status').text(\"Creating sheet...\");\n        $('#wip').show();\n        let statusUpdates = setTimeout(function changeText1() {\n          $('#status').text(\"Reticulating splines...\");\n          statusUpdates = setTimeout(function changeText2() {\n            $('#status').text(\"Talking to Gemini AI...\");\n            statusUpdates = setTimeout(function changeText3() {\n              $('#status').text(\"Gemini AI is reading...\");\n              statusUpdates = setTimeout(function changeText4() {\n                $('#status').text(\"Gemini AI is writing...\");\n                  statusUpdates = setTimeout(function changeText5() {\n                    $('#status').text(\"Preparing delivery...\");\n                  }, 10000);\n              }, 6000);\n            }, 8000);\n          }, 3000);\n        }, 3000);\n        let customPrompt1 = document.getElementById(\"customPrompt1\").value;\n        let customPrompt2 = document.getElementById(\"customPrompt2\").value;\n        let temperature = document.getElementById(\"sliderTemperature\").value / 100;\n        let tokens = document.getElementById(\"sliderTokens\").value;\n        google.script.run\n            .withSuccessHandler(\n                    function(response) {\n                      clearTimeout(statusUpdates);\n                      $('#wip').hide();\n                      $('#status').text(\"Your AI summaries are here! \\u{1F389}\");\n                      $('#run').toggleClass(\"disabledButton\");\n                    })\n            .withFailureHandler(\n                    function(err) {\n                      $('#wip').hide();\n                      $('#status').text(\"Something didn't quite work right. \" + err);\n                      $('#run').toggleClass(\"disabledButton\");\n                    })\n            .doAutoSummarizeAI(customPrompt1,customPrompt2,temperature,tokens);\n      }\n\n    function divToggle(element,caller, text) {\n      var x = document.getElementById(element);\n        if (x.style.display === \"none\") {\n          x.style.display = \"block\";\n          caller.innerHTML = \"<small>&#9650</small>&nbsp;\" + text\n      } else {\n        x.style.display = \"none\";\n          caller.innerHTML = \"<small>&#9660</small>&nbsp;\" + text\n      }\n    }\n    \n\nfunction tempSync(element) {\n\n  var x = document.getElementById(element)\n  var val = x.value\n\n  if (x.id === \"sliderTemperature\") {\n    val = (val / 100).toFixed(1)\n    document.getElementById(\"textTemperature\").value = val\n  } else {\n    val = (val * 100).toFixed(1)\n    document.getElementById(\"sliderTemperature\").value = val\n  }\n}\n\nfunction tokenSync(val) {\n\n    document.getElementById(\"sliderTokens\").value = val\n    document.getElementById(\"textTokens\").value = val\n}\n\n\n    </script>\n</body>\n\n</html>"
  },
  {
    "path": "ai/autosummarize/summarize.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Exports a Google Doc/Sheet/Slide to the requested format.\n *\n * @param {string} fileId - ID of file to export\n * @param {string} targetType - MIME type to export as\n * @return Base64 encoded file content\n */\nfunction exportFile(fileId, targetType = \"application/pdf\") {\n  const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(targetType)}&supportsAllDrives=true`;\n\n  const requestOptions = {\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n  };\n  const response = UrlFetchApp.fetch(exportUrl, requestOptions);\n  const blob = response.getBlob();\n\n  return Utilities.base64Encode(blob.getBytes());\n}\n\n/**\n * Downloads a binary file from Drive.\n *\n * @param {string} fileId - ID of file to export\n * @param {string} targetType - MIME type to export as\n * @return Base64 encoded file content\n */\nfunction downloadFile(fileId) {\n  const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`;\n\n  const requestOptions = {\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n  };\n  const response = UrlFetchApp.fetch(exportUrl, requestOptions);\n  const blob = response.getBlob();\n\n  return Utilities.base64Encode(blob.getBytes());\n}\n\n/**\n * Main function for AutoSummarize AI process.\n */\nfunction summarizeFiles(\n  sourceSheetLinks,\n  customPrompt1,\n  customPrompt2,\n  temperature,\n  tokens,\n) {\n  return sourceSheetLinks.map((fileUrl) => {\n    console.log(\"Processing:\", fileUrl);\n\n    let fileName = \"\";\n    let summary = \"\";\n    let customPrompt1Response = \"\";\n    let customPrompt2Response = \"\";\n\n    if (!fileUrl) {\n      return [\n        \"\",\n        fileName,\n        summary,\n        customPrompt1Response,\n        customPrompt2Response,\n      ];\n    }\n    try {\n      const promptParts = [\n        {\n          text: \"Summarize the following document.\",\n        },\n        {\n          text: \"Return your response as a single paragraph. Reformat any lists as part of the paragraph. Output only the single paragraph as plain text. Do not use more than 3 sentences. Do not use markdown.\",\n        },\n      ];\n      const fileIdMatchPattern = /\\/d\\/(.*?)\\//gi;\n      const match = fileIdMatchPattern.exec(fileUrl);\n      if (!match) {\n        console.log(`Could not extract file ID from URL: ${fileUrl}`);\n        return [\n          fileUrl,\n          fileName,\n          \"Could not extract file ID from URL.\",\n          \"\",\n          \"\",\n        ];\n      }\n      const fileId = match[1];\n\n      // Get file title and type.\n      const currentFile = Drive.Files.get(fileId, { supportsAllDrives: true });\n      const fileMimeType = currentFile.mimeType;\n      fileName = currentFile.name;\n\n      console.log(`Processing ${fileName} (ID: ${fileId})...`);\n\n      // Add file content to the prompt\n      switch (fileMimeType) {\n        case \"application/vnd.google-apps.presentation\":\n        case \"application/vnd.google-apps.document\":\n        case \"application/vnd.google-apps.spreadsheet\":\n          promptParts.push({\n            inlineData: {\n              mimeType: \"application/pdf\",\n              data: exportFile(fileId, \"application/pdf\"),\n            },\n          });\n          break;\n        case \"application/pdf\":\n        case \"image/gif\":\n        case \"image/jpeg\":\n        case \"image/png\":\n          promptParts.push({\n            inlineData: {\n              mimeType: fileMimeType,\n              data: downloadFile(fileId),\n            },\n          });\n          break;\n        default:\n          console.log(`Unsupported file type: ${fileMimeType}`);\n          return [\n            fileUrl,\n            fileName,\n            summary,\n            customPrompt1Response,\n            customPrompt2Response,\n          ];\n      }\n\n      // Prompt for summary\n      const geminiOptions = {\n        temperature,\n        maxOutputTokens: tokens,\n      };\n      summary = getAiSummary(promptParts, geminiOptions);\n\n      // If any custom prompts, request those too\n      if (customPrompt1) {\n        promptParts[0].text = customPrompt1;\n        customPrompt1Response = getAiSummary(promptParts, geminiOptions);\n      }\n      if (customPrompt2) {\n        promptParts[0].text = customPrompt2;\n        customPrompt2Response = getAiSummary(promptParts, geminiOptions);\n      }\n\n      return [\n        fileUrl,\n        fileName,\n        summary,\n        customPrompt1Response,\n        customPrompt2Response,\n      ];\n    } catch (e) {\n      // Add error row values if anything else goes wrong.\n      console.log(e);\n      return [\n        fileUrl,\n        fileName,\n        \"Something went wrong. Make sure you have access to this row's link.\",\n        \"\",\n        \"\",\n      ];\n    }\n  });\n}\n"
  },
  {
    "path": "ai/custom-func-ai-agent/AiVertex.js",
    "content": "/*\nCopyright 2025 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst LOCATION =\n  PropertiesService.getScriptProperties().getProperty(\"LOCATION\");\nconst GEMINI_MODEL_ID =\n  PropertiesService.getScriptProperties().getProperty(\"GEMINI_MODEL_ID\");\nconst REASONING_ENGINE_ID = PropertiesService.getScriptProperties().getProperty(\n  \"REASONING_ENGINE_ID\",\n);\nconst SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty(\n  \"SERVICE_ACCOUNT_KEY\",\n);\n\nconst credentials = credentialsForVertexAI();\n\n/**\n * @param {string} statement The statement to fact-check.\n */\nfunction requestLlmAuditorAdkAiAgent(statement) {\n  return UrlFetchApp.fetch(\n    `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/reasoningEngines/${REASONING_ENGINE_ID}:streamQuery?alt=sse`,\n    {\n      method: \"post\",\n      headers: { Authorization: `Bearer ${credentials.accessToken}` },\n      contentType: \"application/json\",\n      muteHttpExceptions: true,\n      payload: JSON.stringify({\n        class_method: \"async_stream_query\",\n        input: {\n          user_id: \"google_sheets_custom_function_fact_check\",\n          message: statement,\n        },\n      }),\n    },\n  ).getContentText();\n}\n\n/**\n * @param {string} prompt The Gemini prompt to use.\n */\nfunction requestOutputFormatting(prompt) {\n  const response = UrlFetchApp.fetch(\n    `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/publishers/google/models/${GEMINI_MODEL_ID}:generateContent`,\n    {\n      method: \"post\",\n      headers: { Authorization: `Bearer ${credentials.accessToken}` },\n      contentType: \"application/json\",\n      muteHttpExceptions: true,\n      payload: JSON.stringify({\n        contents: [\n          {\n            role: \"user\",\n            parts: [{ text: prompt }],\n          },\n        ],\n        generationConfig: { temperature: 0.1, maxOutputTokens: 2048 },\n        safetySettings: [\n          {\n            category: \"HARM_CATEGORY_HARASSMENT\",\n            threshold: \"BLOCK_NONE\",\n          },\n          {\n            category: \"HARM_CATEGORY_HATE_SPEECH\",\n            threshold: \"BLOCK_NONE\",\n          },\n          {\n            category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n            threshold: \"BLOCK_NONE\",\n          },\n          {\n            category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n            threshold: \"BLOCK_NONE\",\n          },\n        ],\n      }),\n    },\n  );\n  return JSON.parse(response).candidates[0].content.parts[0].text;\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n * Requires use of Service Account Key stored with project.\n *\n * @return {!Object} Containing the Google Cloud project ID and the access token.\n */\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/custom-func-ai-agent/Code.js",
    "content": "/*\nCopyright 2025 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst DEFAULT_OUTPUT_FORMAT =\n  \"Summarize it. Only keep the verdict result and main arguments. \" +\n  \"Do not reiterate the fact being checked. Remove all markdown. \" +\n  \"State the verdit result in a first paragraph in a few words and \" +\n  \"the rest of the summary in a second paragraph.\";\n\n/**\n * Passes a statement to fact-check and, optionally, output formatting instructions.\n *\n * @param {string} statement The statement to fact-check as a string or single cell\n *   reference (data ranges are not supported).\n * @param {string} outputFormat The instructions as a string or single cell reference\n *   (data ranges are not supported).\n *\n * @return The generated and formatted verdict\n * @customfunction\n */\nfunction FACT_CHECK(statement, outputFormat = DEFAULT_OUTPUT_FORMAT) {\n  return requestOutputFormatting(\n    `Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\\n\\n${outputFormat}`,\n  );\n}\n"
  },
  {
    "path": "ai/custom-func-ai-agent/README.md",
    "content": "# Google Sheets Custom Function relying on ADK AI Agent and Gemini model\n\nA [Vertex AI](https://cloud.google.com/vertex-ai) agent-powered **fact checker** custom function for Google Sheets to be used as a bound Apps Script project.\n\n![](./images/showcase.png)\n\n## Tutorial\n\nFor detailed instructions to deploy and run this sample, follow the\n[dedicated tutorial](https://developers.google.com/apps-script/samples/custom-functions/fact-check).\n\n## Overview\n\nThe **Google Sheets custom function** named `FACT_CHECK` integrates the sophisticated, multi-tool, multi-step reasoning capabilities of a **Vertex AI Agent Engine (ADK Agent)** directly into your Google Sheets spreadsheets.\n\nIt operates as an end-to-end solution. It analyzes a statement, grounds its response using the latest web information, and returns the result in the format you need:\n\n  * Usage: `=FACT_CHECK(\"Your statement here\")` for a concise and summarized output. `=FACT_CHECK(\"Your statement here\", \"Your output formatting instructions here\")` for a specific output format.\n  * Reasoning: [**LLM Auditor ADK AI Agent (Python sample)**](https://github.com/google/adk-samples/tree/main/python/agents/llm-auditor).\n  * Output formatting: [**Gemini model**](https://cloud.google.com/vertex-ai/generative-ai/docs/models).\n\n## Prerequisites\n\n* Google Cloud Project with billing enabled.\n\n## Set up your environment\n\n1. Configure the Google Cloud project\n   1. Enable the Vertex AI API\n   1. Create a Service Account and grant the role `Vertex AI User`\n   1. Create a private key with type JSON. This will download the JSON file.\n1. Setup, install, and deploy the LLM Auditor ADK AI Agent sample\n   1. Use Vertex AI\n   1. Use the same Google Cloud project\n   1. Use the location `us-central1`\n   1. Use the Vertex AI Agent Engine\n1. Open an Apps Script project bound to a Google Sheets spreadsheet\n   1. Add a Script Property. Enter `LOCATION` as the property name and `us-central1` as the value. \n   1. Add a Script Property. Enter `GEMINI_MODEL_ID` as the property name and `gemini-2.5-flash-lite` as the value. \n   1. Add a Script Property. Enter `REASONING_ENGINE_ID` as the property name and the ID of the deployed LLM Auditor ADK AI Agent as the value. \n   1. Add a Script Property. Enter `SERVICE_ACCOUNT_KEY` as the property name and paste the JSON key from the service account as the value. \n   1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`\n   1. Set the script files `Code.gs` and `AiVertex.gs` in the Apps Script project with the JS file contents in this project\n"
  },
  {
    "path": "ai/custom-func-ai-agent/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"43\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "ai/custom-func-ai-studio/Code.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Passes a prompt and a data range to Gemini AI.\n *\n * @param {range} range The range of cells.\n * @param {string} prompt The text prompt as a string or single cell reference.\n * @return The Gemini response.\n * @customfunction\n */\nfunction gemini(range, prompt) {\n  return getAiSummary(`For the range of cells ${range}, ${prompt}`);\n}\n"
  },
  {
    "path": "ai/custom-func-ai-studio/README.md",
    "content": "# Google Sheets Custom Function with AI Studio\n\n## Project Description\n\nGoogle Sheets Custom Function to be used as a bound Apps Script project with a Google Sheets Spreadsheet\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Enable Generative Language API - (may skip as is automatically done in step 2)\n1. Create a Google Gemini API Key \n   1. Navigate to https://aistudio.google.com/app/apikey \n   1. Create API key for existing project from step 1\n   1. Copy the generated key for use in the next step.\n1. Open an Apps Script Project bound to a Google Sheets Spreadsheet\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `api_key` as the property name and use the Gemini API Key as the value \n1. Add the project code to Apps Script\n\n## Usage\n\nInsert a custom function in Google Sheets, passing a range and a prompt as parameters\n\nExample: \n\n```\n=gemini(A1:A10,\"Extract colors from the product description\")\n```"
  },
  {
    "path": "ai/custom-func-ai-studio/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "ai/custom-func-ai-studio/gemini.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Packages prompt and necessary settings, then sends a request to the\n * Generative Language API. Returns the text string response, extracted from the\n * Gemini AI response object.\n *\n * @param {string} prompt String representing the prompt for Gemini AI call.\n * @return {string} Result of Gemini AI in string format.\n */\nfunction getAiSummary(prompt) {\n  const data = {\n    contents: [\n      {\n        parts: [\n          {\n            text: prompt,\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0.2,\n      topK: 1,\n      topP: 1,\n      maxOutputTokens: 2048,\n      stopSequences: [],\n    },\n    safetySettings: [\n      {\n        category: \"HARM_CATEGORY_HARASSMENT\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_HATE_SPEECH\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n        threshold: \"BLOCK_NONE\",\n      },\n    ],\n  };\n  const options = {\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: JSON.stringify(data), // Convert the JavaScript object to a JSON string.\n  };\n\n  const apiKey = PropertiesService.getScriptProperties().getProperty(\"api_key\");\n  const response = UrlFetchApp.fetch(\n    `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`,\n    options,\n  );\n\n  const payload = JSON.parse(response.getContentText());\n  const text = payload.candidates[0].content.parts[0].text;\n\n  return text;\n}\n"
  },
  {
    "path": "ai/custom_func_vertex/Code.js",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Passes a prompt and a data range to Gemini AI.\n *\n * @param {range} range The range of cells.\n * @param {string} prompt The text prompt as a string or single cell reference.\n * @return The Gemini response.\n * @customfunction\n */\nfunction gemini(range, prompt) {\n  return getAiSummary(\n    `For the table of data: ${range} Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`,\n  );\n}\n"
  },
  {
    "path": "ai/custom_func_vertex/README.md",
    "content": "# Google Sheets Custom Function with AI Studio\n\n## Project Description\n\nGoogle Sheets Custom Function to be used as a bound Apps Script project with a Google Sheets Spreadsheet.\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Enable the Vertex AI API\n   1. Create a Service Account and grant the role `Vertex AI User`\n   1. Create a private key with type JSON. This will download the JSON file for use in the next section.\n1. Open an Apps Script Project bound to a Google Sheets Spreadsheet\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `model_id` as the property name and `gemini-pro` as the value. \n   1. Add a Script Property. Enter `project_location` as the property name and `us-central1` as the value. \n   1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. \n1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.\n1. Add the project code to Apps Script\n\n## Usage\n\nInsert a custom function in Google Sheets, passing a range and a prompt as parameters\n\nExample: \n\n```\n=gemini(A1:A10,\"Extract colors from the product description\")\n```\n"
  },
  {
    "path": "ai/custom_func_vertex/aiVertex.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst VERTEX_AI_LOCATION =\n  PropertiesService.getScriptProperties().getProperty(\"project_location\");\nconst MODEL_ID =\n  PropertiesService.getScriptProperties().getProperty(\"model_id\");\nconst SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty(\n  \"service_account_key\",\n);\n\n/**\n * Packages prompt and necessary settings, then sends a request to\n * Vertex API. Returns the response as an JSON object extracted from the\n * Vertex API response object.\n *\n * @param prompt - String representing your prompt for Gemini AI.\n */\nfunction getAiSummary(prompt) {\n  const request = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: prompt,\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0.1,\n      maxOutputTokens: 2048,\n    },\n    safetySettings: [\n      {\n        category: \"HARM_CATEGORY_HARASSMENT\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_HATE_SPEECH\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n        threshold: \"BLOCK_NONE\",\n      },\n      {\n        category: \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n        threshold: \"BLOCK_NONE\",\n      },\n    ],\n  };\n\n  const credentials = credentialsForVertexAI();\n\n  const fetchOptions = {\n    method: \"post\",\n    headers: {\n      Authorization: `Bearer ${credentials.accessToken}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true,\n    payload: JSON.stringify(request),\n  };\n\n  const url =\n    `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/` +\n    `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n\n  const response = UrlFetchApp.fetch(url, fetchOptions);\n\n  const payload = JSON.parse(response);\n  const text = payload.candidates[0].content.parts[0].text;\n\n  return text;\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n * Requires use of Service Account Key stored with project\n *\n * @return {!Object} Containing the Cloud Project Id and the access token.\n */\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/custom_func_vertex/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"43\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "ai/devdocs-link-preview/Cards.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Creates the Card to display documentation summary to user.\n *\n * @param {string} pageTitle Title of the page/card section.\n * @param {string} summary Page summary to display.\n * @return {!Card}\n */\nfunction buildCard(pageTitle, summary, showRating = true) {\n  const cardHeader = CardService.newCardHeader().setTitle(\"About this page\");\n\n  const summarySection = CardService.newCardSection().addWidget(\n    CardService.newTextParagraph().setText(summary),\n  );\n\n  const feedbackSection =\n    CardService.newCardSection().setHeader(\"Rate this summary\");\n\n  if (showRating) {\n    const thumbsUpAction = CardService.newAction()\n      .setFunctionName(\"onRatingClicked\")\n      .setParameters({\n        key: \"upVotes\",\n        title: pageTitle,\n        pageSummary: summary,\n      });\n\n    const thumbsDownAction = CardService.newAction()\n      .setFunctionName(\"onRatingClicked\")\n      .setParameters({\n        key: \"downVotes\",\n        title: pageTitle,\n        pageSummary: summary,\n      });\n\n    const thumbsUpButton = CardService.newImageButton()\n      .setIconUrl(\n        \"https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png\",\n      )\n      .setAltText(\"Looks good\")\n      .setOnClickAction(thumbsUpAction);\n\n    const thumbsDownButton = CardService.newImageButton()\n      .setIconUrl(\n        \"https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png\",\n      )\n      .setAltText(\"Not great\")\n      .setOnClickAction(thumbsDownAction);\n\n    const ratingButtons = CardService.newButtonSet()\n      .addButton(thumbsUpButton)\n      .addButton(thumbsDownButton);\n    feedbackSection.addWidget(ratingButtons);\n  } else {\n    feedbackSection.addWidget(\n      CardService.newTextParagraph().setText(\"Thank you for your feedback.\"),\n    );\n  }\n\n  const card = CardService.newCardBuilder()\n    .setHeader(cardHeader)\n    .addSection(summarySection)\n    .addSection(feedbackSection)\n    .build();\n  return card;\n}\n\n/**\n * Creates a Card to let user know an error has occurred.\n *\n * @return {!Card}\n */\nfunction buildErrorCard() {\n  const cardHeader = CardService.newCardHeader().setTitle(\n    \"Uh oh! Something went wrong.\",\n  );\n\n  const errorMessage = CardService.newTextParagraph().setText(\n    \"It looks like Gemini got stage fright.\",\n  );\n\n  const tryAgainButton = CardService.newTextButton()\n    .setText(\"Try again\")\n    .setTextButtonStyle(CardService.TextButtonStyle.TEXT)\n    .setOnClickAction(CardService.newAction().setFunctionName(\"onLinkPreview\"));\n\n  const buttonList = CardService.newButtonSet().addButton(tryAgainButton);\n\n  const mainSection = CardService.newCardSection()\n    .addWidget(errorMessage)\n    .addWidget(buttonList);\n\n  const errorCard = CardService.newCardBuilder()\n    .setHeader(cardHeader)\n    .addSection(mainSection)\n    .build();\n\n  return errorCard;\n}\n"
  },
  {
    "path": "ai/devdocs-link-preview/Helpers.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Wraper around script properties to allow for a default value if unset.\n */\nfunction scriptPropertyWithDefault(key, defaultValue = undefined) {\n  const scriptProperties = PropertiesService.getScriptProperties();\n  const value = scriptProperties.getProperty(key);\n  if (value) {\n    return value;\n  }\n  return defaultValue;\n}\n"
  },
  {
    "path": "ai/devdocs-link-preview/Main.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Creates a link preview card for Google developer documentation links.\n *\n * @param {!Object} event\n * @return {!Card}\n */\nfunction onLinkPreview(event) {\n  const hostApp = event.hostApp;\n  if (!event[hostApp].matchedUrl.url) {\n    return;\n  }\n  const url = event[hostApp].matchedUrl.url;\n  try {\n    const info = getPageSummary(url);\n    const card = buildCard(info.title, info.summary);\n    const linkPreview = CardService.newLinkPreview()\n      .setPreviewCard(card)\n      .setTitle(info.title)\n      .setLinkPreviewTitle(info.title);\n    return linkPreview;\n  } catch (error) {\n    // Log the error\n    console.error(\"Error occurred:\", error);\n    const errorCard = buildErrorCard();\n    return CardService.newActionResponseBuilder()\n      .setNavigation(CardService.newNavigation().updateCard(errorCard))\n      .build();\n  }\n}\n\n/**\n * Action handler for a good rating .\n *\n * @param {!Object} e The event passed from click action.\n * @return {!Card}\n */\nfunction onRatingClicked(e) {\n  const key = e.parameters.key;\n  const title = e.parameters.title;\n  const pageSummary = e.parameters.pageSummary;\n\n  const properties = PropertiesService.getScriptProperties();\n  let rating = Number(properties.getProperty(key) ?? 0);\n  properties.setProperty(key, ++rating);\n\n  const card = buildCard(title, pageSummary, false);\n  const linkPreview = CardService.newLinkPreview()\n    .setPreviewCard(card)\n    .setTitle(title)\n    .setLinkPreviewTitle(title);\n\n  return linkPreview;\n}\n"
  },
  {
    "path": "ai/devdocs-link-preview/README.md",
    "content": "# Google Workspace Add-on - Developer Docs Link previews\n\n\n## Project Description\n\nA Google Workspace Add-on that creates custom link previews for pages on the Google developer documentation site. The link preview uses AI to generate page summaries.\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Enable the Vertex AI API\n   1. Create a Service Account and grant the role `Vertex AI User`\n   1. Create a private key with type JSON. This will download the JSON file for use in the next section.\n1. Open a stand alone Apps Script Project \n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. \n1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.\n1. Add the project code to Apps Script\n\n"
  },
  {
    "path": "ai/devdocs-link-preview/Vertex.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst VERTEX_AI_LOCATION = scriptPropertyWithDefault(\n  \"project_location\",\n  \"us-central1\",\n);\nconst MODEL_ID = scriptPropertyWithDefault(\"model_id\", \"gemini-2.5-flash\");\nconst SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault(\"service_account_key\");\n\n/**\n * Invokes Gemini to extract the title and summary of a given URL. Responses may be cached.\n */\nfunction getPageSummary(targetUrl) {\n  const cachedResponse = CacheService.getScriptCache().get(targetUrl);\n  if (cachedResponse) {\n    return JSON.parse(cachedResponse);\n  }\n\n  const request = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: targetUrl,\n          },\n        ],\n      },\n    ],\n    systemInstruction: {\n      parts: [\n        {\n          text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as \"title\" and the summary as \"summary\"`,\n        },\n      ],\n    },\n    generationConfig: {\n      temperature: 0.2,\n      candidateCount: 1,\n      maxOutputTokens: 2048,\n    },\n  };\n\n  const credentials = credentialsForVertexAI();\n\n  const fetchOptions = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${credentials.accessToken}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true,\n    payload: JSON.stringify(request),\n  };\n\n  const url =\n    `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` +\n    `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n  const response = UrlFetchApp.fetch(url, fetchOptions);\n\n  const responseText = response.getContentText();\n  console.log(responseText);\n  if (response.getResponseCode() >= 400) {\n    console.log(responseText);\n    throw new Error(\"Unable to generate preview,\");\n  }\n  const parsedResponse = JSON.parse(responseText);\n  const modelResponse = parsedResponse.candidates[0].content.parts[0].text;\n  const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\\s\\S]*)(?=^`{3}$)/gm);\n  if (!jsonMatch) {\n    throw new Error(\"Unable to generate preview,\");\n  }\n  const jsonResponse = jsonMatch[0];\n  CacheService.getScriptCache().put(targetUrl, jsonResponse);\n  return JSON.parse(jsonResponse);\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n * Requires use of Service Account Key stored with project\n *\n * @return {!Object} Containing the Cloud Project Id and the access token.\n */\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/devdocs-link-preview/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"43\"\n      }\n    ]\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/workspace.linkpreview\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/cloud-platform\"\n  ],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"DevDocs Previews\",\n      \"logoUrl\": \"https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png\",\n      \"layoutProperties\": {\n        \"primaryColor\": \"#1A73E8\"\n      }\n    },\n    \"docs\": {\n      \"linkPreviewTriggers\": [\n        {\n          \"patterns\": [\n            {\n              \"hostPattern\": \"developers.google.*\"\n            }\n          ],\n          \"runFunction\": \"onLinkPreview\",\n          \"labelText\": \"Page title\",\n          \"logoUrl\": \"https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png\"\n        }\n      ]\n    },\n    \"sheets\": {\n      \"linkPreviewTriggers\": [\n        {\n          \"patterns\": [\n            {\n              \"hostPattern\": \"developers.google.*\"\n            }\n          ],\n          \"runFunction\": \"onLinkPreview\",\n          \"labelText\": \"Page title\",\n          \"logoUrl\": \"https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png\"\n        }\n      ]\n    },\n    \"slides\": {\n      \"linkPreviewTriggers\": [\n        {\n          \"patterns\": [\n            {\n              \"hostPattern\": \"developers.google.*\"\n            }\n          ],\n          \"runFunction\": \"onLinkPreview\",\n          \"labelText\": \"Page title\",\n          \"logoUrl\": \"https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png\"\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "ai/drive-rename/README.md",
    "content": "# Google Workspace Add-on Drive - Name with Intelligence\n\n## Project Description\n\nGoogle Workspace Add-on for Google Drive, which uses AI to recommend new names for the selected Doc in Google Drive by passing the body of the document within the AI prompt for context.\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Enable the Vertex AI API\n   1. Enable Google Drive API\n   1. Configure OAuth consent screen\n   1. Create a Service Account and grant the role Service `Vertex AI User` role\n   1. Create a private key with type JSON. This will download the JSON file for use in the next section.\n1. Open a standalone Apps Script project.\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `model_id` as the property name and `gemini-pro` as the value. \n   1. Add a Script Property. Enter `project_location` as the property name and `us-central1` as the value. \n   1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. \n1. Add `Google Drive API v3` advanced service.\n1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.\n1. Add the project code to Apps Script\n\n\n"
  },
  {
    "path": "ai/drive-rename/ai.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst VERTEX_AI_LOCATION =\n  PropertiesService.getScriptProperties().getProperty(\"project_location\");\nconst MODEL_ID =\n  PropertiesService.getScriptProperties().getProperty(\"model_id\");\nconst SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty(\n  \"service_account_key\",\n);\n\nconst STANDARD_PROMPT = `\n\n Your task is to create 3 potential document names for this content.\n\n Also, create a summary for this content, using 2 to 3 sentences, and don't include formatting.\n\n Format the response as a JSON object with the first field called names and the summary field called summary.\n\n The content is below:\n\n `;\n\n/**\n * Packages prompt and necessary settings, then sends a request to\n * Vertex API. Returns the response as an JSON object extracted from the\n * Vertex API response object.\n *\n * @param prompt - String representing your prompt for Gemini AI.\n */\nfunction getAiSummary(prompt) {\n  const request = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: STANDARD_PROMPT,\n          },\n          {\n            text: prompt,\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0.2,\n      maxOutputTokens: 2048,\n      response_mime_type: \"application/json\",\n    },\n  };\n\n  const credentials = credentialsForVertexAI();\n\n  const fetchOptions = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${credentials.accessToken}`,\n    },\n    contentType: \"application/json\",\n    payload: JSON.stringify(request),\n  };\n\n  const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n\n  const response = UrlFetchApp.fetch(url, fetchOptions);\n\n  const payload = JSON.parse(response.getContentText());\n  const jsonPayload = JSON.parse(payload.candidates[0].content.parts[0].text);\n\n  return jsonPayload;\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n *\n *\n */\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/drive-rename/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"serviceId\": \"drive\",\n        \"version\": \"v3\"\n      }\n    ]\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/drive.addons.metadata.readonly\",\n    \"https://www.googleapis.com/auth/drive.file\",\n    \"https://www.googleapis.com/auth/drive\",\n    \"https://www.googleapis.com/auth/drive.readonly\",\n    \"https://www.googleapis.com/auth/documents\"\n  ],\n  \"urlFetchWhitelist\": [\n    \"https://*.googleusercontent.com/\",\n    \"https://*.googleapis.com/\"\n  ],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Name with Intelligence\",\n      \"logoUrl\": \"https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png\",\n      \"layoutProperties\": {\n        \"primaryColor\": \"#4285f4\",\n        \"secondaryColor\": \"#3f8bca\"\n      }\n    },\n    \"drive\": {\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepageOpened\"\n      },\n      \"onItemsSelectedTrigger\": {\n        \"runFunction\": \"onDriveItemsSelected\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ai/drive-rename/drive.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Renames a file based on user selection / updates card.\n *\n * @param {!Event} e Add-on event context\n * @return {!Card}\n */\nfunction renameFile(e) {\n  const newName = e.formInput.names;\n  const id = e.drive.activeCursorItem.id;\n  DriveApp.getFileById(id).setName(newName);\n\n  const eUpdated = {\n    hostApp: \"drive\",\n    drive: {\n      selectedItems: [[Object]],\n      activeCursorItem: {\n        title: newName,\n        id: id,\n        iconUrl: e.drive.activeCursorItem.iconUrl,\n        mimeType: e.drive.activeCursorItem.mimeType,\n      },\n      commonEventObject: { hostApp: \"DRIVE\", platform: \"WEB\" },\n      clientPlatform: \"web\",\n    },\n  };\n\n  return onCardUpdate(eUpdated);\n}\n\n/**\n * Redraws the same card to force AI to refresh its data.\n *\n * @param {!Event} e Add-on event context\n * @return {!Card}\n */\nfunction updateCard(e) {\n  const id = e.drive.activeCursorItem.id;\n\n  const eConverted = {\n    hostApp: \"drive\",\n    drive: {\n      selectedItems: [[Object]],\n      activeCursorItem: {\n        title: DriveApp.getFileById(id).getName(),\n        id: id,\n        iconUrl: e.drive.activeCursorItem.iconUrl,\n        mimeType: e.drive.activeCursorItem.mimeType,\n      },\n      commonEventObject: { hostApp: \"DRIVE\", platform: \"WEB\" },\n      clientPlatform: \"web\",\n    },\n  };\n\n  return onCardUpdate(eConverted);\n}\n\n/**\n * Fetches the body of given document, using DocumentApp.\n *\n * @param {string} id The Google Document file ID.\n * @return {string} The body of the Google Document.\n */\nfunction getDocumentBody(id) {\n  const doc = DocumentApp.openById(id);\n  const body = doc.getBody();\n  const text = body.getText();\n\n  return text;\n}\n\n/**\n * Fetches the body of given document, using DocsApi.\n *\n * @param {string} id The Google Document file ID.\n * @return {string} The body of the Google Document.\n */\nfunction getDocAPIBody(id) {\n  // Call DOC API REST endpoint to get the file\n  const url = `https://docs.googleapis.com/v1/documents/${id}`;\n\n  const response = UrlFetchApp.fetch(url, {\n    method: \"GET\",\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n    muteHttpExceptions: true,\n  });\n\n  if (response.getResponseCode() !== 200) {\n    throw new Error(`Drive API returned error \\\n    ${response.getResponseCode()} :\\\n     ${response.getContentText()}`);\n  }\n\n  const file = response.getContentText();\n  const data = JSON.parse(file);\n\n  return data.body.content;\n}\n\n/**\n * Sends the given document to the trash folder.\n *\n * @param {!Event} e Add-on event context\n */\nfunction moveFileToTrash(e) {\n  const id = e.drive.activeCursorItem.id;\n  const file = DriveApp.getFileById(id);\n  file.setTrashed(true);\n}\n"
  },
  {
    "path": "ai/drive-rename/main.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Main entry point for add-on when opened.\n *\n * @param e - Add-on event context\n */\nfunction onHomepageOpened(e) {\n  const card = buildHomePage();\n\n  return {\n    action: {\n      navigations: [\n        {\n          pushCard: card,\n        },\n      ],\n    },\n  };\n}\n\n/**\n * Handles selection of a file in Google Drive.\n *\n * @param e - Add-on event context\n */\nfunction onDriveItemsSelected(e) {\n  return {\n    action: {\n      navigations: [\n        {\n          pushCard: buildSelectionPage(e),\n        },\n      ],\n    },\n  };\n}\n\n/**\n * Handles the update of the card on demand.\n *\n * @param e - (Modified) add-on event context\n */\nfunction onCardUpdate(e) {\n  return {\n    action: {\n      navigations: [\n        {\n          updateCard: buildSelectionPage(e),\n        },\n      ],\n    },\n  };\n}\n"
  },
  {
    "path": "ai/drive-rename/ui.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst ICO_HEADER =\n  \"https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png\";\nconst ICON_RENAME =\n  \"https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-18dp/2x/gm_drive_file_rename_outline_googblue_18dp.png\";\nconst ICON_RETRY =\n  \"https://fonts.gstatic.com/s/i/googlematerialicons/refresh/v16/googblue-18dp/2x/gm_refresh_googblue_18dp.png\";\nconst ICON_DELETE =\n  \"https://fonts.gstatic.com/s/i/googlematerialicons/delete/v17/black-18dp/2x/gm_delete_black_18dp.png\";\n\n/**\n * Builds the card for the selected active item.\n *\n * @param e - Add-on event context\n */\nfunction buildSelectionPage(e) {\n  const selected = e.drive.activeCursorItem;\n\n  // Check if Google Doc type, respond unsupported if not\n  if (selected.mimeType !== \"application/vnd.google-apps.document\") {\n    return {\n      sections: [\n        {\n          widgets: [\n            {\n              textParagraph: {\n                text: \"<b>Note</b>: currently only <i>Google Docs<i/> file types are supported.\",\n              },\n            },\n          ],\n        },\n      ],\n      header: buildHeader(),\n    };\n  }\n\n  // Get document body\n  const docBody = getDocumentBody(selected.id);\n\n  //  Create widgets starting with Title\n  const widgets = [\n    {\n      textParagraph: {\n        text: `<b>${selected.title}</b>`,\n      },\n    },\n  ];\n\n  // Check if doc is empty before calling AI\n  if (docBody.length > 1) {\n    // Get AI data\n    const aiResponse = getAiSummary(docBody);\n\n    console.log(\"RESPONSE\");\n\n    console.log(aiResponse);\n\n    //  Add the Summary text\n    widgets.push({\n      decoratedText: {\n        topLabel: \"Summary\",\n        text: aiResponse.summary,\n        wrapText: true,\n      },\n    });\n\n    // Divider\n    widgets.push({ divider: {} });\n\n    // Create an object of items\n    const items = [];\n    for (const name of aiResponse.names) {\n      items.push({\n        text: name,\n        value: name,\n        selected: false,\n      });\n    }\n\n    // Set first item as selected\n    items[0].selected = true;\n\n    // Add the Radio button of 'names' as items\n    widgets.push({\n      selectionInput: {\n        name: \"names\",\n        label: \"Select a new name\",\n        type: \"RADIO_BUTTON\",\n        items: items,\n      },\n    });\n\n    // Create the 'Rename' button\n    widgets.push({\n      buttonList: {\n        buttons: [\n          {\n            text: \"Rename\",\n            icon: {\n              iconUrl: ICON_RENAME,\n              altText: \"Rename\",\n            },\n            onClick: {\n              action: {\n                function: \"renameFile\",\n                parameters: [\n                  {\n                    key: \"id\",\n                    value: selected.id,\n                  },\n                ],\n                loadIndicator: \"SPINNER\",\n              },\n            },\n          },\n          {\n            text: \"\",\n            icon: {\n              iconUrl: ICON_RETRY,\n              altText: \"Retry\",\n            },\n            onClick: {\n              action: {\n                function: \"updateCard\",\n                parameters: [\n                  {\n                    key: \"id\",\n                    value: selected.id,\n                  },\n                ],\n                loadIndicator: \"SPINNER\",\n              },\n            },\n          },\n        ],\n      },\n      horizontalAlignment: \"CENTER\",\n    });\n  } // end if\n\n  // Don't call AI, but offer to delete\n  else {\n    //  Add the Summary text\n    widgets.push({\n      decoratedText: {\n        topLabel: \"Summary\",\n        text: \"Empty document\",\n        wrapText: true,\n      },\n    });\n\n    // Divider\n    widgets.push({ divider: {} });\n\n    // Create the 'Delete' button\n    widgets.push({\n      buttonList: {\n        buttons: [\n          {\n            text: \"Move to trash\",\n            icon: {\n              iconUrl: ICON_DELETE,\n              altText: \"Move to trash\",\n            },\n            onClick: {\n              action: {\n                function: \"moveFileToTrash\",\n                parameters: [\n                  {\n                    key: \"id\",\n                    value: selected.id,\n                  },\n                ],\n                loadIndicator: \"SPINNER\",\n              },\n            },\n            color: {\n              red: 0.961,\n              green: 0.6,\n              blue: 0.667,\n              alpha: 1,\n            },\n          },\n        ],\n      },\n      horizontalAlignment: \"CENTER\",\n    });\n  } // end else\n\n  return {\n    sections: [\n      {\n        widgets,\n      },\n    ],\n    header: buildHeader(),\n  };\n}\n\n/**\n * Builds the header for the Add-on Cards.\n */\nfunction buildHeader() {\n  const header = {\n    title: \"Name with Intelligence\",\n    subtitle: `\"<i>Untitled documents</i>\" no more!`, // Better Doc names w/ Gemini AI\",\n    imageUrl: ICO_HEADER,\n    imageType: \"SQUARE\",\n  };\n  return header;\n}\n\n/**\n * Builds the home page card.\n */\nfunction buildHomePage() {\n  const widgets = [\n    {\n      textParagraph: {\n        text: \"<b>Name with Intelligence</b> enables you to quickly rename any Google Doc using suggestions provided via Google Gemini.\",\n      },\n    },\n    { divider: {} },\n    {\n      textParagraph: {\n        text: \"👉 To use, select a Google Doc to rename. Then choose a new name from the list of AI generated names provided for you. A quick summary of the file is also provided by Google Gemini to help you make your decision.\",\n      },\n    },\n    { divider: {} },\n    {\n      textParagraph: {\n        text: \"<b>Note</b>: currently only <i>Google Docs<i/> file types are supported.\",\n      },\n    },\n  ];\n\n  return {\n    sections: [\n      {\n        widgets,\n      },\n    ],\n    header: buildHeader(),\n  };\n}\n"
  },
  {
    "path": "ai/email-classifier/Cards.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Triggered when the add-on is opened from the Gmail homepage.\n *\n * @param {!Object} e - The event object.\n * @returns {!Card} - The homepage card.\n */\nfunction onHomepageTrigger(e) {\n  return buildHomepageCard();\n}\n\n/**\n * Builds the main card displayed on the Gmail homepage.\n *\n * @returns {!Card} - The homepage card.\n */\nfunction buildHomepageCard() {\n  // Create a new card builder\n  const cardBuilder = CardService.newCardBuilder();\n\n  // Create a card header\n  const cardHeader = CardService.newCardHeader();\n  cardHeader.setImageUrl(\n    \"https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png\",\n  );\n  cardHeader.setImageStyle(CardService.ImageStyle.CIRCLE);\n  cardHeader.setTitle(\"Email Classifier\");\n\n  // Add the header to the card\n  cardBuilder.setHeader(cardHeader);\n\n  // Create a card section\n  const cardSection = CardService.newCardSection();\n\n  // Create buttons for generating sample emails and analyzing sentiment\n  const buttonSet = CardService.newButtonSet();\n\n  // Create \"Classify emails\" button\n  const classifyButton = createFilledButton({\n    text: \"Classify emails\",\n    functionName: \"main\",\n    color: \"#007bff\",\n    icon: \"new_label\",\n  });\n  buttonSet.addButton(classifyButton);\n\n  // Create \"Create Labels\" button\n  const createLabelsButtton = createFilledButton({\n    text: \"Create labels\",\n    functionName: \"createLabels\",\n    color: \"#34A853\",\n    icon: \"add\",\n  });\n\n  // Create \"Remove Labels\" button\n  const removeLabelsButtton = createFilledButton({\n    text: \"Remove labels\",\n    functionName: \"removeLabels\",\n    color: \"#FF0000\",\n    icon: \"delete\",\n  });\n\n  if (labelsCreated()) {\n    buttonSet.addButton(removeLabelsButtton);\n  } else {\n    buttonSet.addButton(createLabelsButtton);\n  }\n\n  // Add the button set to the section\n  cardSection.addWidget(buttonSet);\n\n  // Add the section to the card\n  cardBuilder.addSection(cardSection);\n\n  // Build and return the card\n  return cardBuilder.build();\n}\n\n/**\n * Creates a filled text button with the specified text, function, and color.\n *\n * @param {{text: string, functionName: string, color: string, icon: string}} options\n *   - text: The text to display on the button.\n *   - functionName: The name of the function to call when the button is clicked.\n *   - color: The background color of the button.\n *   - icon: The material icon to display on the button.\n * @returns {!TextButton} - The created text button.\n */\nfunction createFilledButton({ text, functionName, color, icon }) {\n  // Create a new text button\n  const textButton = CardService.newTextButton();\n\n  // Set the button text\n  textButton.setText(text);\n\n  // Set the action to perform when the button is clicked\n  const action = CardService.newAction();\n  action.setFunctionName(functionName);\n  action.setLoadIndicator(CardService.LoadIndicator.SPINNER);\n  textButton.setOnClickAction(action);\n\n  // Set the button style to filled\n  textButton.setTextButtonStyle(CardService.TextButtonStyle.FILLED);\n\n  // Set the background color\n  textButton.setBackgroundColor(color);\n\n  textButton.setMaterialIcon(CardService.newMaterialIcon().setName(icon));\n\n  return textButton;\n}\n\n/**\n * Creates a notification response with the specified text.\n *\n * @param {string} notificationText - The text to display in the notification.\n * @returns {!ActionResponse} - The created action response.\n */\nfunction buildNotificationResponse(notificationText) {\n  // Create a new notification\n  const notification = CardService.newNotification();\n  notification.setText(notificationText);\n\n  // Create a new action response builder\n  const actionResponseBuilder = CardService.newActionResponseBuilder();\n\n  // Set the notification for the action response\n  actionResponseBuilder.setNotification(notification);\n\n  // Build and return the action response\n  return actionResponseBuilder.build();\n}\n\n/**\n * Creates a card to display the spreadsheet link.\n *\n * @param {string} spreadsheetUrl - The URL of the spreadsheet.\n * @returns {!ActionResponse} - The created action response.\n */\nfunction showSpreadsheetLink(spreadsheetUrl) {\n  const updatedCardBuilder = CardService.newCardBuilder();\n\n  updatedCardBuilder.setHeader(\n    CardService.newCardHeader().setTitle(\"Sheet generated!\"),\n  );\n\n  const updatedSection = CardService.newCardSection()\n    .addWidget(\n      CardService.newTextParagraph().setText(\"Click to open the sheet:\"),\n    )\n    .addWidget(\n      CardService.newTextButton().setText(\"Open Sheet\").setOpenLink(\n        CardService.newOpenLink()\n          .setUrl(spreadsheetUrl)\n          .setOpenAs(CardService.OpenAs.FULL_SCREEN) // Opens in a new browser tab/window\n          .setOnClose(CardService.OnClose.NOTHING), // Does nothing when the tab is closed\n      ),\n    )\n    .addWidget(\n      CardService.newTextButton() // Optional: Add a button to go back or refresh\n        .setText(\"Go Back\")\n        .setOnClickAction(\n          CardService.newAction().setFunctionName(\"onHomepageTrigger\"),\n        ), // Go back to the initial state\n    );\n\n  updatedCardBuilder.addSection(updatedSection);\n  const newNavigation = CardService.newNavigation().updateCard(\n    updatedCardBuilder.build(),\n  );\n\n  return CardService.newActionResponseBuilder()\n    .setNavigation(newNavigation) // This updates the current card in the UI\n    .build();\n}\n"
  },
  {
    "path": "ai/email-classifier/ClassifyEmail.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Constructs the prompt for classifying an email.\n *\n * @param {string} subject The subject of the email.\n * @param {string} body The body of the email.\n * @returns {string} The prompt for classifying an email.\n */\nconst classifyEmailPrompt = (subject, body) =>\n  `\nObjective: You are an AI assistant tasked with classifying email threads. Analyze the entire email thread provided below and determine the single most appropriate classification label. Your response must conform to the provided schema.\n\n**Classification Labels & Descriptions:**\n\n* **needs-response**: The sender is explicitly or implicitly expecting a **direct, communicative reply** from me (${ME}) to answer a question, acknowledge receipt of information, confirm understanding, or continue a conversation. **Prioritize this label if the core expectation is purely a written or verbal communication back to the sender.**\n* **action-required**: The email thread requires me (${ME}) to perform a **distinct task, make a formal decision, provide a review leading to approval/rejection, or initiate a process that results in a demonstrable change or outcome.** This label is for actions *beyond* just sending a reply, such as completing a document, setting up a meeting, approving a request, delegating a task, or performing a delegated duty.\n* **for-your-info**: The email thread's primary purpose is to convey information, updates, or announcements. No immediate action or direct reply is expected or required from me (${ME}); the main purpose is for me to be informed and aware. This includes both routine 'FYI' updates and critical announcements where my role is to comprehend, not act or respond.\n\n**Evaluation Criteria - Consider the following:**\n\n* **Sender's Intent & My Role:** What does the sender want me (${ME}) to do, say, or know?\n* **Direct Requests:** Are there explicit questions or calls to action addressed to me (${ME})?\n* **Distinguishing Action vs. Response:**\n    * If the email primarily asks for a *verbal or written communication* (e.g., answering a specific question, providing feedback, confirming receipt, giving thoughts, and is directly addressed to me (${ME})), it's likely \\`needs-response\\`.\n    * If the email requires me to *perform a specific task or make a formal decision that goes beyond simply communicating* (e.g., completing a document, scheduling, approving a request, delegating, implementing a change), it's likely \\`action-required\\`.\n* **Urgency/Deadlines:** Are there time-sensitive elements mentioned?\n* **Last Message Focus:** Give slightly more weight to the content of the most recent messages in the thread.\n* **Keywords:**\n    * Look for terms like \"answer,\" \"reply to,\" \"your thoughts on,\" \"confirm,\" \"acknowledge\" for \\`needs-response\\`.\n    * Look for terms like \"complete,\" \"approve,\" \"review and approve,\" \"sign,\" \"process,\" \"set up,\" \"delegate\" for \\`action-required\\`.\n    * Look for terms like \"FYI,\" \"update,\" \"announcement,\" \"read,\" \"info\" for \\`for-information\\`.\n* **Overall Significance:** Is the topic critical or routine, influencing the *type* of information being conveyed?\n\n**Input:** Email message content\nSubject: ${subject}\n\n--- Email Thread Messages ---\n${body}\n--- End of Email Thread ---\n\n**Output:** Return the single best classification and a brief justification.\nFormat: JSON object with '[Classification]', and '[Reason]'\n`.trim();\n\n/**\n * Classifies an email based on its subject and messages.\n *\n * @param {string} subject The subject of the email.\n * @param {!Array<!GmailMessage>} messages An array of Gmail messages.\n * @returns {!Object} The classification object.\n */\nfunction classifyEmail(subject, messages) {\n  const body = [];\n  for (let i = 0; i < messages.length; i++) {\n    const message = messages[i];\n    body.push(`Message ${i + 1}:`);\n    body.push(`From: ${message.getFrom()}`);\n    body.push(`To:${message.getTo()}`);\n    body.push(\"Body:\");\n    body.push(message.getPlainBody());\n    body.push(\"---\");\n  }\n\n  // Prepare the request payload\n  const payload = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: classifyEmailPrompt(subject, body.join(\"\\n\")),\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0,\n      topK: 1,\n      topP: 0.1,\n      seed: 37,\n      maxOutputTokens: 1024,\n      responseMimeType: \"application/json\",\n      // Expected response format for simpler parsing.\n      responseSchema: {\n        type: \"object\",\n        properties: {\n          classification: {\n            type: \"string\",\n            enum: Object.keys(classificationLabels),\n          },\n          reason: {\n            type: \"string\",\n          },\n        },\n      },\n    },\n  };\n\n  // Prepare the request options\n  const options = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true, // Set to true to inspect the error response\n    payload: JSON.stringify(payload),\n  };\n\n  // Make the API request\n  const response = UrlFetchApp.fetch(API_URL, options);\n\n  // Parse the response. There are two levels of JSON responses to parse.\n  const parsedResponse = JSON.parse(response.getContentText());\n  const text = parsedResponse.candidates[0].content.parts[0].text;\n  const classification = JSON.parse(text);\n  return classification;\n}\n"
  },
  {
    "path": "ai/email-classifier/Code.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Main function to process emails, classify them, and update a spreadsheet.\n * This function searches for unread emails in the inbox from the last 7 days,\n * classifies them based on their subject and content, adds labels to the emails,\n * creates draft responses for emails that need a response, and logs the\n * classification results in a spreadsheet.\n * @return {string} The URL of the spreadsheet.\n */\nfunction main() {\n  // Calculate the date 7 days ago\n  const today = new Date();\n  const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);\n\n  // Create a Sheet\n  const headers = [\"Subject\", \"Classification\", \"Reason\"];\n  const spreadsheet = createSheetWithHeaders(headers);\n\n  // Format the date for the Gmail search query (YYYY/MM/DD)\n  // Using Utilities.formatDate ensures correct formatting based on script\n  // timezone\n  const formattedDate = Utilities.formatDate(\n    sevenDaysAgo,\n    Session.getScriptTimeZone(),\n    \"yyyy/MM/dd\",\n  );\n\n  // Construct the search query\n  const query = `is:unread after:${formattedDate} in:inbox`;\n  console.log(`Searching for emails with query: ${query}`);\n\n  // Search for threads matching the query\n  // Note: GmailApp.search() returns threads where *at least one* message\n  // matches\n  const threads = GmailApp.search(query);\n  createLabels();\n\n  for (const thread of threads) {\n    const messages = thread.getMessages();\n    const subject = thread.getFirstMessageSubject();\n    const { classification, reason } = classifyEmail(subject, messages);\n    console.log(`Classification: ${classification}, Reason: ${reason}`);\n\n    thread.addLabel(classificationLabels[classification].gmailLabel);\n\n    if (classification === \"needs-response\") {\n      const draft = draftEmail(subject, messages);\n      thread.createDraftReplyAll(null, { htmlBody: draft });\n    }\n\n    addDataToSheet(spreadsheet, hyperlink(thread), classification, reason);\n  }\n\n  return showSpreadsheetLink(spreadsheet.getUrl());\n}\n"
  },
  {
    "path": "ai/email-classifier/Constants.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst PROJECT_ID = \"\";\nconst LOCATION = \"us-central1\";\nconst API_ENDPOINT = `${LOCATION}-aiplatform.googleapis.com`;\nconst MODEL = \"gemini-2.5-pro-preview-05-06\";\nconst GENERATE_CONTENT_API = \"generateContent\";\nconst API_URL = `https://${API_ENDPOINT}/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/google/models/${MODEL}:${GENERATE_CONTENT_API}`;\nconst ME = \"\";\n"
  },
  {
    "path": "ai/email-classifier/DraftEmail.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Constructs a prompt for drafting an email.\n *\n * @param {string} subject The subject of the email thread.\n * @param {string} body The body of the email thread.\n * @returns {string} The prompt string.\n */\nconst draftEmailPrompt = (subject, body) =>\n  `\nYou are an AI assistant. Based on the following email thread:\n\nSubject: ${subject}\n\n--- Email Thread Messages ---\n${body}\n--- End of Email Thread ---\n\nTask: Considering all messages in this thread:\n- Help me ${ME} draft a polite and professional reply that addresses the key points from the most recent message(s) in HTML\n- Do NOT include subject of the email\n\nDraft Criteria: Consider the following:\n* Explicit Questions: Are there direct questions posed to me ${ME}, especially in the most recent messages?\n* Calls to Action: Are there clear instructions or requests for the me ${ME}, to *do* something?\n* Urgency/Deadlines: Does the thread mention deadlines or urgent requests?\n* Sender's Intent: What does the sender seem to want?\n* My Role: What am I (${ME}) being asked to do or know?\n* Keywords: Look for terms like \"please,\" \"urgent,\" \"FYI,\" \"question,\" \"task,\" \"review,\" \"approve,\" \"respond,\" \"deadline.\"\n* Last Message Focus: Give slightly more weight to the most recent messages.\n* Overall Significance: Is the topic critical or routine?\n\nOutput: Return the draft message in HTML format.\nFormat: HTML\n\nExample format:\n<!DOCTYPE html>\n<html>\n<head>\n  <title>My Email</title>\n</head>\n<body>\n  <h1>Hello, World!</h1>\n  <p>This is an HTML email sent from Google Apps Script.</p>\n</body>\n</html>\n`.trim();\n\n/**\n * Drafts an email based on the given subject and messages.\n *\n * @param {string} subject The subject of the email thread.\n * @param {!Array<!GmailMessage>} messages An array of Gmail messages.\n * @returns {string|null} The drafted email in HTML format or null if not found.\n */\nfunction draftEmail(subject, messages) {\n  const body = [];\n  for (let i = 0; i < messages.length; i++) {\n    const message = messages[i];\n    body.push(`Message ${i + 1}:`);\n    body.push(`From: ${message.getFrom()}`);\n    body.push(`To:${message.getTo()}`);\n    body.push(\"Body:\");\n    body.push(message.getPlainBody());\n    body.push(\"---\");\n  }\n\n  // Prepare the request payload\n  const payload = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: draftEmailPrompt(subject, body.join(\"\\n\")),\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0,\n      topK: 1,\n      topP: 0.1,\n      seed: 37,\n      maxOutputTokens: 1024,\n      responseMimeType: \"text/plain\",\n    },\n  };\n\n  // Prepare the request options\n  const options = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true, // Set to true to inspect the error response\n    payload: JSON.stringify(payload),\n  };\n\n  // Make the API request\n  const response = UrlFetchApp.fetch(API_URL, options);\n\n  // Parse the response. There are two levels of JSON responses to parse.\n  const parsedResponse = JSON.parse(response.getContentText());\n  const draft = parsedResponse.candidates[0].content.parts[0].text;\n  return extractHtmlContent(draft);\n}\n\n/**\n * Extracts HTML content from a string.\n *\n * @param {string} textString The string to extract HTML content from.\n * @returns {string|null} The HTML content or null if not found.\n */\nfunction extractHtmlContent(textString) {\n  // The regex pattern:\n  // ````html` (literal start marker)\n  // `(.*?)` (capturing group for any character, non-greedily, including newlines)\n  // ` ``` ` (literal end marker)\n  // `s` flag makes '.' match any character including newlines.\n  const match = textString.match(/```html(.*?)```/s);\n  if (match?.[1]) {\n    return match[1]; // Return the content of the first capturing group\n  }\n  return null; // Or an empty string, depending on desired behavior if not found\n}\n"
  },
  {
    "path": "ai/email-classifier/Labels.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst classificationLabels = {\n  \"action-required\": {\n    name: \"🚨 Action Required\",\n    textColor: \"#ffffff\",\n    backgroundColor: \"#1c4587\",\n  },\n  \"needs-response\": {\n    name: \"↪️ Needs Response\",\n    textColor: \"#ffffff\",\n    backgroundColor: \"#16a765\",\n  },\n  \"for-your-info\": {\n    name: \"ℹ️ For Your Info\",\n    textColor: \"#000000\",\n    backgroundColor: \"#fad165\",\n  },\n};\n\n/**\n * Creates Gmail labels based on the classification labels defined in the `classificationLabels` object.\n * If a label already exists, it updates the color. Otherwise, it creates a new label.\n * After creating or updating labels, it logs a message to the console and returns the homepage card.\n * @returns {!CardService.Card} The homepage card.\n */\nfunction createLabels() {\n  for (const labelName in classificationLabels) {\n    const classificationLabel = classificationLabels[labelName];\n    const { name, textColor, backgroundColor } = classificationLabel;\n    let gmailLabel = GmailApp.getUserLabelByName(name);\n\n    if (!gmailLabel) {\n      gmailLabel = GmailApp.createLabel(name);\n      Gmail.Users.Labels.update(\n        {\n          name: name,\n          color: {\n            textColor: textColor,\n            backgroundColor: backgroundColor,\n          },\n        },\n        \"me\",\n        fetchLabelId(name),\n      );\n    }\n\n    classificationLabel.gmailLabel = gmailLabel;\n  }\n\n  console.log(\"Labels created.\");\n  return buildHomepageCard();\n}\n\n/**\n * Checks if all classification labels exist in Gmail.\n * @returns {boolean} True if all labels exist, false otherwise.\n */\nfunction labelsCreated() {\n  for (const labelName in classificationLabels) {\n    const { name } = classificationLabels[labelName];\n    const gmailLabel = GmailApp.getUserLabelByName(name);\n\n    if (!gmailLabel) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\n/**\n * Fetches the ID of a Gmail label by its name.\n * @param {string} name The name of the label.\n * @returns {string} The ID of the label.\n */\nfunction fetchLabelId(name) {\n  return Gmail.Users.Labels.list(\"me\").labels.find((_) => _.name === name).id;\n}\n\n/**\n * Removes all classification labels from Gmail.\n * After removing labels, it logs a message to the console and returns the homepage card.\n * @returns {!CardService.Card} The homepage card.\n */\nfunction removeLabels() {\n  for (const labelName in classificationLabels) {\n    const classificationLabel = classificationLabels[labelName];\n    const gmailLabel = GmailApp.getUserLabelByName(classificationLabel.name);\n\n    if (gmailLabel) {\n      gmailLabel.deleteLabel();\n      classificationLabel.gmailLabel = undefined;\n    }\n  }\n  console.log(\"Labels removed.\");\n  return buildHomepageCard();\n}\n"
  },
  {
    "path": "ai/email-classifier/README.md",
    "content": "# Email Classifier\n\nThis Apps Script project provides a Gmail add-on that classifies emails based on\ntheir content and subject, and performs actions such as adding labels, creating\ndraft responses, and logging results in a Google Sheet. It leverages the Gemini\nAPI for natural language processing.\n\n## Features\n\n*   **Email Classification:** Classifies unread emails in your inbox into three\n    categories:\n    *   `needs-response`: Emails requiring a direct reply.\n    *   `action-required`: Emails requiring a specific task or decision.\n    *   `for-your-info`: Emails for information only, no action needed.\n*   **Labeling:** Adds Gmail labels to emails based on their classification.\n*   **Draft Responses:** Generates draft email responses for emails classified\n    as `needs-response`.\n*   **Spreadsheet Logging:** Logs email details, classification, and reason to a\n    Google Sheet.\n*   **User-Friendly Interface:** Provides a Gmail add-on with buttons for\n    classification, label creation, and removal.\n\n## Setup\n\n### 1. Enable Google APIs\n\n*   Go to the [Google Cloud Console](https://console.cloud.google.com/).\n*   Create or select a project.\n*   **Gemini API:**\n    *   [Enable the Gemini API](https://console.cloud.google.com/flows/enableapi?apiid=aiplatform.googleapis.com)\n*   **Gmail API:**\n    *   [Enable the Gmail API](https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com)\n*   **Sheets API:**\n    *   [Enable the Sheets API](https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com)\n\n### 2. Apps Script Project\n\n1.  **Create a New Project:**\n    *   Go to [script.google.com](https://script.google.com).\n    *   Create a new project.\n1.  **Enable `appsscript.json` Manifest:**\n    *   Go to **Project Settings**.\n    *   Check the **Show \"appsscript.json\" manifest file in editor** option.\n1.  **Associate with Google Cloud Project:**\n    *   In your Apps Script project, go to **Project Settings**.\n    *   Under **Google Cloud Platform (GCP) Project**, click **Change project**.\n    *   Enter your Google Cloud Project number and click **Set project**.\n1.  **Copy Code:**\n    *   Copy the code from each `.gs` file in this directory into the\n        corresponding file in your Apps Script project.\n1.  **Update `Constants.gs`:**\n    *   Replace the placeholder values in `Constants.gs`:\n        *   `PROJECT_ID`: Your Google Cloud Project ID.\n        *   `ME`: Your name.\n1.  **Update `appsscript.json`:**\n    *   Ensure the `appsscript.json` file is configured correctly.\n\n### 3. Configure OAuth Consent Screen\n\nGoogle Workspace add-ons require a consent screen configuration. Configuring\nyour add-on's OAuth consent screen defines what Google displays to users.\n\n1.  **Go to Google Cloud Console:**\n    *   Navigate to the [Google Auth Platform - Branding page](https://console.cloud.google.com/auth/branding).\n1.  **App Information:**\n    *   **App name:** Enter a name for your add-on (e.g., \"Email Classifier\").\n    *   **User support email:** Select your email address.\n    *   **Developer contact information:** Enter your email address.\n    * Click **Next**.\n1. **Audience:**\n    *  Select **Internal**.\n    * Click **Next**.\n1. **Contact Information:**\n    *   Select your email address.\n    * Click **Next**.\n1. **Finish:**\n    *   Check **I agree to the [Google API Services: User Data Policy](https://developers.google.com/terms/api-services-user-data-policy)**.\n    * Click **Continue**.\n1. **Create:**\n    * Click **Create**.\n\n### 4. Deploy the Add-on\n\n1.  **Deploy:**\n    *   Click \"Deploy\" > \"Test deployments\".\n    *   Select \"Gmail add-on\".\n    *   Click \"Install\" to install the add-on for your account.\n\n## How to Run\n\n1.  **Open Gmail:**\n    *   Open Gmail in your browser.\n1.  **Open the Add-on:**\n    *   The \"Email Classifier\" add-on should appear in the right sidebar.\n1.  **Classify Emails:**\n    *   Click the \"Classify emails\" button.\n    *   The add-on will process unread emails from the last 7 days.\n1.  **View Results:**\n    *   A link to the generated Google Sheet will be displayed.\n    *   Open the sheet to view the classification results.\n1.  **Create/Remove Labels:**\n    *   Use the \"Create labels\" or \"Remove labels\" buttons to manage Gmail labels.\n\n## Code Overview\n\n*   **`Cards.gs`:**\n    *   Defines the UI for the Gmail add-on, including buttons and actions.\n*   **`ClassifyEmail.gs`:**\n    *   Constructs prompts for the Gemini API.\n    *   Sends email content to the Gemini API for classification.\n    *   Parses the API response.\n*   **`Code.gs`:**\n    *   Main function to search, classify, label, and log emails.\n*   **`Constants.gs`:**\n    *   Stores project-specific constants (e.g., API URL, project ID, email).\n*   **`DraftEmail.gs`:**\n    *   Constructs prompts for the Gemini API to generate draft responses.\n    *   Sends email content to the Gemini API for draft generation.\n    *   Parses the API response.\n*   **`Labels.gs`:**\n    *   Creates, updates, and removes Gmail labels.\n*   **`Sheet.gs`:**\n    *   Creates and updates Google Sheets for logging.\n*   **`appsscript.json`:**\n    *   Configuration file for the Apps Script project.\n\n## Important Notes\n\n*   **Gemini API Usage:** This project relies on the Gemini API for natural\n    language processing. Make sure you have the API enabled and have sufficient\n    quota.\n*   **OAuth Scopes:** The `appsscript.json` file includes the necessary OAuth\n    scopes for Gmail, Sheets, and the Gemini API.\n*   **Error Handling:** The code includes basic error handling, but you may need\n    to add more robust error handling for production use.\n*   **Rate Limits:** Be mindful of API rate limits, especially when processing\n    large numbers of emails.\n*   **Security:** Ensure that you are handling user data securely.\n\n## Disclaimer\n\nThis code is provided as-is, without any warranty. Use at your own risk.\n\nFeel free to modify and adapt this code to your specific needs.\n"
  },
  {
    "path": "ai/email-classifier/Sheet.gs",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Creates a spreadsheet with the given headers.\n * @param {!Array<string>} headers The headers for the spreadsheet.\n * @return {!Spreadsheet} The created spreadsheet.\n */\nfunction createSheetWithHeaders(headers) {\n  const today = new Date().toLocaleString();\n  const spreadsheet = SpreadsheetApp.create(`Emails from ${today}`);\n  const sheet = spreadsheet.getActiveSheet();\n  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);\n  addTable(spreadsheet);\n  console.log(`Successfully created spreadsheet: ${spreadsheet.getUrl()}`);\n  return spreadsheet;\n}\n\n/**\n * Adds data to the spreadsheet.\n * @param {!Spreadsheet} spreadsheet The spreadsheet to add data to.\n * @param {string} subject The subject of the email.\n * @param {string} classification The classification of the email.\n * @param {string} reason The reason for the classification.\n */\nfunction addDataToSheet(spreadsheet, subject, classification, reason) {\n  const sheet = spreadsheet.getActiveSheet();\n  const newRow = [subject, classification, reason];\n  sheet.appendRow(newRow);\n}\n\n/**\n * Creates a hyperlink for the given thread.\n * @param {!GmailThread} thread The thread to create a hyperlink for.\n * @return {string} The hyperlink.\n */\nfunction hyperlink(thread) {\n  const link = `https://mail.google.com/mail/u/0/#inbox/${thread.getId()}`;\n  return `=HYPERLINK(\"${link}\", \"${thread.getFirstMessageSubject()}\")`;\n}\n\n/**\n * Adds a table to the spreadsheet with a dropdown for classification.\n * @param {!Spreadsheet} ss The spreadsheet to add the table to.\n */\nfunction addTable(ss) {\n  const values = Object.keys(classificationLabels).map((label) => {\n    return { userEnteredValue: label };\n  });\n  const addTableRequest = {\n    requests: [\n      {\n        addTable: {\n          table: {\n            name: \"Email classification\",\n            range: {\n              sheetId: 0,\n              startColumnIndex: 0,\n              endColumnIndex: 2,\n            },\n            columnProperties: [\n              {\n                columnIndex: 1,\n                columnType: \"DROPDOWN\",\n                dataValidationRule: {\n                  condition: { type: \"ONE_OF_LIST\", values: values },\n                },\n              },\n            ],\n          },\n        },\n      },\n    ],\n  };\n\n  Sheets.Spreadsheets.batchUpdate(addTableRequest, ss.getId());\n}\n"
  },
  {
    "path": "ai/email-classifier/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Gmail\",\n        \"version\": \"v1\",\n        \"serviceId\": \"gmail\"\n      },\n      {\n        \"userSymbol\": \"Sheets\",\n        \"version\": \"v4\",\n        \"serviceId\": \"sheets\"\n      }\n    ]\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/cloud-platform\",\n    \"https://www.googleapis.com/auth/gmail.addons.execute\",\n    \"https://www.googleapis.com/auth/gmail.modify\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/spreadsheets\"\n  ],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Email Classifier\",\n      \"logoUrl\": \"https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png\"\n    },\n    \"gmail\": {\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepageTrigger\",\n        \"enabled\": true\n      }\n    }\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "ai/gmail-sentiment-analysis/Cards.gs",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Builds the card for to display in the sidepanel of gmail.\n * @return {CardService.Card} The card to show to the user.\n */\n\nfunction buildCard_GmailHome(notifyOk = false) {\n  const imageUrl =\n    \"https://icons.iconarchive.com/icons/roundicons/100-free-solid/48/spy-icon.png\";\n  const image = CardService.newImage().setImageUrl(imageUrl);\n\n  const cardHeader = CardService.newCardHeader()\n    .setImageUrl(imageUrl)\n    .setImageStyle(CardService.ImageStyle.CIRCLE)\n    .setTitle(\"Analyze your GMail\");\n\n  const action = CardService.newAction().setFunctionName(\"analyzeSentiment\");\n  const button = CardService.newTextButton()\n    .setText(\"Identify angry customers\")\n    .setOnClickAction(action)\n    .setTextButtonStyle(CardService.TextButtonStyle.FILLED);\n  const buttonSet = CardService.newButtonSet().addButton(button);\n\n  const section = CardService.newCardSection()\n    .setHeader(\"Emails sentiment analysis\")\n    .addWidget(buttonSet);\n\n  const card = CardService.newCardBuilder()\n    .setHeader(cardHeader)\n    .addSection(section);\n\n  /**\n   * This builds the card that contains the footer that informs\n   * the user about the successful execution of the Add-on.\n   */\n\n  if (notifyOk === true) {\n    const fixedFooter = CardService.newFixedFooter().setPrimaryButton(\n      CardService.newTextButton()\n        .setText(\"Analysis complete\")\n        .setOnClickAction(\n          CardService.newAction().setFunctionName(\"buildCard_GmailHome\"),\n        ),\n    );\n    card.setFixedFooter(fixedFooter);\n  }\n  return card.build();\n}\n"
  },
  {
    "path": "ai/gmail-sentiment-analysis/Code.gs",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Callback for rendering the homepage card.\n * @return {CardService.Card} The card to show to the user.\n */\nfunction onHomepage(e) {\n  return buildCard_GmailHome();\n}\n"
  },
  {
    "path": "ai/gmail-sentiment-analysis/Gmail.gs",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Callback for initiating the sentiment analysis.\n * @return {CardService.Card} The card to show to the user.\n */\n\nfunction analyzeSentiment() {\n  emailSentiment();\n  return buildCard_GmailHome(true);\n}\n\n/**\n * Gets the last 10 threads in the inbox and the corresponding messages.\n * Fetches the label that should be applied to negative messages.\n * The processSentiment is called on each message\n * and tested with RegExp to check for a negative answer from the model\n */\n\nfunction emailSentiment() {\n  const threads = GmailApp.getInboxThreads(0, 10);\n  const msgs = GmailApp.getMessagesForThreads(threads);\n  const label_upset = GmailApp.getUserLabelByName(\"UPSET TONE 😡\");\n  let currentPrediction;\n\n  for (let i = 0; i < msgs.length; i++) {\n    for (let j = 0; j < msgs[i].length; j++) {\n      const emailText = msgs[i][j].getPlainBody();\n      currentPrediction = processSentiment(emailText);\n      if (currentPrediction === true) {\n        label_upset.addToThread(msgs[i][j].getThread());\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "ai/gmail-sentiment-analysis/README.md",
    "content": "# Gmail sentiment analysis with Vertex AI\n\n## Project Description\n\nGoogle Workspace Add-on that extends Gmail and adds sentiment analysis capabilities.\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Enable the Vertex AI API\n   1. Create a Service Account and grant the role `Vertex AI User`\n   1. Create a private key with type JSON. This will download the JSON file for use in the next section.\n1. Open an Apps Script Project bound to a Google Sheets Spreadsheet\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add a Script Property. Enter `service_account_key` as the property name and paste the JSON key from the service account as the value. \n1. Add OAuth2 v43 Apps Script Library using the ID `1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF`.\n1. Add the project code to Apps Script\n\n## Usage\n\n1. Create a label in Gmail with this exact text and emojy (case sensitive!): UPSET TONE 😡\n1. In Gmail, click on the Productivity toolbox icon (icon of a spy) in the sidepanel.\n1. The sidepanel will open up. Grant the Add-on autorization to run.\n1. The Add-on will load. Click on the blue button \"Identify angry customers.\"\n1. Close the Add-on by clicking on the X in the top right corner.\n1. It can take a couple of minutes until the label is applied to the messages that have a negative tone.\n1. If you don't want to wait until the labels are added, you can refresh the browser."
  },
  {
    "path": "ai/gmail-sentiment-analysis/Vertex.gs",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst PROJECT_ID = \"ADD YOUR GCP PROJECT ID HERE\";\nconst VERTEX_AI_LOCATION = \"europe-west2\";\nconst MODEL_ID = \"gemini-2.5-pro\";\nconst SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty(\n  \"service_account_key\",\n);\n\n/**\n * Packages prompt and necessary settings, then sends a request to\n * Vertex API.\n * A check is performed to see if the response from Vertex AI contains FALSE as a value.\n * Returns the outcome of that check which is a boolean.\n *\n * @param emailText - Email message that is sent to the model.\n */\n\nfunction processSentiment(emailText) {\n  const prompt = `Analyze the following message: ${emailText}. If the sentiment of this message is negative, answer with FALSE. If the sentiment of this message is neutral or positive, answer with TRUE. Do not use any other words than the ones requested in this prompt as a response!`;\n\n  const request = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: prompt,\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0.9,\n      maxOutputTokens: 1024,\n    },\n  };\n\n  const credentials = credentialsForVertexAI();\n\n  const fetchOptions = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${credentials.accessToken}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true,\n    payload: JSON.stringify(request),\n  };\n\n  const url =\n    `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/` +\n    `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n\n  const response = UrlFetchApp.fetch(url, fetchOptions);\n  const payload = JSON.parse(response.getContentText());\n\n  const regex = /FALSE/;\n\n  return regex.test(payload.candidates[0].content.parts[0].text);\n}\n\n/**\n * Gets credentials required to call Vertex API using a Service Account.\n * Requires use of Service Account Key stored with project\n *\n * @return {!Object} Containing the Cloud Project Id and the access token.\n */\n\nfunction credentialsForVertexAI() {\n  const credentials = SERVICE_ACCOUNT_KEY;\n  if (!credentials) {\n    throw new Error(\"service_account_key script property must be set.\");\n  }\n\n  const parsedCredentials = JSON.parse(credentials);\n\n  const service = OAuth2.createService(\"Vertex\")\n    .setTokenUrl(\"https://oauth2.googleapis.com/token\")\n    .setPrivateKey(parsedCredentials.private_key)\n    .setIssuer(parsedCredentials.client_email)\n    .setPropertyStore(PropertiesService.getScriptProperties())\n    .setScope(\"https://www.googleapis.com/auth/cloud-platform\");\n  return {\n    projectId: parsedCredentials.project_id,\n    accessToken: service.getAccessToken(),\n  };\n}\n"
  },
  {
    "path": "ai/gmail-sentiment-analysis/appsscript.json",
    "content": "{\n  \"timeZone\": \"Europe/Madrid\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"version\": \"43\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\"\n      }\n    ]\n  },\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Productivity toolbox\",\n      \"logoUrl\": \"https://icons.iconarchive.com/icons/roundicons/100-free-solid/64/spy-icon.png\",\n      \"useLocaleFromApp\": true\n    },\n    \"gmail\": {\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepage\",\n        \"enabled\": true\n      }\n    }\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "ai/standup-chat-app/README.md",
    "content": "# Chat API - Stand up with AI\n\n## Project Description\n\nGoogle Chat application that creates AI summaries of a consolidation Chat threads and posts them back within the top-level Chat message. Use case is using AI to streamline Stand up content within Google Chat.\n\n## Prerequisites\n\n* Google Cloud Project (aka Standard Cloud Project for Apps Script) with billing enabled\n\n## Set up your environment\n\n1. Create a Cloud Project\n   1. Configure OAuth consent screen\n   1. Enable the Admin SDK API\n   1. Enable the Generative Language API\n   1. Enable and configure the Google Chat API with the following values:\n      1. App status: Live - available to users\n      1. App name: “Standup”\n      1. Avatar URL: “https://www.gstatic.com/images/branding/productlogos/chat_2020q4/v8/web-24dp/logo_chat_2020q4_color_2x_web_24dp.png”\n      1. Description: “Standup App”\n      1. Enable Interactive features: Disabled\n1. Create a Google Gemini API Key \n   1. Navigate to https://aistudio.google.com/app/apikey \n   1. Create API key for existing project from step 1\n   1. Copy the generated key \n1. Create and open a standalone Apps Script project\n   1. From Project Settings, change project to GCP project number of Cloud Project from step 1\n   1. Add the following script properties:\n      1. Set `API_KEY` with the API key previously generated as the value.\n      1. Set `SPREADSHEET_ID` with the file ID of a blank spreadsheet. \n      1. Set `SPACE_NAME` to the resource name of a Chat space (e.g. `spaces/AAAXYZ`)\n   1. Enable the Google Chat advanced service\n   1. Enable the AdminDirectory advanced service \n1. Add the project code to Apps Script\n1. Enable triggers:\n   1. Add Time-driven to run function `standup` at the desired interval frequency (e.g. Week timer)\n   1. Add Time-driven to run function `summarize` at the desired interval frequency (e.g. Hour timer)\n"
  },
  {
    "path": "ai/standup-chat-app/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Chat\",\n        \"serviceId\": \"chat\",\n        \"version\": \"v1\"\n      },\n      {\n        \"userSymbol\": \"AdminDirectory\",\n        \"serviceId\": \"admin\",\n        \"version\": \"directory_v1\"\n      }\n    ]\n  },\n  \"webapp\": {\n    \"executeAs\": \"USER_ACCESSING\",\n    \"access\": \"DOMAIN\"\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/chat.messages\",\n    \"https://www.googleapis.com/auth/spreadsheets\",\n    \"https://www.googleapis.com/auth/admin.directory.user.readonly\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/chat.spaces.create\",\n    \"https://www.googleapis.com/auth/chat.spaces\",\n    \"https://www.googleapis.com/auth/chat.spaces.readonly\",\n    \"https://www.googleapis.com/auth/chat.spaces.create\",\n    \"https://www.googleapis.com/auth/chat.delete\",\n    \"https://www.googleapis.com/auth/chat.memberships\",\n    \"https://www.googleapis.com/auth/chat.memberships.app\",\n    \"https://www.googleapis.com/auth/userinfo.email\"\n  ]\n}\n"
  },
  {
    "path": "ai/standup-chat-app/db.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/** @typedef {object} Message\n * @property {string} name\n * @property {string} text\n * @property {object} sender\n * @property {string} sender.type\n * @property {string} sender.name\n * @property {object[]} annotations\n * @property {number} annotations.startIndex\n * @property {string} annotations.type\n * @property {object} annotations.userMention\n * @property {number} annotations.length\n * @property {string} formattedText\n * @property {string} createTime\n * @property {string} argumentText\n * @property {object} thread\n * @property {string} thread.name\n * @property {object} space\n * @property {string} space.name\n */\n\nclass DB {\n  /**\n   * params {String} spreadsheetId\n   */\n  constructor(spreadsheetId) {\n    this.spreadsheetId = spreadsheetId;\n    this.sheetName = \"Messages\";\n  }\n\n  /**\n   * @returns {SpreadsheetApp.Sheet}\n   */\n  get sheet() {\n    const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId);\n    let sheet = spreadsheet.getSheetByName(this.sheetName);\n\n    // create if it does not exist\n    if (sheet === undefined) {\n      sheet = spreadsheet.insertSheet();\n      sheet.setName(this.sheetName);\n    }\n\n    return sheet;\n  }\n\n  /**\n   * @returns {Message|undefined}\n   */\n  get last() {\n    const lastRow = this.sheet.getLastRow();\n    if (lastRow === 0) return undefined;\n    return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]);\n  }\n\n  /**\n   * @params {Chat_v1.Chat.V1.Schema.Message} message\n   */\n  append(message) {\n    this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]);\n  }\n}\n\n/**\n * Test function for DB Object\n */\nfunction testDB() {\n  const db = new DB(SPREADSHEET_ID);\n\n  let thread = db.last;\n  if (thread === undefined) return;\n  console.log(thread);\n\n  db.rowOffset = 1;\n  thread = db.last;\n  if (thread === undefined) return;\n  console.log(thread);\n}\n"
  },
  {
    "path": "ai/standup-chat-app/gemini.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Makes a simple content-only call to Gemini AI.\n *\n * @param {string} text Prompt to pass to Gemini API.\n * @param {string} API_KEY Developer API Key enabled to call Gemini.\n *\n * @return {string} Response from AI call.\n */\nfunction generateContent(text, API_KEY) {\n  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;\n\n  return JSON.parse(\n    UrlFetchApp.fetch(url, {\n      method: \"POST\",\n      headers: {\n        \"content-type\": \"application/json\",\n      },\n      payload: JSON.stringify({\n        contents: [\n          {\n            parts: [{ text }],\n          },\n        ],\n      }),\n    }).getContentText(),\n  );\n}\n"
  },
  {
    "path": "ai/standup-chat-app/main.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/** TODO\n * Update global variables for your project settings\n * */\nconst API_KEY = PropertiesService.getScriptProperties().getProperty(\"API_KEY\");\nconst SPREADSHEET_ID =\n  PropertiesService.getScriptProperties().getProperty(\"SPREADSHEET_ID\"); // e.g. \"1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0\"\nconst SPACE_NAME =\n  PropertiesService.getScriptProperties().getProperty(\"SPACE_NAME\"); // e.g. \"spaces/AAAABCa12Cc\"\n\nconst SUMMARY_HEADER = \"\\n\\n*Gemini Generated Summary*\\n\\n\";\n\n/**\n * Sends the message to create new standup instance.\n * Called by trigger on interval of standup, e.g. Weekly\n *\n * @return {string} The thread name of the message sent.\n */\nfunction standup() {\n  const db = new DB(SPREADSHEET_ID);\n\n  const last = db.last;\n\n  let text = `<users/all> Please share your weekly update here.\\n\\n*Source Code*: <https://script.google.com/corp/home/projects/${ScriptApp.getScriptId()}/edit|Apps Script>`;\n\n  if (last) {\n    text += `\\n*Last Week*: <${linkToThread(last)}|View thread>`;\n  }\n\n  const message = Chat.Spaces.Messages.create(\n    {\n      text,\n    },\n    PropertiesService.getScriptProperties().getProperty(\"spaceName\"), // Demo replaces => SPACE_NAME\n  );\n\n  db.append(message);\n\n  console.log(`Thread Name: ${message.thread.name}`);\n  return message.thread.name;\n}\n\n/**\n * Uses AI to create a summary of messages for a stand up period.\n * Called by trigger on interval required to summarize, e.g. Hourly\n *\n * @return n/a\n */\nfunction summarize() {\n  const db = new DB(SPREADSHEET_ID);\n  const last = db.last;\n\n  if (last === undefined) return;\n\n  const filter = `thread.name=${last.thread.name}`;\n  let { messages } = Chat.Spaces.Messages.list(\n    PropertiesService.getScriptProperties().getProperty(\"spaceName\"),\n    { filter },\n  ); // Demo replaces => SPACE_NAME\n\n  messages = (messages ?? [])\n    .slice(1)\n    .filter((message) => message.slashCommand === undefined);\n\n  if (messages.length === 0) {\n    return;\n  }\n\n  const history = messages\n    .map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`)\n    .join(\"/n\");\n\n  const response = generateContent(\n    `Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`,\n    API_KEY,\n  );\n  const summary = response.candidates[0].content?.parts[0].text;\n\n  if (summary === undefined) {\n    return;\n  }\n\n  Chat.Spaces.Messages.update(\n    {\n      text: last.formattedText + SUMMARY_HEADER + summary.replace(\"**\", \"*\"),\n    },\n    last.name,\n    { update_mask: \"text\" },\n  );\n}\n\n/**\n * Gets the display name from AdminDirectory Services.\n *\n * @param {!Object} sender\n * @return {string} User name on success | 'Unknown' if not.\n */\nfunction getSenderDisplayName(sender) {\n  try {\n    const user = AdminDirectory.Users.get(sender.name.replace(\"users/\", \"\"), {\n      projection: \"BASIC\",\n      viewType: \"domain_public\",\n    });\n    return user.name.displayName ?? user.name.fullName;\n  } catch (e) {\n    console.error(\"Unable to get display name\");\n    return \"Unknown\";\n  }\n}\n\nconst cachedGetSenderDisplayName = memoize(getSenderDisplayName);\n\n/**\n * @params {Chat_v1.Chat.V1.Schema.Message|Message} message\n * @returns {String}\n */\nfunction linkToThread(message) {\n  // https://chat.google.com/room/SPACE/THREAD/\n  return `https://chat.google.com/room/${message.space.name.split(\"/\").pop()}/${message.thread.name.split(\"/\").pop()}`;\n}\n"
  },
  {
    "path": "ai/standup-chat-app/memoize.js",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * A generic hash function that takes a string and computes a hash using the\n * specified algorithm.\n *\n * @param {string} str - The string to hash.\n * @param {Utilities.DigestAlgorithm} algorithm - The algorithm to use to\n *  compute the hash. Defaults to MD5.\n * @returns {string} The base64 encoded hash of the string.\n */\nfunction hash(str, algorithm = Utilities.DigestAlgorithm.MD5) {\n  const digest = Utilities.computeDigest(algorithm, str);\n\n  return Utilities.base64Encode(digest);\n}\n\n/**\n * Memoizes a function by caching its results based on the arguments passed.\n *\n * @param {Function} func - The function to be memoized.\n * @param {number} [ttl=600] - The time to live in seconds for the cached\n *  result. The maximum value is 600.\n * @param {Cache} [cache=CacheService.getScriptCache()] - The cache to store the\n *  memoized results.\n * @returns {Function} - The memoized function.\n *\n * @example\n *\n * const cached = memoize(myFunction);\n * cached(1, 2, 3); // The result will be cached\n * cached(1, 2, 3); // The cached result will be returned\n * cached(4, 5, 6); // A new result will be calculated and cached\n */\nfunction memoize(func, ttl = 600, cache = CacheService.getScriptCache()) {\n  return (...args) => {\n    // consider a more robust input to the hash function to handler complex\n    // types such as functions, dates, and regex\n    const key = hash(JSON.stringify([func.toString(), ...args]));\n\n    const cached = cache.get(key);\n\n    if (cached != null) {\n      return JSON.parse(cached);\n    }\n    const result = func(...args);\n    cache.put(key, JSON.stringify(result), ttl);\n    return result;\n  };\n}\n"
  },
  {
    "path": "apps-script/execute/target.js",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_api_execute]\n/**\n * Return the set of folder names contained in the user's root folder as an\n * object (with folder IDs as keys).\n * @return {Object} A set of folder names keyed by folder ID.\n */\nfunction getFoldersUnderRoot() {\n  const root = DriveApp.getRootFolder();\n  const folders = root.getFolders();\n  const folderSet = {};\n  while (folders.hasNext()) {\n    const folder = folders.next();\n    folderSet[folder.getId()] = folder.getName();\n  }\n  return folderSet;\n}\n// [END apps_script_api_execute]\n"
  },
  {
    "path": "biome.json",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/1.9.4/schema.json\",\n  \"vcs\": {\n    \"enabled\": true,\n    \"clientKind\": \"git\",\n    \"useIgnoreFile\": true\n  },\n  \"formatter\": {\n    \"enabled\": true,\n    \"indentWidth\": 2,\n    \"indentStyle\": \"space\",\n    \"lineWidth\": 80\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"recommended\": true\n    }\n  },\n  \"files\": {\n    \"ignore\": [\"moment.gs\", \"**/dist\", \"**/target\", \"**/pkg\", \"**/node_modules\"]\n  }\n}\n"
  },
  {
    "path": "calendar/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START calendar_quickstart]\n/**\n * Lists 10 upcoming events in the user's calendar.\n * @see https://developers.google.com/calendar/api/v3/reference/events/list\n */\nfunction listUpcomingEvents() {\n  const calendarId = \"primary\";\n  // Add query parameters in optionalArgs\n  const optionalArgs = {\n    timeMin: new Date().toISOString(),\n    showDeleted: false,\n    singleEvents: true,\n    maxResults: 10,\n    orderBy: \"startTime\",\n    // use other optional query parameter here as needed.\n  };\n  try {\n    // call Events.list method to list the calendar events using calendarId optional query parameter\n    const response = Calendar.Events.list(calendarId, optionalArgs);\n    const events = response.items;\n    if (events.length === 0) {\n      console.log(\"No upcoming events found\");\n      return;\n    }\n    // Print the calendar events\n    for (const event of events) {\n      let when = event.start.dateTime;\n      if (!when) {\n        when = event.start.date;\n      }\n      console.log(\"%s (%s)\", event.summary, when);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception from Calendar API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END calendar_quickstart]\n"
  },
  {
    "path": "chat/advanced-service/AppAuthenticationUtils.gs",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START chat_authentication_utils]\n\n// This script provides configuration and helper functions for app authentication.\n// It may require modifications to work in your environment.\n\n// For more information on app authentication, see\n// https://developers.google.com/workspace/chat/authenticate-authorize-chat-app\n\nconst APP_AUTH_OAUTH_SCOPES = [\"https://www.googleapis.com/auth/chat.bot\"];\n// Warning: This example uses a service account private key, it should always be stored in a\n// secure location.\nconst SERVICE_ACCOUNT = {\n  // TODO(developer): Replace with the Google Chat credentials to use for app authentication,\n  // the service account private key's JSON.\n};\n\n/**\n * Authenticates the app service by using the OAuth2 library.\n *\n * @return {Object} the authenticated app service\n */\nfunction getService_() {\n  return OAuth2.createService(SERVICE_ACCOUNT.client_email)\n    .setTokenUrl(SERVICE_ACCOUNT.token_uri)\n    .setPrivateKey(SERVICE_ACCOUNT.private_key)\n    .setIssuer(SERVICE_ACCOUNT.client_email)\n    .setSubject(SERVICE_ACCOUNT.client_email)\n    .setScope(APP_AUTH_OAUTH_SCOPES)\n    .setCache(CacheService.getUserCache())\n    .setLock(LockService.getUserLock())\n    .setPropertyStore(PropertiesService.getScriptProperties());\n}\n\n/**\n * Generates headers with the app credentials to use to make Google Chat API calls.\n *\n * @return {Object} the header with credentials\n */\nfunction getHeaderWithAppCredentials() {\n  return {\n    Authorization: `Bearer ${getService_().getAccessToken()}`,\n  };\n}\n\n// [END chat_authentication_utils]\n"
  },
  {
    "path": "chat/advanced-service/Main.gs",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// This script provides each code sample in a separate function.\n// It may require modifications to work in your environment.\n\n// For more information on user authentication, see\n// https://developers.google.com/workspace/chat/authenticate-authorize-chat-user\n\n// For more information on app authentication, see\n// https://developers.google.com/workspace/chat/authenticate-authorize-chat-app\n\n// [START chat_create_membership_user_cred]\n/**\n * This sample shows how to create membership with user credential for a human user\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.memberships'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMembershipUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const membership = {\n    member: {\n      // TODO(developer): Replace USER_NAME here\n      name: \"users/USER_NAME\",\n      // User type for the membership\n      type: \"HUMAN\",\n    },\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Members.create(membership, parent);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_membership_user_cred]\n\n// [START chat_create_membership_user_cred_for_app]\n/**\n * This sample shows how to create membership with app credential for an app\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.memberships.app'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMembershipUserCredForApp() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const membership = {\n    member: {\n      // Member name for app membership, do not change this\n      name: \"users/app\",\n      // User type for the membership\n      type: \"BOT\",\n    },\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Members.create(membership, parent);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_membership_user_cred_for_app]\n\n// [START chat_create_membership_user_cred_for_group]\n/**\n * This sample shows how to create membership with user credential for a group\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.memberships'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMembershipUserCredForGroup() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const membership = {\n    groupMember: {\n      // TODO(developer): Replace GROUP_NAME here\n      name: \"groups/GROUP_NAME\",\n    },\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Members.create(membership, parent);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_membership_user_cred_for_group]\n\n// [START chat_create_message_app_cred]\n/**\n * This sample shows how to create message with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction createMessageAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const message = {\n    text:\n      \"👋🌎 Hello world! I created this message by calling \" +\n      \"the Chat API's `messages.create()` method.\",\n    cardsV2: [\n      {\n        card: {\n          header: {\n            title: \"About this message\",\n            imageUrl:\n              \"https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/info/default/24px.svg\",\n          },\n          sections: [\n            {\n              header: \"Contents\",\n              widgets: [\n                {\n                  textParagraph: {\n                    text:\n                      \"🔡 <b>Text</b> which can include \" +\n                      \"hyperlinks 🔗, emojis 😄🎉, and @mentions 🗣️.\",\n                  },\n                },\n                {\n                  textParagraph: {\n                    text:\n                      \"🖼️ A <b>card</b> to display visual elements\" +\n                      \"and request information such as text 🔤, \" +\n                      \"dates and times 📅, and selections ☑️.\",\n                  },\n                },\n                {\n                  textParagraph: {\n                    text:\n                      \"👉🔘 An <b>accessory widget</b> which adds \" +\n                      \"a button to the bottom of a message.\",\n                  },\n                },\n              ],\n            },\n            {\n              header: \"What's next\",\n              collapsible: true,\n              widgets: [\n                {\n                  textParagraph: {\n                    text: \"❤️ <a href='https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces.messages.reactions/create'>Add a reaction</a>.\",\n                  },\n                },\n                {\n                  textParagraph: {\n                    text:\n                      \"🔄 <a href='https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces.messages/patch'>Update</a> \" +\n                      \"or ❌ <a href='https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces.messages/delete'>delete</a> \" +\n                      \"the message.\",\n                  },\n                },\n              ],\n            },\n          ],\n        },\n      },\n    ],\n    accessoryWidgets: [\n      {\n        buttonList: {\n          buttons: [\n            {\n              text: \"View documentation\",\n              icon: { materialIcon: { name: \"link\" } },\n              onClick: {\n                openLink: {\n                  url: \"https://developers.google.com/workspace/chat/create-messages\",\n                },\n              },\n            },\n          ],\n        },\n      },\n    ],\n  };\n  const parameters = {};\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(\n    message,\n    parent,\n    parameters,\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_app_cred]\n\n// [START chat_create_message_user_cred]\n/**\n * This sample shows how to create message with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const message = {\n    text:\n      \"👋🌎 Hello world!\" +\n      \"Text messages can contain things like:\\n\\n\" +\n      \"* Hyperlinks 🔗\\n\" +\n      \"* Emojis 😄🎉\\n\" +\n      \"* Mentions of other Chat users `@` \\n\\n\" +\n      \"For details, see the \" +\n      \"<https://developers.google.com/workspace/chat/format-messages\" +\n      \"|Chat API developer documentation>.\",\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred]\n\n// [START chat_create_message_user_cred_at_mention]\n/**\n * This sample shows how to create message with user credential with a user mention\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCredAtMention() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  const message = {\n    // The user with USER_NAME will be mentioned if they are in the space\n    // TODO(developer): Replace USER_NAME here\n    text: \"Hello <users/USER_NAME>!\",\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred_at_mention]\n\n// [START chat_create_message_user_cred_message_id]\n/**\n * This sample shows how to create message with user credential with message id\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCredMessageId() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  // Message id lets chat apps get, update or delete a message without needing\n  // to store the system assigned ID in the message's resource name\n  const messageId = \"client-MESSAGE-ID\";\n  const message = { text: \"Hello with user credential!\" };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent, {\n    messageId: messageId,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred_message_id]\n\n// [START chat_create_message_user_cred_request_id]\n/**\n * This sample shows how to create message with user credential with request id\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCredRequestId() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  // Specifying an existing request ID returns the message created with\n  // that ID instead of creating a new message\n  const requestId = \"REQUEST_ID\";\n  const message = { text: \"Hello with user credential!\" };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent, {\n    requestId: requestId,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred_request_id]\n\n// [START chat_create_message_user_cred_thread_key]\n/**\n * This sample shows how to create message with user credential with thread key\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCredThreadKey() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  // Creates the message as a reply to the thread specified by thread_key\n  // If it fails, the message starts a new thread instead\n  const messageReplyOption = \"REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD\";\n  const message = {\n    text: \"Hello with user credential!\",\n    thread: {\n      // Thread key specifies a thread and is unique to the chat app\n      // that sets it\n      threadKey: \"THREAD_KEY\",\n    },\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent, {\n    messageReplyOption: messageReplyOption,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred_thread_key]\n\n// [START chat_create_message_user_cred_thread_name]\n/**\n * This sample shows how to create message with user credential with thread name\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createMessageUserCredThreadName() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here.\n  const parent = \"spaces/SPACE_NAME\";\n  // Creates the message as a reply to the thread specified by thread.name\n  // If it fails, the message starts a new thread instead\n  const messageReplyOption = \"REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD\";\n  const message = {\n    text: \"Hello with user credential!\",\n    thread: {\n      // Resource name of a thread that uniquely identify a thread\n      // TODO(developer): Replace SPACE_NAME and THREAD_NAME here\n      name: \"spaces/SPACE_NAME/threads/THREAD_NAME\",\n    },\n  };\n\n  // Make the request\n  const response = Chat.Spaces.Messages.create(message, parent, {\n    messageReplyOption: messageReplyOption,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_message_user_cred_thread_name]\n\n// [START chat_create_space_user_cred]\n/**\n * This sample shows how to create space with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction createSpaceUserCred() {\n  // Initialize request argument(s)\n  const space = {\n    spaceType: \"SPACE\",\n    // TODO(developer): Replace DISPLAY_NAME here\n    displayName: \"DISPLAY_NAME\",\n  };\n\n  // Make the request\n  const response = Chat.Spaces.create(space);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_create_space_user_cred]\n\n// [START chat_delete_message_app_cred]\n/**\n * This sample shows how to delete a message with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction deleteMessageAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n  const parameters = {};\n\n  // Make the request\n  const response = Chat.Spaces.Messages.remove(\n    name,\n    parameters,\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_delete_message_app_cred]\n\n// [START chat_delete_message_user_cred]\n/**\n * This sample shows how to delete a message with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages'\n * referenced in the manifest file (appsscript.json).\n */\nfunction deleteMessageUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n\n  // Make the request\n  const response = Chat.Spaces.Messages.remove(name);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_delete_message_user_cred]\n\n// [START chat_get_membership_app_cred]\n/**\n * This sample shows how to get membership with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction getMembershipAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MEMBER_NAME here\n  const name = \"spaces/SPACE_NAME/members/MEMBER_NAME\";\n  const parameters = {};\n\n  // Make the request\n  const response = Chat.Spaces.Members.get(\n    name,\n    parameters,\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_membership_app_cred]\n\n// [START chat_get_membership_user_cred]\n/**\n * This sample shows how to get membership with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.memberships.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction getMembershipUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MEMBER_NAME here\n  const name = \"spaces/SPACE_NAME/members/MEMBER_NAME\";\n\n  // Make the request\n  const response = Chat.Spaces.Members.get(name);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_membership_user_cred]\n\n// [START chat_get_message_app_cred]\n/**\n * This sample shows how to get message with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction getMessageAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n  const parameters = {};\n\n  // Make the request\n  const response = Chat.Spaces.Messages.get(\n    name,\n    parameters,\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_message_app_cred]\n\n// [START chat_get_message_user_cred]\n/**\n * This sample shows how to get message with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction getMessageUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n\n  // Make the request\n  const response = Chat.Spaces.Messages.get(name);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_message_user_cred]\n\n// [START chat_get_space_app_cred]\n/**\n * This sample shows how to get space with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction getSpaceAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const name = \"spaces/SPACE_NAME\";\n  const parameters = {};\n\n  // Make the request\n  const response = Chat.Spaces.get(\n    name,\n    parameters,\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_space_app_cred]\n\n// [START chat_get_space_user_cred]\n/**\n * This sample shows how to get space with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction getSpaceUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const name = \"spaces/SPACE_NAME\";\n\n  // Make the request\n  const response = Chat.Spaces.get(name);\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_get_space_user_cred]\n\n// [START chat_list_memberships_app_cred]\n/**\n * This sample shows how to list memberships with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction listMembershipsAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const parent = \"spaces/SPACE_NAME\";\n  // Filter membership by type (HUMAN or BOT) or role (ROLE_MEMBER or\n  // ROLE_MANAGER)\n  const filter = 'member.type = \"HUMAN\"';\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.Members.list(\n      parent,\n      {\n        filter: filter,\n        pageSize: 10,\n        pageToken: pageToken,\n      },\n      getHeaderWithAppCredentials(),\n    );\n    // Handle response pages\n    if (responsePage.memberships) {\n      for (const membership of responsePage.memberships) {\n        console.log(membership);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_list_memberships_app_cred]\n\n// [START chat_list_memberships_user_cred]\n/**\n * This sample shows how to list memberships with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.memberships.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction listMembershipsUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const parent = \"spaces/SPACE_NAME\";\n  // Filter membership by type (HUMAN or BOT) or role (ROLE_MEMBER or\n  // ROLE_MANAGER)\n  const filter = 'member.type = \"HUMAN\"';\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.Members.list(parent, {\n      filter: filter,\n      pageSize: 10,\n      pageToken: pageToken,\n    });\n    // Handle response pages\n    if (responsePage.memberships) {\n      for (const membership of responsePage.memberships) {\n        console.log(membership);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_list_memberships_user_cred]\n\n// [START chat_list_messages_user_cred]\n/**\n * This sample shows how to list messages with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction listMessagesUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const parent = \"spaces/SPACE_NAME\";\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.Messages.list(parent, {\n      pageSize: 10,\n      pageToken: pageToken,\n    });\n    // Handle response pages\n    if (responsePage.messages) {\n      for (const message of responsePage.messages) {\n        console.log(message);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_list_messages_user_cred]\n\n// [START chat_list_spaces_app_cred]\n/**\n * This sample shows how to list spaces with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction listSpacesAppCred() {\n  // Initialize request argument(s)\n  // Filter spaces by space type (SPACE or GROUP_CHAT or DIRECT_MESSAGE)\n  const filter = 'space_type = \"SPACE\"';\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.list(\n      {\n        filter: filter,\n        pageSize: 10,\n        pageToken: pageToken,\n      },\n      getHeaderWithAppCredentials(),\n    );\n    // Handle response pages\n    if (responsePage.spaces) {\n      for (const space of responsePage.spaces) {\n        console.log(space);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_list_spaces_app_cred]\n\n// [START chat_list_spaces_user_cred]\n/**\n * This sample shows how to list spaces with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction listSpacesUserCred() {\n  // Initialize request argument(s)\n  // Filter spaces by space type (SPACE or GROUP_CHAT or DIRECT_MESSAGE)\n  const filter = 'space_type = \"SPACE\"';\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.list({\n      filter: filter,\n      pageSize: 10,\n      pageToken: pageToken,\n    });\n    // Handle response pages\n    if (responsePage.spaces) {\n      for (const space of responsePage.spaces) {\n        console.log(space);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_list_spaces_user_cred]\n\n// [START chat_set_up_space_user_cred]\n/**\n * This sample shows how to set up a named space with one initial member with\n * user credential.\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces.create'\n * referenced in the manifest file (appsscript.json).\n */\nfunction setUpSpaceUserCred() {\n  // Initialize request argument(s)\n  const space = {\n    spaceType: \"SPACE\",\n    // TODO(developer): Replace DISPLAY_NAME here\n    displayName: \"DISPLAY_NAME\",\n  };\n  const memberships = [\n    {\n      member: {\n        // TODO(developer): Replace USER_NAME here\n        name: \"users/USER_NAME\",\n        // User type for the membership\n        type: \"HUMAN\",\n      },\n    },\n  ];\n\n  // Make the request\n  const response = Chat.Spaces.setup({\n    space: space,\n    memberships: memberships,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_set_up_space_user_cred]\n\n// [START chat_update_message_app_cred]\n/**\n * This sample shows how to update a message with app credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.bot'\n * used by service accounts.\n */\nfunction updateMessageAppCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n  const message = {\n    text: \"Text updated with app credential!\",\n    cardsV2: [\n      {\n        card: {\n          header: {\n            title: \"Card updated with app credential!\",\n            imageUrl:\n              \"https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/info/default/24px.svg\",\n          },\n        },\n      },\n    ],\n  };\n  // The field paths to update. Separate multiple values with commas or use\n  // `*` to update all field paths.\n  const updateMask = \"text,cardsV2\";\n\n  // Make the request\n  const response = Chat.Spaces.Messages.patch(\n    message,\n    name,\n    {\n      updateMask: updateMask,\n    },\n    getHeaderWithAppCredentials(),\n  );\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_update_message_app_cred]\n\n// [START chat_update_message_user_cred]\n/**\n * This sample shows how to update a message with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.messages'\n * referenced in the manifest file (appsscript.json).\n */\nfunction updateMessageUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME and MESSAGE_NAME here\n  const name = \"spaces/SPACE_NAME/messages/MESSAGE_NAME\";\n  const message = {\n    text: \"Updated with user credential!\",\n  };\n  // The field paths to update. Separate multiple values with commas or use\n  // `*` to update all field paths.\n  const updateMask = \"text\";\n\n  // Make the request\n  const response = Chat.Spaces.Messages.patch(message, name, {\n    updateMask: updateMask,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_update_message_user_cred]\n\n// [START chat_update_space_user_cred]\n/**\n * This sample shows how to update a space with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces'\n * referenced in the manifest file (appsscript.json).\n */\nfunction updateSpaceUserCred() {\n  // Initialize request argument(s)\n  // TODO(developer): Replace SPACE_NAME here\n  const name = \"spaces/SPACE_NAME\";\n  const space = {\n    displayName: \"New space display name\",\n  };\n  // The field paths to update. Separate multiple values with commas or use\n  // `*` to update all field paths.\n  const updateMask = \"displayName\";\n\n  // Make the request\n  const response = Chat.Spaces.patch(space, name, {\n    updateMask: updateMask,\n  });\n\n  // Handle the response\n  console.log(response);\n}\n// [END chat_update_space_user_cred]\n"
  },
  {
    "path": "chat/advanced-service/README.md",
    "content": "# Google Chat API - Advanced Service samples\n\n## Set up\n\n1. Follow the Google Chat app quickstart for Apps Script\n   https://developers.google.com/workspace/chat/quickstart/apps-script-app and\n   open the resulting Apps Script project in a web browser.\n\n1. Override the Apps Script project contents with the files `appsscript.json`,\n   `AppAuthenticationUtils.gs`, and `Main.gs` from this code sample directory.\n\n1. To run samples that use app credentials:\n\n   1. Create a service account. For steps, see\n      [Authenticate as a Google Chat app](https://developers.google.com/workspace/chat/authenticate-authorize-chat-app).\n\n   1. Open `AppAuthenticationUtils.gs` and set the value of the constant `SERVICE_ACCOUNT` to\n      the private key's JSON of the service account that you created in the previous step.\n\n## Run\n\nIn the `Main.gs` file, each function contains a sample that calls a Chat API method\nusing either app or user authentication. To run one of the samples, select the name\nof the function from the dropdown menu and click `Run`.\n"
  },
  {
    "path": "chat/advanced-service/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/chat.spaces\",\n    \"https://www.googleapis.com/auth/chat.spaces.create\",\n    \"https://www.googleapis.com/auth/chat.spaces.readonly\",\n    \"https://www.googleapis.com/auth/chat.memberships\",\n    \"https://www.googleapis.com/auth/chat.memberships.app\",\n    \"https://www.googleapis.com/auth/chat.memberships.readonly\",\n    \"https://www.googleapis.com/auth/chat.messages\",\n    \"https://www.googleapis.com/auth/chat.messages.create\",\n    \"https://www.googleapis.com/auth/chat.messages.readonly\"\n  ],\n  \"chat\": {},\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Chat\",\n        \"version\": \"v1\",\n        \"serviceId\": \"chat\"\n      }\n    ],\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"version\": \"43\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "chat/quickstart/Code.gs",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START chat_quickstart]\n/**\n * This quickstart sample shows how to list spaces with user credential\n *\n * It relies on the OAuth2 scope 'https://www.googleapis.com/auth/chat.spaces.readonly'\n * referenced in the manifest file (appsscript.json).\n */\nfunction listSpaces() {\n  // Initialize request argument(s)\n  // Filter spaces by space type (SPACE or GROUP_CHAT or DIRECT_MESSAGE)\n  const filter = 'space_type = \"SPACE\"';\n\n  // Iterate through the response pages using page tokens\n  let responsePage;\n  let pageToken = null;\n  do {\n    // Request response pages\n    responsePage = Chat.Spaces.list({\n      filter: filter,\n      pageToken: pageToken,\n    });\n    // Handle response pages\n    if (responsePage.spaces) {\n      for (const space of responsePage.spaces) {\n        console.log(space);\n      }\n    }\n    // Update the page token to the next one\n    pageToken = responsePage.nextPageToken;\n  } while (pageToken);\n}\n// [END chat_quickstart]\n"
  },
  {
    "path": "chat/quickstart/README.md",
    "content": "# Google Chat Apps Script Quickstart\n\nComplete the steps described in the [quickstart instructions](\nhttps://developers.google.com/workspace/chat/api/guides/quickstart/apps-script),\nand in about five minutes you'll have a simple Apps Script application\nthat makes requests to the Google Chat API.\n\n## Run\n\nAfter following the quickstart setup instructions, execute the function `listSpaces`\nfrom the Apps Script console.\n"
  },
  {
    "path": "chat/quickstart/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": [\"https://www.googleapis.com/auth/chat.spaces.readonly\"],\n  \"chat\": {},\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Chat\",\n        \"version\": \"v1\",\n        \"serviceId\": \"chat\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "classroom/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_quickstart]\n/**\n * Lists 10 course names and ids.\n */\nfunction listCourses() {\n  /**  here pass pageSize Query parameter as argument to get maximum number of result\n   * @see https://developers.google.com/classroom/reference/rest/v1/courses/list\n   */\n  const optionalArgs = {\n    pageSize: 10,\n    // Use other parameter here if needed\n  };\n  try {\n    // call courses.list() method to list the courses in classroom\n    const response = Classroom.Courses.list(optionalArgs);\n    const courses = response.courses;\n    if (!courses || courses.length === 0) {\n      console.log(\"No courses found.\");\n      return;\n    }\n    // Print the course names and IDs of the courses\n    for (const course of courses) {\n      console.log(\"%s (%s)\", course.name, course.id);\n    }\n  } catch (err) {\n    // TODO (developer)- Handle Courses.list() exception from Classroom API\n    // get errors like PERMISSION_DENIED/INVALID_ARGUMENT/NOT_FOUND\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END classroom_quickstart]\n"
  },
  {
    "path": "classroom/snippets/addAlias.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_add_alias]\n/**\n * Updates the section and room of Google Classroom.\n * @param {string} course_id\n * @see https://developers.google.com/classroom/reference/rest/v1/courses.aliases/create\n */\nfunction addAlias(course_id) {\n  const alias = {\n    alias: \"p:bio_101\",\n  };\n  try {\n    const course_alias = Classroom.Courses.Aliases.create(alias, course_id);\n    console.log(\"%s successfully added as an alias!\", course_alias.alias);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\n      \"Request to add alias %s failed with error %s.\",\n      alias.alias,\n      err.message,\n    );\n  }\n}\n// [END classroom_add_alias]\n"
  },
  {
    "path": "classroom/snippets/courseUpdate.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_update_course]\n/**\n * Updates the section and room of Google Classroom.\n * @param {string} courseId\n * @see https://developers.google.com/classroom/reference/rest/v1/courses/update\n */\nfunction courseUpdate(courseId) {\n  try {\n    // Get the course using course ID\n    let course = Classroom.Courses.get(courseId);\n    course.section = \"Period 3\";\n    course.room = \"302\";\n    // Update the course\n    course = Classroom.Courses.update(course, courseId);\n    console.log('Course \"%s\" updated.', course.name);\n  } catch (e) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed to update the course with error %s\", e.message);\n  }\n}\n// [END classroom_update_course]\n"
  },
  {
    "path": "classroom/snippets/createAlias.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_create_alias]\n/**\n * Creates Course with an alias specified\n */\nfunction createAlias() {\n  let course = {\n    id: \"p:bio_101\",\n    name: \"10th Grade Biology\",\n    section: \"Period 2\",\n    descriptionHeading: \"Welcome to 10th Grade Biology\",\n    description:\n      \"We'll be learning about the structure of living creatures from a combination \" +\n      \"of textbooks, guest lectures, and lab work. Expect to be excited!\",\n    room: \"301\",\n    ownerId: \"me\",\n    courseState: \"PROVISIONED\",\n  };\n  try {\n    // Create the course using course details.\n    course = Classroom.Courses.create(course);\n    console.log(\"Course created: %s (%s)\", course.name, course.id);\n  } catch (err) {\n    // TODO (developer) - Handle Courses.create() exception\n    console.log(\n      \"Failed to create course %s with an error %s\",\n      course.name,\n      err.message,\n    );\n  }\n}\n// [END classroom_create_alias]\n"
  },
  {
    "path": "classroom/snippets/createCourse.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_create_course]\n/**\n * Creates 10th Grade Biology Course.\n * @see https://developers.google.com/classroom/reference/rest/v1/courses/create\n * return {string} Id of created course\n */\nfunction createCourse() {\n  let course = {\n    name: \"10th Grade Biology\",\n    section: \"Period 2\",\n    descriptionHeading: \"Welcome to 10th Grade Biology\",\n    description:\n      \"We'll be learning about the structure of living creatures from a combination \" +\n      \"of textbooks, guest lectures, and lab work. Expect to be excited!\",\n    room: \"301\",\n    ownerId: \"me\",\n    courseState: \"PROVISIONED\",\n  };\n  try {\n    // Create the course using course details.\n    course = Classroom.Courses.create(course);\n    console.log(\"Course created: %s (%s)\", course.name, course.id);\n    return course.id;\n  } catch (err) {\n    // TODO (developer) - Handle Courses.create() exception\n    console.log(\n      \"Failed to create course %s with an error %s\",\n      course.name,\n      err.message,\n    );\n  }\n}\n// [END classroom_create_course]\n"
  },
  {
    "path": "classroom/snippets/getCourse.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_get_course]\n/**\n * Retrieves course by id.\n * @param {string} courseId\n * @see https://developers.google.com/classroom/reference/rest/v1/courses/get\n */\nfunction getCourse(courseId) {\n  try {\n    // Get the course details using course id\n    const course = Classroom.Courses.get(courseId);\n    console.log('Course \"%s\" found. ', course.name);\n  } catch (err) {\n    // TODO (developer) - Handle Courses.get() exception of Handle Classroom API\n    console.log(\n      \"Failed to found course %s with error %s \",\n      courseId,\n      err.message,\n    );\n  }\n}\n// [END classroom_get_course]\n"
  },
  {
    "path": "classroom/snippets/listCourses.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_list_courses]\n/**\n * Lists all course names and ids.\n * @see https://developers.google.com/classroom/reference/rest/v1/courses/list\n */\nfunction listCourses() {\n  let courses = [];\n  const pageToken = null;\n  const optionalArgs = {\n    pageToken: pageToken,\n    pageSize: 100,\n  };\n  try {\n    const response = Classroom.Courses.list(optionalArgs);\n    courses = response.courses;\n    if (courses.length === 0) {\n      console.log(\"No courses found.\");\n      return;\n    }\n    // Print the courses available in classroom\n    console.log(\"Courses:\");\n    for (const course in courses) {\n      console.log(\"%s (%s)\", courses[course].name, courses[course].id);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END classroom_list_courses]\n"
  },
  {
    "path": "classroom/snippets/patchCourse.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START classroom_patch_course]\n/**\n * Updates the section and room of Google Classroom.\n * @param {string} courseId\n * @see https://developers.google.com/classroom/reference/rest/v1/courses/patch\n */\nfunction coursePatch(courseId) {\n  const course = {\n    section: \"Period 3\",\n    room: \"302\",\n  };\n  const options = {\n    updateMask: \"section,room\",\n  };\n  // Update section and room in course.\n  const updatedCourse = Classroom.Courses.patch(course, courseId, options);\n  console.log(`Course \"${updatedCourse.name}\" updated.`);\n}\n// [END classroom_patch_course]\n"
  },
  {
    "path": "classroom/snippets/test_classroom_snippets.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests createCourse function of createCourse.gs\n * @return {string} courseId course id of created course\n */\nfunction itShouldCreateCourse() {\n  console.log(\"> itShouldCreateCourse\");\n  const courseId = createCourse();\n  return courseId;\n}\n\n/**\n * Tests getCourse function of getCourse.gs\n * @param {string} courseId course id\n */\nfunction itShouldGetCourse(courseId) {\n  console.log(\"> itShouldGetCourse\");\n  getCourse(courseId);\n}\n\n/**\n * Tests createAlias function of createAlias.gs\n */\nfunction itShouldCreateAlias() {\n  console.log(\"> itShouldCreateAlias\");\n  createAlias();\n}\n\n/**\n * Tests addAlias function of addAlias.gs\n * @param {string} courseId course id\n */\nfunction itShouldAddAlias(courseId) {\n  console.log(\"> itShouldAddAlias\");\n  addAlias(courseId);\n}\n\n/**\n * Tests courseUpdate function of courseUpdate.gs\n * @param {string} courseId course id\n */\nfunction itShouldUpdateCourse(courseId) {\n  console.log(\"> itShouldUpdateCourse\");\n  courseUpdate(courseId);\n}\n\n/**\n * Tests coursePatch function of patchCourse.gs\n * @param {string} courseId course id\n */\nfunction itShouldPatchCourse(courseId) {\n  console.log(\"> itShouldPatchCourse\");\n  coursePatch(courseId);\n}\n\n/**\n * Tests listCourses function of listCourses.gs\n */\nfunction itShouldListCourses() {\n  console.log(\"> itShouldListCourses\");\n  listCourses();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  const courseId = itShouldCreateCourse();\n  itShouldGetCourse(courseId);\n  itShouldCreateAlias();\n  itShouldAddAlias(courseId);\n  itShouldUpdateCourse(courseId);\n  itShouldPatchCourse(courseId);\n  itShouldListCourses();\n}\n"
  },
  {
    "path": "data-studio/appsscript.json",
    "content": "{\n  \"dataStudio\": {\n    \"name\": \"Nucleus by Hooli\",\n    \"company\": \"Hooli Inc.\",\n    \"companyUrl\": \"https://hooli.xyz\",\n    \"logoUrl\": \"https://hooli.xyz/middle-out-optimized/nucleus/logo.png\",\n    \"addonUrl\": \"https://hooli.xyz/data-studio-connector\",\n    \"supportUrl\": \"https://hooli.xyz/data-studio-connector/support\",\n    \"description\": \"Nucleus by Hooli connector lets you connect to your data in Data Studio using Nucleus middle out optimization. You will need an account on hooli.xyz to use this connector. Create your account at https://hooli.xyz/signup\",\n    \"shortDescription\": \"Connect to your data using Nucleus middle out optimization\",\n    \"privacyPolicyUrl\": \"https://hooli.xyz/privacy\",\n    \"termsOfServiceUrl\": \"https://hooli.xyz/tos\",\n    \"authType\": [\"NONE\"],\n    \"feeType\": [\"PAID\"],\n    \"sources\": [\n      \"HOOLI_CHAT_LOG\",\n      \"ENDFRAME_SERVER_STREAM\",\n      \"RETINABYTE_USER_ANALYTICS\"\n    ],\n    \"templates\": {\n      \"default\": \"872223s89f5fdkjnd983kjf\"\n    }\n  },\n  \"urlFetchWhitelist\": [\"https://api.hooli.xyz/\", \"https://hooli.xyz/\"]\n}\n"
  },
  {
    "path": "data-studio/appsscript2.json",
    "content": "{\n  \"dataStudio\": {\n    \"name\": \"npm Downloads - Build Guide\",\n    \"logoUrl\": \"https://raw.githubusercontent.com/npm/logos/master/%22npm%22%20lockup/npm-logo-simplifed-with-white-space.png\",\n    \"company\": \"Build Guide User\",\n    \"companyUrl\": \"https://developers.google.com/datastudio/\",\n    \"addonUrl\": \"https://github.com/google/datastudio/tree/master/community-connectors/npm-downloads\",\n    \"supportUrl\": \"https://github.com/google/datastudio/issues\",\n    \"description\": \"Get npm package download counts.\",\n    \"sources\": [\"npm\"]\n  }\n}\n"
  },
  {
    "path": "data-studio/auth.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_get_auth_type_oauth2]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypeOAuth2() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc.newAuthTypeResponse().setAuthType(cc.AuthType.OAUTH2).build();\n}\n// [END apps_script_data_studio_get_auth_type_oauth2]\n\n// [START apps_script_data_studio_get_auth_type_path_user_pass]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypePathUserPass() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc\n    .newAuthTypeResponse()\n    .setAuthType(cc.AuthType.PATH_USER_PASS)\n    .setHelpUrl(\"https://www.example.org/connector-auth-help\")\n    .build();\n}\n// [END apps_script_data_studio_get_auth_type_path_user_pass]\n\n// [START apps_script_data_studio_get_auth_type_user_pass]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypeUserPass() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc\n    .newAuthTypeResponse()\n    .setAuthType(cc.AuthType.USER_PASS)\n    .setHelpUrl(\"https://www.example.org/connector-auth-help\")\n    .build();\n}\n// [END apps_script_data_studio_get_auth_type_user_pass]\n\n// [START apps_script_data_studio_get_auth_type_user_token]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypeUserToken() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc\n    .newAuthTypeResponse()\n    .setAuthType(cc.AuthType.USER_TOKEN)\n    .setHelpUrl(\"https://www.example.org/connector-auth-help\")\n    .build();\n}\n// [END apps_script_data_studio_get_auth_type_user_token]\n\n// [START apps_script_data_studio_get_auth_type_key]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypeKey() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc\n    .newAuthTypeResponse()\n    .setAuthType(cc.AuthType.KEY)\n    .setHelpUrl(\"https://www.example.org/connector-auth-help\")\n    .build();\n}\n// [END apps_script_data_studio_get_auth_type_key]\n\n// [START apps_script_data_studio_get_auth_type_none]\n/**\n * Returns the Auth Type of this connector.\n * @return {object} The Auth type.\n */\nfunction getAuthTypeNone() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc.newAuthTypeResponse().setAuthType(cc.AuthType.NONE).build();\n}\n// [END apps_script_data_studio_get_auth_type_none]\n\n// [START apps_script_data_studio_auth_reset_oauth2]\n/**\n * Resets the auth service.\n */\nfunction resetAuthOAuth2() {\n  getOAuthService().reset();\n}\n// [END apps_script_data_studio_auth_reset_oauth2]\n\n// [START apps_script_data_studio_auth_reset_path_user]\n/**\n * Resets the auth service.\n */\nfunction resetAuthPathUser() {\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.deleteProperty(\"dscc.path\");\n  userProperties.deleteProperty(\"dscc.username\");\n  userProperties.deleteProperty(\"dscc.password\");\n}\n// [END apps_script_data_studio_auth_reset_path_user]\n\n// [START apps_script_data_studio_auth_reset_user]\n/**\n * Resets the auth service.\n */\nfunction resetAuthUser() {\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.deleteProperty(\"dscc.username\");\n  userProperties.deleteProperty(\"dscc.password\");\n}\n// [END apps_script_data_studio_auth_reset_user]\n\n// [START apps_script_data_studio_auth_reset_user_token]\n/**\n * Resets the auth service.\n */\nfunction resetAuthUserToken() {\n  const userTokenProperties = PropertiesService.getUserProperties();\n  userTokenProperties.deleteProperty(\"dscc.username\");\n  userTokenProperties.deleteProperty(\"dscc.password\");\n}\n// [END apps_script_data_studio_auth_reset_user_token]\n\n// [START apps_script_data_studio_auth_reset_key]\n/**\n * Resets the auth service.\n */\nfunction resetAuthKey() {\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.deleteProperty(\"dscc.key\");\n}\n// [END apps_script_data_studio_auth_reset_key]\n\n// [START apps_script_data_studio_auth_valid_oauth2]\n/**\n * Returns true if the auth service has access.\n * @return {boolean} True if the auth service has access.\n */\nfunction isAuthValidOAuth2() {\n  return getOAuthService().hasAccess();\n}\n// [END apps_script_data_studio_auth_valid_oauth2]\n\n// [START apps_script_data_studio_auth_valid_path_user_pass]\n/**\n * Returns true if the auth service has access.\n * @return {boolean} True if the auth service has access.\n */\nfunction isAuthValidPathUserPass() {\n  const userProperties = PropertiesService.getUserProperties();\n  const path = userProperties.getProperty(\"dscc.path\");\n  const userName = userProperties.getProperty(\"dscc.username\");\n  const password = userProperties.getProperty(\"dscc.password\");\n  // This assumes you have a validateCredentials function that\n  // can validate if the userName and password are correct.\n  return validateCredentials(path, userName, password);\n}\n// [END apps_script_data_studio_auth_valid_path_user_pass]\n\n// [START apps_script_data_studio_auth_valid_user_pass]\n/**\n * Returns true if the auth service has access.\n * @return {boolean} True if the auth service has access.\n */\nfunction isAuthValidUserPass() {\n  const userProperties = PropertiesService.getUserProperties();\n  const userName = userProperties.getProperty(\"dscc.username\");\n  const password = userProperties.getProperty(\"dscc.password\");\n  // This assumes you have a validateCredentials function that\n  // can validate if the userName and password are correct.\n  return validateCredentials(userName, password);\n}\n// [END apps_script_data_studio_auth_valid_user_pass]\n\n// [START apps_script_data_studio_auth_valid_user_token]\n/**\n * Returns true if the auth service has access.\n * @return {boolean} True if the auth service has access.\n */\nfunction isAuthValidUserToken() {\n  const userProperties = PropertiesService.getUserProperties();\n  const userName = userProperties.getProperty(\"dscc.username\");\n  const token = userProperties.getProperty(\"dscc.token\");\n  // This assumes you have a validateCredentials function that\n  // can validate if the userName and token are correct.\n  return validateCredentials(userName, token);\n}\n// [END apps_script_data_studio_auth_valid_user_token]\n\n// [START apps_script_data_studio_auth_valid_key]\n/**\n * Returns true if the auth service has access.\n * @return {boolean} True if the auth service has access.\n */\nfunction isAuthValidKey() {\n  const userProperties = PropertiesService.getUserProperties();\n  const key = userProperties.getProperty(\"dscc.key\");\n  // This assumes you have a validateKey function that can validate\n  // if the key is valid.\n  return validateKey(key);\n}\n// [END apps_script_data_studio_auth_valid_key]\n\n// [START apps_script_data_studio_auth_library]\n/**\n * Returns the configured OAuth Service.\n * @return {Service} The OAuth Service\n */\nfunction getOAuthService() {\n  return OAuth2.createService(\"exampleService\")\n    .setAuthorizationBaseUrl(\"...\")\n    .setTokenUrl(\"...\")\n    .setClientId(\"...\")\n    .setClientSecret(\"...\")\n    .setPropertyStore(PropertiesService.getUserProperties())\n    .setCallbackFunction(\"authCallback\")\n    .setScope(\"...\");\n}\n// [END apps_script_data_studio_auth_library]\n\n// [START apps_script_data_studio_auth_callback]\n/**\n * The OAuth callback.\n * @param {object} request The request data received from the OAuth flow.\n * @return {HtmlOutput} The HTML output to show to the user.\n */\nfunction authCallback(request) {\n  const authorized = getOAuthService().handleCallback(request);\n  if (authorized) {\n    return HtmlService.createHtmlOutput(\"Success! You can close this tab.\");\n  }\n  return HtmlService.createHtmlOutput(\"Denied. You can close this tab\");\n}\n// [END apps_script_data_studio_auth_callback]\n\n// [START apps_script_data_studio_auth_urls]\n/**\n * Gets the 3P authorization URL.\n * @return {string} The authorization URL.\n * @see https://developers.google.com/apps-script/reference/script/authorization-info\n */\nfunction get3PAuthorizationUrls() {\n  return getOAuthService().getAuthorizationUrl();\n}\n// [END apps_script_data_studio_auth_urls]\n\n// [START apps_script_data_studio_auth_set_credentials_path_user_pass]\n/**\n * Sets the credentials.\n * @param {Request} request The set credentials request.\n * @return {object} An object with an errorCode.\n */\nfunction setCredentialsPathUserPass(request) {\n  const creds = request.userPass;\n  const path = creds.path;\n  const username = creds.username;\n  const password = creds.password;\n\n  // Optional\n  // Check if the provided path, username and password are valid through\n  // a call to your service. You would have to have a `checkForValidCreds`\n  // function defined for this to work.\n  const validCreds = checkForValidCreds(path, username, password);\n  if (!validCreds) {\n    return {\n      errorCode: \"INVALID_CREDENTIALS\",\n    };\n  }\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.setProperty(\"dscc.path\", path);\n  userProperties.setProperty(\"dscc.username\", username);\n  userProperties.setProperty(\"dscc.password\", password);\n  return {\n    errorCode: \"NONE\",\n  };\n}\n// [END apps_script_data_studio_auth_set_credentials_path_user_pass]\n\n// [START apps_script_data_studio_auth_set_credentials_user_pass]\n/**\n * Sets the credentials.\n * @param {Request} request The set credentials request.\n * @return {object} An object with an errorCode.\n */\nfunction setCredentialsUserPass(request) {\n  const creds = request.userPass;\n  const username = creds.username;\n  const password = creds.password;\n\n  // Optional\n  // Check if the provided username and password are valid through a\n  // call to your service. You would have to have a `checkForValidCreds`\n  // function defined for this to work.\n  const validCreds = checkForValidCreds(username, password);\n  if (!validCreds) {\n    return {\n      errorCode: \"INVALID_CREDENTIALS\",\n    };\n  }\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.setProperty(\"dscc.username\", username);\n  userProperties.setProperty(\"dscc.password\", password);\n  return {\n    errorCode: \"NONE\",\n  };\n}\n// [END apps_script_data_studio_auth_set_credentials_user_pass]\n\n// [START apps_script_data_studio_auth_set_credentials_user_token]\n/**\n * Sets the credentials.\n * @param {Request} request The set credentials request.\n * @return {object} An object with an errorCode.\n */\nfunction setCredentialsUserToken(request) {\n  const creds = request.userToken;\n  const username = creds.username;\n  const token = creds.token;\n\n  // Optional\n  // Check if the provided username and token are valid through a\n  // call to your service. You would have to have a `checkForValidCreds`\n  // function defined for this to work.\n  const validCreds = checkForValidCreds(username, token);\n  if (!validCreds) {\n    return {\n      errorCode: \"INVALID_CREDENTIALS\",\n    };\n  }\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.setProperty(\"dscc.username\", username);\n  userProperties.setProperty(\"dscc.token\", token);\n  return {\n    errorCode: \"NONE\",\n  };\n}\n// [END apps_script_data_studio_auth_set_credentials_user_token]\n\n// [START apps_script_data_studio_auth_set_credentials_key]\n/**\n * Sets the credentials.\n * @param {Request} request The set credentials request.\n * @return {object} An object with an errorCode.\n */\nfunction setCredentialsKey(request) {\n  const key = request.key;\n\n  // Optional\n  // Check if the provided key is valid through a call to your service.\n  // You would have to have a `checkForValidKey` function defined for\n  // this to work.\n  const validKey = checkForValidKey(key);\n  if (!validKey) {\n    return {\n      errorCode: \"INVALID_CREDENTIALS\",\n    };\n  }\n  const userProperties = PropertiesService.getUserProperties();\n  userProperties.setProperty(\"dscc.key\", key);\n  return {\n    errorCode: \"NONE\",\n  };\n}\n// [END apps_script_data_studio_auth_set_credentials_key]\n"
  },
  {
    "path": "data-studio/build.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_build_get_config]\n/**\n * Builds the Community Connector config.\n * @return {Config} The Community Connector config.\n * @see https://developers.google.com/apps-script/reference/data-studio/config\n */\nfunction getConfig() {\n  const cc = DataStudioApp.createCommunityConnector();\n  const config = cc.getConfig();\n\n  config\n    .newInfo()\n    .setId(\"instructions\")\n    .setText(\"Enter npm package names to fetch their download count.\");\n\n  config\n    .newTextInput()\n    .setId(\"package\")\n    .setName(\"Enter a single package name.\")\n    .setHelpText(\"for example, googleapis or lighthouse\")\n    .setPlaceholder(\"googleapis\")\n    .setAllowOverride(true);\n\n  config.setDateRangeRequired(true);\n\n  return config.build();\n}\n// [END apps_script_data_studio_build_get_config]\n\n// [START apps_script_data_studio_build_get_fields]\n/**\n * Builds the Community Connector fields object.\n * @return {Fields} The Community Connector fields.\n * @see https://developers.google.com/apps-script/reference/data-studio/fields\n */\nfunction getFields() {\n  const cc = DataStudioApp.createCommunityConnector();\n  const fields = cc.getFields();\n  const types = cc.FieldType;\n  const aggregations = cc.AggregationType;\n\n  fields\n    .newDimension()\n    .setId(\"packageName\")\n    .setName(\"Package Name\")\n    .setType(types.TEXT);\n\n  fields\n    .newDimension()\n    .setId(\"day\")\n    .setName(\"Day\")\n    .setType(types.YEAR_MONTH_DAY);\n\n  fields\n    .newMetric()\n    .setId(\"downloads\")\n    .setName(\"Downloads\")\n    .setType(types.NUMBER)\n    .setAggregation(aggregations.SUM);\n\n  return fields;\n}\n\n/**\n * Builds the Community Connector schema.\n * @param {object} request The request.\n * @return {object} The schema.\n */\nfunction getSchema(request) {\n  const fields = getFields().build();\n  return { schema: fields };\n}\n// [END apps_script_data_studio_build_get_fields]\n\n// [START apps_script_data_studio_build_get_data]\n/**\n * Constructs an object with values as rows.\n * @param {Fields} requestedFields The requested fields.\n * @param {object[]} response The response.\n * @param {string} packageName The package name.\n * @return {object} An object containing rows with values.\n */\nfunction responseToRows(requestedFields, response, packageName) {\n  // Transform parsed data and filter for requested fields\n  return response.map((dailyDownload) => {\n    const row = [];\n    for (const field of requestedFields.asArray()) {\n      switch (field.getId()) {\n        case \"day\":\n          row.push(dailyDownload.day.replace(/-/g, \"\"));\n          break;\n        case \"downloads\":\n          row.push(dailyDownload.downloads);\n          break;\n        case \"packageName\":\n          row.push(packageName);\n          break;\n        default:\n          row.push(\"\");\n      }\n    }\n    return { values: row };\n  });\n}\n\n/**\n * Gets the data for the community connector\n * @param {object} request The request.\n * @return {object} The data.\n */\nfunction getData(request) {\n  const requestedFieldIds = request.fields.map((field) => field.name);\n  const requestedFields = getFields().forIds(requestedFieldIds);\n\n  // Fetch and parse data from API\n  const url = [\n    \"https://api.npmjs.org/downloads/range/\",\n    request.dateRange.startDate,\n    \":\",\n    request.dateRange.endDate,\n    \"/\",\n    request.configParams.package,\n  ];\n  const response = UrlFetchApp.fetch(url.join(\"\"));\n  const parsedResponse = JSON.parse(response).downloads;\n  const rows = responseToRows(\n    requestedFields,\n    parsedResponse,\n    request.configParams.package,\n  );\n\n  return {\n    schema: requestedFields.build(),\n    rows: rows,\n  };\n}\n// [END apps_script_data_studio_build_get_data]\n\n// [START apps_script_data_studio_build_get_auth_type]\n/**\n * Gets the Auth type.\n * @return {object} The auth type.\n */\nfunction getAuthType() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc.newAuthTypeResponse().setAuthType(cc.AuthType.NONE).build();\n}\n// [END apps_script_data_studio_build_get_auth_type]\n"
  },
  {
    "path": "data-studio/caas.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_caas_example]\nconst sqlString =\n  \"\" +\n  \"SELECT \" +\n  \"  _TABLE_SUFFIX AS yyyymm, \" +\n  \"  ROUND(SUM(IF(fcp.start < @fast_fcp, fcp.density, 0)), 4) AS fast_fcp, \" +\n  \"  ROUND(SUM(IF(fcp.start >= 1000 AND fcp.start < 3000, fcp.density, 0)), 4) AS avg_fcp, \" +\n  \"  ROUND(SUM(IF(fcp.start >= 3000, fcp.density, 0)), 4) AS slow_fcp \" +\n  \"FROM \" +\n  \"  `chrome-ux-report.all.*`, \" +\n  \"  UNNEST(first_contentful_paint.histogram.bin) AS fcp \" +\n  \"WHERE \" +\n  \"  origin = @url \" +\n  \"GROUP BY \" +\n  \"  yyyymm \" +\n  \"ORDER BY \" +\n  \"  yyyymm \";\n\n/**\n * Gets the config.\n * @param {object} request The request.\n * @return {Config} The Community Connector config.\n */\nfunction getConfig(request) {\n  const cc = DataStudioApp.createCommunityConnector();\n  const config = cc.getConfig();\n\n  config\n    .newTextInput()\n    .setId(\"projectId\")\n    .setName(\"BigQuery Billing Project ID\")\n    .setPlaceholder(\"556727765207\");\n\n  config\n    .newTextInput()\n    .setId(\"url\")\n    .setName(\"Enter your url\")\n    .setAllowOverride(true)\n    .setPlaceholder(\"www.example.com\");\n\n  config.setDateRangeRequired(true);\n\n  return config.build();\n}\n\n/**\n * Gets the fields.\n * @param {object} request The request.\n * @return {Fields} The Community Connector fields.\n */\nfunction getFields() {\n  const cc = DataStudioApp.createCommunityConnector();\n  const fields = cc.getFields();\n  const types = cc.FieldType;\n\n  fields\n    .newDimension()\n    .setId(\"yyyymm\")\n    .setName(\"yyyymm\")\n    .setType(types.YEAR_MONTH);\n\n  fields\n    .newMetric()\n    .setId(\"fast_fcp\")\n    .setName(\"fast_fcp\")\n    .setType(types.NUMBER);\n\n  fields.newMetric().setId(\"avg_fcp\").setName(\"avg_fcp\").setType(types.NUMBER);\n\n  fields\n    .newMetric()\n    .setId(\"slow_fcp\")\n    .setName(\"slow_fcp\")\n    .setType(types.NUMBER);\n\n  return fields;\n}\n\n/**\n * Gets the schema.\n * @param {object} request\n * @return {object} The connector's schema.\n */\nfunction getSchema(request) {\n  return {\n    schema: getFields().build(),\n  };\n}\n\n/**\n * Gets the connector's data.\n * @param {object} request The request.\n * @return {object} The data response.\n */\nfunction getData(request) {\n  const url = request.configParams?.url;\n  const projectId = request.configParams?.projectId;\n  const authToken = ScriptApp.getOAuthToken();\n  const response = {\n    dataConfig: {\n      type: \"BIGQUERY\",\n      bigQueryConnectorConfig: {\n        billingProjectId: projectId,\n        query: sqlString,\n        useStandardSql: true,\n        queryParameters: [\n          {\n            name: \"url\",\n            parameterType: {\n              type: \"STRING\",\n            },\n            parameterValue: {\n              value: url,\n            },\n          },\n          {\n            name: \"fast_fcp\",\n            parameterType: {\n              type: \"INT64\",\n            },\n            parameterValue: {\n              value: `${1000}`,\n            },\n          },\n        ],\n      },\n    },\n    authConfig: {\n      accessToken: authToken,\n    },\n  };\n  return response;\n}\n\n/**\n * Gets the auth type.\n * @return {object} The auth type.\n */\nfunction getAuthType() {\n  const cc = DataStudioApp.createCommunityConnector();\n  return cc.newAuthTypeResponse().setAuthType(cc.AuthType.NONE).build();\n}\n// [END apps_script_data_studio_caas_example]\n"
  },
  {
    "path": "data-studio/data-source.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_params]\nconst configParams = [\n  {\n    type: \"TEXTINPUT\",\n    name: \"ZipCode\",\n    displayName: \"ZIP Code\",\n    parameterControl: {\n      allowOverride: true,\n    },\n  },\n  {\n    type: \"SELECT_SINGLE\",\n    name: \"units\",\n    displayName: \"Units\",\n    parameterControl: {\n      allowOverride: true,\n    },\n    options: [\n      {\n        label: \"Metric\",\n        value: \"metric\",\n      },\n      {\n        label: \"Imperial\",\n        value: \"imperial\",\n      },\n      {\n        label: \"Kelvin\",\n        value: \"kelvin\",\n      },\n    ],\n  },\n  {\n    type: \"TEXTINPUT\",\n    name: \"Days\",\n    displayName: \"Days to forecast\",\n  },\n];\n// [END apps_script_data_studio_params]\n"
  },
  {
    "path": "data-studio/errors.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_error_ds_user]\nfunction showErrorToAllUsers() {\n  try {\n    // Code that might fail.\n    throw new Error(\"Something went wrong\");\n  } catch (e) {\n    throw new Error(\"DS_USER:This will be shown to admin & non-admin.\");\n  }\n}\n\nfunction showErrorToAdminUsers() {\n  // Only admin users will see the following error.\n  try {\n    // Code that might fail.\n    throw new Error(\"Something went wrong\");\n  } catch (e) {\n    throw new Error(\"This message will only be shown to admin users\");\n  }\n}\n// [END apps_script_data_studio_error_ds_user]\n\n// [START apps_script_data_studio_error_helper]\n/**\n * Throws an error that complies with the community connector spec.\n * @param {string} message The error message.\n * @param {boolean} userSafe Determines whether this message is safe to show\n *     to non-admin users of the connector. true to show the message, false\n *     otherwise. false by default.\n */\nfunction throwConnectorError(message, userSafe) {\n  let safeMessage = message;\n  const isUserSafe =\n    typeof userSafe !== \"undefined\" && typeof userSafe === \"boolean\"\n      ? userSafe\n      : false;\n  if (isUserSafe) {\n    safeMessage = `DS_USER:${message}`;\n  }\n\n  throw new Error(safeMessage);\n}\n// [END apps_script_data_studio_error_helper]\n\n// [START apps_script_data_studio_error_logging]\n/**\n * Log an error that complies with the community connector spec.\n * @param {Error} originalError The original error that occurred.\n * @param {string} message Additional details about the error to include in\n *    the log entry.\n */\nfunction logConnectorError(originalError, message) {\n  const logEntry = [\n    \"Original error (Message): \",\n    originalError,\n    \"(\",\n    message,\n    \")\",\n  ];\n  console.error(logEntry.join(\"\")); // Log to Stackdriver.\n}\n// [END apps_script_data_studio_error_logging]\n\n// [START apps_script_data_studio_error_error]\nfunction showErrorToNonAdminUsers() {\n  // Error message that will be shown to a non-admin users.\n  try {\n    // Code that might fail.\n    throw new Error(\"Something went wrong\");\n  } catch (e) {\n    logConnectorError(e, \"quota_hour_exceeded\"); // Log to Stackdriver.\n    throwConnectorError(\n      \"You've exceeded the hourly quota. Try again later.\",\n      true,\n    );\n  }\n}\n// [END apps_script_data_studio_error_error]\n"
  },
  {
    "path": "data-studio/links.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_links]\n// These variables should be filled in as necessary for your connector.\nlet configJSON;\nlet templateId;\nlet deploymentId;\n\nconst params = [];\n\nconst jsonString = JSON.stringify(configJSON);\nconst encoded = encodeURIComponent(jsonString);\nparams.push(`connectorConfig=${encoded}`);\n\nparams.push(`reportTemplateId=${templateId}`);\n\nparams.push(`connectorId=${deploymentId}`);\n\nconst joinedParams = params.join(\"&\");\nconst URL = `https://datastudio.google.com/datasources/create?${joinedParams}`;\n// [END apps_script_data_studio_links]\n"
  },
  {
    "path": "data-studio/manifest.gs",
    "content": ""
  },
  {
    "path": "data-studio/semantics.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_data_studio_manual]\nconst schema = [\n  {\n    name: \"Income\",\n    label: \"Income (in USD)\",\n    dataType: \"NUMBER\",\n    semantics: {\n      conceptType: \"METRIC\",\n      semanticGroup: \"CURRENCY\",\n      semanticType: \"CURRENCY_USD\",\n    },\n  },\n  {\n    name: \"Filing Year\",\n    label: \"Year in which you filed the taxes.\",\n    dataType: \"STRING\",\n    semantics: {\n      conceptType: \"METRIC\",\n      semanticGroup: \"DATE_OR_TIME\",\n      semanticType: \"YEAR\",\n    },\n  },\n];\n// [END apps_script_data_studio_manual]\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Google Docs Add-ons\n\n## Cursor Inspector\n\nThis add-on allows you to inspect the current state of the cursor or selection within a document. The information is presented in a sidebar and updates automatically every few seconds.\n\n## Translate\n\nThis add-on allows you to translate selected text from a set of source languages to a set of destination languages.\n"
  },
  {
    "path": "docs/cursorInspector/README.md",
    "content": "# Cursor Inspector\n\nCursor Inspector is a sample script for Google Docs that allows you to inspect\nthe current state of the cursor or selection within a document. The information\nis presented in a sidebar and updates automatically every few seconds. The data\npresented corresponds with the\n[`Cursor`](https://developers.google.com/apps-script/reference/document/cursor)\nand\n[`Selection`](https://developers.google.com/apps-script/reference/document/selection)\nclasses of the API.\n\n![Cursor Inspector screenshot](screenshot.png)\n\n## Try it out\n\nFor your convience we have deployed the script into a Google Docs\n[document](https://docs.google.com/document/d/1v6S7IkDL_YIaVn1rBcVbqFr3rbNUX9_kLfFc00WTtx8/view)\nthat you can copy and use. Follow the instructions in the document to get\nstarted.\n"
  },
  {
    "path": "docs/cursorInspector/cursorInspector.gs",
    "content": "// Copyright 2013 Google Inc. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n/**\n * Runs when the document is opened.\n */\nfunction onOpen() {\n  DocumentApp.getUi()\n    .createMenu(\"Inspector\")\n    .addItem(\"Show sidebar\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Show the sidebar.\n */\nfunction showSidebar() {\n  DocumentApp.getUi().showSidebar(\n    HtmlService.createTemplateFromFile(\"Sidebar\")\n      .evaluate()\n      .setTitle(\"Cursor Inspector\")\n      .setWidth(350),\n  );\n}\n\n/**\n * Returns the contents of an HTML file.\n * @param {string} file The name of the file to retrieve.\n * @return {string} The content of the file.\n */\nfunction include(file) {\n  return HtmlService.createTemplateFromFile(file).evaluate().getContent();\n}\n\n/**\n * Gets the current cursor and selector information for the document.\n * @return {Object} The infomration.\n */\nfunction getDocumentInfo() {\n  const document = DocumentApp.getActiveDocument();\n  const cursor = document.getCursor();\n  const selection = document.getSelection();\n  const result = {};\n  if (cursor) {\n    result.cursor = {\n      element: getElementInfo(cursor.getElement()),\n      offset: cursor.getOffset(),\n      surroundingText: cursor.getSurroundingText().getText(),\n      surroundingTextOffset: cursor.getSurroundingTextOffset(),\n    };\n  }\n  if (selection) {\n    result.selection = {\n      selectedElements: selection\n        .getSelectedElements()\n        .map((selectedElement) => ({\n          element: getElementInfo(selectedElement.getElement()),\n          partial: selectedElement.isPartial(),\n          startOffset: selectedElement.getStartOffset(),\n          endOffsetInclusive: selectedElement.getEndOffsetInclusive(),\n        })),\n    };\n  }\n  return result;\n}\n\n/**\n * Gets information about a given element.\n * @param {Element} element The element.\n * @return {Object} The information.\n */\nfunction getElementInfo(element) {\n  return {\n    type: String(element.getType()),\n  };\n}\n"
  },
  {
    "path": "docs/cursorInspector/sidebar.css.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!-- External Libraries -->\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://code.jquery.com/ui/1.10.0/themes/smoothness/jquery-ui.css\" />\n\n<!-- Custom Styles -->\n<style>\nhtml {\n  height: 100%;\n}\nbody {\n  font-family: arial, sans-serif;\n  font-size: 13px;\n  height: 100%;\n  padding: 10px 12px;\n}\ntable {\n  border-spacing: 0;\n}\ntd {\n  vertical-align: top;\n}\nfieldset {\n  margin-bottom: 1em;\n}\ninput {\n  width: 160px;\n}\n\n.na {\n  color: gray;\n  text-align: center;\n}\n\n#results {\n  height: 99%;\n  overflow: auto;\n}\n#selection table {\n  width: 100%;\n}\n#selection th, #selection td {\n  border: 1px solid gray;\n}\n#error {\n  background-color: red;\n  color: white;\n  display: none;\n  padding: 1em;\n}\n#loading {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  /* background-color: rgba(200,200,200,0.6);  */\n  background-color: rgba(200,200,200,1);\n  font-weight: bold;\n}\n#loading-content {\n  position: relative;\n  top: 50%;\n  margin: 0 auto;\n  width: 100px;\n  height: 50px;\n  line-height: 50px;\n  vertical-align: middle;\n  background-color: white;\n  border: 1px solid black;\n  text-align: center;\n}\n</style>\n"
  },
  {
    "path": "docs/cursorInspector/sidebar.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<?!= include('sidebar.css') ?>\n\n<div id=\"loading\">\n  <div id=\"loading-content\">Loading ...</div>\n</div>\n\n<div id=\"error\"></div>\n\n<div id=\"results\">\n  <fieldset id=\"cursor\">\n    <legend>Cursor</legend>\n    <table>\n      <tr>\n        <td><label>Element Type:</label></td>\n        <td><input id=\"element-type\" type=\"text\" /></td>\n      </tr>\n      <tr>\n        <td><label>Offset:</label></td>\n        <td><input id=\"offset\" type=\"text\" /></td>\n      </tr>\n      <tr>\n        <td><label>Surrounding Text:</label></td>\n        <td><input id=\"surrounding-text\" type=\"text\" /></td>\n      </tr>\n      <tr>\n        <td><label>Surrounding Text Offset:</label></td>\n        <td><input id=\"surrounding-text-offset\" type=\"text\" /></td>\n      </tr>\n    </table>\n  </fieldset>\n\n  <fieldset id=\"selection\">\n    <legend>Selection</legend>\n    <table>\n      <thead>\n        <tr>\n          <th>Element</th>\n          <th>Partial</th>\n          <th>Start</th>\n          <th>End</th>\n        </tr>\n      </thead>\n      <tbody></tbody>\n    </table>\n  </fieldset>\n\n  <p>\n    Automatically refreshed every few seconds.<br/>\n    Last updated <strong id=\"lastupdated\"></strong>.\n  </p>\n  <button id=\"toggle\" onclick=\"toggleRefresh();\" data-stoptext=\"◾ Stop\" data-resumetext=\"▶ Resume\">◾ Stop</button>\n</div>\n\n<?!= include('sidebar.js') ?>\n"
  },
  {
    "path": "docs/cursorInspector/sidebar.js.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!-- Libraries -->\n<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js\"></script>\n<script src=\"https://code.jquery.com/ui/1.10.0/jquery-ui.min.js\"></script>\n\n<!-- Custom JavaScript -->\n<script>\n// Constants.\nvar REFRESH_WAIT_SECONDS = 1;\nvar INACTIVITY_TIMEOUT_MINUTES = 2;\n\n// Global variables.\nvar lastResult;\nvar refreshOn = true;\nvar lastChangedTime;\nvar timeoutId;\n\n// On page load.\n$(function() {\n  $('#loading').hide();\n  refreshState();\n});\n\n/**\n * Refreshes the state information in the sidebar.\n */\nfunction refreshState() {\n  google.script.run.withFailureHandler(function(error) {\n    showError(error);\n    tick(null);\n  }).withSuccessHandler(function(result) {\n    hideError();\n    if (!lastResult || !jsonEquals(result.cursor, lastResult.cursor)) {\n      updateCursor(result.cursor);\n    }\n    if (!lastResult || !jsonEquals(result.selection, lastResult.selection)) {\n      updateSelection(result.selection);\n    }\n    tick(result);\n  }).getDocumentInfo();\n}\n\n/**\n * Updates the cursor information in the sidebar.\n * @param {Object} cursor The cursor information.\n */\nfunction updateCursor(cursor) {\n  if (cursor) {\n    $('#cursor input').removeAttr('disabled');\n    updateElement('element-type', cursor.element.type);\n    updateElement('offset', cursor.offset);\n    updateElement('surrounding-text', cursor.surroundingText);\n    updateElement('surrounding-text-offset', cursor.surroundingTextOffset);\n  } else {\n    $('#cursor input').val('').attr('disabled', 'true');\n  }\n}\n\n/**\n * Updates the selection information in the sidebar.\n * @param {Object} selection The selection information.\n */\nfunction updateSelection(selection) {\n  var tableBody = $('#selection table tbody');\n  tableBody.children().remove();\n  if (selection) {\n    selection.selectedElements.forEach(function(selectedElement) {\n      var row  = $('<tr>');\n      var data = [\n        selectedElement.element.type,\n        selectedElement.partial,\n        selectedElement.startOffset,\n        selectedElement.endOffsetInclusive\n      ];\n      data.forEach(function(value) {\n        row.append($('<td>').text(value));\n      });\n      tableBody.append(row);\n    });\n    tableBody.effect(\"highlight\", { duration: 1500 });\n  } else {\n    tableBody.append($('<tr><td class=\"na\" colspan=\"4\">None</td></tr>'));\n  }\n}\n\n/**\n * Shows an error message in the sidebar.\n * @param {string} error The error returned by the server.\n */\nfunction showError(error) {\n  $('#error').text(error).show();\n  $('#results').css('color', 'gray');\n}\n\n/**\n * Hides any error message in the sidebar.\n */\nfunction hideError() {\n  $('#error').hide();\n  $('#results').css('color', 'inherit');\n}\n\n/**\n * Updates the state of the document and sets up the next refresh.\n * @param {Object} result The last result, if any.\n */\nfunction tick(result) {\n  if (result) {\n    if (!jsonEquals(result, lastResult)) {\n      lastChangedTime = new Date();\n    }\n    lastResult = result;\n  }\n  $('#lastupdated').text(new Date().toLocaleTimeString());\n  if (isInactive()) {\n    toggleRefresh();\n    lastResult = null;\n    lastChangedTime = null;\n  }\n  if (refreshOn) {\n    timeoutId = window.setTimeout(refreshState, REFRESH_WAIT_SECONDS * 1000);\n  }\n}\n\n/**\n * Determines if the document is inactive.\n * @return {Boolean} True if the document is inactive, false otherwise.\n */\nfunction isInactive() {\n  var now = new Date();\n  return lastChangedTime && now.getTime() - lastChangedTime.getTime() > INACTIVITY_TIMEOUT_MINUTES * 60 * 1000;\n}\n\n/**\n * Toggles whether or not automatic refreshing is on, and updated the button.\n */\nfunction toggleRefresh() {\n  var toggle = $('#toggle');\n  if (refreshOn) {\n    refreshOn = false;\n    window.clearTimeout(timeoutId);\n    toggle.text(toggle.data('resumetext'));\n  } else {\n    refreshOn = true;\n    refreshState();\n    toggle.text(toggle.data('stoptext'));\n  }\n}\n\n/**\n * Updates an element with a new value and highlights it if there is a change.\n * @param {string} elementId The ID of the element to update.\n * @param {string} value The new value of the element.\n */\nfunction updateElement(elementId, value) {\n  var element = $(document.getElementById(elementId));\n  if (String(element.val()) != String(value)) {\n    element.val(value).effect(\"highlight\", { duration: 1500 });\n  }\n}\n\n/**\n * Determines if two Objects have the JSON structure.\n * @param {Object} a The first object.\n * @param {Object} b The second object.\n * @return {Boolean} True if both objects have the same JSON structure, false otherwise.\n */\nfunction jsonEquals(a, b) {\n  return JSON.stringify(a) == JSON.stringify(b);\n}\n</script>\n"
  },
  {
    "path": "docs/dialog2sidebar/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Runs when the document opens, populating the menu.\n */\nfunction onOpen() {\n  DocumentApp.getUi()\n    .createMenu(\"Sidebar\")\n    .addItem(\"Show\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Shows the sidebar in the document.\n */\nfunction showSidebar() {\n  const page = HtmlService.createTemplateFromFile(\"Sidebar\")\n    .evaluate()\n    .setTitle(\"Sidebar\");\n  DocumentApp.getUi().showSidebar(page);\n}\n\n/**\n * Open a dialog in the document.\n * @return {string} The dialog ID.\n */\nfunction openDialog() {\n  const dialogId = Utilities.base64Encode(Math.random());\n  const template = HtmlService.createTemplateFromFile(\"Dialog\");\n  template.dialogId = dialogId;\n  const page = template.evaluate().setTitle(\"Dialog\");\n  DocumentApp.getUi().showDialog(page);\n  return dialogId;\n}\n\n/**\n * Include the contents of the given file into the HTML content.\n * @param {string} filename The filename\n * @return {string} The content of the rendered file.\n */\nfunction include(filename) {\n  return HtmlService.createHtmlOutputFromFile(filename).getContent();\n}\n"
  },
  {
    "path": "docs/dialog2sidebar/Dialog.html",
    "content": "<!DOCTYPE html>\n<!--\n  Copyright 2015 Google Inc. All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License\n-->\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n  </head>\n  <body>\n    <form onsubmit=\"onSubmit(); return false;\">\n      <p>Content goes here...</p>\n      <section class=\"button-bar\">\n        <input type=\"submit\" id=\"type\" class=\"action\" value=\"Submit\" />\n        <button onclick=\"cancel(); return false;\">Cancel</button>\n      </section>\n    </form>\n\n    <script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js\"></script>\n    <?!= include('Intercom.js'); ?>\n    <script>\n      /**\n       * The ID of this dialog. This is set once when the template is rendered.\n       */\n      var DIALOG_ID = '<?= dialogId ?>';\n\n      /**\n       * How often to check-in with the server, in milliseconds.\n       */\n      var CHECKIN_INTERVAL_MS = 500;\n\n      /**\n       * Instance of the Intercom.js library.\n       */\n      var intercom = Intercom.getInstance();\n\n      // Sets up an interval to check-in with the server every few seconds, so we can tell\n      // if it's been X-ed out.\n      window.setInterval(function() {\n        intercom.emit(DIALOG_ID, 'checkIn');\n      }, CHECKIN_INTERVAL_MS);\n\n      /**\n       * Runs when the form is submitted.\n       */\n      function onSubmit() {\n        // Here is where you would add custom logic specific to your form.\n        // You may need to make additional google.script.run calls to store various information\n        // collected in the dialog.\n        intercom.emit(DIALOG_ID, 'done');\n        close();\n      }\n\n      /**\n       * Runs when the cancel button is clicked.\n       */\n      function cancel() {\n        intercom.emit(DIALOG_ID, 'aborted');\n        close();\n      }\n\n      /**\n       * Closes the dialog.\n       */\n      function close() {\n        google.script.host.close();\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/dialog2sidebar/Intercom.js.html",
    "content": "<!--\n  Copyright © 2012 DIY Co\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License\n-->\n<script>\n/*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */\nvar Intercom=function(){var g=function(){};g.createInterface=function(b){return{on:function(a,c){\"undefined\"===typeof this[b]&&(this[b]={});this[b].hasOwnProperty(a)||(this[b][a]=[]);this[b][a].push(c)},off:function(a,c){\"undefined\"!==typeof this[b]&&this[b].hasOwnProperty(a)&&i.removeItem(c,this[b][a])},trigger:function(a){if(\"undefined\"!==typeof this[b]&&this[b].hasOwnProperty(a))for(var c=Array.prototype.slice.call(arguments,1),e=0;e<this[b][a].length;e++)this[b][a][e].apply(this[b][a][e],c)}}};\nvar m=g.createInterface(\"_handlers\");g.prototype._on=m.on;g.prototype._off=m.off;g.prototype._trigger=m.trigger;var n=g.createInterface(\"handlers\");g.prototype.on=function(){n.on.apply(this,arguments);Array.prototype.unshift.call(arguments,\"on\");this._trigger.apply(this,arguments)};g.prototype.off=n.off;g.prototype.trigger=n.trigger;var f=window.localStorage;\"undefined\"===typeof f&&(f={getItem:function(){},setItem:function(){},removeItem:function(){}});var i={},h=function(){return(65536*(1+Math.random())|\n0).toString(16).substring(1)};i.guid=function(){return h()+h()+\"-\"+h()+\"-\"+h()+\"-\"+h()+\"-\"+h()+h()+h()};i.throttle=function(b,a){var c=0;return function(){var e=(new Date).getTime();e-c>b&&(c=e,a.apply(this,arguments))}};i.extend=function(b,a){if(\"undefined\"===typeof b||!b)b={};if(\"object\"===typeof a)for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b};i.removeItem=function(b,a){for(var c=a.length-1;0<=c;c--)a[c]===b&&a.splice(c,1);return a};var d=function(){var b=this,a=(new Date).getTime();\nthis.origin=i.guid();this.lastMessage=a;this.bindings=[];this.receivedIDs={};this.previousValues={};a=function(){b._onStorageEvent.apply(b,arguments)};window.attachEvent?document.attachEvent(\"onstorage\",a):window.addEventListener(\"storage\",a,!1)};d.prototype._transaction=function(b){var a=this,c=!1,e=!1,p=null,d=function(){if(!c){var g=(new Date).getTime(),s=parseInt(f.getItem(l)||0);s&&1E3>g-s?(e||(a._on(\"storage\",d),e=!0),p=window.setTimeout(d,20)):(c=!0,f.setItem(l,g),b(),e&&a._off(\"storage\",d),\np&&window.clearTimeout(p),f.removeItem(l))}};d()};d.prototype._cleanup_emit=i.throttle(100,function(){this._transaction(function(){for(var b=(new Date).getTime()-t,a=0,c=JSON.parse(f.getItem(j)||\"[]\"),e=c.length-1;0<=e;e--)c[e].timestamp<b&&(c.splice(e,1),a++);0<a&&f.setItem(j,JSON.stringify(c))})});d.prototype._cleanup_once=i.throttle(100,function(){var b=this;this._transaction(function(){var a,c=JSON.parse(f.getItem(k)||\"{}\");(new Date).getTime();var e=0;for(a in c)b._once_expired(a,c)&&(delete c[a],\ne++);0<e&&f.setItem(k,JSON.stringify(c))})});d.prototype._once_expired=function(b,a){if(!a||!a.hasOwnProperty(b)||\"object\"!==typeof a[b])return!0;var c=a[b].ttl||u,e=(new Date).getTime();return a[b].timestamp<e-c};d.prototype._localStorageChanged=function(b,a){if(b&&b.key)return b.key===a;var c=f.getItem(a);if(c===this.previousValues[a])return!1;this.previousValues[a]=c;return!0};d.prototype._onStorageEvent=function(b){var b=b||window.event,a=this;this._localStorageChanged(b,j)&&this._transaction(function(){for(var b=\n(new Date).getTime(),e=f.getItem(j),e=JSON.parse(e||\"[]\"),d=0;d<e.length;d++)if(e[d].origin!==a.origin&&!(e[d].timestamp<a.lastMessage)){if(e[d].id){if(a.receivedIDs.hasOwnProperty(e[d].id))continue;a.receivedIDs[e[d].id]=!0}a.trigger(e[d].name,e[d].payload)}a.lastMessage=b});this._trigger(\"storage\",b)};d.prototype._emit=function(b,a,c){if((c=\"string\"===typeof c||\"number\"===typeof c?String(c):null)&&c.length){if(this.receivedIDs.hasOwnProperty(c))return;this.receivedIDs[c]=!0}var e={id:c,name:b,origin:this.origin,\ntimestamp:(new Date).getTime(),payload:a},d=this;this._transaction(function(){var c=f.getItem(j)||\"[]\",c=[c.substring(0,c.length-1),\"[]\"===c?\"\":\",\",JSON.stringify(e),\"]\"].join(\"\");f.setItem(j,c);d.trigger(b,a);window.setTimeout(function(){d._cleanup_emit()},50)})};d.prototype.bind=function(b,a){for(var c=0;c<d.bindings.length;c++){var e=d.bindings[c].factory(b,a||null,this);e&&this.bindings.push(e)}};d.prototype.emit=function(b,a){this._emit.apply(this,arguments);this._trigger(\"emit\",b,a)};d.prototype.once=\nfunction(b,a,c){if(d.supported){var e=this;this._transaction(function(){var d=JSON.parse(f.getItem(k)||\"{}\");e._once_expired(b,d)&&(d[b]={},d[b].timestamp=(new Date).getTime(),\"number\"===typeof c&&(d[b].ttl=1E3*c),f.setItem(k,JSON.stringify(d)),a(),window.setTimeout(function(){e._cleanup_once()},50))})}};i.extend(d.prototype,g.prototype);d.bindings=[];d.supported=\"undefined\"!==typeof f;var j=\"intercom\",k=\"intercom_once\",l=\"intercom_lock\",t=5E4,u=36E5;d.destroy=function(){f.removeItem(l);f.removeItem(j);\nf.removeItem(k)};var q=null;d.getInstance=function(){q||(q=new d);return q};var r=function(b,a,c){a=i.extend({id:null,send:!0,receive:!0},a);if(a.receive){var d=[],f=function(f){-1===d.indexOf(f)&&(d.push(f),b.on(f,function(b){var d=\"function\"===typeof a.id?a.id(f,b):null,e=\"function\"===typeof a.receive?a.receive(f,b):!0;(e||\"boolean\"!==typeof e)&&c._emit(f,b,d)}))},g;for(g in c.handlers)for(var h=0;h<c.handlers[g].length;h++)f(g,c.handlers[g][h]);c._on(\"on\",f)}a.send&&c._on(\"emit\",function(c,d){var e=\n\"function\"===typeof a.send?a.send(c,d):!0;(e||\"boolean\"!==typeof e)&&b.emit(c,d)})};r.factory=function(b,a,c){return\"undefined\"===typeof b.socket?!1:new r(b,a,c)};d.bindings.push(r);return d}();\n</script>\n"
  },
  {
    "path": "docs/dialog2sidebar/README.md",
    "content": "# Dialog to Sidebar Communication in Apps Script\n\nThis script demonstrates a method of setting up a communication channel between\na dialog and a sidebar in Apps Script. This helps solve the common problem\nof having your sidebar know when a dialog is opened, is submitted, closed, etc.\n\nWith the introduction of the IFRAME sandbox mode, HtmlService UIs can take\nadvantage of the\n[HTML5 localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).\nThe open source library [intercom.js](https://github.com/diy/intercom.js/)\nbuilds a messaging system on top of this API, allowing for dialogs and sidebars\nin the same browser to communicate with each other.\n\nAn overview of the process is as follows:\n\n* The sidebar requests a new dialog to be opened.\n* The backend generates a new ID for the dialog, opens the dialog (passing in\n  that ID as a template parameter), and sends the ID back to the sidebar.\n* The sidebar listens for events on the dialog's intercom.js channel.\n* The dialog regularly \"checks in\" with the sidebar, resetting a\n  [timer](https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Timers).\n* When the user completes the dialog (by clicking either the \"OK\" or \"Cancel\"\n  button) it sends this status change to the sidebar.\n* If the sidebar's timer actually fires, that means the dialog hasn't checked in\n  recently, and it is considered \"lost\".\n"
  },
  {
    "path": "docs/dialog2sidebar/Sidebar.html",
    "content": "<!DOCTYPE html>\n<!--\n  Copyright 2015 Google Inc. All rights reserved.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      https://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License\n-->\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n  </head>\n  <body>\n    <div class=\"sidebar\">\n      <button id=\"open\" class=\"action\" onclick=\"openDialog();\">Open Dialog</button>\n      <pre id=\"output\"></pre>\n    </div>\n\n    <script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js\"></script>\n    <?!= include('Intercom.js'); ?>\n    <script>\n      /**\n       * How long to wait for the dialog to check-in before assuming it's been\n       * closed, in milliseconds.\n       */\n      var DIALOG_TIMEOUT_MS = 2000;\n\n      /**\n       * Holds a mapping from dialog ID to the ID of the timeout that is used to\n       * check if it was lost. This is needed so we can cancel the timeout when\n       * the dialog is closed.\n       */\n      var timeoutIds = {};\n\n      /**\n       * Instance of the Intercom.js library.\n       */\n      var intercom = Intercom.getInstance();\n\n      /**\n       * Open the dialog.\n       */\n      function openDialog() {\n        google.script.run.withSuccessHandler(onDialogOpened)\n            .withFailureHandler(onError)\n            .openDialog();\n      }\n\n      /**\n       * Callback to run after the dialog has been opened.\n       * @param {string} dialogId The ID of the dialog.\n       */\n      function onDialogOpened(dialogId) {\n        $('#output').append('Dialog opened\\n');\n        // Setup event listeners.\n        intercom.on(dialogId, function(state) {\n          switch(state) {\n            case 'done':\n              $('#output').append('Dialog submitted.\\n');\n              forget(dialogId);\n              break;\n            case 'aborted':\n              $('#output').append('Dialog cancelled.\\n');\n              forget(dialogId);\n              break;\n            case 'checkIn':\n              forget(dialogId);\n              watch(dialogId);\n              break;\n            case 'lost':\n              $('#output').append('Dialog lost.\\n');\n              break;\n            default:\n              throw 'Unknown dialog state: ' + state;\n          }\n        });\n      }\n\n      function onError(exception) {\n        $('#output').append('Error: ' + exception + '\\n');\n      }\n\n      /**\n       * Watch the given dialog, to detect when it's been X-ed out.\n       * @param {string} dialogId The ID of the dialog to watch.\n       */\n      function watch(dialogId) {\n        timeoutIds[dialogId] = window.setTimeout(function() {\n          intercom.emit(dialogId, 'lost');\n        }, DIALOG_TIMEOUT_MS);\n      }\n\n      /**\n       * Stop watching the given dialog.\n       * @param {string} dialogId The ID of the dialog to watch.\n       */\n      function forget(dialogId) {\n        if (timeoutIds[dialogId]) {\n          window.clearTimeout(timeoutIds[dialogId]);\n        }\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START docs_quickstart]\n/**\n * Prints the title of the sample document:\n * https://docs.google.com/document/d/195j9eDD3ccgjQRttHhJPymLJUCOUjs-jmwTrekvdjFE/edit\n * @see https://developers.google.com/docs/api/reference/rest/v1/documents/get\n */\nfunction printDocTitle() {\n  const documentId = \"195j9eDD3ccgjQRttHhJPymLJUCOUjs-jmwTrekvdjFE\";\n  const doc = Docs.Documents.get(documentId, { includeTabsContent: true });\n  console.log(`The title of the doc is: ${doc.title}`);\n}\n// [END docs_quickstart]\n"
  },
  {
    "path": "docs/translate/README.md",
    "content": "# Translate\n\nTranslate is a sample script for Google Docs that allows you to translate\nselected text from a set of source languages to a set of destination languages.\nThe resulting translation can then be inserted back into the Google Document.\nThis sample was originally designed as a\n[quickstart](https://developers.google.com/apps-script/quickstart/docs).\n\n![Google Docs Translate Quickstart](https://developers.google.com/apps-script/images/quickstart-translate.png)\n"
  },
  {
    "path": "docs/translate/sidebar.html",
    "content": "<!--\nCopyright 2018 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START apps_script_docs_translate_quickstart] -->\n<!DOCTYPE html>\n<html>\n<head>\n  <base target=\"_top\">\n  <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n  <!-- The CSS package above applies Google styling to buttons and other elements. -->\n\n  <style>\n    .branding-below {\n      bottom: 56px;\n      top: 0;\n    }\n    .branding-text {\n      left: 7px;\n      position: relative;\n      top: 3px;\n    }\n    .col-contain {\n      overflow: hidden;\n    }\n    .col-one {\n      float: left;\n      width: 50%;\n    }\n    .logo {\n      vertical-align: middle;\n    }\n    .radio-spacer {\n      height: 20px;\n    }\n    .width-100 {\n      width: 100%;\n    }\n  </style>\n  <title></title>\n</head>\n<body>\n<div class=\"sidebar branding-below\">\n  <form>\n    <div class=\"block col-contain\">\n      <div class=\"col-one\">\n        <b>Selected text</b>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-auto\" value=\"\" checked=\"checked\">\n          <label for=\"radio-origin-auto\">Auto-detect</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-en\" value=\"en\">\n          <label for=\"radio-origin-en\">English</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-fr\" value=\"fr\">\n          <label for=\"radio-origin-fr\">French</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-de\" value=\"de\">\n          <label for=\"radio-origin-de\">German</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-ja\" value=\"ja\">\n          <label for=\"radio-origin-ja\">Japanese</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"origin\" id=\"radio-origin-es\" value=\"es\">\n          <label for=\"radio-origin-es\">Spanish</label>\n        </div>\n      </div>\n      <div>\n        <b>Translate into</b>\n        <div class=\"radio-spacer\">\n        </div>\n        <div>\n          <input type=\"radio\" name=\"dest\" id=\"radio-dest-en\" value=\"en\">\n          <label for=\"radio-dest-en\">English</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"dest\" id=\"radio-dest-fr\" value=\"fr\">\n          <label for=\"radio-dest-fr\">French</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"dest\" id=\"radio-dest-de\" value=\"de\">\n          <label for=\"radio-dest-de\">German</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"dest\" id=\"radio-dest-ja\" value=\"ja\" checked=\"checked\">\n          <label for=\"radio-dest-ja\">Japanese</label>\n        </div>\n        <div>\n          <input type=\"radio\" name=\"dest\" id=\"radio-dest-es\" value=\"es\">\n          <label for=\"radio-dest-es\">Spanish</label>\n        </div>\n      </div>\n    </div>\n    <div class=\"block form-group\">\n      <label for=\"translated-text\"><b>Translation</b></label>\n      <textarea class=\"width-100\" id=\"translated-text\" rows=\"10\"></textarea>\n    </div>\n    <div class=\"block\">\n      <input type=\"checkbox\" id=\"save-prefs\">\n      <label for=\"save-prefs\">Use these languages by default</label>\n    </div>\n    <div class=\"block\" id=\"button-bar\">\n      <button class=\"blue\" id=\"run-translation\">Translate</button>\n      <button id=\"insert-text\">Insert</button>\n    </div>\n  </form>\n</div>\n\n<div class=\"sidebar bottom\">\n  <img alt=\"Add-on logo\" class=\"logo\" src=\"https://www.gstatic.com/images/branding/product/1x/translate_48dp.png\" width=\"27\" height=\"27\">\n  <span class=\"gray branding-text\">Translate sample by Google</span>\n</div>\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * On document load, assign click handlers to each button and try to load the\n   * user's origin and destination language preferences if previously set.\n   */\n  $(function() {\n    $('#run-translation').click(runTranslation);\n    $('#insert-text').click(insertText);\n    google.script.run.withSuccessHandler(loadPreferences)\n            .withFailureHandler(showError).getPreferences();\n  });\n\n  /**\n   * Callback function that populates the origin and destination selection\n   * boxes with user preferences from the server.\n   *\n   * @param {Object} languagePrefs The saved origin and destination languages.\n   */\n  function loadPreferences(languagePrefs) {\n    $('input:radio[name=\"origin\"]')\n            .filter('[value=' + languagePrefs.originLang + ']')\n            .attr('checked', true);\n    $('input:radio[name=\"dest\"]')\n            .filter('[value=' + languagePrefs.destLang + ']')\n            .attr('checked', true);\n  }\n\n  /**\n   * Runs a server-side function to translate the user-selected text and update\n   * the sidebar UI with the resulting translation.\n   */\n  function runTranslation() {\n    this.disabled = true;\n    $('#error').remove();\n    const origin = $('input[name=origin]:checked').val();\n    const dest = $('input[name=dest]:checked').val();\n    const savePrefs = $('#save-prefs').is(':checked');\n    google.script.run\n            .withSuccessHandler(\n                    function(textAndTranslation, element) {\n                      $('#translated-text').val(textAndTranslation.translation);\n                      element.disabled = false;\n                    })\n            .withFailureHandler(\n                    function(msg, element) {\n                      showError(msg, $('#button-bar'));\n                      element.disabled = false;\n                    })\n            .withUserObject(this)\n            .getTextAndTranslation(origin, dest, savePrefs);\n  }\n\n  /**\n   * Runs a server-side function to insert the translated text into the document\n   * at the user's cursor or selection.\n   */\n  function insertText() {\n    this.disabled = true;\n    $('#error').remove();\n    google.script.run\n            .withSuccessHandler(\n                    function(returnSuccess, element) {\n                      element.disabled = false;\n                    })\n            .withFailureHandler(\n                    function(msg, element) {\n                      showError(msg, $('#button-bar'));\n                      element.disabled = false;\n                    })\n            .withUserObject(this)\n            .insertText($('#translated-text').val());\n  }\n\n  /**\n   * Inserts a div that contains an error message after a given element.\n   *\n   * @param {string} msg The error message to display.\n   * @param {DOMElement} element The element after which to display the error.\n   */\n  function showError(msg, element) {\n    const div = $('<div id=\"error\" class=\"error\">' + msg + '</div>');\n    $(element).after(div);\n  }\n</script>\n</body>\n</html>\n<!-- [END apps_script_docs_translate_quickstart] -->\n"
  },
  {
    "path": "docs/translate/translate.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_docs_translate_quickstart]\n/**\n * @OnlyCurrentDoc\n *\n * The above comment directs Apps Script to limit the scope of file\n * access for this add-on. It specifies that this add-on will only\n * attempt to read or modify the files in which the add-on is used,\n * and not all of the user's files. The authorization request message\n * presented to users will reflect this limited scope.\n */\n\n/**\n * Creates a menu entry in the Google Docs UI when the document is opened.\n * This method is only used by the regular add-on, and is never called by\n * the mobile add-on version.\n *\n * @param {object} e The event parameter for a simple onOpen trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode.\n */\nfunction onOpen(e) {\n  DocumentApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Start\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed.\n * This method is only used by the regular add-on, and is never called by\n * the mobile add-on version.\n *\n * @param {object} e The event parameter for a simple onInstall trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode. (In practice, onInstall triggers always\n *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or\n *     AuthMode.NONE.)\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar in the document containing the add-on's user interface.\n * This method is only used by the regular add-on, and is never called by\n * the mobile add-on version.\n */\nfunction showSidebar() {\n  const ui =\n    HtmlService.createHtmlOutputFromFile(\"sidebar\").setTitle(\"Translate\");\n  DocumentApp.getUi().showSidebar(ui);\n}\n\n/**\n * Gets the text the user has selected. If there is no selection,\n * this function displays an error message.\n *\n * @return {Array.<string>} The selected text.\n */\nfunction getSelectedText() {\n  const selection = DocumentApp.getActiveDocument().getSelection();\n  const text = [];\n  if (selection) {\n    const elements = selection.getSelectedElements();\n    for (let i = 0; i < elements.length; ++i) {\n      if (elements[i].isPartial()) {\n        const element = elements[i].getElement().asText();\n        const startIndex = elements[i].getStartOffset();\n        const endIndex = elements[i].getEndOffsetInclusive();\n\n        text.push(element.getText().substring(startIndex, endIndex + 1));\n      } else {\n        const element = elements[i].getElement();\n        // Only translate elements that can be edited as text; skip images and\n        // other non-text elements.\n        if (element.editAsText) {\n          const elementText = element.asText().getText();\n          // This check is necessary to exclude images, which return a blank\n          // text element.\n          if (elementText) {\n            text.push(elementText);\n          }\n        }\n      }\n    }\n  }\n  if (!text.length) throw new Error(\"Please select some text.\");\n  return text;\n}\n\n/**\n * Gets the stored user preferences for the origin and destination languages,\n * if they exist.\n * This method is only used by the regular add-on, and is never called by\n * the mobile add-on version.\n *\n * @return {Object} The user's origin and destination language preferences, if\n *     they exist.\n */\nfunction getPreferences() {\n  const userProperties = PropertiesService.getUserProperties();\n  return {\n    originLang: userProperties.getProperty(\"originLang\"),\n    destLang: userProperties.getProperty(\"destLang\"),\n  };\n}\n\n/**\n * Gets the user-selected text and translates it from the origin language to the\n * destination language. The languages are notated by their two-letter short\n * form. For example, English is 'en', and Spanish is 'es'. The origin language\n * may be specified as an empty string to indicate that Google Translate should\n * auto-detect the language.\n *\n * @param {string} origin The two-letter short form for the origin language.\n * @param {string} dest The two-letter short form for the destination language.\n * @param {boolean} savePrefs Whether to save the origin and destination\n *     language preferences.\n * @return {Object} Object containing the original text and the result of the\n *     translation.\n */\nfunction getTextAndTranslation(origin, dest, savePrefs) {\n  if (savePrefs) {\n    PropertiesService.getUserProperties()\n      .setProperty(\"originLang\", origin)\n      .setProperty(\"destLang\", dest);\n  }\n  const text = getSelectedText().join(\"\\n\");\n  return {\n    text: text,\n    translation: translateText(text, origin, dest),\n  };\n}\n\n/**\n * Replaces the text of the current selection with the provided text, or\n * inserts text at the current cursor location. (There will always be either\n * a selection or a cursor.) If multiple elements are selected, only inserts the\n * translated text in the first element that can contain text and removes the\n * other elements.\n *\n * @param {string} newText The text with which to replace the current selection.\n */\nfunction insertText(newText) {\n  const selection = DocumentApp.getActiveDocument().getSelection();\n  if (selection) {\n    let replaced = false;\n    const elements = selection.getSelectedElements();\n    if (\n      elements.length === 1 &&\n      elements[0].getElement().getType() ===\n        DocumentApp.ElementType.INLINE_IMAGE\n    ) {\n      throw new Error(\"Can't insert text into an image.\");\n    }\n    for (let i = 0; i < elements.length; ++i) {\n      if (elements[i].isPartial()) {\n        const element = elements[i].getElement().asText();\n        const startIndex = elements[i].getStartOffset();\n        const endIndex = elements[i].getEndOffsetInclusive();\n        element.deleteText(startIndex, endIndex);\n        if (!replaced) {\n          element.insertText(startIndex, newText);\n          replaced = true;\n        } else {\n          // This block handles a selection that ends with a partial element. We\n          // want to copy this partial text to the previous element so we don't\n          // have a line-break before the last partial.\n          const parent = element.getParent();\n          const remainingText = element.getText().substring(endIndex + 1);\n          parent.getPreviousSibling().asText().appendText(remainingText);\n          // We cannot remove the last paragraph of a doc. If this is the case,\n          // just remove the text within the last paragraph instead.\n          if (parent.getNextSibling()) {\n            parent.removeFromParent();\n          } else {\n            element.removeFromParent();\n          }\n        }\n      } else {\n        const element = elements[i].getElement();\n        if (!replaced && element.editAsText) {\n          // Only translate elements that can be edited as text, removing other\n          // elements.\n          element.clear();\n          element.asText().setText(newText);\n          replaced = true;\n        } else {\n          // We cannot remove the last paragraph of a doc. If this is the case,\n          // just clear the element.\n          if (element.getNextSibling()) {\n            element.removeFromParent();\n          } else {\n            element.clear();\n          }\n        }\n      }\n    }\n  } else {\n    const cursor = DocumentApp.getActiveDocument().getCursor();\n    const surroundingText = cursor.getSurroundingText().getText();\n    const surroundingTextOffset = cursor.getSurroundingTextOffset();\n\n    // If the cursor follows or preceds a non-space character, insert a space\n    // between the character and the translation. Otherwise, just insert the\n    // translation.\n    let textToInsert = newText;\n    if (surroundingText) {\n      if (surroundingTextOffset > 0) {\n        if (surroundingText.charAt(surroundingTextOffset - 1) !== \" \") {\n          textToInsert = ` ${textToInsert}`;\n        }\n      }\n      if (surroundingTextOffset < surroundingText.length) {\n        if (surroundingText.charAt(surroundingTextOffset) !== \" \") {\n          textToInsert += \" \";\n        }\n      }\n    }\n    cursor.insertText(textToInsert);\n  }\n}\n\n/**\n * Given text, translate it from the origin language to the destination\n * language. The languages are notated by their two-letter short form. For\n * example, English is 'en', and Spanish is 'es'. The origin language may be\n * specified as an empty string to indicate that Google Translate should\n * auto-detect the language.\n *\n * @param {string} text text to translate.\n * @param {string} origin The two-letter short form for the origin language.\n * @param {string} dest The two-letter short form for the destination language.\n * @return {string} The result of the translation, or the original text if\n *     origin and dest languages are the same.\n */\nfunction translateText(text, origin, dest) {\n  if (origin === dest) return text;\n  return LanguageApp.translate(text, origin, dest);\n}\n// [END apps_script_docs_translate_quickstart]\n"
  },
  {
    "path": "drive/activity/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START drive_activity_quickstart]\n/**\n * Lists activity for a Drive user.\n */\nfunction listActivity() {\n  const optionalArgs = {\n    source: \"drive.google.com\",\n    \"drive.ancestorId\": \"root\",\n    pageSize: 10,\n  };\n  const response = AppsActivity.Activities.list(optionalArgs);\n  const activities = response.activities;\n  if (activities && activities.length > 0) {\n    console.log(\"Recent activity:\");\n    for (i = 0; i < activities.length; i++) {\n      const activity = activities[i];\n      const event = activity.combinedEvent;\n      const user = event.user;\n      const target = event.target;\n      if (user == null || target == null) {\n      } else {\n        const time = new Date(Number(event.eventTimeMillis));\n        console.log(\n          \"%s: %s, %s, %s (%s)\",\n          time,\n          user.name,\n          event.primaryEventType,\n          target.name,\n          target.mimeType,\n        );\n      }\n    }\n  } else {\n    console.log(\"No recent activity\");\n  }\n}\n// [END drive_activity_quickstart]\n"
  },
  {
    "path": "drive/activity-v2/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START drive_activity_v2_quickstart]\n/**\n * Lists 10 activity for a Drive user.\n * @see https://developers.google.com/drive/activity/v2/reference/rest/v2/activity/query\n */\nfunction listDriveActivity() {\n  const request = {\n    pageSize: 10,\n    // Use other parameter here if needed.\n  };\n  try {\n    // Activity.query method is used Query past activity in Google Drive.\n    const response = DriveActivity.Activity.query(request);\n    const activities = response.activities;\n    if (!activities || activities.length === 0) {\n      console.log(\"No activity.\");\n      return;\n    }\n    console.log(\"Recent activity:\");\n    for (const activity of activities) {\n      // get time information of activity.\n      const time = getTimeInfo(activity);\n      // get the action details/information\n      const action = getActionInfo(activity.primaryActionDetail);\n      // get the actor's details of activity\n      const actors = activity.actors.map(getActorInfo);\n      // get target information of activity.\n      const targets = activity.targets.map(getTargetInfo);\n      // print the time,actor,action and targets of drive activity.\n      console.log(\"%s: %s, %s, %s\", time, actors, action, targets);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle error from drive activity API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * @param {object} object\n * @return {string}  Returns the name of a set property in an object, or else \"unknown\".\n */\nfunction getOneOf(object) {\n  for (const key in object) {\n    return key;\n  }\n  return \"unknown\";\n}\n\n/**\n * @param {object} activity Activity object.\n * @return {string} Returns a time associated with an activity.\n */\nfunction getTimeInfo(activity) {\n  if (\"timestamp\" in activity) {\n    return activity.timestamp;\n  }\n  if (\"timeRange\" in activity) {\n    return activity.timeRange.endTime;\n  }\n  return \"unknown\";\n}\n\n/**\n * @param {object} actionDetail The primary action details of the activity.\n * @return {string} Returns the type of action.\n */\nfunction getActionInfo(actionDetail) {\n  return getOneOf(actionDetail);\n}\n\n/**\n * @param {object} user The User object.\n * @return {string}  Returns user information, or the type of user if not a known user.\n */\nfunction getUserInfo(user) {\n  if (\"knownUser\" in user) {\n    const knownUser = user.knownUser;\n    const isMe = knownUser.isCurrentUser || false;\n    return isMe ? \"people/me\" : knownUser.personName;\n  }\n  return getOneOf(user);\n}\n\n/**\n * @param {object} actor The Actor object.\n * @return {string} Returns actor information, or the type of actor if not a user.\n */\nfunction getActorInfo(actor) {\n  if (\"user\" in actor) {\n    return getUserInfo(actor.user);\n  }\n  return getOneOf(actor);\n}\n\n/**\n * @param {object} target The Target object.\n * @return {string} Returns the type of a target and an associated title.\n */\nfunction getTargetInfo(target) {\n  if (\"driveItem\" in target) {\n    const title = target.driveItem.title || \"unknown\";\n    return `driveItem:\"${title}\"`;\n  }\n  if (\"drive\" in target) {\n    const title = target.drive.title || \"unknown\";\n    return `drive:\"${title}\"`;\n  }\n  if (\"fileComment\" in target) {\n    const parent = target.fileComment.parent || {};\n    const title = parent.title || \"unknown\";\n    return `fileComment:\"${title}\"`;\n  }\n  return `${getOneOf(target)}:unknown`;\n}\n// [END drive_activity_v2_quickstart]\n"
  },
  {
    "path": "drive/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START drive_quickstart]\n/**\n * Lists the names and IDs of up to 10 files.\n */\nfunction listFiles() {\n  try {\n    // Files.list method returns the list of files in drive.\n    const files = Drive.Files.list({\n      fields: \"nextPageToken, items(id, title)\",\n      maxResults: 10,\n    }).items;\n    // Print the title and id of files available in drive\n    for (const file of files) {\n      console.log(\"%s (%s)\", file.title, file.id);\n    }\n  } catch (err) {\n    // TODO(developer)-Handle Files.list() exception\n    console.log(\"failed with error %s\", err.message);\n  }\n}\n// [END drive_quickstart]\n"
  },
  {
    "path": "forms/README.md",
    "content": "# Google Forms Add-ons\n\n## [Notification Add-on](https://developers.google.com/apps-script/quickstart/forms-add-on)\n\nThis add-on allows Form creators to automatically\nsend email notifications when a form is submitted. In addition, the\nadd-on allows form creators to be notified when they have received\nresponses.\n\n![Form Notifications](https://developers.google.com/apps-script/images/quickstart-form-notifications.png)\n"
  },
  {
    "path": "forms/notifications/README.md",
    "content": "# Form Notifications Add-on for Google Forms\n\nA sample Google Apps Script add-on for Google Forms.\n\n## Introduction\n\nGoogle Apps Script allows developers to construct 'add-ons' -- small\napplications which extend and support Google Docs, Google Sheets,\nand now Google Forms.\n\nThis sample shows how to construct a Google Forms add-on called\n[Form Notifications](https://chrome.google.com/webstore/detail/form-notifications/bbpdeojefjfhaelgljjcadpcckdfcdod).\nThis add-on allows Form creators to automatically\nsend email notifications when a form is submitted. In addition, the\nadd-on allows form creators to be notified when they have received\nresponses.\n\nThis sample makes use of the following Apps Script concepts:\n\n* Google Forms Add-ons\n* Events and Triggers (specifically, onFormSubmit triggers)\n* Templated HTML\n* Dialogs and Sidebars\n* Sending Email with Apps Script\n\n## Getting Started\n\nYou can install the [Form Notifications](https://chrome.google.com/webstore/detail/form-notifications/bbpdeojefjfhaelgljjcadpcckdfcdod) add-on from the add-on\nstore.\n\nIf you would like to try re-building it yourself, you can follow the\ndirections provided in the [Add-on for Google Forms Quickstart](https://developers.google.com/apps-script/quickstart/forms-add-on) documentation.\n\n## Learn more\n\nTo continue learning about how to extend Google Docs, Sheets and Forms\nwith Apps Script, take a look at the following resources:\n\n* [Guide to Add-ons](https://developers.google.com/apps-script/add-ons/)\n* [Forms Service Reference](https://developers.google.com/apps-script/reference/forms)\n\n## Support\n\n- Stack Overflow Tag: [google-apps-script](http://stackoverflow.com/questions/tagged/google-apps-script)\n"
  },
  {
    "path": "forms/notifications/about.html",
    "content": "<!--Copyright Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. -->\n\n<!-- [START apps_script_forms_notifications_quickstart] -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n    <!-- The CSS package above applies Google styling to buttons and other elements. -->\n  </head>\n  <body>\n    <div>\n      <p>\n      <i>Form Notifications</i> was created as an sample add-on, and is meant\n      for demonstration purposes only. It should not be used for complex or\n      important workflows.\n      </p>\n      <p>\n      The number of notifications this add-on produces are limited by the owner's\n      available email quota; it will not send email notifications if the owner's\n      daily email quota has been exceeded. Collaborators using this add-on on the\n      same form will be able to adjust the notification settings, but will not be\n      able to disable the notification triggers set by other collaborators.\n      </p>\n    </div>\n  </body>\n</html>\n<!-- [END apps_script_forms_notifications_quickstart] -->\n"
  },
  {
    "path": "forms/notifications/authorizationEmail.html",
    "content": "<!--Copyright Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. -->\n\n<!-- [START apps_script_forms_notifications_quickstart] -->\n<p>The Google Forms add-on <i>Form Notifications</i> is set to run automatically\nwhenever a form is submitted. The add-on was recently updated and it needs you\nto re-authorize it to run on your behalf.</p>\n\n<p>The add-on's automatic functions are temporarily disabled until you\nre-authorize the add-on. You can accomplish this by opening one of the forms\nusing the add-on and running the add-on through the menu. Alternatively, you can\nclick this link to approve authorization directly:</p>\n\n<p><a href=\"<?= url ?>\">Click here</a> to re-authorize the add-on.</p>\n\n<p>This notification email will be sent to you at most once per day until the\nadd-on is re-authorized.</p>\n\n<hr>\n\n<p style=\"font-size:80%\">This automatic message was sent to you via the <i>Form\nNotifications</i> add-on for Google Forms.\n<?= notice ?></p>\n<!-- [END apps_script_forms_notifications_quickstart] -->\n"
  },
  {
    "path": "forms/notifications/creatorNotification.html",
    "content": "<!--Copyright Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. -->\n\n<!-- [START apps_script_forms_notifications_quickstart] -->\n<p><i>Form Notifications</i> (a Google Forms add-on) has detected that the form\ntitled <a href=\"<?= formUrl?>\"><b><?= title ?></b></a> has received\n<?= responses ?> responses so far.</p>\n\n<p><a href=\"<?= summary ?>\">Summary of form responses</a></p>\n\n<p>You are receiving this email because an editor of this form configured\n<i>Form Notifications</i> to alert you every time this form receives\n<b><?= responseStep ?></b> responses.</p>\n\n<p>To change this setting, or to stop receiving these notifications, have the\nform owner or editors open the form and adjust the <i>Form Notifications</i>\nadd-on configuration via the \"Configure notifications\" menu item.</p>\n\n<hr>\n\n<p style=\"font-size:80%\">This automatic message was sent to you via the <i>Form\nNotifications</i> add-on for Google Forms.\n<?= notice ?></p>\n<!-- [END apps_script_forms_notifications_quickstart] -->\n"
  },
  {
    "path": "forms/notifications/notification.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_forms_notifications_quickstart]\n/**\n * @OnlyCurrentDoc\n *\n * The above comment directs Apps Script to limit the scope of file\n * access for this add-on. It specifies that this add-on will only\n * attempt to read or modify the files in which the add-on is used,\n * and not all of the user's files. The authorization request message\n * presented to users will reflect this limited scope.\n */\n\n/**\n * A global constant String holding the title of the add-on. This is\n * used to identify the add-on in the notification emails.\n */\nconst ADDON_TITLE = \"Form Notifications\";\n\n/**\n * A global constant 'notice' text to include with each email\n * notification.\n */\nconst NOTICE =\n  \"Form Notifications was created as an sample add-on, and is\" +\n  \" meant for\" +\n  \"demonstration purposes only. It should not be used for complex or important\" +\n  \"workflows. The number of notifications this add-on produces are limited by the\" +\n  \"owner's available email quota; it will not send email notifications if the\" +\n  \"owner's daily email quota has been exceeded. Collaborators using this add-on on\" +\n  \"the same form will be able to adjust the notification settings, but will not be\" +\n  \"able to disable the notification triggers set by other collaborators.\";\n\n/**\n * Adds a custom menu to the active form to show the add-on sidebar.\n *\n * @param {object} e The event parameter for a simple onOpen trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode.\n */\nfunction onOpen(e) {\n  try {\n    FormApp.getUi()\n      .createAddonMenu()\n      .addItem(\"Configure notifications\", \"showSidebar\")\n      .addItem(\"About\", \"showAbout\")\n      .addToUi();\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Runs when the add-on is installed.\n *\n * @param {object} e The event parameter for a simple onInstall trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode. (In practice, onInstall triggers always\n *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or\n *     AuthMode.NONE).\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar in the form containing the add-on's user interface for\n * configuring the notifications this add-on will produce.\n */\nfunction showSidebar() {\n  try {\n    const ui =\n      HtmlService.createHtmlOutputFromFile(\"sidebar\").setTitle(\n        \"Form Notifications\",\n      );\n    FormApp.getUi().showSidebar(ui);\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Opens a purely-informational dialog in the form explaining details about\n * this add-on.\n */\nfunction showAbout() {\n  try {\n    const ui = HtmlService.createHtmlOutputFromFile(\"about\")\n      .setWidth(420)\n      .setHeight(270);\n    FormApp.getUi().showModalDialog(ui, \"About Form Notifications\");\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Save sidebar settings to this form's Properties, and update the onFormSubmit\n * trigger as needed.\n *\n * @param {Object} settings An Object containing key-value\n *      pairs to store.\n */\nfunction saveSettings(settings) {\n  try {\n    PropertiesService.getDocumentProperties().setProperties(settings);\n    adjustFormSubmitTrigger();\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Queries the User Properties and adds additional data required to populate\n * the sidebar UI elements.\n *\n * @return {Object} A collection of Property values and\n *     related data used to fill the configuration sidebar.\n */\nfunction getSettings() {\n  try {\n    const settings = PropertiesService.getDocumentProperties().getProperties();\n\n    // Use a default email if the creator email hasn't been provided yet.\n    if (!settings.creatorEmail) {\n      settings.creatorEmail = Session.getEffectiveUser().getEmail();\n    }\n\n    // Get text field items in the form and compile a list\n    //   of their titles and IDs.\n    const form = FormApp.getActiveForm();\n    const textItems = form.getItems(FormApp.ItemType.TEXT);\n\n    settings.textItems = [];\n    for (let i = 0; i < textItems.length; i++) {\n      settings.textItems.push({\n        title: textItems[i].getTitle(),\n        id: textItems[i].getId(),\n      });\n    }\n    return settings;\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Adjust the onFormSubmit trigger based on user's requests.\n */\nfunction adjustFormSubmitTrigger() {\n  try {\n    const form = FormApp.getActiveForm();\n    const triggers = ScriptApp.getUserTriggers(form);\n    const settings = PropertiesService.getDocumentProperties();\n    const triggerNeeded =\n      settings.getProperty(\"creatorNotify\") === \"true\" ||\n      settings.getProperty(\"respondentNotify\") === \"true\";\n\n    // Create a new trigger if required; delete existing trigger\n    // if it is not needed.\n    let existingTrigger = null;\n    for (let i = 0; i < triggers.length; i++) {\n      if (triggers[i].getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT) {\n        existingTrigger = triggers[i];\n        break;\n      }\n    }\n    if (triggerNeeded && !existingTrigger) {\n      const trigger = ScriptApp.newTrigger(\"respondToFormSubmit\")\n        .forForm(form)\n        .onFormSubmit()\n        .create();\n    } else if (!triggerNeeded && existingTrigger) {\n      ScriptApp.deleteTrigger(existingTrigger);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Responds to a form submission event if an onFormSubmit trigger has been\n * enabled.\n *\n * @param {Object} e The event parameter created by a form\n *      submission; see\n *      https://developers.google.com/apps-script/understanding_events\n */\nfunction respondToFormSubmit(e) {\n  try {\n    const settings = PropertiesService.getDocumentProperties();\n    const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);\n\n    // Check if the actions of the trigger require authorizations that have not\n    // been supplied yet -- if so, warn the active user via email (if possible).\n    // This check is required when using triggers with add-ons to maintain\n    // functional triggers.\n    if (\n      authInfo.getAuthorizationStatus() ===\n      ScriptApp.AuthorizationStatus.REQUIRED\n    ) {\n      // Re-authorization is required. In this case, the user needs to be alerted\n      // that they need to reauthorize; the normal trigger action is not\n      // conducted, since authorization needs to be provided first. Send at\n      // most one 'Authorization Required' email a day, to avoid spamming users\n      // of the add-on.\n      sendReauthorizationRequest();\n    } else {\n      // All required authorizations have been granted, so continue to respond to\n      // the trigger event.\n\n      // Check if the form creator needs to be notified; if so, construct and\n      // send the notification.\n      if (settings.getProperty(\"creatorNotify\") === \"true\") {\n        sendCreatorNotification();\n      }\n\n      // Check if the form respondent needs to be notified; if so, construct and\n      // send the notification. Be sure to respect the remaining email quota.\n      if (\n        settings.getProperty(\"respondentNotify\") === \"true\" &&\n        MailApp.getRemainingDailyQuota() > 0\n      ) {\n        sendRespondentNotification(e.response);\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Called when the user needs to reauthorize. Sends the user of the\n * add-on an email explaining the need to reauthorize and provides\n * a link for the user to do so. Capped to send at most one email\n * a day to prevent spamming the users of the add-on.\n */\nfunction sendReauthorizationRequest() {\n  try {\n    const settings = PropertiesService.getDocumentProperties();\n    const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);\n    const lastAuthEmailDate = settings.getProperty(\"lastAuthEmailDate\");\n    const today = new Date().toDateString();\n    if (lastAuthEmailDate !== today) {\n      if (MailApp.getRemainingDailyQuota() > 0) {\n        const template =\n          HtmlService.createTemplateFromFile(\"authorizationEmail\");\n        template.url = authInfo.getAuthorizationUrl();\n        template.notice = NOTICE;\n        const message = template.evaluate();\n        MailApp.sendEmail(\n          Session.getEffectiveUser().getEmail(),\n          \"Authorization Required\",\n          message.getContent(),\n          {\n            name: ADDON_TITLE,\n            htmlBody: message.getContent(),\n          },\n        );\n      }\n      settings.setProperty(\"lastAuthEmailDate\", today);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Sends out creator notification email(s) if the current number\n * of form responses is an even multiple of the response step\n * setting.\n */\nfunction sendCreatorNotification() {\n  try {\n    const form = FormApp.getActiveForm();\n    const settings = PropertiesService.getDocumentProperties();\n    let responseStep = settings.getProperty(\"responseStep\");\n    responseStep = responseStep ? Number.parseInt(responseStep) : 10;\n\n    // If the total number of form responses is an even multiple of the\n    // response step setting, send a notification email(s) to the form\n    // creator(s). For example, if the response step is 10, notifications\n    // will be sent when there are 10, 20, 30, etc. total form responses\n    // received.\n    if (form.getResponses().length % responseStep === 0) {\n      const addresses = settings.getProperty(\"creatorEmail\").split(\",\");\n      if (MailApp.getRemainingDailyQuota() > addresses.length) {\n        const template = HtmlService.createTemplateFromFile(\n          \"creatorNotification\",\n        );\n        template.summary = form.getSummaryUrl();\n        template.responses = form.getResponses().length;\n        template.title = form.getTitle();\n        template.responseStep = responseStep;\n        template.formUrl = form.getEditUrl();\n        template.notice = NOTICE;\n        const message = template.evaluate();\n        MailApp.sendEmail(\n          settings.getProperty(\"creatorEmail\"),\n          `${form.getTitle()}: Form submissions detected`,\n          message.getContent(),\n          {\n            name: ADDON_TITLE,\n            htmlBody: message.getContent(),\n          },\n        );\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n\n/**\n * Sends out respondent notification emails.\n *\n * @param {FormResponse} response FormResponse object of the event\n *      that triggered this notification\n */\nfunction sendRespondentNotification(response) {\n  try {\n    const form = FormApp.getActiveForm();\n    const settings = PropertiesService.getDocumentProperties();\n    const emailId = settings.getProperty(\"respondentEmailItemId\");\n    const emailItem = form.getItemById(Number.parseInt(emailId));\n    const respondentEmail = response\n      .getResponseForItem(emailItem)\n      .getResponse();\n    if (respondentEmail) {\n      const template = HtmlService.createTemplateFromFile(\n        \"respondentNotification\",\n      );\n      template.paragraphs = settings.getProperty(\"responseText\").split(\"\\n\");\n      template.notice = NOTICE;\n      const message = template.evaluate();\n      MailApp.sendEmail(\n        respondentEmail,\n        settings.getProperty(\"responseSubject\"),\n        message.getContent(),\n        {\n          name: form.getTitle(),\n          htmlBody: message.getContent(),\n        },\n      );\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", e.error);\n  }\n}\n// [END apps_script_forms_notifications_quickstart]\n"
  },
  {
    "path": "forms/notifications/respondentNotification.html",
    "content": "<!--Copyright Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. -->\n\n<!-- [START apps_script_forms_notifications_quickstart] -->\n<? for (var i = 0; i < paragraphs.length; i++) { ?>\n  <p><?= paragraphs[i] ?></p>\n<? } ?>\n\n<hr>\n\n<p style=\"font-size:80%\">This automatic message was sent to you via the <i>Form\nNotifications</i> add-on for Google Forms.\n<?= notice ?></p>\n<!-- [END apps_script_forms_notifications_quickstart] -->\n"
  },
  {
    "path": "forms/notifications/sidebar.html",
    "content": "<!--Copyright Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License. -->\n\n<!-- [START apps_script_forms_notifications_quickstart] -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n    <!-- The CSS package above applies Google styling to buttons and other elements. -->\n    <style>\n    .branding-below {\n      bottom: 54px;\n      top: 0;\n    }\n    .branding-text {\n      left: 7px;\n      position: relative;\n      top: 3px;\n    }\n    .logo {\n      vertical-align: middle;\n    }\n    .width-100 {\n      width: 100%;\n      box-sizing: border-box;\n      -webkit-box-sizing: border-box;\n      -moz-box-sizing: border-box;\n    }\n    label {\n      font-weight: bold;\n    }\n    #creator-options,\n    #respondent-options {\n      background-color: #eee;\n      border-color: #eee;\n      border-width: 5px;\n      border-style: solid;\n      display: none;\n    }\n    #creator-email,\n    #respondent-email,\n    #button-bar,\n    #submit-subject {\n      margin-bottom: 10px;\n    }\n\n    #response-step {\n      display: inline;\n    }\n    </style>\n  </head>\n  <body>\n    <div class=\"sidebar branding-below\">\n      <form>\n        <div class=\"block\">\n          <input type=\"checkbox\" id=\"creator-notify\">\n          <label for=\"creator-notify\">Notify me</label>\n        </div>\n        <div class=\"block form-group\" id=\"creator-options\">\n          <label for=\"creator-email\">\n            My email addresses (comma-separated)\n          </label>\n          <input type=\"text\" class=\"width-100\" id=\"creator-email\">\n          <label for=\"response-step\">Send notifications after every</label>\n          <input type=\"number\" id=\"response-step\" value=\"10\"\n              min=\"1\" max=\"99999\"> responses (default 10)\n        </div>\n\n        <div class=\"block\">\n          <input type=\"checkbox\" id=\"respondent-notify\">\n          <label for=\"respondent-notify\">Notify respondents</label>\n        </div>\n        <div class=\"block form-group\" id=\"respondent-options\">\n          <label for=\"respondent-email\">\n            Which question asks for their email?\n          </label>\n          <select class=\"width-100\" id=\"respondent-email\"></select>\n          <label for=\"submit-subject\">\n            Notification email subject:\n          </label>\n          <input type=\"text\" class=\"width-100\" id=\"submit-subject\">\n          <label for=\"submit-notice\">Notification email body:</label>\n          <textarea rows=\"8\" cols=\"40\" id=\"submit-notice\"\n              class=\"width-100\"></textarea>\n        </div>\n\n        <div class=\"block\" id=\"button-bar\">\n          <button class=\"action\" id=\"save-settings\">Save</button>\n        </div>\n      </form>\n    </div>\n\n    <div class=\"sidebar bottom\">\n      <img alt=\"Add-on logo\" class=\"logo\" width=\"25\"\n          src=\"https://g-suite-documentation-images.firebaseapp.com/images/newFormNotificationsicon.png\">\n      <span class=\"gray branding-text\">Form Notifications by Google</span>\n    </div>\n\n    <script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\">\n    </script>\n    <script>\n      /**\n       * On document load, assign required handlers to each element,\n       * and attempt to load any saved settings.\n       */\n      $(function() {\n        $('#save-settings').click(saveSettingsToServer);\n        $('#creator-notify').click(toggleCreatorNotify);\n        $('#respondent-notify').click(toggleRespondentNotify);\n        $('#response-step').change(validateNumber);\n        google.script.run\n           .withSuccessHandler(loadSettings)\n           .withFailureHandler(showStatus)\n           .withUserObject($('#button-bar').get())\n           .getSettings();\n      });\n\n      /**\n       * Callback function that populates the notification options using\n       * previously saved values.\n       *\n       * @param {Object} settings The saved settings from the client.\n       */\n      function loadSettings(settings) {\n        $('#creator-email').val(settings.creatorEmail);\n        $('#response-step').val(!settings.responseStep ?\n           10 : settings.responseStep);\n        $('#submit-subject').val(!settings.responseSubject ?\n           'Thank you for filling out our form!' :\n           settings.responseSubject);\n        $('#submit-notice').val(!settings.responseText ?\n           'Thank you for responding to our form!' :\n           settings.responseText);\n\n        if (settings.creatorNotify === 'true') {\n          $('#creator-notify').prop('checked', true);\n          $('#creator-options').show();\n        }\n\n        if (settings.respondentNotify === 'true') {\n          $('#respondent-notify').prop('checked', true);\n          $('#respondent-options').show();\n        }\n\n        // Fill the respondent email select box with the\n        // titles given to the form's text Items. Also include\n        // the form Item IDs as values so that they can be\n        // easily recovered during the Save operation.\n        for (var i = 0; i < settings.textItems.length; i++) {\n          var option = $('<option>').attr('value', settings.textItems[i]['id'])\n              .text(settings.textItems[i]['title']);\n          $('#respondent-email').append(option);\n        }\n        $('#respondent-email').val(settings.respondentEmailItemId);\n      }\n\n      /**\n       * Toggles the visibility of the form creator notification options.\n       */\n      function toggleCreatorNotify() {\n        $('#status').remove();\n        if ($('#creator-notify').is(':checked')) {\n          $('#creator-options').show();\n        } else {\n          $('#creator-options').hide();\n        }\n      }\n\n      /**\n       * Toggles the visibility of the form sumbitter notification options.\n       */\n      function toggleRespondentNotify() {\n        $('#status').remove();\n        if($('#respondent-notify').is(':checked')) {\n          $('#respondent-options').show();\n        } else {\n          $('#respondent-options').hide();\n        }\n      }\n\n      /**\n       * Ensures that the entered step is a number between 1\n       * and 99999, inclusive.\n       */\n      function validateNumber() {\n        var value = $('#response-step').val();\n        if (!value) {\n          $('#response-step').val(10);\n        } else if (value < 1) {\n          $('#response-step').val(1);\n        } else if (value > 99999) {\n          $('#response-step').val(99999);\n        }\n      }\n\n      /**\n       * Collects the options specified in the add-on sidebar and sends them to\n       * be saved as Properties on the server.\n       */\n      function saveSettingsToServer() {\n        this.disabled = true;\n        $('#status').remove();\n        var creatorNotify = $('#creator-notify').is(':checked');\n        var respondentNotify = $('#respondent-notify').is(':checked');\n        var settings = {\n          'creatorNotify': creatorNotify,\n          'respondentNotify': respondentNotify\n        };\n\n        // Only save creator options if notify is turned on\n        if (creatorNotify) {\n          settings.responseStep = $('#response-step').val();\n          settings.creatorEmail = $('#creator-email').val().trim();\n\n          // Abort save if entered email is blank\n          if (!settings.creatorEmail) {\n            showStatus('Enter an owner email', $('#button-bar'));\n            this.disabled = false;\n            return;\n          }\n        }\n\n        // Only save respondent options if notify is turned on\n        if (respondentNotify) {\n          settings.respondentEmailItemId = $('#respondent-email').val();\n          settings.responseSubject = $('#submit-subject').val();\n          settings.responseText = $('#submit-notice').val();\n        }\n\n        // Save the settings on the server\n        google.script.run\n            .withSuccessHandler(\n              function(msg, element) {\n                showStatus('Saved settings', $('#button-bar'));\n                element.disabled = false;\n              })\n            .withFailureHandler(\n              function(msg, element) {\n                showStatus(msg, $('#button-bar'));\n                element.disabled = false;\n              })\n            .withUserObject(this)\n            .saveSettings(settings);\n      }\n\n      /**\n       * Inserts a div that contains an status message after a given element.\n       *\n       * @param {String} msg The status message to display.\n       * @param {Object} element The element after which to display the Status.\n       */\n      function showStatus(msg, element) {\n         var div = $('<div>')\n             .attr('id', 'status')\n             .attr('class','error')\n             .text(msg);\n        $(element).after(div);\n      }\n    </script>\n  </body>\n</html>\n<!-- [END apps_script_forms_notifications_quickstart] -->\n"
  },
  {
    "path": "forms-api/demos/AppsScriptFormsAPIWebApp/Code.gs",
    "content": "// Copyright 2021 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     https://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nfunction doGet() {\n  return HtmlService.createTemplateFromFile(\"Main\").evaluate();\n}\n"
  },
  {
    "path": "forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs",
    "content": "// Copyright 2021 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     https://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// Global constants. Customize as needed.\nconst formsAPIUrl = \"https://forms.googleapis.com/v1/forms/\";\nconst formId = \"<YOUR_FORM_ID>\";\nconst topicName = \"projects/<YOUR_TOPIC_PATH>\";\n\n// To setup pub/sub topics, see:\n//   https://cloud.google.com/pubsub/docs/building-pubsub-messaging-system\n\n/**\n * Forms API Method: forms.create\n * POST https://forms.googleapis.com/v1/forms\n */\nfunction create(title) {\n  const accessToken = ScriptApp.getOAuthToken();\n  const jsonTitle = JSON.stringify({\n    info: {\n      title: title,\n    },\n  });\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n    },\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: jsonTitle,\n  };\n\n  console.log(`Forms API POST options was: ${JSON.stringify(options)}`);\n  const response = UrlFetchApp.fetch(formsAPIUrl, options);\n  console.log(`Response from Forms API was: ${JSON.stringify(response)}`);\n\n  return `${response}`;\n}\n\n/**\n * Forms API Method: forms.get\n * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId}\n */\nfunction get(formId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"get\",\n  };\n\n  try {\n    const response = UrlFetchApp.fetch(formsAPIUrl + formId, options);\n    console.log(`Response from Forms API was: ${response}`);\n    return `${response}`;\n  } catch (e) {\n    console.log(JSON.stringify(e));\n    return `Error:${JSON.stringify(e)}<br/><br/>Unable to find Form with formId:<br/>${formId}`;\n  }\n}\n\n/**\n * Forms API Method: forms.batchUpdate\n * POST https://forms.googleapis.com/v1/forms/{formId}:batchUpdate\n */\nfunction batchUpdate(formId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  // Request body to add a description to a Form\n  const update = {\n    requests: [\n      {\n        updateFormInfo: {\n          info: {\n            description:\n              \"Please complete this quiz based on this week's readings for class.\",\n          },\n          updateMask: \"description\",\n        },\n      },\n    ],\n  };\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n    },\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: JSON.stringify(update),\n    muteHttpExceptions: true,\n  };\n\n  const response = UrlFetchApp.fetch(\n    `${formsAPIUrl + formId}:batchUpdate`,\n    options,\n  );\n  console.log(`Response code from API: ${response.getResponseCode()}`);\n  return response.getResponseCode();\n}\n\n/**\n * Forms API Method: forms.responses.get\n * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId}\n */\nfunction responsesGet(formId, responseId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"get\",\n  };\n\n  try {\n    const response = UrlFetchApp.fetch(\n      `${formsAPIUrl + formId}/responses/${responseId}`,\n      options,\n    );\n    console.log(`Response from Forms.responses.get was: ${response}`);\n    return `${response}`;\n  } catch (e) {\n    console.log(JSON.stringify(e));\n    return `Error:${JSON.stringify(e)}`;\n  }\n}\n\n/**\n * Forms API Method: forms.responses.list\n * GET https://forms.googleapis.com/v1/forms/{formId}/responses\n */\nfunction responsesList(formId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"get\",\n  };\n\n  try {\n    const response = UrlFetchApp.fetch(\n      `${formsAPIUrl + formId}/responses`,\n      options,\n    );\n    console.log(`Response from Forms.responses was: ${response}`);\n    return `${response}`;\n  } catch (e) {\n    console.log(JSON.stringify(e));\n    return `Error:${JSON.stringify(e)}`;\n  }\n}\n\n/**\n * Forms API Method: forms.watches.create\n * POST https://forms.googleapis.com/v1/forms/{formId}/watches\n */\nfunction createWatch(formId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  const myWatch = {\n    watch: {\n      target: {\n        topic: {\n          topicName: topicName,\n        },\n      },\n      eventType: \"RESPONSES\",\n    },\n  };\n  console.log(`myWatch is: ${JSON.stringify(myWatch)}`);\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n    },\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: JSON.stringify(myWatch),\n    muteHttpExceptions: false,\n  };\n  console.log(`options are: ${JSON.stringify(options)}`);\n  console.log(`formsAPIURL was: ${formsAPIUrl}`);\n\n  const response = UrlFetchApp.fetch(\n    `${formsAPIUrl + formId}/watches`,\n    options,\n  );\n  console.log(response);\n  return `${response}`;\n}\n\n/**\n * Forms API Method: forms.watches.delete\n * DELETE https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId}\n */\nfunction deleteWatch(formId, watchId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  console.log(`formsAPIUrl is: ${formsAPIUrl}`);\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"delete\",\n    muteHttpExceptions: false,\n  };\n\n  try {\n    const response = UrlFetchApp.fetch(\n      `${formsAPIUrl + formId}/watches/${watchId}`,\n      options,\n    );\n    console.log(response);\n    return `${response}`;\n  } catch (e) {\n    console.log(`API Error: ${JSON.stringify(e)}`);\n    return JSON.stringify(e);\n  }\n}\n\n/**\n * Forms API Method: forms.watches.list\n * GET https://forms.googleapis.com/v1/forms/{formId}/watches\n */\nfunction watchesList(formId) {\n  console.log(`formId is: ${formId}`);\n  const accessToken = ScriptApp.getOAuthToken();\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"get\",\n  };\n  try {\n    const response = UrlFetchApp.fetch(\n      `${formsAPIUrl + formId}/watches`,\n      options,\n    );\n    console.log(response);\n    return `${response}`;\n  } catch (e) {\n    console.log(`API Error: ${JSON.stringify(e)}`);\n    return JSON.stringify(e);\n  }\n}\n\n/**\n * Forms API Method: forms.watches.renew\n * POST https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId}:renew\n */\nfunction renewWatch(formId, watchId) {\n  const accessToken = ScriptApp.getOAuthToken();\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${accessToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"post\",\n  };\n\n  try {\n    const response = UrlFetchApp.fetch(\n      `${formsAPIUrl + formId}/watches/${watchId}:renew`,\n      options,\n    );\n    console.log(response);\n    return `${response}`;\n  } catch (e) {\n    console.log(`API Error: ${JSON.stringify(e)}`);\n    return JSON.stringify(e);\n  }\n}\n"
  },
  {
    "path": "forms-api/demos/AppsScriptFormsAPIWebApp/Main.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n\n<head>\n   <title>Main Web Page</title>\n  <base target=\"_top\">\n  <style>\n    table,\n    th,\n    td {\n      table-layout: fixed;\n    }\n\n    th,\n    td {\n      padding: 5px;\n      vertical-align: top;\n    }\n\n    tr {\n      background-color: #f5f5f5;\n    }\n\n    h1 {\n      font-size: 30px;\n    }\n\n    h2 {\n      font-size: 25px;\n    }\n\n    p {\n      font-size: 14px;\n    }\n\n    b {\n      font-size: 16px;\n    }\n\n    a {\n      font-size: 12px;\n    }\n\n    div {\n      font-size: 14px;\n      padding: 10px 20px 15px;\n    }\n\n    a[disabled],\n    a[disabled]:hover {\n      pointer-events: none;\n      color: #e1e1e1;\n    }\n\n    .formInputs {\n      width: 100%;\n    }\n\n    div.scroll {\n      height: 600px;\n      width: 300px;\n      overflow: auto;\n      border: 1px solid #666;\n      background-color: #ccc;\n      padding: 8px;\n    }\n\n    pre {\n      font-size: 10px;\n    }\n  </style>\n  <script>\n    function resetFormId() {\n      let status = document.getElementById('status');\n      status.innerHTML = '';\n\n      let newFormId = document.getElementById('globalFormId').value;\n      document.getElementById('formAnchor').href =\n        'https://docs.google.com/forms/d/' + newFormId + '/edit';\n      document.getElementById('formAnchor').style.visibility = 'visible';\n    }\n\n    function create() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Creating new form...\";\n      const newFormTitle = document.getElementById('newFormTitle');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function create: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response in client from Forms API: ' + JSON\n            .stringify(result));\n          let resObj = JSON.parse(result);\n          let newFormId = resObj[\"formId\"];\n          console.log('New Form Id is: ' + newFormId);\n          document.getElementById('globalFormId').value = newFormId;\n          resetFormId();\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).create(newFormTitle.value);\n    }\n\n    function get() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Get form by id...\";\n      const formId = document.getElementById('globalFormId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function get: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).get(formId.value);\n    }\n\n    function batchUpdate() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Batch update...\";\n      const formId = document.getElementById('globalFormId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function batchUpdate: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>Success! Response code from API was: ' +\n            result + '</pre>';\n\n        }).batchUpdate(globalFormId.value);\n    }\n\n\n    function responsesList() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Get form by id...\";\n      const formId = document.getElementById('globalFormId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function responseList: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).responsesList(formId.value);\n    }\n\n    function responsesGet() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Get response by id...\";\n      const formId = document.getElementById('globalFormId');\n      const respId = document.getElementById('responseId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function responseList: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).responsesGet(formId.value, respId.value);\n    }\n\n    function watchesList() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Get watches ...\";\n      const formId = document.getElementById('globalFormId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function responseList: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).watchesList(formId.value);\n    }\n\n    function createWatch() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Create watch ...\";\n      const formId = document.getElementById('globalFormId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function createWatch: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).createWatch(formId.value);\n    }\n\n    function deleteWatch() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Delete watch ...\";\n      const formId = document.getElementById('globalFormId');\n      const watchId = document.getElementById('watchId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function deleteWatch: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).deleteWatch(formId.value, watchId.value);\n    }\n\n    function renewWatch() {\n      const status = document.getElementById('status');\n      status.innerHTML = \"Renew watch ...\";\n      const formId = document.getElementById('globalFormId');\n      const watchId = document.getElementById('renewWatchId');\n\n      google.script.run\n        .withFailureHandler(function(error) {\n          console.log('Error calling server function renewWatch: ' + error);\n        })\n        .withSuccessHandler(function(result) {\n          console.log('Raw response from Forms API: ' + result);\n          status.innerHTML = '<pre>' + result + '</pre>';\n\n        }).renewWatch(formId.value, watchId.value);\n    }\n  </script>\n</head>\n\n<body style=\"font-family: arial\">\n  <h2><img src=\"https://www.gstatic.com/images/branding/product/2x/forms_2020q4_48dp.png\" alt=\"Forms API logo\" style=\"width:40px;height:40px;\">Forms API &#38; Apps Script Testing\n    Application - v.1 </h2>\n  <div>\n    <strong>Form Id: </strong><input type=\"text\" id=\"globalFormId\" size=\"50\" value=\"<?= formId ?>\">\n    <button id=\"resetFormButton\" onclick=\"resetFormId()\">Set form id</button>\n    <a id=\"formAnchor\" style=\"visibility: hidden\" href=\"<?= formId ?>\" target=\"_blank\">Open in Forms</a>\n  </div>\n  <table style=\"width:100%\">\n    <tr>\n      <th style=\"width:316px\">Methods</th>\n      <th>Status</th>\n    </tr>\n    <tr>\n      <td>\n        <div class=\"scroll\">\n          <table style=\"width:100px\">\n            <tr>\n              <div>\n                <b>forms.create</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms/create\" target=\"_blank\">(spec)</a><br />\n                Form title: <input type=\"text\" id=\"newFormTitle\" class=\"formInputs\" value=\"My new Form!\">\n                <button id=\"createButton\" onclick=\"create()\">Create\n                  Form</button>\n              </div>\n\n            </tr>\n            <tr>\n              <div>\n                <b>forms.get</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms/get\" target=\"_blank\">(spec)</a><br />\n                <button id=\"getButton\" onclick=\"get()\">Get Form by id</button>\n              </div>\n\n            </tr>\n            <tr>\n              <div>\n                <b>forms.batchUpdate</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms/batchUpdate\" target=\"_blank\">(spec)</a><br />\n                <!--Form id: <input type=\"text\" id=\"batchId\" class=\"formInputs\" value=\"<?= formId ?>\" ><br/>-->\n                <button id=\"batchUpdateButton\" onclick=\"batchUpdate()\">batchUpdate</button>\n              </div>\n\n            </tr>\n            <tr>\n              <div>\n                <b>forms.responses.list</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.responses/list\" target=\"_blank\">(spec)</a><br />\n                <button id=\"respListButton\" onclick=\"responsesList()\">List a\n                  form's responses</button>\n              </div>\n              <br />\n            </tr>\n            <tr>\n              <div>\n                <b>forms.responses.get</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.responses/get\" target=\"_blank\">(spec)</a><br />\n                Response id: <input type=\"text\" id=\"responseId\" class=\"formInputs\"><br />\n                <button id=\"getListButton\" onclick=\"responsesGet()\">Get one\n                  response from the form</button>\n              </div>\n              <br />\n            </tr>\n            <tr>\n              <div>\n                <b>forms.watches.create</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.watches/create\" target=\"_blank\">(spec)</a><br />\n                <button id=\"createWatchButton\" onclick=\"createWatch()\">Create\n                  Watch</button>\n              </div>\n              <br />\n            </tr>\n            <tr>\n              <div>\n                <b>forms.watches.delete</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.watches/delete\" target=\"_blank\">(spec)</a><br />\n                Watch id: <input type=\"text\" id=\"watchId\" class=\"formInputs\"><br />\n                <button id=\"deleteWatchButton\" onclick=\"deleteWatch()\">Delete\n                  Watch</button>\n              </div>\n              <br />\n            </tr>\n            <tr>\n              <div>\n                <b>forms.watches.list</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.watches/list\" target=\"_blank\">(spec)</a><br />\n                <button id=\"watchListButton\" onclick=\"watchesList()\">Get Watches\n                  by Form id</button>\n              </div>\n              <br />\n            </tr>\n            <tr>\n              <div>\n                <b>forms.watches.renew</b>\n                <a href=\"https://developers.google.com/forms/api/reference/rest/v1/forms.watches/renew\" target=\"_blank\">(spec)</a><br />\n                Watch id:<input type=\"text\" id=\"renewWatchId\" class=\"formInputs\"><br />\n                <button id=\"renewWatchButton\" onclick=\"renewWatch()\">Renew\n                  Watch</button>\n\n              </div>\n            </tr>\n          </table>\n        </div>\n      </td>\n      <td>\n        <div id=\"status\" />\n      </td>\n    </tr>\n  </table>\n  <div>\n    Forms API <a href=\"https://developers.google.com/forms/api/reference/rest\" target=\"_blank\">Reference</a>\n  </div>\n\n</body>\n</html>"
  },
  {
    "path": "forms-api/demos/AppsScriptFormsAPIWebApp/README.md",
    "content": "# Google Forms API Apps Script web app\n\nThis solution demonstrates how to interact with the new Google Forms API directly from Apps Script using REST calls, not the native Apps Script Forms Service.\n\n## General setup\n\n* Enable the Forms API for your Google Cloud project\n\n## Web app setup\n\n1. Create a new blank Apps Script project.\n\n1. Click **Project Settings**, then:\n    * Check **Show \"appsscript.json\" manifest file in editor**.\n    * Enter the project number of the Google Cloud project that has the\n       Forms API enabled and click **Change project**.\n\n1. Copy the contents of the Apps Script, HTML and JSON files into your\n   Apps Script project.\n\n1. Edit the `FormsAPI.gs` file to customize the constants.\n    * `formId`: Choose a `formId` from an existing form.\n    * `topicName`: Optional, if using watches (pub/sub).\n\n      Note: Further project setup is required to use the watch features. To\n      set up pub/sub topics, see\n      [Google Cloud Pubsub](https://cloud.google.com/pubsub/docs/building-pubsub-messaging-system)\n      for additional details.\n\n1. Deploy the project as a Web app, authorize access and click on the\n   deployment URL.\n\n"
  },
  {
    "path": "forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {},\n  \"webapp\": {\n    \"executeAs\": \"USER_DEPLOYING\",\n    \"access\": \"MYSELF\"\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/drive\",\n    \"https://www.googleapis.com/auth/drive.readonly\",\n    \"https://www.googleapis.com/auth/forms.body\",\n    \"https://www.googleapis.com/auth/forms.body.readonly\",\n    \"https://www.googleapis.com/auth/forms.responses.readonly\",\n    \"https://www.googleapis.com/auth/userinfo.email\"\n  ]\n}\n"
  },
  {
    "path": "forms-api/snippets/README.md",
    "content": "# Forms API\n\nTo run, you must set up your GCP project to use the Forms API.\nSee: [Forms API](https://developers.google.com/forms/api/)\n"
  },
  {
    "path": "forms-api/snippets/retrieve_all_responses.gs",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START forms_retrieve_all_responses]\nfunction callFormsAPI() {\n  console.log(\"Calling the Forms API!\");\n  const formId = \"<YOUR_FORM_ID>\";\n\n  // Get OAuth Token\n  const OAuthToken = ScriptApp.getOAuthToken();\n  console.log(`OAuth token is: ${OAuthToken}`);\n\n  const formsAPIUrl = `https://forms.googleapis.com/v1/forms/${formId}/responses`;\n  console.log(`formsAPIUrl is: ${formsAPIUrl}`);\n\n  const options = {\n    headers: {\n      Authorization: `Bearer ${OAuthToken}`,\n      Accept: \"application/json\",\n    },\n    method: \"get\",\n  };\n\n  const response = UrlFetchApp.fetch(formsAPIUrl, options);\n  console.log(`Response from forms.responses was: ${response}`);\n}\n// [END forms_retrieve_all_responses]\n"
  },
  {
    "path": "gmail/README.md",
    "content": "# Apps Scripts for Gmail\n\nSample Google Apps Script functions for Gmail.\n\n## [Mail Merge](https://developers.google.com/apps-script/articles/mail_merge)\n\nThis tutorial shows an easy way to collect information from different users in a spreadsheet using Google Forms, then leverage it to generate and distribute personalized emails.\n\n## [Sending Emails](https://developers.google.com/apps-script/articles/sending_emails)\n\nThis tutorial shows how to use Spreadsheet data to send emails to different people.\n\n## [Inline Image](inlineimage/inlineimage.gs)\n\nThis example shows how to send an HTML email that includes an inline image attachment.\n"
  },
  {
    "path": "gmail/add-ons/appsscript.json",
    "content": "{\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/gmail.addons.execute\",\n    \"https://www.googleapis.com/auth/gmail.addons.current.message.metadata\",\n    \"https://www.googleapis.com/auth/gmail.modify\"\n  ],\n  \"gmail\": {\n    \"name\": \"Gmail Add-on Quickstart - QuickLabels\",\n    \"logoUrl\": \"https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png\",\n    \"contextualTriggers\": [\n      {\n        \"unconditional\": {},\n        \"onTriggerFunction\": \"buildAddOn\"\n      }\n    ],\n    \"openLinkUrlPrefixes\": [\"https://mail.google.com/\"],\n    \"primaryColor\": \"#4285F4\",\n    \"secondaryColor\": \"#4285F4\"\n  }\n}\n"
  },
  {
    "path": "gmail/add-ons/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_gmail_quick_start]\n/**\n * Returns the array of cards that should be rendered for the current\n * e-mail thread. The name of this function is specified in the\n * manifest 'onTriggerFunction' field, indicating that this function\n * runs every time the add-on is started.\n *\n * @param {Object} e The data provided by the Gmail UI.\n * @return {Card[]}\n */\nfunction buildAddOn(e) {\n  // Activate temporary Gmail add-on scopes.\n  const accessToken = e.messageMetadata.accessToken;\n  GmailApp.setCurrentMessageAccessToken(accessToken);\n\n  const messageId = e.messageMetadata.messageId;\n  const message = GmailApp.getMessageById(messageId);\n\n  // Get user and thread labels as arrays to enable quick sorting and indexing.\n  const threadLabels = message.getThread().getLabels();\n  const labels = getLabelArray(GmailApp.getUserLabels());\n  const labelsInUse = getLabelArray(threadLabels);\n\n  // Create a section for that contains all user Labels.\n  const section = CardService.newCardSection().setHeader(\n    '<font color=\"#1257e0\"><b>Available User Labels</b></font>',\n  );\n\n  // Create a checkbox group for user labels that are added to prior section.\n  const checkboxGroup = CardService.newSelectionInput()\n    .setType(CardService.SelectionInputType.CHECK_BOX)\n    .setFieldName(\"labels\")\n    .setOnChangeAction(CardService.newAction().setFunctionName(\"toggleLabel\"));\n\n  // Add checkbox with name and selected value for each User Label.\n  for (let i = 0; i < labels.length; i++) {\n    checkboxGroup.addItem(\n      labels[i],\n      labels[i],\n      labelsInUse.indexOf(labels[i]) !== -1,\n    );\n  }\n\n  // Add the checkbox group to the section.\n  section.addWidget(checkboxGroup);\n\n  // Build the main card after adding the section.\n  const card = CardService.newCardBuilder()\n    .setHeader(\n      CardService.newCardHeader()\n        .setTitle(\"Quick Label\")\n        .setImageUrl(\n          \"https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png\",\n        ),\n    )\n    .addSection(section)\n    .build();\n\n  return [card];\n}\n\n/**\n * Updates the labels on the current thread based on\n * user selections. Runs via the OnChangeAction for\n * each CHECK_BOX created.\n *\n * @param {Object} e The data provided by the Gmail UI.\n */\nfunction toggleLabel(e) {\n  const selected = e.formInputs.labels;\n\n  // Activate temporary Gmail add-on scopes.\n  const accessToken = e.messageMetadata.accessToken;\n  GmailApp.setCurrentMessageAccessToken(accessToken);\n\n  const messageId = e.messageMetadata.messageId;\n  const message = GmailApp.getMessageById(messageId);\n  const thread = message.getThread();\n\n  if (selected != null) {\n    for (const label of GmailApp.getUserLabels()) {\n      if (selected.indexOf(label.getName()) !== -1) {\n        thread.addLabel(label);\n      } else {\n        thread.removeLabel(label);\n      }\n    }\n  } else {\n    for (const label of GmailApp.getUserLabels()) {\n      thread.removeLabel(label);\n    }\n  }\n}\n\n/**\n * Converts an GmailLabel object to a array of strings.\n * Used for easy sorting and to determine if a value exists.\n *\n * @param {labelsObjects} A GmailLabel object array.\n * @return {lables[]} An array of labels names as strings.\n */\nfunction getLabelArray(labelsObjects) {\n  const labels = [];\n\n  for (let i = 0; i < labelsObjects.length; i++) {\n    labels[i] = labelsObjects[i].getName();\n  }\n  labels.sort();\n  return labels;\n}\n\n// [END apps_script_gmail_quick_start]\n"
  },
  {
    "path": "gmail/inlineimage/inlineimage.gs",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction sendEmailToMyself() {\n  // You can use this method to test the welcome email.\n  sendEmailWithInlineImage(Session.getActiveUser().getEmail());\n}\n\nfunction sendEmailWithInlineImage(toAddress) {\n  const options = {};\n  const imageName = \"cat_emoji\";\n  // The URL \"cid:cat_emoji\" means that the inline attachment named \"cat_emoji\" would be used.\n  options.htmlBody = `Welcome! <img src=\"cid:${imageName}\" alt=\"Cat Emoji\" />`;\n  options.inlineImages = {\n    [imageName]: Utilities.newBlob(getImageBinary(), \"image/png\", imageName),\n  };\n  GmailApp.sendEmail(toAddress, \"Welcome!\", \"Welcome!\", options);\n}\n\nfunction getImageBinary() {\n  // Cat Face Emoji from https://github.com/googlefonts/noto-emoji/blob/main/png/32/emoji_u1f431.png, Base64 encoded.\n  const catPngBase64 =\n    \"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+BJREFUeNrsV01ME1EQnpaltPK3iAT0oAsxMSYmlIOaGBO2etCDMTVq8CYl3jRBvehBI0YPehIPxhvFkxo1gHpQE9P15+KtROJFI6sxhEKwW6FY27o6s/vabpd9tPUn8eAkj8e+nTffzDez814B/kX5oXT4/7A9GceAk12Xg/IkThIOFUfIJb9XfgNYxCmMI8iWNLTXZNVx2zYEGTiwOUKe/wZ4xAJOIhIbXAdQuo2/dacB6i8gP7X0dA43hSsEJ+eJST9UtZv2fIdyr5d1wMyRsMkcBSd6y2WCRT5C0RrgZKN6K4C3x1FfcFw1QSFvYP4sWk4SE1F426gyRyVo/mbqzdUgiK6BoEcBkv35yAsBcEUoGRIZ8uwA+PYAQHeNgPsHzUv1MjYyfT0lwZ1S4Cz6DNNG8LoMX8+XLfz/9XZhXwUOaMUJTQJ8OYnRvSqs1VpAyCEaTu++T5p7aa7AgXGTzlfmRsq93cCKbHHE1qjt7FAAORvZidyqwm1E7BuNlORtoRoNou8iK0INi1DQ+emhWqBhpqQdm5HKK8JoWTVhB8o5wv02k+bA7moFX5ICfKmV7cQfErdDBys6MNTpLAzeS4AynirLoLagQ+jyLOw7G3PaI9lbsT0FQfuOwMkpwwmS8KkW6N1Vv6wDJ67NwfDjebPaxr9C/L5kV5GthWj/Cjrt2jlwkrGXiyUZUGPZIjYcWOgeGhrqxSHnGaAFKqVE5rq/sXqOa1ysK923pFahSF/u9Oaf3yS2wJsvm/2szhRrCuhBfjGzV6xyZ6Gr6Tm0eT8YLwYON8HAjbhhrH9/Y97Y+eE4KFEzOqlNgCvHmg2dK0ebjci1pI76DXn9d/OdkNa9sGdNOOrbOXGC1wciC1lRTus1sNIT40ZJwIHjU0VrkcE1IPu93D2f063wMbkB4ukWTU1uJAbUvr6+kAvpP44PhyllDdWfJcGVkbauepJngCehS7Mw/MgsNtnvg5GLrcumiBjwuFPgqUopq3dHAjwG6Mw/xzPStEeF8OkWCG6vNWhuP/TRmOMPJQM8x8zkrbVGWqzyNHYQ6oQELGbrFWTgKhGJDGh5LWLi5ofFbtEzC6sxej/WwZICQ6P7zsSMiNXpjAFO0nXkE/jX18DoyyTOniXgJDtb78B0ah281raNsV5DTU9xMXCR9QAl1HExbL82WT8rKr7ou7Tx3H+gASOvgqt3E8Y7azHyyge7baDUrbi8A+nXpAsdiC57IWHX8PN/ATxkB3dkoNyCrEA0Bj5a0ZUMN5ADAfsFokLgQXb+j3JxKrjnB9nvBpFTpLmjnM77ZzhG2fH+X/5t+SnAAE+HjvApIyOGAAAAAElFTkSuQmCC\";\n  return Utilities.base64Decode(catPngBase64);\n}\n"
  },
  {
    "path": "gmail/markup/Code.gs",
    "content": "// [START gmail_send_email_with_markup]\n/**\n * Send an email with schemas in order to test email markup.\n */\nfunction testSchemas() {\n  try {\n    const htmlBody =\n      HtmlService.createHtmlOutputFromFile(\"mail_template\").getContent();\n\n    MailApp.sendEmail({\n      to: Session.getActiveUser().getEmail(),\n      subject: `Test Email markup - ${new Date()}`,\n      htmlBody: htmlBody,\n    });\n  } catch (err) {\n    console.log(err.message);\n  }\n}\n// [END gmail_send_email_with_markup]\n"
  },
  {
    "path": "gmail/markup/mail_template.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <script type=\"application/ld+json\">\n    {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"EmailMessage\",\n      \"description\": \"Check this out\",\n      \"potentialAction\": {\n        \"@type\": \"ViewAction\",\n        \"target\": \"https://www.youtube.com/watch?v=eH8KwfdkSqU\"\n      }\n    }\n    </script>\n  </head>\n  <body>\n    <p>\n      This a test for a Go-To action in Gmail.\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "gmail/quickstart/quickstart.gs",
    "content": "/**\n * Copyright  Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START gmail_quickstart]\n/**\n * Lists all labels in the user's mailbox\n * @see https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list\n */\nfunction listLabels() {\n  try {\n    // Gmail.Users.Labels.list() API returns the list of all Labels in user's mailbox\n    const response = Gmail.Users.Labels.list(\"me\");\n    if (!response || response.labels.length === 0) {\n      // TODO (developer) - No labels are returned from the response\n      console.log(\"No labels found.\");\n      return;\n    }\n    // Print the Labels that are available.\n    console.log(\"Labels:\");\n    for (const label of response.labels) {\n      console.log(\"- %s\", label.name);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception on Labels.list() API\n    console.log(\"Labels.list() API failed with error %s\", err.toString());\n  }\n}\n// [END gmail_quickstart]\n"
  },
  {
    "path": "gmail/sendingEmails/sendingEmails.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START gmail_send_emails]\n/**\n * Sends emails with data from the current spreadsheet.\n */\nfunction sendEmails() {\n  try {\n    const sheet = SpreadsheetApp.getActiveSheet(); // Get the active sheet in spreadsheet\n    const startRow = 2; // First row of data to process\n    const numRows = 2; // Number of rows to process\n    const dataRange = sheet.getRange(startRow, 1, numRows, 2); // Fetch the range of cells A2:B3\n    const data = dataRange.getValues(); // Fetch values for each row in the Range.\n    for (const row of data) {\n      const emailAddress = row[0]; // First column\n      const message = row[1]; // Second column\n      const subject = \"Sending emails from a Spreadsheet\";\n      MailApp.sendEmail(emailAddress, subject, message); // Send emails to emailAddresses which are presents in First column\n    }\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_send_emails]\n\n// [START gmail_send_non_duplicate_emails]\n/**\n * Sends non-duplicate emails with data from the current spreadsheet.\n */\nfunction sendNonDuplicateEmails() {\n  const EMAIL_SENT = \"email sent\"; //This constant is used to write the message in Column C of Sheet\n  try {\n    const sheet = SpreadsheetApp.getActiveSheet(); // Get the active sheet in spreadsheet\n    const startRow = 2; // First row of data to process\n    const numRows = 2; // Number of rows to process\n    const dataRange = sheet.getRange(startRow, 1, numRows, 3); // Fetch the range of cells A2:B3\n    const data = dataRange.getValues(); // Fetch values for each row in the Range.\n    for (let i = 0; i < data.length; ++i) {\n      const row = data[i];\n      const emailAddress = row[0]; // First column\n      const message = row[1]; // Second column\n      const emailSent = row[2]; // Third column\n      if (emailSent === EMAIL_SENT) {\n        console.log(\"Email already sent\");\n        return;\n      }\n      const subject = \"Sending emails from a Spreadsheet\";\n      MailApp.sendEmail(emailAddress, subject, message); // Send emails to emailAddresses which are presents in First column\n      sheet.getRange(startRow + i, 3).setValue(EMAIL_SENT);\n      SpreadsheetApp.flush(); // Make sure the cell is updated right away in case the script is interrupted\n    }\n  } catch (err) {\n    console.log(err);\n  }\n}\n// [END gmail_send_non_duplicate_emails]\n"
  },
  {
    "path": "gmail-sentiment-analysis/.clasp.json",
    "content": "{\n  \"scriptId\": \"1Z2gfvr0oYn68ppDtQbv0qIuKKVWhvwOTr-gCE0GFKVjNk8NDlpfJAGAr\"\n}\n"
  },
  {
    "path": "gmail-sentiment-analysis/Cards.gs",
    "content": "/*\nCopyright 2024-2025 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Builds the main card displayed on the Gmail homepage.\n *\n * @returns {Card} - The homepage card.\n */\nfunction buildHomepageCard() {\n  // Create a new card builder\n  const cardBuilder = CardService.newCardBuilder();\n\n  // Create a card header\n  const cardHeader = CardService.newCardHeader();\n  cardHeader.setImageUrl(\n    \"https://fonts.gstatic.com/s/i/googlematerialicons/mail/v6/black-24dp/1x/gm_mail_black_24dp.png\",\n  );\n  cardHeader.setImageStyle(CardService.ImageStyle.CIRCLE);\n  cardHeader.setTitle(\"Analyze your Gmail\");\n\n  // Add the header to the card\n  cardBuilder.setHeader(cardHeader);\n\n  // Create a card section\n  const cardSection = CardService.newCardSection();\n\n  // Create buttons for generating sample emails and analyzing sentiment\n  const buttonSet = CardService.newButtonSet();\n\n  // Create \"Generate sample emails\" button\n  const generateButton = createFilledButton(\n    \"Generate sample emails\",\n    \"generateSampleEmails\",\n    \"#34A853\",\n  );\n  buttonSet.addButton(generateButton);\n\n  // Create \"Analyze emails\" button\n  const analyzeButton = createFilledButton(\n    \"Analyze emails\",\n    \"analyzeSentiment\",\n    \"#FF0000\",\n  );\n  buttonSet.addButton(analyzeButton);\n\n  // Add the button set to the section\n  cardSection.addWidget(buttonSet);\n\n  // Add the section to the card\n  cardBuilder.addSection(cardSection);\n\n  // Build and return the card\n  return cardBuilder.build();\n}\n\n/**\n * Creates a filled text button with the specified text, function, and color.\n *\n * @param {string} text - The text to display on the button.\n * @param {string} functionName - The name of the function to call when the button is clicked.\n * @param {string} color - The background color of the button.\n * @returns {TextButton} - The created text button.\n */\nfunction createFilledButton(text, functionName, color) {\n  // Create a new text button\n  const textButton = CardService.newTextButton();\n\n  // Set the button text\n  textButton.setText(text);\n\n  // Set the action to perform when the button is clicked\n  const action = CardService.newAction();\n  action.setFunctionName(functionName);\n  textButton.setOnClickAction(action);\n\n  // Set the button style to filled\n  textButton.setTextButtonStyle(CardService.TextButtonStyle.FILLED);\n\n  // Set the background color\n  textButton.setBackgroundColor(color);\n\n  return textButton;\n}\n\n/**\n * Creates a notification response with the specified text.\n *\n * @param {string} notificationText - The text to display in the notification.\n * @returns {ActionResponse} - The created action response.\n */\nfunction buildNotificationResponse(notificationText) {\n  // Create a new notification\n  const notification = CardService.newNotification();\n  notification.setText(notificationText);\n\n  // Create a new action response builder\n  const actionResponseBuilder = CardService.newActionResponseBuilder();\n\n  // Set the notification for the action response\n  actionResponseBuilder.setNotification(notification);\n\n  // Build and return the action response\n  return actionResponseBuilder.build();\n}\n"
  },
  {
    "path": "gmail-sentiment-analysis/Code.gs",
    "content": "/*\nCopyright 2024 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Triggered when the add-on is opened from the Gmail homepage.\n *\n * @param {Object} e - The event object.\n * @returns {Card} - The homepage card.\n */\nfunction onHomepageTrigger(e) {\n  return buildHomepageCard();\n}\n"
  },
  {
    "path": "gmail-sentiment-analysis/Gmail.gs",
    "content": "/*\nCopyright 2024-2025 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Analyzes the sentiment of the first 10 threads in the inbox\n * and labels them accordingly.\n *\n * @returns {ActionResponse} - A notification confirming completion.\n */\nfunction analyzeSentiment() {\n  // Analyze and label emails\n  analyzeAndLabelEmailSentiment();\n\n  // Return a notification\n  return buildNotificationResponse(\"Successfully completed sentiment analysis\");\n}\n\n/**\n * Analyzes the sentiment of emails and applies appropriate labels.\n */\nfunction analyzeAndLabelEmailSentiment() {\n  // Define label names\n  const labelNames = [\"HAPPY TONE 😊\", \"NEUTRAL TONE 😐\", \"UPSET TONE 😡\"];\n\n  // Get or create labels for each sentiment\n  const positiveLabel =\n    GmailApp.getUserLabelByName(labelNames[0]) ||\n    GmailApp.createLabel(labelNames[0]);\n  const neutralLabel =\n    GmailApp.getUserLabelByName(labelNames[1]) ||\n    GmailApp.createLabel(labelNames[1]);\n  const negativeLabel =\n    GmailApp.getUserLabelByName(labelNames[2]) ||\n    GmailApp.createLabel(labelNames[2]);\n\n  // Get the first 10 threads in the inbox\n  const threads = GmailApp.getInboxThreads(0, 10);\n\n  // Iterate through each thread\n  for (const thread of threads) {\n    // Iterate through each message in the thread\n    const messages = thread.getMessages();\n    for (const message of messages) {\n      // Get the plain text body of the message\n      const emailBody = message.getPlainBody();\n\n      // Analyze the sentiment of the email body\n      const sentiment = processSentiment(emailBody);\n\n      // Apply the appropriate label based on the sentiment\n      if (sentiment === \"positive\") {\n        thread.addLabel(positiveLabel);\n      } else if (sentiment === \"neutral\") {\n        thread.addLabel(neutralLabel);\n      } else if (sentiment === \"negative\") {\n        thread.addLabel(negativeLabel);\n      }\n    }\n  }\n}\n\n/**\n * Generates sample emails for testing the sentiment analysis.\n *\n * @returns {ActionResponse} - A notification confirming email generation.\n */\nfunction generateSampleEmails() {\n  // Get the current user's email address\n  const userEmail = Session.getActiveUser().getEmail();\n\n  // Define sample emails\n  const sampleEmails = [\n    {\n      subject: \"Thank you for amazing service!\",\n      body: \"Hi, I really enjoyed working with you. Thank you again!\",\n      name: \"Customer A\",\n    },\n    {\n      subject: \"Request for information\",\n      body: \"Hello, I need more information on your recent product launch. Thank you.\",\n      name: \"Customer B\",\n    },\n    {\n      subject: \"Complaint!\",\n      body: \"\",\n      htmlBody: `<p>Hello, You are late in delivery, again.</p>\n<p>Please contact me ASAP before I cancel our subscription.</p>`,\n      name: \"Customer C\",\n    },\n  ];\n\n  // Send each sample email\n  for (const email of sampleEmails) {\n    GmailApp.sendEmail(userEmail, email.subject, email.body, {\n      name: email.name,\n      htmlBody: email.htmlBody,\n    });\n  }\n\n  // Return a notification\n  return buildNotificationResponse(\"Successfully generated sample emails\");\n}\n"
  },
  {
    "path": "gmail-sentiment-analysis/README.md",
    "content": "# Gmail Sentiment Analysis with Gemini and Vertex AI\n\nThis project guides you through building a Google Workspace Add-on that\nleverages Gemini and Vertex AI for conducting sentiment analysis on emails in\nGmail. The add-on automatically identifies emails with different tones and\nlabels them accordingly, helping prioritize customer service responses or\nidentify potentially sensitive emails.\n\n> [!NOTE]\nYou can also run this lab on [https://www.cloudskillsboost.google/catalog_lab/31942](Cloud Skills Boost).\n\n## What you'll learn\n\n* Build a Google Workspace Add-on\n* Integrate Vertex AI with Google Workspace\n* Implement OAuth2 authentication\n* Apply sentiment analysis\n* Utilize Apps Script\n\n## Setup and Requirements\n\n* **Web Browser:** Chrome (recommended)\n* **Dedicated Time:** Set aside uninterrupted time.\n* **Incognito/Private Window:**  **Important:** Use an incognito or private browsing window to prevent conflicts with your personal accounts.\n\n## Steps\n\n### Setup Google Cloud Platform\n\n1. Create a new project.\n2. Associate a billing account with the project.\n3. Enable the Vertex AI API.\n\n### Setup an Apps Script Project\n\n1. Navigate to [https://script.google.com](Apps Script homepage).\n2. Click **New project**.\n3. Rename the project to \"Gmail Sentiment Analysis with Gemini and Vertex AI\".\n4. In Project Settings (gear icon), select \"Show 'appsscript.json' manifest file in editor\".\n5. In Project Settings, under Google Cloud Platform (GCP) Project, click **Change project**.\n6. Copy the **Project number** (numerical value, not Project ID) from Cloud Console.\n7. Paste the Project number into the Apps Script project settings and click **Set project**.\n8. Click the **OAuth Consent details** link in the error message.\n9. Click **Configure Consent Screen**.\n10. Click on **Get started** and follow the prompts to configure the consent screen as follows:\n    1.  Set the App name to \"Gmail Sentiment Analysis with Gemini and Vertex AI\".\n    2.  Set the User support email to your email.\n    3.  Select **Internal** for Audience.\n    4.  Set the email address under Contact Information to your email.\n    5.  Review and agree to the \"Google API Services: User Data Policy\".\n    6.  Click on **Create**.\n11. Return to the Apps Script tab and set the project number again. You should not get an error this time.\n\n### Populate the Apps Script project with code\n\n1. Replace the content of `appsscript.json` and `Code.gs` with the code from this repo.\n2. Create new files (`Cards`, `Gmail`, `Vertex`) and replace the contect with the relevant code from this repo.\n3. Open the `Vertex.gs` file and replace the `PROJECT_ID` value with your Google Cloud project ID.\n4. Make sure to save the content before proceeding.\n\n### Deploy the Add-on\n\n1. On the Apps Script screen, click **Deploy > Test deployments**.\n2. Confirm **Gmail** is listed under Application(s) and click **Install**.\n3. Click **Done**.\n\n### Verify Installation\n\nOpen [https://mail.google.com/](Gmail) and expand the right side panel. You should see a new add-on icon in the right side panel.\n\n**Troubleshooting:**\n\n* Refresh the browser if the add-on isn't visible.\n* Uninstall and reinstall the add-on from the Test deployments window if it's still missing.\n\n### Run the Add-on\n\n1. **Open the Add-on:** Click the add-on icon in the Gmail side panel.\n2. **Authorize the Add-on:** Grant the necessary permissions for the add-on to access your inbox and connect with Vertex AI.\n3. **Generate sample emails:** Click the green \"Generate sample emails\" button.\n4. **Wait for emails:** Wait for the sample emails to appear in your inbox, or refresh your inbox.\n5. **Start the analysis:** Click the red \"Analyze emails\" button.\n6. **Wait for labels:** Wait for the \"UPSET TONE 😡\" label to appear on negative emails, or refresh.\n7. **Close the Add-on:** Click the X in the top right corner of the side panel.\n\n\n## Congratulations!\n\nYou've completed the Gmail Sentiment Analysis with Gemini and Vertex AI lab!\nYou now have a functional Gmail add-on for prioritizing emails. Experiment\nfurther by customizing the sentiment analysis or adding new features!\n"
  },
  {
    "path": "gmail-sentiment-analysis/Vertex.gs",
    "content": "/*\nCopyright 2024-2025 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Replace with your project ID\nconst PROJECT_ID = \"[ADD YOUR GCP PROJECT ID HERE]\";\n\n// Location for your Vertex AI model\nconst VERTEX_AI_LOCATION = \"us-central1\";\n\n// Model ID to use for sentiment analysis\nconst MODEL_ID = \"gemini-2.5-flash\";\n\n/**\n * Sends the email text to Vertex AI for sentiment analysis.\n *\n * @param {string} emailText - The text of the email to analyze.\n * @returns {string} - The sentiment of the email ('positive', 'negative', or 'neutral').\n */\nfunction processSentiment(emailText) {\n  // Construct the API endpoint URL\n  const apiUrl = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`;\n\n  // Prepare the request payload\n  const payload = {\n    contents: [\n      {\n        role: \"user\",\n        parts: [\n          {\n            text: `Analyze the sentiment of the following message: ${emailText}`,\n          },\n        ],\n      },\n    ],\n    generationConfig: {\n      temperature: 0.9,\n      maxOutputTokens: 1024,\n      responseMimeType: \"application/json\",\n      // Expected response format for simpler parsing.\n      responseSchema: {\n        type: \"object\",\n        properties: {\n          response: {\n            type: \"string\",\n            enum: [\"positive\", \"negative\", \"neutral\"],\n          },\n        },\n      },\n    },\n  };\n\n  // Prepare the request options\n  const options = {\n    method: \"POST\",\n    headers: {\n      Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,\n    },\n    contentType: \"application/json\",\n    muteHttpExceptions: true, // Set to true to inspect the error response\n    payload: JSON.stringify(payload),\n  };\n\n  // Make the API request\n  const response = UrlFetchApp.fetch(apiUrl, options);\n\n  // Parse the response. There are two levels of JSON responses to parse.\n  const parsedResponse = JSON.parse(response.getContentText());\n  const sentimentResponse = JSON.parse(\n    parsedResponse.candidates[0].content.parts[0].text,\n  ).response;\n\n  // Return the sentiment\n  return sentimentResponse;\n}\n"
  },
  {
    "path": "gmail-sentiment-analysis/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Toronto\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/cloud-platform\",\n    \"https://www.googleapis.com/auth/gmail.addons.execute\",\n    \"https://www.googleapis.com/auth/gmail.labels\",\n    \"https://www.googleapis.com/auth/gmail.modify\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/userinfo.email\"\n  ],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Sentiment Analysis\",\n      \"logoUrl\": \"https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png\"\n    },\n    \"gmail\": {\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepageTrigger\",\n        \"enabled\": true\n      }\n    }\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "mashups/sheets2calendar.gs",
    "content": "/**\n * Create a new calendar event for every row in a spreadsheet. This code assumes\n * that the data is in the first sheet (workbook) in the spreadsheet and has the\n * columns \"Title\", \"Description\", and \"Emails\" in that order, with multiple\n * email addresses separated by a comma.\n */\nfunction createEventsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Create an event for each row.\n  for (const row of data) {\n    const title = row[0];\n    const description = row[1];\n    const emailsStr = row[2];\n\n    // Split the emails into an array and remove extra whitespace.\n    const emails = emailsStr.split(\",\").map((email) => email.trim());\n\n    const now = new Date();\n    // Start the event at the next hour mark.\n    const start = new Date(now);\n    start.setHours(start.getHours() + 1);\n    start.setMinutes(0);\n    start.setSeconds(0);\n    start.setMilliseconds(0);\n    // End the event after 30 minutes.\n    const end = new Date(start);\n    end.setMinutes(end.getMinutes() + 30);\n\n    // Create the calendar event and invite the guests.\n    const event = CalendarApp.createEvent(title, start, end).setDescription(\n      description,\n    );\n    for (const email of emails) {\n      event.addGuest(email);\n    }\n\n    // Add yourself as a guest and mark yourself as attending.\n    event.addGuest(Session.getActiveUser().getEmail());\n    event.setMyStatus(CalendarApp.GuestStatus.YES);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2chat.gs",
    "content": "/**\n * @typedef {Object} SheetEditEvent\n * @property {string} oldValue The old value of the cell.\n * @property {string} value The new value of the cell.\n */\n\n/**\n * Posts a message to a Hangouts Chat room every time the spreadsheet is edited.\n * This script must be attached to the spreadsheet (created in Google Sheets under\n * \"Tools > Script editor\") and installed as a trigger:\n * - Click \"Edit > Current project's triggers\" in the Apps Script UI.\n * - Click \"Add a new trigger\".\n * - Select the function \"sendChatMessageOnEdit\" and the event\n *   \"From spreadsheet\", \"On edit\".\n * - Click \"Save\".\n *\n * @param {SheetEditEvent} e The onEdit event object.\n */\nfunction sendChatMessageOnEdit(e) {\n  const range = SpreadsheetApp.getActiveRange();\n  const value = range.getValue();\n  const oldValue = e.oldValue;\n  const ss = range.getSheet().getParent();\n\n  // Construct the message to send, based on the old and new value of the cell.\n  let changeMessage;\n  if (oldValue && value) {\n    changeMessage = Utilities.formatString(\n      'changed from \"%s\" to \"%s\"',\n      oldValue,\n      value,\n    );\n  } else if (value) {\n    changeMessage = Utilities.formatString('set to \"%s\"', value);\n  } else {\n    changeMessage = \"cleared\";\n  }\n  const message = Utilities.formatString(\n    \"The range %s was %s. <%s|Open spreadsheet>.\",\n    range.getA1Notation(),\n    changeMessage,\n    ss.getUrl(),\n  );\n\n  // Follow these steps to create an incomming webhook URL for your chat room:\n  // https://developers.google.com/hangouts/chat/how-tos/webhooks#define_an_incoming_webhook\n  const webhookUrl = \"ENTER INCOMMING WEBHOOK URL HERE\";\n\n  // Use the spreadsheet's ID as a thread key, so that all messages go into the\n  // same thread.\n  const url = `${webhookUrl}&threadKey=${ss.getId()}`;\n\n  // Send the message.\n  UrlFetchApp.fetch(url, {\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: JSON.stringify({\n      text: message,\n    }),\n  });\n}\n"
  },
  {
    "path": "mashups/sheets2contacts.gs",
    "content": "/**\n * Create a new contact for every row in a spreadsheet. This code assumes that\n * the data is in the first sheet (workbook) in the spreadsheet and has the\n * columns \"First Name\", \"Last Name\", and \"Email\" in that order.\n */\nfunction createContactsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Send a contact for each row.\n  for (const row of data) {\n    const firstName = row[0];\n    const lastName = row[1];\n    const email = row[2];\n    ContactsApp.createContact(firstName, lastName, email);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2docs.gs",
    "content": "/**\n * Create a new document for every row in a spreadsheet. This code assumes that\n * the data is in the first sheet (workbook) in the spreadsheet and has the\n * columns \"Title\", \"Content\", and \"Emails\" in that order, with multiple email\n * addresses separated by a comma.\n */\nfunction createDocsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Create a document for each row.\n  for (const row of data) {\n    const title = row[0];\n    const content = row[1];\n    const emailsStr = row[2];\n\n    // Split the emails into an array and remove extra whitespace.\n    const emails = emailsStr.split(\",\").map((email) => email.trim());\n\n    // Create the document, append the content, and share it out.\n    const doc = DocumentApp.create(title);\n    doc.getBody().appendParagraph(content);\n    doc.addEditors(emails);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2drive.gs",
    "content": "/**\n * Create a PDF file in Google Drive for every row in a spreadsheet. This\n * code assumes that the data is in the first sheet (workbook) in the\n * spreadsheet and has the columns \"File Name\", \"HTML Content\", and \"Emails\" in that\n * order, with multiple email addresses separated by a comma.\n */\nfunction createDriveFilesFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Create a PDF in Google Drive for each row.\n  for (const row of data) {\n    const fileName = row[0];\n    const htmlContent = row[1];\n    const emailsStr = row[2];\n\n    // Split the emails into an array and remove extra whitespace.\n    const emails = emailsStr.split(\",\").map((email) => email.trim());\n\n    // Convert the HTML content to PDF.\n    const html = Utilities.newBlob(htmlContent, \"text/html\");\n    const pdf = html.getAs(\"application/pdf\");\n\n    // Create the Drive file and share it out.\n    const file = DriveApp.createFile(pdf).setName(fileName);\n    file.addEditors(emails);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2forms.gs",
    "content": "/**\n * Create a new form for every row in a spreadsheet. This code assumes that the\n * data is in the first sheet (workbook) in the spreadsheet and has the\n * columns \"Title\", \"Question\", and \"Emails\" in that order, with multiple email\n * addresses separated by a comma.\n */\nfunction createFormsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Create a form for each row.\n  for (const row of data) {\n    const title = row[0];\n    const question = row[1];\n    const emailsStr = row[2];\n\n    // Split the emails into an array and remove extra whitespace.\n    const emails = emailsStr.split(\",\").map((email) => email.trim());\n\n    // Create the form, append the question, and share it out.\n    const form = FormApp.create(title);\n    form.addTextItem().setTitle(question);\n    form.addEditors(emails);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2gmail.gs",
    "content": "/**\n * Sends an email for every row in a spreadsheet. This code assumes that the\n * data is in the first sheet (workbook) in the spreadsheet and has the columns\n * \"Subject\", \"HTML Message\", and \"Emails\" in that order, with multiple email\n * addresses separated by a comma.\n */\nfunction sendEmailsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Send an email for each row.\n  for (const row of data) {\n    const subject = row[0];\n    const htmlMessage = row[1];\n    const emails = row[2];\n\n    // Send the email.\n    GmailApp.sendEmail(emails, subject, \"\", {\n      htmlBody: htmlMessage,\n    });\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2maps.gs",
    "content": "/**\n * A custom function that gets the county (or equivalent administrative\n * district) that an address lies within. Use within a cell like:\n *\n * =COUNTY(\"76 9th Ave, New York NY\")\n *\n * This script must be attached to the spreadsheet (created in Google Sheets\n * under \"Tools > Script editor\").\n *\n * @param {String} address The address to lookup.\n * @return {String} The county (or equivalent) the address is within.\n * @customFunction\n */\nfunction COUNTY(address) {\n  const results = Maps.newGeocoder().geocode(address).results;\n  if (!results || results.length === 0) {\n    throw new Error(\"Unknown address\");\n  }\n  /** @type {{long_name: string, types: string[]}[]} */\n  const addressComponents = results[0].address_components;\n  const counties = addressComponents.filter(\n    (component) => component.types.indexOf(\"administrative_area_level_2\") >= 0,\n  );\n  if (!counties.length) {\n    throw new Error(\"Unable to determine county\");\n  }\n  return counties[0].long_name;\n}\n"
  },
  {
    "path": "mashups/sheets2slides.gs",
    "content": "/**\n * Create a new presentation for every row in a spreadsheet. This code assumes\n * that the data is in the first sheet (workbook) in the spreadsheet and has the\n * columns \"Title\", \"Content\", and \"Emails\" in that order, with multiple email\n * addresses separated by a comma.\n */\nfunction createPresentationsFromSpreadsheet() {\n  // Open the spreadsheet and get the data.\n  const ss = SpreadsheetApp.openByUrl(\"ENTER SPREADSHEET URL HERE\");\n  const sheet = ss.getSheets()[0];\n  /** @type {string[][]} */\n  const data = sheet.getDataRange().getValues();\n\n  // Remove any frozen rows from the data, since they contain headers.\n  data.splice(sheet.getFrozenRows());\n\n  // Create a presentation for each row.\n  for (const row of data) {\n    const title = row[0];\n    const content = row[1];\n    const emailsStr = row[2];\n\n    // Split the emails into an array and remove extra whitespace.\n    const emails = emailsStr.split(\",\").map((email) => email.trim());\n\n    // Create the presentation, insert a new slide at the start, append the content,\n    // and share it out.\n    const presentation = SlidesApp.create(title);\n    const slide = presentation.insertSlide(\n      0,\n      SlidesApp.PredefinedLayout.MAIN_POINT,\n    );\n    const textBox = slide.getShapes()[0];\n    textBox.getText().appendParagraph(content);\n    presentation.addEditors(emails);\n  }\n}\n"
  },
  {
    "path": "mashups/sheets2translate.gs",
    "content": "/**\n * Whenever a cell is edited and it's value is a string, add a note to the cell\n * with the English translation of the cell's content.\n *\n * For example, type \"la gato\" into a cell and this script will add a note\n * with the text \"the cat\".\n *\n * This script must be attached to the spreadsheet (created in Google Sheets\n * under \"Tools > Script editor\").\n */\nfunction onEdit() {\n  const range = SpreadsheetApp.getActiveRange();\n  const value = range.getValue();\n  if (typeof value === \"string\") {\n    const translated = LanguageApp.translate(value, \"\", \"en\");\n    range.setNote(translated);\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"googleworkspace-apps-script-samples\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Apps Script samples for [Google Workspace](https://developers.google.com/apps-script/) docs.\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"keywords\": [\n    \"Google Workspace\",\n    \"Apps Script\",\n    \"Calendar\",\n    \"Drive\",\n    \"Sheets\",\n    \"Slides\",\n    \"API\"\n  ],\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"1.9.4\",\n    \"@types/google-apps-script\": \"^2.0.8\",\n    \"@types/node\": \"^24.10.1\",\n    \"tsx\": \"^4.20.6\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"scripts\": {\n    \"lint\": \"tsx .github/scripts/biome-gs.ts lint\",\n    \"format\": \"tsx .github/scripts/biome-gs.ts format\",\n    \"check\": \"tsx .github/scripts/check-gs.ts\"\n  },\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@10.15.1\",\n  \"engines\": {\n    \"node\": \">=20\"\n  }\n}\n"
  },
  {
    "path": "people/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START people_quickstart]\n/**\n * @typedef {Object} EmailAddress\n * @see https://developers.google.com/people/api/rest/v1/people#Person\n * @property {string} value\n * Note: This is a partial definition.\n */\n\n/**\n * @typedef {Object} Name\n * @see https://developers.google.com/people/api/rest/v1/people#Person\n * @property {string} displayName\n * Note: This is a partial definition.\n */\n\n/**\n * @typedef {Object} Person\n * @see https://developers.google.com/people/api/rest/v1/people#Person\n * @property {Name[]} names\n * @property {EmailAddress[]} [emailAddresses]\n * Note: This is a partial definition.\n */\n\n/**\n * @typedef {Object} Connection\n * @see https://developers.google.com/people/api/rest/v1/people.connections/list\n * @property {Person[]} connections\n * Note: This is a partial definition.\n */\n\n/**\n * Print the display name if available for 10 connections.\n */\nfunction listConnectionNames() {\n  // Use the People API to list the connections of the logged in user.\n  // See: https://developers.google.com/people/api/rest/v1/people.connections/list\n  if (!People || !People.People || !People.People.Connections) {\n    throw new Error(\"People service not enabled.\");\n  }\n  const connections = People.People.Connections.list(\"people/me\", {\n    pageSize: 10,\n    personFields: \"names,emailAddresses\",\n  });\n  if (!connections.connections) {\n    console.log(\"No connections found.\");\n    return;\n  }\n  for (const person of connections.connections) {\n    if (\n      person.names &&\n      person.names.length > 0 &&\n      person.names[0].displayName\n    ) {\n      console.log(person.names[0].displayName);\n    } else {\n      console.log(\"No display name found for connection.\");\n    }\n  }\n}\n// [END people_quickstart]\n"
  },
  {
    "path": "picker/README.md",
    "content": "# File Picker Sample\n\nThis sample shows how to create a \"file-open\" dialog in Google Sheets thatallows the user to select a file from their Drive. It does so by loading [Google Picker](https://developers.google.com/picker/), for this purpose. More information is available in the Apps Script guide [Dialogs and Sidebars in Google Workspace Documents](https://developers.google.com/apps-script/guides/dialogs#file-open_dialogs).\n\nNote that this sample expects to be [bound](https://developers.google.com/apps-script/guides/bound) to a spreadsheet.\n"
  },
  {
    "path": "picker/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/script.container.ui\",\n    \"https://www.googleapis.com/auth/drive.file\"\n  ],\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"version\": \"v3\",\n        \"serviceId\": \"drive\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "picker/code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START picker_code]\n/**\n * Creates a custom menu in Google Sheets when the spreadsheet opens.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createMenu(\"Picker\")\n    .addItem(\"Start\", \"showPicker\")\n    .addToUi();\n}\n\n/**\n * Displays an HTML-service dialog in Google Sheets that contains client-side\n * JavaScript code for the Google Picker API.\n */\nfunction showPicker() {\n  const html = HtmlService.createHtmlOutputFromFile(\"dialog.html\")\n    .setWidth(800)\n    .setHeight(600)\n    .setSandboxMode(HtmlService.SandboxMode.IFRAME);\n  SpreadsheetApp.getUi().showModalDialog(html, \"Select a file\");\n}\n// Ensure the Drive API is enabled.\nif (!Drive) {\n  throw new Error(\"Please enable the Drive advanced service.\");\n}\n\n/**\n * Checks that the file can be accessed.\n * @param {string} fileId The ID of the file.\n * @return {Object} The file resource.\n */\nfunction getFile(fileId) {\n  return Drive.Files.get(fileId, { fields: \"*\" });\n}\n\n/**\n * Gets the user's OAuth 2.0 access token so that it can be passed to Picker.\n * This technique keeps Picker from needing to show its own authorization\n * dialog, but is only possible if the OAuth scope that Picker needs is\n * available in Apps Script. In this case, the function includes an unused call\n * to a DriveApp method to ensure that Apps Script requests access to all files\n * in the user's Drive.\n *\n * @return {string} The user's OAuth 2.0 access token.\n */\nfunction getOAuthToken() {\n  return ScriptApp.getOAuthToken();\n}\n// [END picker_code]\n"
  },
  {
    "path": "picker/dialog.html",
    "content": "<!--\nCopyright 2018 Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START picker_html] -->\n<!DOCTYPE html>\n<html>\n  <head>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\"\n    />\n    <style>\n      #result {\n        display: flex;\n        flex-direction: column;\n        gap: 0.25em;\n      }\n\n      pre {\n        font-size: x-small;\n        max-height: 25vh;\n        overflow-y: scroll;\n        background: #eeeeee;\n        padding: 1em;\n        border: 1px solid #cccccc;\n      }\n    </style>\n    <script>\n      // TODO: Replace the value for DEVELOPER_KEY with the API key obtained\n      // from the Google Developers Console.\n      const DEVELOPER_KEY = \"AIza...\";\n      // TODO: Replace the value for CLOUD_PROJECT_NUMBER with the project\n      // number obtained from the Google Developers Console.\n      const CLOUD_PROJECT_NUMBER = \"1234567890\";\n\n      let pickerApiLoaded = false;\n      let oauthToken;\n\n      /**\n       * Loads the Google Picker API.\n       */\n      function onApiLoad() {\n        gapi.load(\"picker\", {\n          callback: function () {\n            pickerApiLoaded = true;\n          },\n        });\n      }\n\n      /**\n       * Gets the user's OAuth 2.0 access token from the server-side script so that\n       * it can be passed to Picker. This technique keeps Picker from needing to\n       * show its own authorization dialog, but is only possible if the OAuth scope\n       * that Picker needs is available in Apps Script. Otherwise, your Picker code\n       * will need to declare its own OAuth scopes.\n       */\n      function getOAuthToken() {\n        google.script.run\n          .withSuccessHandler((token) => {\n            oauthToken = token;\n            createPicker(token);\n          })\n          .withFailureHandler(showError)\n          .getOAuthToken();\n      }\n\n      /**\n       * Creates a Picker that can access the user's spreadsheets. This function\n       * uses advanced options to hide the Picker's left navigation panel and\n       * default title bar.\n       *\n       * @param {string} token An OAuth 2.0 access token that lets Picker access the\n       *     file type specified in the addView call.\n       */\n      function createPicker(token) {\n        document.getElementById(\"result\").innerHTML = \"\";\n\n        if (pickerApiLoaded && token) {\n          const picker = new google.picker.PickerBuilder()\n            // Instruct Picker to display only spreadsheets in Drive. For other\n            // views, see https://developers.google.com/picker/reference/picker.viewid\n            .addView(\n              new google.picker.DocsView(\n                google.picker.ViewId.SPREADSHEETS\n              ).setOwnedByMe(true)\n            )\n            // Hide the navigation panel so that Picker fills more of the dialog.\n            .enableFeature(google.picker.Feature.NAV_HIDDEN)\n            // Hide the title bar since an Apps Script dialog already has a title.\n            .hideTitleBar()\n            .setOAuthToken(token)\n            .setDeveloperKey(DEVELOPER_KEY)\n            .setAppId(CLOUD_PROJECT_NUMBER)\n            .setCallback(pickerCallback)\n            .setOrigin(google.script.host.origin)\n            .build();\n          picker.setVisible(true);\n        } else {\n          showError(\"Unable to load the file picker.\");\n        }\n      }\n\n      /**\n       * @typedef {Object} PickerResponse\n       * @property {string} action\n       * @property {PickerDocument[]} docs\n       */\n\n      /**\n       * @typedef {Object} PickerDocument\n       * @property {string} id\n       * @property {string} name\n       * @property {string} mimeType\n       * @property {string} url\n       * @property {string} lastEditedUtc\n       */\n\n      /**\n       * A callback function that extracts the chosen document's metadata from the\n       * response object. For details on the response object, see\n       * https://developers.google.com/picker/reference/picker.responseobject\n       *\n       * @param {PickerResponse} data The response object.\n       */\n      function pickerCallback(data) {\n        const action = data[google.picker.Response.ACTION];\n        if (action == google.picker.Action.PICKED) {\n          handlePicked(data);\n        } else if (action == google.picker.Action.CANCEL) {\n          document.getElementById(\"result\").innerHTML = \"Picker canceled.\";\n        }\n      }\n\n      /**\n       * Handles `\"PICKED\"` responsed from the Google Picker.\n       *\n       * @param {PickerResponse} data The response object.\n       */\n      function handlePicked(data) {\n        const doc = data[google.picker.Response.DOCUMENTS][0];\n        const id = doc[google.picker.Document.ID];\n\n        google.script.run\n          .withSuccessHandler((driveFilesGetResponse) => {\n            // Render the response from Picker and the Drive.Files.Get API.\n            const resultElement = document.getElementById(\"result\");\n            resultElement.innerHTML = \"\";\n\n            for (const response of [\n              {\n                title: \"Picker response\",\n                content: JSON.stringify(data, null, 2),\n              },\n              {\n                title: \"Drive.Files.Get response\",\n                content: JSON.stringify(driveFilesGetResponse, null, 2),\n              },\n            ]) {\n              const titleElement = document.createElement(\"h3\");\n              titleElement.appendChild(document.createTextNode(response.title));\n              resultElement.appendChild(titleElement);\n\n              const contentElement = document.createElement(\"pre\");\n              contentElement.appendChild(\n                document.createTextNode(response.content)\n              );\n              resultElement.appendChild(contentElement);\n            }\n          })\n          .withFailureHandler(showError)\n          .getFile(data[google.picker.Response.DOCUMENTS][0].id);\n      }\n\n      /**\n       * Displays an error message within the #result element.\n       *\n       * @param {string} message The error message to display.\n       */\n      function showError(message) {\n        document.getElementById(\"result\").innerHTML = \"Error: \" + message;\n      }\n    </script>\n  </head>\n\n  <body>\n    <div>\n      <button onclick=\"getOAuthToken()\">Select a file</button>\n      <div id=\"result\"></div>\n    </div>\n    <script src=\"https://apis.google.com/js/api.js?onload=onApiLoad\"></script>\n  </body>\n</html>\n<!-- [END picker_html] -->\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nonlyBuiltDependencies:\n  - '@biomejs/biome'\n  - esbuild\n"
  },
  {
    "path": "service/jdbc.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Replace the variables in this block with real values.\n * You can find the \"Instance connection name\" in the Google Cloud\n * Platform Console, on the instance Overview page.\n */\nconst connectionName = \"Instance_connection_name\";\nconst rootPwd = \"root_password\";\nconst user = \"user_name\";\nconst userPwd = \"user_password\";\nconst db = \"database_name\";\n\nconst root = \"root\";\nconst instanceUrl = `jdbc:google:mysql://${connectionName}`;\nconst dbUrl = `${instanceUrl}/${db}`;\n\n// [START apps_script_jdbc_create]\n/**\n * Create a new database within a Cloud SQL instance.\n */\nfunction createDatabase() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(instanceUrl, root, rootPwd);\n    conn.createStatement().execute(`CREATE DATABASE ${db}`);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * Create a new user for your database with full privileges.\n */\nfunction createUser() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, root, rootPwd);\n\n    const stmt = conn.prepareStatement(\"CREATE USER ? IDENTIFIED BY ?\");\n    stmt.setString(1, user);\n    stmt.setString(2, userPwd);\n    stmt.execute();\n\n    conn.createStatement().execute(`GRANT ALL ON \\`%\\`.* TO ${user}`);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * Create a new table in the database.\n */\nfunction createTable() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n    conn\n      .createStatement()\n      .execute(\n        \"CREATE TABLE entries \" +\n          \"(guestName VARCHAR(255), content VARCHAR(255), \" +\n          \"entryID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(entryID));\",\n      );\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END apps_script_jdbc_create]\n\n// [START apps_script_jdbc_write]\n/**\n * Write one row of data to a table.\n */\nfunction writeOneRecord() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n\n    const stmt = conn.prepareStatement(\n      \"INSERT INTO entries \" + \"(guestName, content) values (?, ?)\",\n    );\n    stmt.setString(1, \"First Guest\");\n    stmt.setString(2, \"Hello, world\");\n    stmt.execute();\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * Write 500 rows of data to a table in a single batch.\n */\nfunction writeManyRecords() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n    conn.setAutoCommit(false);\n\n    const start = new Date();\n    const stmt = conn.prepareStatement(\n      \"INSERT INTO entries \" + \"(guestName, content) values (?, ?)\",\n    );\n    for (let i = 0; i < 500; i++) {\n      stmt.setString(1, `Name ${i}`);\n      stmt.setString(2, `Hello, world ${i}`);\n      stmt.addBatch();\n    }\n\n    const batch = stmt.executeBatch();\n    conn.commit();\n    conn.close();\n\n    const end = new Date();\n    console.log(\"Time elapsed: %sms for %s rows.\", end - start, batch.length);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * Write 500 rows of data to a table in a single batch.\n * Recommended for faster writes\n */\nfunction writeManyRecordsUsingExecuteBatch() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n    conn.setAutoCommit(false);\n\n    const start = new Date();\n    const stmt = conn.prepareStatement(\n      \"INSERT INTO entries \" + \"(guestName, content) values (?, ?)\",\n    );\n    const params = [];\n    for (let i = 0; i < 500; i++) {\n      params.push([`Name ${i}`, `Hello, world ${i}`]);\n    }\n\n    const batch = stmt.executeBatch(params);\n    conn.commit();\n    conn.close();\n\n    const end = new Date();\n    console.log(\"Time elapsed: %sms for %s rows.\", end - start, batch.length);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END apps_script_jdbc_write]\n\n// [START apps_script_jdbc_read]\n/**\n * Read up to 1000 rows of data from the table and log them.\n */\nfunction readFromTable() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n    const start = new Date();\n    const stmt = conn.createStatement();\n    stmt.setMaxRows(1000);\n    const results = stmt.executeQuery(\"SELECT * FROM entries\");\n    const numCols = results.getMetaData().getColumnCount();\n\n    while (results.next()) {\n      let rowString = \"\";\n      for (let col = 0; col < numCols; col++) {\n        rowString += `${results.getString(col + 1)}\\t`;\n      }\n      console.log(rowString);\n    }\n\n    results.close();\n    stmt.close();\n\n    const end = new Date();\n    console.log(\"Time elapsed: %sms\", end - start);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n\n/**\n * Read up to 1000 rows of data from the table and log them.\n * Recommended for faster reads\n */\nfunction readFromTableUsingGetRows() {\n  try {\n    const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd);\n    const start = new Date();\n    const stmt = conn.createStatement();\n    stmt.setMaxRows(1000);\n    const results = stmt.executeQuery(\"SELECT * FROM entries\");\n    const numCols = results.getMetaData().getColumnCount();\n    const getRowArgs = [];\n    for (let col = 0; col < numCols; col++) {\n      getRowArgs.push(`getString(${col + 1})`);\n    }\n    const rows = results.getRows(getRowArgs.join(\",\"));\n    for (let i = 0; i < rows.length; i++) {\n      console.log(rows[i].join(\"\\t\"));\n    }\n\n    results.close();\n    stmt.close();\n\n    const end = new Date();\n    console.log(\"Time elapsed: %sms\", end - start);\n  } catch (err) {\n    // TODO(developer) - Handle exception from the API\n    console.log(\"Failed with an error %s\", err.message);\n  }\n}\n// [END apps_script_jdbc_read]\n"
  },
  {
    "path": "service/propertyService.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// @see- https://developers.google.com/apps-script/guides/properties\n/**\n * Save or set the property in each three property store.\n */\nfunction saveSingleProperty() {\n  // [START apps_script_property_service_save_data_single_value]\n  try {\n    // Set a property in each of the three property stores.\n    const scriptProperties = PropertiesService.getScriptProperties();\n    const userProperties = PropertiesService.getUserProperties();\n    const documentProperties = PropertiesService.getDocumentProperties();\n\n    scriptProperties.setProperty(\"SERVER_URL\", \"http://www.example.com/\");\n    userProperties.setProperty(\"DISPLAY_UNITS\", \"metric\");\n    documentProperties.setProperty(\n      \"SOURCE_DATA_ID\",\n      \"1j3GgabZvXUF177W0Zs_2v--H6SPCQb4pmZ6HsTZYT5k\",\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_save_data_single_value]\n}\n\n/**\n * Save the multiple script properties.\n */\nfunction saveMultipleProperties() {\n  // [START apps_script_property_service_save_data_multiple_value]\n  try {\n    // Set multiple script properties in one call.\n    const scriptProperties = PropertiesService.getScriptProperties();\n    scriptProperties.setProperties({\n      cow: \"moo\",\n      sheep: \"baa\",\n      chicken: \"cluck\",\n    });\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_save_data_multiple_value]\n}\n\n/**\n * Read single value for user property.\n */\nfunction readSingleProperty() {\n  // [START apps_script_property_service_read_data_single_value]\n  try {\n    // Get the value for the user property 'DISPLAY_UNITS'.\n    const userProperties = PropertiesService.getUserProperties();\n    const units = userProperties.getProperty(\"DISPLAY_UNITS\");\n    console.log(\"values of units %s\", units);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_read_data_single_value]\n}\n\n/**\n * Read the multiple script properties.\n */\nfunction readAllProperties() {\n  // [START apps_script_property_service_read_multiple_data_value]\n  try {\n    // Get multiple script properties in one call, then log them all.\n    const scriptProperties = PropertiesService.getScriptProperties();\n    const data = scriptProperties.getProperties();\n    for (const key in data) {\n      console.log(\"Key: %s, Value: %s\", key, data[key]);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_read_multiple_data_value]\n}\n\n/**\n * Update the user property value.\n */\nfunction updateProperty() {\n  // [START apps_script_property_service_modify_data]\n  try {\n    // Change the unit type in the user property 'DISPLAY_UNITS'.\n    const userProperties = PropertiesService.getUserProperties();\n    let units = userProperties.getProperty(\"DISPLAY_UNITS\");\n    units = \"imperial\"; // Only changes local value, not stored value.\n    userProperties.setProperty(\"DISPLAY_UNITS\", units); // Updates stored value.\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_modify_data]\n}\n\n/**\n * Delete the single user property.\n */\nfunction deleteSingleProperty() {\n  // [START apps_script_property_service_delete_data_single_value]\n  try {\n    // Delete the user property 'DISPLAY_UNITS'.\n    const userProperties = PropertiesService.getUserProperties();\n    userProperties.deleteProperty(\"DISPLAY_UNITS\");\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_delete_data_single_value]\n}\n\n/**\n * Delete all user properties in the current script.\n */\nfunction deleteAllUserProperties() {\n  // [START apps_script_property_service_delete_all_data]\n  try {\n    // Get user properties in the current script.\n    const userProperties = PropertiesService.getUserProperties();\n    // Delete all user properties in the current script.\n    userProperties.deleteAllProperties();\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n  // [END apps_script_property_service_delete_all_data]\n}\n"
  },
  {
    "path": "service/test_jdbc.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests createDatabase function of jdbc.gs\n */\nfunction itShouldCreateDatabase() {\n  console.log(\"itShouldCreateDatabase\");\n  createDatabase();\n}\n\n/**\n * Tests createUser function of jdbc.gs\n */\nfunction itShouldCreateUser() {\n  console.log(\"itShouldCreateUser\");\n  createUser();\n}\n\n/**\n * Tests createTable function of jdbc.gs\n */\nfunction itShouldCreateTable() {\n  console.log(\"itShouldCreateTable\");\n  createTable();\n}\n\n/**\n * Tests writeOneRecord function of jdbc.gs\n */\nfunction itShouldWriteOneRecord() {\n  console.log(\"itShouldWriteOneRecord\");\n  writeOneRecord();\n}\n\n/**\n * Tests writeManyRecords function of jdbc.gs\n */\nfunction itShouldWriteManyRecords() {\n  console.log(\"itShouldWriteManyRecords\");\n  writeManyRecords();\n}\n\n/**\n * Tests writeManyRecordsUsingExecuteBatch function of jdbc.gs\n */\nfunction itShouldWriteManyRecordsUsingExecuteBatch() {\n  console.log(\"itShouldWriteManyRecordsUsingExecuteBatch\");\n  writeManyRecordsUsingExecuteBatch();\n}\n\n/**\n * Tests readFromTable function of jdbc.gs\n */\nfunction itShouldReadFromTable() {\n  console.log(\"itShouldReadFromTable\");\n  readFromTable();\n}\n\n/**\n * Tests readFromTableUsingGetRows function of jdbc.gs\n */\nfunction itShouldReadFromTableUsingGetRows() {\n  console.log(\"itShouldReadFromTableUsingGetRows\");\n  readFromTableUsingGetRows();\n}\n\n/**\n * Runs all the tests\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateDatabase();\n  itShouldCreateUser();\n  itShouldCreateTable();\n  itShouldWriteOneRecord();\n  itShouldWriteManyRecords();\n  itShouldReadFromTable();\n  itShouldReadFromTableUsingGetRows();\n  itShouldWriteManyRecordsUsingExecuteBatch();\n}\n"
  },
  {
    "path": "service/test_propertyServices.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Run all tests for propertyService.gs\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> itShouldSaveSingleProperty\");\n  saveSingleProperty();\n  console.log(\"> itShouldSaveMultipleProperties\");\n  saveMultipleProperties();\n  console.log(\"> itShouldReadSingleProperty\");\n  readSingleProperty();\n  console.log(\"> itShouldReadAllProperties\");\n  readAllProperties();\n  // The tests below are successful if they run without any extra output\n  console.log(\"> itShouldUpdateProperty\");\n  updateProperty();\n  console.log(\"> itShouldDeleteSingleProperty\");\n  deleteSingleProperty();\n  console.log(\"> itShouldDeleteAllUserProperties\");\n  deleteAllUserProperties();\n}\n"
  },
  {
    "path": "sheets/README.md",
    "content": "# Quickstart: Apps Scripts for Google Sheets\n\nSample Google Apps Script add-ons and menus, and custom functions for Google Sheets.\n\n## Date Add and Subtract\n\nDate Add and Subtract is a sample add-on for Google Sheets that provides custom functions for date manipulation.\n\n## [Managing Responses for Google Forms](https://developers.google.com/apps-script/quickstart/forms)\n\nCreate a Google Form based on data in a spreadsheet that emails Google Calendar invites and a personalized Google Doc to everyone who responds.\n\n![Quickstart Forms](https://developers.google.com/apps-script/images/quickstart-forms.png)\n\n## [Menus and Custom Functions](https://developers.google.com/apps-script/quickstart/custom-functions)\n\nCreate a spreadsheet with custom functions, menu items, and automated procedures.\n\n![Quickstart Custom Functions](https://developers.google.com/apps-script/images/quickstart-custom-functions.png)\n\n## [Bracket Maker](https://developers.google.com/apps-script/articles/bracket_maker)\n\nThis tutorial shows you how to use the Spreadsheets service to create Tournament Brackets similar to College Basketball's March Madness. You can use this tutorial to easily create your own brackets.\n\n## [Removing Duplicates](https://developers.google.com/apps-script/articles/removing_duplicates)\n\nThis tutorial shows how to avoid duplicates when you want to automate the process of copying data in Google Workspace and specifically how to remove duplicate rows in spreadsheet data.\n"
  },
  {
    "path": "sheets/api/helpers.gs",
    "content": "const filesToDelete = [];\n/**\n * Helper methods for Google Sheets tests.\n */\nfunction Helpers() {\n  this.filesToDelete = [];\n}\n\nHelpers.prototype.reset = function () {\n  this.filesToDelete = [];\n};\n\nHelpers.prototype.deleteFileOnCleanup = function (id) {\n  this.filesToDelete.push(id);\n};\n\nHelpers.prototype.cleanup = () => {\n  filesToDelete.forEach(Drive.Files.remove);\n};\n\nHelpers.prototype.createTestSpreadsheet = function () {\n  const spreadsheet = SpreadsheetApp.create(\"Test Spreadsheet\");\n  for (let i = 0; i < 3; ++i) {\n    spreadsheet.appendRow([1, 2, 3]);\n  }\n  this.deleteFileOnCleanup(spreadsheet.getId());\n  return spreadsheet.getId();\n};\n\nHelpers.prototype.populateValues = (spreadsheetId) => {\n  const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest();\n  const repeatCellRequest = Sheets.newRepeatCellRequest();\n\n  const values = [];\n  for (let i = 0; i < 10; ++i) {\n    values[i] = [];\n    for (let j = 0; j < 10; ++j) {\n      values[i].push(\"Hello\");\n    }\n  }\n  const range = \"A1:J10\";\n  SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values);\n  SpreadsheetApp.flush();\n};\n"
  },
  {
    "path": "sheets/api/spreadsheet_snippets.gs",
    "content": "/**\n * Copyright  Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Google Sheets API Snippets.\n */\nfunction Snippets() {}\n\n// [START sheets_create]\n/**\n * Creates a new sheet using the sheets advanced services\n * @param {string} title the name of the sheet to be created\n * @returns {string} the spreadsheet ID\n */\nSnippets.prototype.create = (title) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.create() is more appropriate.\n  try {\n    const sheet = Sheets.newSpreadsheet();\n    sheet.properties = Sheets.newSpreadsheetProperties();\n    sheet.properties.title = title;\n    const spreadsheet = Sheets.Spreadsheets.create(sheet);\n\n    return spreadsheet.spreadsheetId;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_create]\n\n// [START sheets_batch_update]\n/**\n * Updates the specified sheet using advanced sheet services\n * @param {string} spreadsheetId id of the spreadsheet to be updated\n * @param {string} title name of the sheet in the spreadsheet to be updated\n * @param {string} find string to be replaced\n * @param {string} replacement the string to replace the old data\n * @returns {*} the updated spreadsheet\n */\nSnippets.prototype.batchUpdate = (spreadsheetId, title, find, replacement) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.getActiveSpreadsheet()\n  //     .getRange(range).setValues(values) is more appropriate.\n\n  try {\n    // Change the spreadsheet's title.\n    const updateSpreadsheetPropertiesRequest =\n      Sheets.newUpdateSpreadsheetPropertiesRequest();\n    updateSpreadsheetPropertiesRequest.properties =\n      Sheets.newSpreadsheetProperties();\n    updateSpreadsheetPropertiesRequest.properties.title = title;\n    updateSpreadsheetPropertiesRequest.fields = \"title\";\n\n    // Find and replace text.\n    const findReplaceRequest = Sheets.newFindReplaceRequest();\n    findReplaceRequest.find = find;\n    findReplaceRequest.replacement = replacement;\n    findReplaceRequest.allSheets = true;\n\n    const requests = [Sheets.newRequest(), Sheets.newRequest()];\n    requests[0].updateSpreadsheetProperties =\n      updateSpreadsheetPropertiesRequest;\n    requests[1].findReplace = findReplaceRequest;\n\n    const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest();\n    batchUpdateRequest.requests = requests;\n\n    // Add additional requests (operations)\n    const result = Sheets.Spreadsheets.batchUpdate(\n      batchUpdateRequest,\n      spreadsheetId,\n    );\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_batch_update]\n\n// [START sheets_get_values]\n/**\n * Gets the values of the cells in the specified range\n * @param {string} spreadsheetId id of the spreadsheet\n * @param {string} range specifying the start and end cells of the range\n * @returns {*} Values in the range\n */\nSnippets.prototype.getValues = (spreadsheetId, range) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.getActiveSpreadsheet()\n  //     .getRange(range).getValues(values) is more appropriate.\n  try {\n    const result = Sheets.Spreadsheets.Values.get(spreadsheetId, range);\n    const numRows = result.values ? result.values.length : 0;\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_get_values]\n\n// [START sheets_batch_get_values]\n/**\n * Get the values in the specified ranges\n * @param {string} spreadsheetId spreadsheet's ID\n * @param {list<string>} _ranges The span of ranges\n * @returns {*} spreadsheet information and values\n */\nSnippets.prototype.batchGetValues = (spreadsheetId, _ranges) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.getActiveSpreadsheet()\n  //     .getRange(range).getValues(values) is more appropriate.\n  let ranges = [\n    //Range names ...\n  ];\n  // [START_EXCLUDE silent]\n  ranges = _ranges;\n  // [END_EXCLUDE]\n  try {\n    const result = Sheets.Spreadsheets.Values.batchGet(spreadsheetId, {\n      ranges: ranges,\n    });\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_batch_get_values]\n\n// [START sheets_update_values]\n/**\n * Updates the values in the specified range\n * @param {string} spreadsheetId spreadsheet's ID\n * @param {string} range the range of cells in spreadsheet\n * @param {string} valueInputOption determines how the input should be interpreted\n * @see\n * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption\n * @param {list<list<string>>} _values list of string lists to input\n * @returns {*} spreadsheet with updated values\n */\nSnippets.prototype.updateValues = (\n  spreadsheetId,\n  range,\n  valueInputOption,\n  _values,\n) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.getActiveSpreadsheet()\n  //     .getRange(range).setValues(values) is more appropriate.\n  let values = [\n    [\n      // Cell values ...\n    ],\n    // Additional rows ...\n  ];\n  // [START_EXCLUDE silent]\n  values = _values;\n  // [END_EXCLUDE]\n\n  try {\n    const valueRange = Sheets.newValueRange();\n    valueRange.values = values;\n    const result = Sheets.Spreadsheets.Values.update(\n      valueRange,\n      spreadsheetId,\n      range,\n      { valueInputOption: valueInputOption },\n    );\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_update_values]\n\n// [START sheets_batch_update_values]\n/**\n * Updates the values in the specified range\n * @param {string} spreadsheetId spreadsheet's ID\n * @param {string} range range of cells of the spreadsheet\n * @param {string} valueInputOption determines how the input should be interpreted\n * @see\n * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption\n * @param {list<list<string>>} _values list of string values to input\n * @returns {*} spreadsheet with updated values\n */\nSnippets.prototype.batchUpdateValues = (\n  spreadsheetId,\n  range,\n  valueInputOption,\n  _values,\n) => {\n  // This code uses the Sheets Advanced Service, but for most use cases\n  // the built-in method SpreadsheetApp.getActiveSpreadsheet()\n  //     .getRange(range).setValues(values) is more appropriate.\n  let values = [\n    [\n      // Cell values ...\n    ],\n    // Additional rows ...\n  ];\n  // [START_EXCLUDE silent]\n  values = _values;\n  // [END_EXCLUDE]\n\n  try {\n    const valueRange = Sheets.newValueRange();\n    valueRange.range = range;\n    valueRange.values = values;\n\n    const batchUpdateRequest = Sheets.newBatchUpdateValuesRequest();\n    batchUpdateRequest.data = valueRange;\n    batchUpdateRequest.valueInputOption = valueInputOption;\n\n    const result = Sheets.Spreadsheets.Values.batchUpdate(\n      batchUpdateRequest,\n      spreadsheetId,\n    );\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_batch_update_values]\n\n// [START sheets_append_values]\n/**\n * Appends values to the specified range\n * @param {string} spreadsheetId spreadsheet's ID\n * @param {string} range range of cells in the spreadsheet\n * @param valueInputOption determines how the input should be interpreted\n * @see\n * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption\n * @param {list<string>} _values list of rows of values to input\n * @returns {*} spreadsheet with appended values\n */\nSnippets.prototype.appendValues = (\n  spreadsheetId,\n  range,\n  valueInputOption,\n  _values,\n) => {\n  let values = [\n    [\n      // Cell values ...\n    ],\n    // Additional rows ...\n  ];\n  // [START_EXCLUDE silent]\n  values = _values;\n  // [END_EXCLUDE]\n  try {\n    const valueRange = Sheets.newRowData();\n    valueRange.values = values;\n\n    const appendRequest = Sheets.newAppendCellsRequest();\n    appendRequest.sheetId = spreadsheetId;\n    appendRequest.rows = [valueRange];\n\n    const result = Sheets.Spreadsheets.Values.append(\n      valueRange,\n      spreadsheetId,\n      range,\n      { valueInputOption: valueInputOption },\n    );\n    return result;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_append_values]\n\n// [START sheets_pivot_tables]\n/**\n * Create pivot table\n * @param {string} spreadsheetId spreadsheet ID\n * @returns {*} pivot table's spreadsheet\n */\nSnippets.prototype.pivotTable = (spreadsheetId) => {\n  try {\n    const spreadsheet = SpreadsheetApp.openById(spreadsheetId);\n\n    // Create two sheets for our pivot table, assume we have one.\n    const sheet = spreadsheet.getSheets()[0];\n    sheet.copyTo(spreadsheet);\n\n    const sourceSheetId = spreadsheet.getSheets()[0].getSheetId();\n    const targetSheetId = spreadsheet.getSheets()[1].getSheetId();\n\n    // Create pivot table\n    const pivotTable = Sheets.newPivotTable();\n\n    const gridRange = Sheets.newGridRange();\n    gridRange.sheetId = sourceSheetId;\n    gridRange.startRowIndex = 0;\n    gridRange.startColumnIndex = 0;\n    gridRange.endRowIndex = 20;\n    gridRange.endColumnIndex = 7;\n    pivotTable.source = gridRange;\n\n    const pivotRows = Sheets.newPivotGroup();\n    pivotRows.sourceColumnOffset = 1;\n    pivotRows.showTotals = true;\n    pivotRows.sortOrder = \"ASCENDING\";\n    pivotTable.rows = pivotRows;\n\n    const pivotColumns = Sheets.newPivotGroup();\n    pivotColumns.sourceColumnOffset = 4;\n    pivotColumns.sortOrder = \"ASCENDING\";\n    pivotColumns.showTotals = true;\n    pivotTable.columns = pivotColumns;\n\n    const pivotValue = Sheets.newPivotValue();\n    pivotValue.summarizeFunction = \"COUNTA\";\n    pivotValue.sourceColumnOffset = 4;\n    pivotTable.values = [pivotValue];\n\n    // Create other metadata for the updateCellsRequest\n    const cellData = Sheets.newCellData();\n    cellData.pivotTable = pivotTable;\n\n    const rows = Sheets.newRowData();\n    rows.values = cellData;\n\n    const start = Sheets.newGridCoordinate();\n    start.sheetId = targetSheetId;\n    start.rowIndex = 0;\n    start.columnIndex = 0;\n\n    const updateCellsRequest = Sheets.newUpdateCellsRequest();\n    updateCellsRequest.rows = rows;\n    updateCellsRequest.start = start;\n    updateCellsRequest.fields = \"pivotTable\";\n\n    // Batch update our spreadsheet\n    const batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest();\n    const updateCellsRawRequest = Sheets.newRequest();\n    updateCellsRawRequest.updateCells = updateCellsRequest;\n    batchUpdate.requests = [updateCellsRawRequest];\n    const response = Sheets.Spreadsheets.batchUpdate(\n      batchUpdate,\n      spreadsheetId,\n    );\n\n    return response;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_pivot_tables]\n\n// [START sheets_conditional_formatting]\n/**\n * conditional formatting\n * @param {string} spreadsheetId spreadsheet ID\n * @returns {*} spreadsheet\n */\nSnippets.prototype.conditionalFormatting = (spreadsheetId) => {\n  try {\n    const myRange = Sheets.newGridRange();\n    myRange.sheetId = 0;\n    myRange.startRowIndex = 0;\n    myRange.endRowIndex = 11;\n    myRange.startColumnIndex = 0;\n    myRange.endColumnIndex = 4;\n\n    // Request 1\n    const rule1ConditionalValue = Sheets.newConditionValue();\n    rule1ConditionalValue.userEnteredValue = \"=GT($D2,median($D$2:$D$11))\";\n\n    const rule1ConditionFormat = Sheets.newCellFormat();\n    rule1ConditionFormat.textFormat = Sheets.newTextFormat();\n    rule1ConditionFormat.textFormat.foregroundColor = Sheets.newColor();\n    rule1ConditionFormat.textFormat.foregroundColor.red = 0.8;\n\n    const rule1Condition = Sheets.newBooleanCondition();\n    rule1Condition.type = \"CUSTOM_FORMULA\";\n    rule1Condition.values = [rule1ConditionalValue];\n\n    const rule1BooleanRule = Sheets.newBooleanRule();\n    rule1BooleanRule.condition = rule1Condition;\n    rule1BooleanRule.format = rule1ConditionFormat;\n\n    const rule1 = Sheets.newConditionalFormatRule();\n    rule1.ranges = [myRange];\n    rule1.booleanRule = rule1BooleanRule;\n\n    const request1 = Sheets.newRequest();\n    const addConditionalFormatRuleRequest1 =\n      Sheets.newAddConditionalFormatRuleRequest();\n    addConditionalFormatRuleRequest1.rule = rule1;\n    addConditionalFormatRuleRequest1.index = 0;\n    request1.addConditionalFormatRule = addConditionalFormatRuleRequest1;\n\n    // Request 2\n    const rule2ConditionalValue = Sheets.newConditionValue();\n    rule2ConditionalValue.userEnteredValue = \"=LT($D2,median($D$2:$D$11))\";\n\n    const rule2ConditionFormat = Sheets.newCellFormat();\n    rule2ConditionFormat.textFormat = Sheets.newTextFormat();\n    rule2ConditionFormat.textFormat.foregroundColor = Sheets.newColor();\n    rule2ConditionFormat.textFormat.foregroundColor.red = 1;\n    rule2ConditionFormat.textFormat.foregroundColor.green = 0.4;\n    rule2ConditionFormat.textFormat.foregroundColor.blue = 0.4;\n\n    const rule2Condition = Sheets.newBooleanCondition();\n    rule2Condition.type = \"CUSTOM_FORMULA\";\n    rule2Condition.values = [rule2ConditionalValue];\n\n    const rule2BooleanRule = Sheets.newBooleanRule();\n    rule2BooleanRule.condition = rule2Condition;\n    rule2BooleanRule.format = rule2ConditionFormat;\n\n    const rule2 = Sheets.newConditionalFormatRule();\n    rule2.ranges = [myRange];\n    rule2.booleanRule = rule2BooleanRule;\n\n    const request2 = Sheets.newRequest();\n    const addConditionalFormatRuleRequest2 =\n      Sheets.newAddConditionalFormatRuleRequest();\n    addConditionalFormatRuleRequest2.rule = rule2;\n    addConditionalFormatRuleRequest2.index = 0;\n    request2.addConditionalFormatRule = addConditionalFormatRuleRequest2;\n\n    // Batch send the requests\n    const requests = [request1, request2];\n    const batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest();\n    batchUpdate.requests = requests;\n    const response = Sheets.Spreadsheets.batchUpdate(\n      batchUpdate,\n      spreadsheetId,\n    );\n    return response;\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n};\n// [END sheets_conditional_formatting]\n"
  },
  {
    "path": "sheets/api/test_spreadsheet_snippets.gs",
    "content": "const snippets = new Snippets();\nconst helpers = new Helpers();\n\n/**\n * A simple exists assertion check. Expects a value to exist. Errors if DNE.\n * @param {any} value A value that is expected to exist.\n */\nfunction expectToExist(value) {\n  if (value) {\n    console.log(\"TEST: Exists\");\n  } else {\n    throw new Error(\"TEST: DNE\");\n  }\n}\n\n/**\n * A simple exists assertion check for primatives (no nested objects).\n * Expects actual to equal expected. Logs the output.\n * @param {any} actual The actual value.\n * @param {any} expected The expected value.\n */\nfunction expectToEqual(actual, expected) {\n  console.log(\"TEST: actual: %s = expected: %s\", actual, expected);\n  if (actual !== expected) {\n    console.log(\"TEST: actual: %s expected: %s\", actual, expected);\n  }\n}\n\n/**\n * Runs all tests.\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateASpreadsheet();\n  itShouldBatchUpdateASpreadsheet();\n  itShouldGetSpreadsheetValues();\n  itShouldBatchGetSpreadsheetValues();\n  itShouldUpdateSpreadsheetValues();\n  itShouldBatchUpdateSpreadsheetValues();\n  itShouldAppendValuesToASpreadsheet();\n  itShouldCreatePivotTables();\n  itShouldConditionallyFormat();\n}\n\n/**\n * Tests creating a spreadsheet.\n */\nfunction itShouldCreateASpreadsheet() {\n  const spreadsheetId = snippets.create(\"Title\");\n  expectToExist(spreadsheetId);\n  helpers.deleteFileOnCleanup(spreadsheetId);\n}\n\n/**\n * Tests updating a spreadsheet.\n */\nfunction itShouldBatchUpdateASpreadsheet() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.batchUpdate(\n    spreadsheetId,\n    \"New Title\",\n    \"Hello\",\n    \"Goodbye\",\n  );\n  const replies = result.replies;\n  expectToEqual(replies.length, 2);\n  const findReplaceResponse = replies[1].findReplace;\n  expectToEqual(findReplaceResponse.occurrencesChanged, 100);\n}\n\n/**\n * Tests getting a spreadsheet value.\n */\nfunction itShouldGetSpreadsheetValues() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.getValues(spreadsheetId, \"A1:C2\");\n  const values = result.values;\n  expectToEqual(values.length, 2);\n  expectToEqual(values[0].length, 3);\n}\n\n/**\n * Tests batch getting spreadsheet values.\n */\nfunction itShouldBatchGetSpreadsheetValues() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.batchGetValues(spreadsheetId, [\"A1:A3\", \"B1:C1\"]);\n  expectToExist(result);\n  expectToEqual(result.valueRanges.length, 2);\n  expectToEqual(result.valueRanges[0].values.length, 3);\n}\n\n/**\n * Tests updating spreadsheet values.\n */\nfunction itShouldUpdateSpreadsheetValues() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  const result = snippets.updateValues(spreadsheetId, \"A1:B2\", \"USER_ENTERED\", [\n    [\"A\", \"B\"],\n    [\"C\", \"D\"],\n  ]);\n  expectToEqual(result.updatedRows, 2);\n  expectToEqual(result.updatedColumns, 2);\n  expectToEqual(result.updatedCells, 4);\n}\n\n/**\n * Test batch updating spreadsheet values.\n */\nfunction itShouldBatchUpdateSpreadsheetValues() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  const result = snippets.batchUpdateValues(\n    spreadsheetId,\n    \"A1:B2\",\n    \"USER_ENTERED\",\n    [\n      [\"A\", \"B\"],\n      [\"C\", \"D\"],\n    ],\n  );\n  expectToEqual(result.totalUpdatedRows, 2);\n  expectToEqual(result.totalUpdatedColumns, 2);\n  expectToEqual(result.totalUpdatedCells, 4);\n}\n\n/**\n * Test appending values to a spreadsheet.\n */\nfunction itShouldAppendValuesToASpreadsheet() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.appendValues(\n    spreadsheetId,\n    \"Sheet1\",\n    \"USER_ENTERED\",\n    [\n      [\"A\", \"B\"],\n      [\"C\", \"D\"],\n    ],\n  );\n  const updates = result.updates;\n  expectToEqual(updates.updatedRows, 2);\n  expectToEqual(updates.updatedColumns, 2);\n  expectToEqual(updates.updatedCells, 4);\n}\n\n/**\n * Test creating pivot tables.\n */\nfunction itShouldCreatePivotTables() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.pivotTable(spreadsheetId);\n  expectToExist(result);\n}\n\n/**\n * Test conditionally formatting spreadsheets.\n */\nfunction itShouldConditionallyFormat() {\n  const spreadsheetId = helpers.createTestSpreadsheet();\n  helpers.populateValues(spreadsheetId);\n  const result = snippets.conditionalFormatting(spreadsheetId);\n  expectToExist(spreadsheetId);\n  expectToEqual(result.replies.length, 2);\n}\n"
  },
  {
    "path": "sheets/customFunctions/btc.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// See https://support.coinbase.com/customer/en/portal/articles/1914910-how-can-i-generate-api-keys-within-coinbase-commerce-\nconst COINBASE_API_TOKEN = \"\"; // TODO\n/**\n * Get's the bitcoin price at a date.\n *\n * @param {string} date The date in yyyy-MM-dd format. Defaults to today.\n * @return The value of BTC in USD at the date. From Coinbase's API\n * @customfunction\n */\nfunction BTC(date) {\n  let dateObject = new Date();\n  if (date) {\n    dateObject = new Date(date);\n  }\n  const dateString = Utilities.formatDate(dateObject, \"GMT\", \"yyyy-MM-dd\");\n  const res = UrlFetchApp.fetch(\n    `https://api.coinbase.com/v2/prices/BTC-USD/spot?date=${dateString}`,\n    {\n      headers: {\n        \"CB-VERSION\": \"2016-10-10\",\n        Authorization: `Bearer ${COINBASE_API_TOKEN}`,\n      },\n    },\n  );\n  const json = JSON.parse(res.getContentText());\n  return json.data.amount;\n}\n"
  },
  {
    "path": "sheets/customFunctions/customFunctions.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_sheets_custom_functions_quickstart]\n/**\n * @OnlyCurrentDoc Limits the script to only accessing the current sheet.\n */\n\n/**\n * A special function that runs when the spreadsheet is open, used to add a\n * custom menu to the spreadsheet.\n */\nfunction onOpen() {\n  try {\n    const spreadsheet = SpreadsheetApp.getActive();\n    const menuItems = [\n      { name: \"Prepare sheet...\", functionName: \"prepareSheet_\" },\n      { name: \"Generate step-by-step...\", functionName: \"generateStepByStep_\" },\n    ];\n    spreadsheet.addMenu(\"Directions\", menuItems);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * A custom function that converts meters to miles.\n *\n * @param {Number} meters The distance in meters.\n * @return {Number} The distance in miles.\n */\nfunction metersToMiles(meters) {\n  if (typeof meters !== \"number\") {\n    return null;\n  }\n  return (meters / 1000) * 0.621371;\n}\n\n/**\n * A custom function that gets the driving distance between two addresses.\n *\n * @param {String} origin The starting address.\n * @param {String} destination The ending address.\n * @return {Number} The distance in meters.\n */\nfunction drivingDistance(origin, destination) {\n  const directions = getDirections_(origin, destination);\n  return directions.routes[0].legs[0].distance.value;\n}\n\n/**\n * A function that adds headers and some initial data to the spreadsheet.\n */\nfunction prepareSheet_() {\n  try {\n    const sheet = SpreadsheetApp.getActiveSheet().setName(\"Settings\");\n    const headers = [\n      \"Start Address\",\n      \"End Address\",\n      \"Driving Distance (meters)\",\n      \"Driving Distance (miles)\",\n    ];\n    const initialData = [\n      \"350 5th Ave, New York, NY 10118\",\n      \"405 Lexington Ave, New York, NY 10174\",\n    ];\n    sheet.getRange(\"A1:D1\").setValues([headers]).setFontWeight(\"bold\");\n    sheet.getRange(\"A2:B2\").setValues([initialData]);\n    sheet.setFrozenRows(1);\n    sheet.autoResizeColumns(1, 4);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Creates a new sheet containing step-by-step directions between the two\n * addresses on the \"Settings\" sheet that the user selected.\n */\nfunction generateStepByStep_() {\n  try {\n    const spreadsheet = SpreadsheetApp.getActive();\n    const settingsSheet = spreadsheet.getSheetByName(\"Settings\");\n    settingsSheet.activate();\n\n    // Prompt the user for a row number.\n    const selectedRow = Browser.inputBox(\n      \"Generate step-by-step\",\n      \"Please enter the row number of\" +\n        \" the\" +\n        \" addresses to use\" +\n        ' (for example, \"2\"):',\n      Browser.Buttons.OK_CANCEL,\n    );\n    if (selectedRow === \"cancel\") {\n      return;\n    }\n    const rowNumber = Number(selectedRow);\n    if (\n      Number.isNaN(rowNumber) ||\n      rowNumber < 2 ||\n      rowNumber > settingsSheet.getLastRow()\n    ) {\n      Browser.msgBox(\n        \"Error\",\n        Utilities.formatString('Row \"%s\" is not valid.', selectedRow),\n        Browser.Buttons.OK,\n      );\n      return;\n    }\n\n    // Retrieve the addresses in that row.\n    const row = settingsSheet.getRange(rowNumber, 1, 1, 2);\n    const rowValues = row.getValues();\n    const origin = rowValues[0][0];\n    const destination = rowValues[0][1];\n    if (!origin || !destination) {\n      Browser.msgBox(\n        \"Error\",\n        \"Row does not contain two addresses.\",\n        Browser.Buttons.OK,\n      );\n      return;\n    }\n\n    // Get the raw directions information.\n    const directions = getDirections_(origin, destination);\n\n    // Create a new sheet and append the steps in the directions.\n    const sheetName = `Driving Directions for Row ${rowNumber}`;\n    let directionsSheet = spreadsheet.getSheetByName(sheetName);\n    if (directionsSheet) {\n      directionsSheet.clear();\n      directionsSheet.activate();\n    } else {\n      directionsSheet = spreadsheet.insertSheet(\n        sheetName,\n        spreadsheet.getNumSheets(),\n      );\n    }\n    const sheetTitle = Utilities.formatString(\n      \"Driving Directions from %s to %s\",\n      origin,\n      destination,\n    );\n    const headers = [\n      [sheetTitle, \"\", \"\"],\n      [\"Step\", \"Distance (Meters)\", \"Distance (Miles)\"],\n    ];\n    const newRows = [];\n    for (const step of directions.routes[0].legs[0].steps) {\n      // Remove HTML tags from the instructions.\n      const instructions = step.html_instructions\n        .replace(/<br>|<div.*?>/g, \"\\n\")\n        .replace(/<.*?>/g, \"\");\n      newRows.push([instructions, step.distance.value]);\n    }\n    directionsSheet.getRange(1, 1, headers.length, 3).setValues(headers);\n    directionsSheet\n      .getRange(headers.length + 1, 1, newRows.length, 2)\n      .setValues(newRows);\n    directionsSheet\n      .getRange(headers.length + 1, 3, newRows.length, 1)\n      .setFormulaR1C1(\"=METERSTOMILES(R[0]C[-1])\");\n\n    // Format the new sheet.\n    directionsSheet.getRange(\"A1:C1\").merge().setBackground(\"#ddddee\");\n    directionsSheet.getRange(\"A1:2\").setFontWeight(\"bold\");\n    directionsSheet.setColumnWidth(1, 500);\n    directionsSheet.getRange(\"B2:C\").setVerticalAlignment(\"top\");\n    directionsSheet.getRange(\"C2:C\").setNumberFormat(\"0.00\");\n    const stepsRange = directionsSheet\n      .getDataRange()\n      .offset(2, 0, directionsSheet.getLastRow() - 2);\n    setAlternatingRowBackgroundColors_(stepsRange, \"#ffffff\", \"#eeeeee\");\n    directionsSheet.setFrozenRows(2);\n    SpreadsheetApp.flush();\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Sets the background colors for alternating rows within the range.\n * @param {Range} range The range to change the background colors of.\n * @param {string} oddColor The color to apply to odd rows (relative to the\n *     start of the range).\n * @param {string} evenColor The color to apply to even rows (relative to the\n *     start of the range).\n */\nfunction setAlternatingRowBackgroundColors_(range, oddColor, evenColor) {\n  const backgrounds = [];\n  for (let row = 1; row <= range.getNumRows(); row++) {\n    const rowBackgrounds = [];\n    for (let column = 1; column <= range.getNumColumns(); column++) {\n      if (row % 2 === 0) {\n        rowBackgrounds.push(evenColor);\n      } else {\n        rowBackgrounds.push(oddColor);\n      }\n    }\n    backgrounds.push(rowBackgrounds);\n  }\n  range.setBackgrounds(backgrounds);\n}\n\n/**\n * A shared helper function used to obtain the full set of directions\n * information between two addresses. Uses the Apps Script Maps Service.\n *\n * @param {String} origin The starting address.\n * @param {String} destination The ending address.\n * @return {Object} The directions response object.\n */\nfunction getDirections_(origin, destination) {\n  const directionFinder = Maps.newDirectionFinder();\n  directionFinder.setOrigin(origin);\n  directionFinder.setDestination(destination);\n  const directions = directionFinder.getDirections();\n  if (directions.status !== \"OK\") {\n    throw directions.error_message;\n  }\n  return directions;\n}\n// [END apps_script_sheets_custom_functions_quickstart]\n"
  },
  {
    "path": "sheets/dateAddAndSubtract/README.md",
    "content": "# Date Add and Subtract\n\nDate Add and Subtract is a sample\n[add-on for Google Sheets](https://developers.google.com/apps-script/add-ons)\nthat provides custom functions for date manipulation. The script uses the\n[Moment.js](http://momentjs.com/) JavaScript library, which is included directly\nin the Apps Script project.\n\n![Date Add and Subtract screenshot](screenshot.png)\n\n## Try it out\n\nFor your convience we have published the script to the Google Sheets\n[add-ons store](https://chrome.google.com/webstore/detail/date-add-and-subtract/mhdmhddjinipgjhpicaidhpimlmgnflb).\nInstall the add-on via the store and follow the instructions to get started.\n"
  },
  {
    "path": "sheets/dateAddAndSubtract/dateAddAndSubtract.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Provides the custom functions DATEADD and DATESUBTRACT and\n * the helper functions that they use.\n * @OnlyCurrentDoc\n */\n\n/**\n * The list of valid unit identifiers.\n */\nconst VALID_UNITS = [\n  \"year\",\n  \"month\",\n  \"week\",\n  \"day\",\n  \"hour\",\n  \"minute\",\n  \"second\",\n  \"millisecond\",\n];\n\n/**\n * Runs when the add-on is installed.\n */\nfunction onInstall() {\n  onOpen();\n}\n\n/**\n * Runs when the document is opened, creating the add-on's menu. Custom function\n * add-ons need at least one menu item, since the add-on is only enabled in the\n * current spreadsheet when a function is run.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Use in this spreadsheet\", \"use\")\n    .addToUi();\n}\n\n/**\n * Enables the add-on on for the current spreadsheet (simply by running) and\n * shows a popup informing the user of the new functions that are available.\n */\nfunction use() {\n  const title = \"Date Custom Functions\";\n  const message =\n    \"The functions DATEADD and DATESUBTRACT are now available in \" +\n    \"this spreadsheet. More information is available in the function help \" +\n    \"box that appears when you start using them in a formula.\";\n  const ui = SpreadsheetApp.getUi();\n  ui.alert(title, message, ui.ButtonSet.OK);\n}\n\n/**\n * Adds some amount of time to a date.\n * @param {Date|Range} date The date to add to, or a range of dates.\n * @param {string|Range} unit The unit of time to add, or a range of units.\n *    Possible values include:\n *    `years`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, and\n *    `milliseconds`. You can also use the shorthand notation for these units\n *    which are `y`, `M`, `w`, `d`, `h`, `m`, `s`, `ms` respectively.\n * @param {number|Range} amount The amount of the specified unit to add, or a\n *    range of amounts.\n * @return {Date} The new date.\n * @customFunction\n */\nfunction DATEADD(date, unit, amount) {\n  const args = [date, unit, amount];\n  return multimap(args, (date, unit, amount) => {\n    validateParameters(date, unit, amount);\n    return moment(date).add(unit, amount).toDate();\n  });\n}\n\n/**\n * @customFunction\n * A test function for DATEADD\n * @param {string|Range} date The date to add to.\n * @param {string|Range} unit The unit of time to add.\n * @param {number|Range} amount The amount of the specified unit to add.\n * @return {string} The date in a string.\n */\nfunction DATETEST(date, unit, amount) {\n  return JSON.stringify(DATEADD(date, unit, amount)); // eslint-disable-line new-cap\n}\n\n/**\n * Subtracts some amount of time from a date.\n * @param {Date|Range} date The date to subtract from, or a range of dates.\n * @param {string|Range} unit The unit of time to subtract, or a range of units.\n *    Possible values include:\n *    `years`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, and\n *    `milliseconds`. You can also use the shorthand notation for these units\n *    which are `y`, `M`, `w`, `d`, `h`, `m`, `s`, `ms` respectively.\n * @param {number|Range} amount The amount of the specified unit to subtract, or\n *     a range of amounts.\n * @return {Date} The new date.\n * @customFunction\n */\nfunction DATESUBTRACT(date, unit, amount) {\n  const args = [date, unit, amount];\n  return multimap(args, (date, unit, amount) => {\n    validateParameters(date, unit, amount);\n    return moment(date).subtract(unit, amount).toDate();\n  });\n}\n\n/**\n * Validates that the date, unit, and amount supplied are compatible with\n * Moment, throwing an exception if any of the parameters are invalid.\n * @param {Date} date The date to add to or subtract from.\n * @param {string} unit The unit of time to add/subtract.\n * @param {number} amount The amount of the specified unit to add/subtract.\n */\nfunction validateParameters(date, unit, amount) {\n  if (\n    date === undefined ||\n    typeof date === \"number\" ||\n    !moment(date).isValid()\n  ) {\n    throw Utilities.formatString(\n      'Parameter 1 expects a date value, but \"%s\" ' +\n        \"cannot be coerced to a date.\",\n      date,\n    );\n  }\n  if (VALID_UNITS.indexOf(moment.normalizeUnits(unit)) < 0) {\n    throw Utilities.formatString(\n      \"Parameter 2 expects a unit identifier, but \" +\n        '\"%s\" is not a valid identifier.',\n      unit,\n    );\n  }\n  if (Number.isNaN(Number(amount))) {\n    throw Utilities.formatString(\n      \"Parameter 3 expects a number value, but \" +\n        '\"%s\" cannot be coerced to a number.',\n      amount,\n    );\n  }\n}\n\n/**\n * Applies a function to a set of arguments, looping over arrays in those\n * arguments. Similar to Array.map, except that it can map the function across\n * multiple arrays, passing forward non-array values.\n * @param {Array} args The arguments to map against.\n * @param {Function} func The function to apply.\n * @return {Array} The results of the mapping.\n */\nfunction multimap(args, func) {\n  // Determine the length of the arrays.\n  const lengths = args.map((arg) => {\n    if (Array.isArray(arg)) {\n      return arg.length;\n    }\n    return 0;\n  });\n  const max = Math.max.apply(null, lengths);\n\n  // If there aren't any arrays, just call the function.\n  if (max === 0) {\n    return func(...args);\n  }\n\n  // Ensure all the arrays are the same length.\n  // Arrays of length 1 are exempted, since they are assumed to be rows/columns\n  // that should apply to each row/column in the other sets.\n  for (const length of lengths) {\n    if (length !== max && length > 1) {\n      throw new Error(`All input ranges must be the same size: ${max}`);\n    }\n  }\n\n  // Recursively apply the map function to each element in the arrays.\n  const result = [];\n  for (let i = 0; i < max; i++) {\n    const params = args.map((arg) => {\n      if (Array.isArray(arg)) {\n        return arg.length === 1 ? arg[0] : arg[i];\n      }\n      return arg;\n    });\n    result.push(multimap(params, func));\n  }\n  return result;\n}\n\n/**\n * Convert the array-like arguments object into a real array.\n * @param {Arguments} args The arguments object to convert.\n * @return {Array} The equivalent array.\n */\nfunction toArray(args) {\n  return Array.prototype.slice.call(args);\n}\n"
  },
  {
    "path": "sheets/dateAddAndSubtract/moment.gs",
    "content": "// ! moment.js\n// ! version : 2.10.6\n// ! authors : Tim Wood, Iskren Chernev, Moment.js contributors\n// ! license : MIT\n// ! momentjs.com\n\n/*\nCopyright (c) 2011-2015 Tim Wood, Iskren Chernev, Moment.js contributors\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n*/\n\n!function(a, b) {\n'object'==typeof exports&&'undefined'!=typeof module?module.exports=b():'function'==typeof define&&define.amd?define(b):a.moment=b();\n}(this, function() {\n'use strict'; function a() {\nreturn Hc(...arguments);\n} function b(a) {\nHc=a;\n} function c(a) {\nreturn '[object Array]'===Object.prototype.toString.call(a);\n} function d(a) {\nreturn a instanceof Date||'[object Date]'===Object.prototype.toString.call(a);\n} function e(a, b) {\nvar c, d=[]; for (c=0; c<a.length; ++c)d.push(b(a[c], c)); return d;\n} function f(a, b) {\nreturn Object.prototype.hasOwnProperty.call(a, b);\n} function g(a, b) {\nfor (var c in b)f(b, c)&&(a[c]=b[c]); return f(b, 'toString')&&(a.toString=b.toString), f(b, 'valueOf')&&(a.valueOf=b.valueOf), a;\n} function h(a, b, c, d) {\nreturn Ca(a, b, c, d, !0).utc();\n} function i() {\nreturn {empty: !1, unusedTokens: [], unusedInput: [], overflow: -2, charsLeftOver: 0, nullInput: !1, invalidMonth: null, invalidFormat: !1, userInvalidated: !1, iso: !1};\n} function j(a) {\nreturn null==a._pf&&(a._pf=i()), a._pf;\n} function k(a) {\nif (null==a._isValid) {\nvar b=j(a); a._isValid=!(isNaN(a._d.getTime())||!(b.overflow<0)||b.empty||b.invalidMonth||b.invalidWeekday||b.nullInput||b.invalidFormat||b.userInvalidated), a._strict&&(a._isValid=a._isValid&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour);\n} return a._isValid;\n} function l(a) {\nvar b=h(NaN); return null!=a?g(j(b), a):j(b).userInvalidated=!0, b;\n} function m(a, b) {\nvar c, d, e; if ('undefined'!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject), 'undefined'!=typeof b._i&&(a._i=b._i), 'undefined'!=typeof b._f&&(a._f=b._f), 'undefined'!=typeof b._l&&(a._l=b._l), 'undefined'!=typeof b._strict&&(a._strict=b._strict), 'undefined'!=typeof b._tzm&&(a._tzm=b._tzm), 'undefined'!=typeof b._isUTC&&(a._isUTC=b._isUTC), 'undefined'!=typeof b._offset&&(a._offset=b._offset), 'undefined'!=typeof b._pf&&(a._pf=j(b)), 'undefined'!=typeof b._locale&&(a._locale=b._locale), Jc.length>0) for (c in Jc)d=Jc[c], e=b[d], 'undefined'!=typeof e&&(a[d]=e); return a;\n} function n(b) {\nm(this, b), this._d=new Date(null!=b._d?b._d.getTime():NaN), Kc===!1&&(Kc=!0, a.updateOffset(this), Kc=!1);\n} function o(a) {\nreturn a instanceof n||null!=a&&null!=a._isAMomentObject;\n} function p(a) {\nreturn 0>a?Math.ceil(a):Math.floor(a);\n} function q(a) {\nvar b=+a, c=0; return 0!==b&&isFinite(b)&&(c=p(b)), c;\n} function r(a, b, c) {\nvar d, e=Math.min(a.length, b.length), f=Math.abs(a.length-b.length), g=0; for (d=0; e>d; d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++; return g+f;\n} function s() {} function t(a) {\nreturn a?a.toLowerCase().replace('_', '-'):a;\n} function u(a) {\nfor (var b, c, d, e, f=0; f<a.length;) {\nfor (e=t(a[f]).split('-'), b=e.length, c=t(a[f+1]), c=c?c.split('-'):null; b>0;) {\nif (d=v(e.slice(0, b).join('-'))) return d; if (c&&c.length>=b&&r(e, c, !0)>=b-1) break; b--;\n}f++;\n} return null;\n} function v(a) {\nvar b=null; if (!Lc[a]&&'undefined'!=typeof module&&module&&module.exports) {\ntry {\nb=Ic._abbr, require('./locale/'+a), w(b);\n} catch (c) {}\n} return Lc[a];\n} function w(a, b) {\nvar c; return a&&(c='undefined'==typeof b?y(a):x(a, b), c&&(Ic=c)), Ic._abbr;\n} function x(a, b) {\nreturn null!==b?(b.abbr=a, Lc[a]=Lc[a]||new s, Lc[a].set(b), w(a), Lc[a]):(delete Lc[a], null);\n} function y(a) {\nvar b; if (a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr), !a) return Ic; if (!c(a)) {\nif (b=v(a)) return b; a=[a];\n} return u(a);\n} function z(a, b) {\nvar c=a.toLowerCase(); Mc[c]=Mc[c+'s']=Mc[b]=a;\n} function A(a) {\nreturn 'string'==typeof a?Mc[a]||Mc[a.toLowerCase()]:void 0;\n} function B(a) {\nvar b, c, d={}; for (c in a)f(a, c)&&(b=A(c), b&&(d[b]=a[c])); return d;\n} function C(b, c) {\nreturn function(d) {\nreturn null!=d?(E(this, b, d), a.updateOffset(this, c), this):D(this, b);\n};\n} function D(a, b) {\nreturn a._d['get'+(a._isUTC?'UTC':'')+b]();\n} function E(a, b, c) {\nreturn a._d['set'+(a._isUTC?'UTC':'')+b](c);\n} function F(a, b) {\nvar c; if ('object'==typeof a) for (c in a) this.set(c, a[c]); else if (a=A(a), 'function'==typeof this[a]) return this[a](b); return this;\n} function G(a, b, c) {\nvar d=''+Math.abs(a), e=b-d.length, f=a>=0; return (f?c?'+':'':'-')+Math.pow(10, Math.max(0, e)).toString().substr(1)+d;\n} function H(a, b, c, d) {\nvar e=d; 'string'==typeof d&&(e=function() {\nreturn this[d]();\n}), a&&(Qc[a]=e), b&&(Qc[b[0]]=function() {\nreturn G(e.apply(this, arguments), b[1], b[2]);\n}), c&&(Qc[c]=function() {\nreturn this.localeData().ordinal(e.apply(this, arguments), a);\n});\n} function I(a) {\nreturn a.match(/\\[[\\s\\S]/)?a.replace(/^\\[|\\]$/g, ''):a.replace(/\\\\/g, '');\n} function J(a) {\nvar b, c, d=a.match(Nc); for (b=0, c=d.length; c>b; b++)Qc[d[b]]?d[b]=Qc[d[b]]:d[b]=I(d[b]); return function(e) {\nvar f=''; for (b=0; c>b; b++)f+=d[b]instanceof Function?d[b].call(e, a):d[b]; return f;\n};\n} function K(a, b) {\nreturn a.isValid()?(b=L(b, a.localeData()), Pc[b]=Pc[b]||J(b), Pc[b](a)):a.localeData().invalidDate();\n} function L(a, b) {\nfunction c(a) {\nreturn b.longDateFormat(a)||a;\n} var d=5; for (Oc.lastIndex=0; d>=0&&Oc.test(a);)a=a.replace(Oc, c), Oc.lastIndex=0, d-=1; return a;\n} function M(a) {\nreturn 'function'==typeof a&&'[object Function]'===Object.prototype.toString.call(a);\n} function N(a, b, c) {\ndd[a]=M(b)?b:function(a) {\nreturn a&&c?c:b;\n};\n} function O(a, b) {\nreturn f(dd, a)?dd[a](b._strict, b._locale):new RegExp(P(a));\n} function P(a) {\nreturn a.replace('\\\\', '').replace(/\\\\(\\[)|\\\\(\\])|\\[([^\\]\\[]*)\\]|\\\\(.)/g, function(a, b, c, d, e) {\nreturn b||c||d||e;\n}).replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n} function Q(a, b) {\nvar c, d=b; for ('string'==typeof a&&(a=[a]), 'number'==typeof b&&(d=function(a, c) {\nc[b]=q(a);\n}), c=0; c<a.length; c++)ed[a[c]]=d;\n} function R(a, b) {\nQ(a, function(a, c, d, e) {\nd._w=d._w||{}, b(a, d._w, d, e);\n});\n} function S(a, b, c) {\nnull!=b&&f(ed, a)&&ed[a](b, c._a, c, a);\n} function T(a, b) {\nreturn new Date(Date.UTC(a, b+1, 0)).getUTCDate();\n} function U(a) {\nreturn this._months[a.month()];\n} function V(a) {\nreturn this._monthsShort[a.month()];\n} function W(a, b, c) {\nvar d, e, f; for (this._monthsParse||(this._monthsParse=[], this._longMonthsParse=[], this._shortMonthsParse=[]), d=0; 12>d; d++) {\nif (e=h([2e3, d]), c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp('^'+this.months(e, '').replace('.', '')+'$', 'i'), this._shortMonthsParse[d]=new RegExp('^'+this.monthsShort(e, '').replace('.', '')+'$', 'i')), c||this._monthsParse[d]||(f='^'+this.months(e, '')+'|^'+this.monthsShort(e, ''), this._monthsParse[d]=new RegExp(f.replace('.', ''), 'i')), c&&'MMMM'===b&&this._longMonthsParse[d].test(a)) return d; if (c&&'MMM'===b&&this._shortMonthsParse[d].test(a)) return d; if (!c&&this._monthsParse[d].test(a)) return d;\n}\n} function X(a, b) {\nvar c; return 'string'==typeof b&&(b=a.localeData().monthsParse(b), 'number'!=typeof b)?a:(c=Math.min(a.date(), T(a.year(), b)), a._d['set'+(a._isUTC?'UTC':'')+'Month'](b, c), a);\n} function Y(b) {\nreturn null!=b?(X(this, b), a.updateOffset(this, !0), this):D(this, 'Month');\n} function Z() {\nreturn T(this.year(), this.month());\n} function $(a) {\nvar b, c=a._a; return c&&-2===j(a).overflow&&(b=c[gd]<0||c[gd]>11?gd:c[hd]<1||c[hd]>T(c[fd], c[gd])?hd:c[id]<0||c[id]>24||24===c[id]&&(0!==c[jd]||0!==c[kd]||0!==c[ld])?id:c[jd]<0||c[jd]>59?jd:c[kd]<0||c[kd]>59?kd:c[ld]<0||c[ld]>999?ld:-1, j(a)._overflowDayOfYear&&(fd>b||b>hd)&&(b=hd), j(a).overflow=b), a;\n} function _(b) {\na.suppressDeprecationWarnings===!1&&'undefined'!=typeof console&&console.warn&&console.warn('Deprecation warning: '+b);\n} function aa(a, b) {\nvar c=!0; return g(function() {\nreturn c&&(_(a+'\\n'+(new Error).stack), c=!1), b.apply(this, arguments);\n}, b);\n} function ba(a, b) {\nod[a]||(_(b), od[a]=!0);\n} function ca(a) {\nvar b, c, d=a._i, e=pd.exec(d); if (e) {\nfor (j(a).iso=!0, b=0, c=qd.length; c>b; b++) {\nif (qd[b][1].exec(d)) {\na._f=qd[b][0]; break;\n}\n} for (b=0, c=rd.length; c>b; b++) {\nif (rd[b][1].exec(d)) {\na._f+=(e[6]||' ')+rd[b][0]; break;\n}\n}d.match(ad)&&(a._f+='Z'), va(a);\n} else a._isValid=!1;\n} function da(b) {\nvar c=sd.exec(b._i); return null!==c?void(b._d=new Date(+c[1])):(ca(b), void(b._isValid===!1&&(delete b._isValid, a.createFromInputFallback(b))));\n} function ea(a, b, c, d, e, f, g) {\nvar h=new Date(a, b, c, d, e, f, g); return 1970>a&&h.setFullYear(a), h;\n} function fa(a) {\nvar b=new Date(Date.UTC.apply(null, arguments)); return 1970>a&&b.setUTCFullYear(a), b;\n} function ga(a) {\nreturn ha(a)?366:365;\n} function ha(a) {\nreturn a%4===0&&a%100!==0||a%400===0;\n} function ia() {\nreturn ha(this.year());\n} function ja(a, b, c) {\nvar d, e=c-b, f=c-a.day(); return f>e&&(f-=7), e-7>f&&(f+=7), d=Da(a).add(f, 'd'), {week: Math.ceil(d.dayOfYear()/7), year: d.year()};\n} function ka(a) {\nreturn ja(a, this._week.dow, this._week.doy).week;\n} function la() {\nreturn this._week.dow;\n} function ma() {\nreturn this._week.doy;\n} function na(a) {\nvar b=this.localeData().week(this); return null==a?b:this.add(7*(a-b), 'd');\n} function oa(a) {\nvar b=ja(this, 1, 4).week; return null==a?b:this.add(7*(a-b), 'd');\n} function pa(a, b, c, d, e) {\nvar f, g=6+e-d, h=fa(a, 0, 1+g), i=h.getUTCDay(); return e>i&&(i+=7), c=null!=c?1*c:e, f=1+g+7*(b-1)-i+c, {year: f>0?a:a-1, dayOfYear: f>0?f:ga(a-1)+f};\n} function qa(a) {\nvar b=Math.round((this.clone().startOf('day')-this.clone().startOf('year'))/864e5)+1; return null==a?b:this.add(a-b, 'd');\n} function ra(a, b, c) {\nreturn null!=a?a:null!=b?b:c;\n} function sa(a) {\nvar b=new Date; return a._useUTC?[b.getUTCFullYear(), b.getUTCMonth(), b.getUTCDate()]:[b.getFullYear(), b.getMonth(), b.getDate()];\n} function ta(a) {\nvar b, c, d, e, f=[]; if (!a._d) {\nfor (d=sa(a), a._w&&null==a._a[hd]&&null==a._a[gd]&&ua(a), a._dayOfYear&&(e=ra(a._a[fd], d[fd]), a._dayOfYear>ga(e)&&(j(a)._overflowDayOfYear=!0), c=fa(e, 0, a._dayOfYear), a._a[gd]=c.getUTCMonth(), a._a[hd]=c.getUTCDate()), b=0; 3>b&&null==a._a[b]; ++b)a._a[b]=f[b]=d[b]; for (;7>b; b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b]; 24===a._a[id]&&0===a._a[jd]&&0===a._a[kd]&&0===a._a[ld]&&(a._nextDay=!0, a._a[id]=0), a._d=(a._useUTC?fa:ea)(...f), null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm), a._nextDay&&(a._a[id]=24);\n}\n} function ua(a) {\nvar b, c, d, e, f, g, h; b=a._w, null!=b.GG||null!=b.W||null!=b.E?(f=1, g=4, c=ra(b.GG, a._a[fd], ja(Da(), 1, 4).year), d=ra(b.W, 1), e=ra(b.E, 1)):(f=a._locale._week.dow, g=a._locale._week.doy, c=ra(b.gg, a._a[fd], ja(Da(), f, g).year), d=ra(b.w, 1), null!=b.d?(e=b.d, f>e&&++d):e=null!=b.e?b.e+f:f), h=pa(c, d, e, g, f), a._a[fd]=h.year, a._dayOfYear=h.dayOfYear;\n} function va(b) {\nif (b._f===a.ISO_8601) return void ca(b); b._a=[], j(b).empty=!0; var c, d, e, f, g, h=''+b._i, i=h.length, k=0; for (e=L(b._f, b._locale).match(Nc)||[], c=0; c<e.length; c++)f=e[c], d=(h.match(O(f, b))||[])[0], d&&(g=h.substr(0, h.indexOf(d)), g.length>0&&j(b).unusedInput.push(g), h=h.slice(h.indexOf(d)+d.length), k+=d.length), Qc[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f), S(f, d, b)):b._strict&&!d&&j(b).unusedTokens.push(f); j(b).charsLeftOver=i-k, h.length>0&&j(b).unusedInput.push(h), j(b).bigHour===!0&&b._a[id]<=12&&b._a[id]>0&&(j(b).bigHour=void 0), b._a[id]=wa(b._locale, b._a[id], b._meridiem), ta(b), $(b);\n} function wa(a, b, c) {\nvar d; return null==c?b:null!=a.meridiemHour?a.meridiemHour(b, c):null!=a.isPM?(d=a.isPM(c), d&&12>b&&(b+=12), d||12!==b||(b=0), b):b;\n} function xa(a) {\nvar b, c, d, e, f; if (0===a._f.length) return j(a).invalidFormat=!0, void(a._d=new Date(NaN)); for (e=0; e<a._f.length; e++)f=0, b=m({}, a), null!=a._useUTC&&(b._useUTC=a._useUTC), b._f=a._f[e], va(b), k(b)&&(f+=j(b).charsLeftOver, f+=10*j(b).unusedTokens.length, j(b).score=f, (null==d||d>f)&&(d=f, c=b)); g(a, c||b);\n} function ya(a) {\nif (!a._d) {\nvar b=B(a._i); a._a=[b.year, b.month, b.day||b.date, b.hour, b.minute, b.second, b.millisecond], ta(a);\n}\n} function za(a) {\nvar b=new n($(Aa(a))); return b._nextDay&&(b.add(1, 'd'), b._nextDay=void 0), b;\n} function Aa(a) {\nvar b=a._i, e=a._f; return a._locale=a._locale||y(a._l), null===b||void 0===e&&''===b?l({nullInput: !0}):('string'==typeof b&&(a._i=b=a._locale.preparse(b)), o(b)?new n($(b)):(c(e)?xa(a):e?va(a):d(b)?a._d=b:Ba(a), a));\n} function Ba(b) {\nvar f=b._i; void 0===f?b._d=new Date:d(f)?b._d=new Date(+f):'string'==typeof f?da(b):c(f)?(b._a=e(f.slice(0), function(a) {\nreturn parseInt(a, 10);\n}), ta(b)):'object'==typeof f?ya(b):'number'==typeof f?b._d=new Date(f):a.createFromInputFallback(b);\n} function Ca(a, b, c, d, e) {\nvar f={}; return 'boolean'==typeof c&&(d=c, c=void 0), f._isAMomentObject=!0, f._useUTC=f._isUTC=e, f._l=c, f._i=a, f._f=b, f._strict=d, za(f);\n} function Da(a, b, c, d) {\nreturn Ca(a, b, c, d, !1);\n} function Ea(a, b) {\nvar d, e; if (1===b.length&&c(b[0])&&(b=b[0]), !b.length) return Da(); for (d=b[0], e=1; e<b.length; ++e)(!b[e].isValid()||b[e][a](d))&&(d=b[e]); return d;\n} function Fa() {\nvar a=[].slice.call(arguments, 0); return Ea('isBefore', a);\n} function Ga() {\nvar a=[].slice.call(arguments, 0); return Ea('isAfter', a);\n} function Ha(a) {\nvar b=B(a), c=b.year||0, d=b.quarter||0, e=b.month||0, f=b.week||0, g=b.day||0, h=b.hour||0, i=b.minute||0, j=b.second||0, k=b.millisecond||0; this._milliseconds=+k+1e3*j+6e4*i+36e5*h, this._days=+g+7*f, this._months=+e+3*d+12*c, this._data={}, this._locale=y(), this._bubble();\n} function Ia(a) {\nreturn a instanceof Ha;\n} function Ja(a, b) {\nH(a, 0, 0, function() {\nvar a=this.utcOffset(), c='+'; return 0>a&&(a=-a, c='-'), c+G(~~(a/60), 2)+b+G(~~a%60, 2);\n});\n} function Ka(a) {\nvar b=(a||'').match(ad)||[], c=b[b.length-1]||[], d=(c+'').match(xd)||['-', 0, 0], e=+(60*d[1])+q(d[2]); return '+'===d[0]?e:-e;\n} function La(b, c) {\nvar e, f; return c._isUTC?(e=c.clone(), f=(o(b)||d(b)?+b:+Da(b))-+e, e._d.setTime(+e._d+f), a.updateOffset(e, !1), e):Da(b).local();\n} function Ma(a) {\nreturn 15*-Math.round(a._d.getTimezoneOffset()/15);\n} function Na(b, c) {\nvar d, e=this._offset||0; return null!=b?('string'==typeof b&&(b=Ka(b)), Math.abs(b)<16&&(b=60*b), !this._isUTC&&c&&(d=Ma(this)), this._offset=b, this._isUTC=!0, null!=d&&this.add(d, 'm'), e!==b&&(!c||this._changeInProgress?bb(this, Ya(b-e, 'm'), 1, !1):this._changeInProgress||(this._changeInProgress=!0, a.updateOffset(this, !0), this._changeInProgress=null)), this):this._isUTC?e:Ma(this);\n} function Oa(a, b) {\nreturn null!=a?('string'!=typeof a&&(a=-a), this.utcOffset(a, b), this):-this.utcOffset();\n} function Pa(a) {\nreturn this.utcOffset(0, a);\n} function Qa(a) {\nreturn this._isUTC&&(this.utcOffset(0, a), this._isUTC=!1, a&&this.subtract(Ma(this), 'm')), this;\n} function Ra() {\nreturn this._tzm?this.utcOffset(this._tzm):'string'==typeof this._i&&this.utcOffset(Ka(this._i)), this;\n} function Sa(a) {\nreturn a=a?Da(a).utcOffset():0, (this.utcOffset()-a)%60===0;\n} function Ta() {\nreturn this.utcOffset()> this.clone().month(0).utcOffset()||this.utcOffset()> this.clone().month(5).utcOffset();\n} function Ua() {\nif ('undefined'!=typeof this._isDSTShifted) return this._isDSTShifted; var a={}; if (m(a, this), a=Aa(a), a._a) {\nvar b=a._isUTC?h(a._a):Da(a._a); this._isDSTShifted=this.isValid()&&r(a._a, b.toArray())>0;\n} else this._isDSTShifted=!1; return this._isDSTShifted;\n} function Va() {\nreturn !this._isUTC;\n} function Wa() {\nreturn this._isUTC;\n} function Xa() {\nreturn this._isUTC&&0===this._offset;\n} function Ya(a, b) {\nvar c, d, e, g=a, h=null; return Ia(a)?g={ms: a._milliseconds, d: a._days, M: a._months}:'number'==typeof a?(g={}, b?g[b]=a:g.milliseconds=a):(h=yd.exec(a))?(c='-'===h[1]?-1:1, g={y: 0, d: q(h[hd])*c, h: q(h[id])*c, m: q(h[jd])*c, s: q(h[kd])*c, ms: q(h[ld])*c}):(h=zd.exec(a))?(c='-'===h[1]?-1:1, g={y: Za(h[2], c), M: Za(h[3], c), d: Za(h[4], c), h: Za(h[5], c), m: Za(h[6], c), s: Za(h[7], c), w: Za(h[8], c)}):null==g?g={}:'object'==typeof g&&('from'in g||'to'in g)&&(e=_a(Da(g.from), Da(g.to)), g={}, g.ms=e.milliseconds, g.M=e.months), d=new Ha(g), Ia(a)&&f(a, '_locale')&&(d._locale=a._locale), d;\n} function Za(a, b) {\nvar c=a&&parseFloat(a.replace(',', '.')); return (isNaN(c)?0:c)*b;\n} function $a(a, b) {\nvar c={milliseconds: 0, months: 0}; return c.months=b.month()-a.month()+12*(b.year()-a.year()), a.clone().add(c.months, 'M').isAfter(b)&&--c.months, c.milliseconds=+b-+a.clone().add(c.months, 'M'), c;\n} function _a(a, b) {\nvar c; return b=La(b, a), a.isBefore(b)?c=$a(a, b):(c=$a(b, a), c.milliseconds=-c.milliseconds, c.months=-c.months), c;\n} function ab(a, b) {\nreturn function(c, d) {\nvar e, f; return null===d||isNaN(+d)||(ba(b, 'moment().'+b+'(period, number) is deprecated. Please use moment().'+b+'(number, period).'), f=c, c=d, d=f), c='string'==typeof c?+c:c, e=Ya(c, d), bb(this, e, a), this;\n};\n} function bb(b, c, d, e) {\nvar f=c._milliseconds, g=c._days, h=c._months; e=null==e?!0:e, f&&b._d.setTime(+b._d+f*d), g&&E(b, 'Date', D(b, 'Date')+g*d), h&&X(b, D(b, 'Month')+h*d), e&&a.updateOffset(b, g||h);\n} function cb(a, b) {\nvar c=a||Da(), d=La(c, this).startOf('day'), e=this.diff(d, 'days', !0), f=-6>e?'sameElse':-1>e?'lastWeek':0>e?'lastDay':1>e?'sameDay':2>e?'nextDay':7>e?'nextWeek':'sameElse'; return this.format(b&&b[f]||this.localeData().calendar(f, this, Da(c)));\n} function db() {\nreturn new n(this);\n} function eb(a, b) {\nvar c; return b=A('undefined'!=typeof b?b:'millisecond'), 'millisecond'===b?(a=o(a)?a:Da(a), +this>+a):(c=o(a)?+a:+Da(a), c<+this.clone().startOf(b));\n} function fb(a, b) {\nvar c; return b=A('undefined'!=typeof b?b:'millisecond'), 'millisecond'===b?(a=o(a)?a:Da(a), +a>+this):(c=o(a)?+a:+Da(a), +this.clone().endOf(b)<c);\n} function gb(a, b, c) {\nreturn this.isAfter(a, c)&&this.isBefore(b, c);\n} function hb(a, b) {\nvar c; return b=A(b||'millisecond'), 'millisecond'===b?(a=o(a)?a:Da(a), +this===+a):(c=+Da(a), +this.clone().startOf(b)<=c&&c<=+this.clone().endOf(b));\n} function ib(a, b, c) {\nvar d, e, f=La(a, this), g=6e4*(f.utcOffset()-this.utcOffset()); return b=A(b), 'year'===b||'month'===b||'quarter'===b?(e=jb(this, f), 'quarter'===b?e/=3:'year'===b&&(e/=12)):(d=this-f, e='second'===b?d/1e3:'minute'===b?d/6e4:'hour'===b?d/36e5:'day'===b?(d-g)/864e5:'week'===b?(d-g)/6048e5:d), c?e:p(e);\n} function jb(a, b) {\nvar c, d, e=12*(b.year()-a.year())+(b.month()-a.month()), f=a.clone().add(e, 'months'); return 0>b-f?(c=a.clone().add(e-1, 'months'), d=(b-f)/(f-c)):(c=a.clone().add(e+1, 'months'), d=(b-f)/(c-f)), -(e+d);\n} function kb() {\nreturn this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');\n} function lb() {\nvar a=this.clone().utc(); return 0<a.year()&&a.year()<=9999?'function'==typeof Date.prototype.toISOString?this.toDate().toISOString():K(a, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'):K(a, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');\n} function mb(b) {\nvar c=K(this, b||a.defaultFormat); return this.localeData().postformat(c);\n} function nb(a, b) {\nreturn this.isValid()?Ya({to: this, from: a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate();\n} function ob(a) {\nreturn this.from(Da(), a);\n} function pb(a, b) {\nreturn this.isValid()?Ya({from: this, to: a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate();\n} function qb(a) {\nreturn this.to(Da(), a);\n} function rb(a) {\nvar b; return void 0===a?this._locale._abbr:(b=y(a), null!=b&&(this._locale=b), this);\n} function sb() {\nreturn this._locale;\n} function tb(a) {\nswitch (a=A(a)) {\ncase 'year': this.month(0); case 'quarter': case 'month': this.date(1); case 'week': case 'isoWeek': case 'day': this.hours(0); case 'hour': this.minutes(0); case 'minute': this.seconds(0); case 'second': this.milliseconds(0);\n} return 'week'===a&&this.weekday(0), 'isoWeek'===a&&this.isoWeekday(1), 'quarter'===a&&this.month(3*Math.floor(this.month()/3)), this;\n} function ub(a) {\nreturn a=A(a), void 0===a||'millisecond'===a?this:this.startOf(a).add(1, 'isoWeek'===a?'week':a).subtract(1, 'ms');\n} function vb() {\nreturn +this._d-6e4*(this._offset||0);\n} function wb() {\nreturn Math.floor(+this/1e3);\n} function xb() {\nreturn this._offset?new Date(+this):this._d;\n} function yb() {\nvar a=this; return [a.year(), a.month(), a.date(), a.hour(), a.minute(), a.second(), a.millisecond()];\n} function zb() {\nvar a=this; return {years: a.year(), months: a.month(), date: a.date(), hours: a.hours(), minutes: a.minutes(), seconds: a.seconds(), milliseconds: a.milliseconds()};\n} function Ab() {\nreturn k(this);\n} function Bb() {\nreturn g({}, j(this));\n} function Cb() {\nreturn j(this).overflow;\n} function Db(a, b) {\nH(0, [a, a.length], 0, b);\n} function Eb(a, b, c) {\nreturn ja(Da([a, 11, 31+b-c]), b, c).week;\n} function Fb(a) {\nvar b=ja(this, this.localeData()._week.dow, this.localeData()._week.doy).year; return null==a?b:this.add(a-b, 'y');\n} function Gb(a) {\nvar b=ja(this, 1, 4).year; return null==a?b:this.add(a-b, 'y');\n} function Hb() {\nreturn Eb(this.year(), 1, 4);\n} function Ib() {\nvar a=this.localeData()._week; return Eb(this.year(), a.dow, a.doy);\n} function Jb(a) {\nreturn null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3);\n} function Kb(a, b) {\nreturn 'string'!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a), 'number'==typeof a?a:null):parseInt(a, 10);\n} function Lb(a) {\nreturn this._weekdays[a.day()];\n} function Mb(a) {\nreturn this._weekdaysShort[a.day()];\n} function Nb(a) {\nreturn this._weekdaysMin[a.day()];\n} function Ob(a) {\nvar b, c, d; for (this._weekdaysParse=this._weekdaysParse||[], b=0; 7>b; b++) if (this._weekdaysParse[b]||(c=Da([2e3, 1]).day(b), d='^'+this.weekdays(c, '')+'|^'+this.weekdaysShort(c, '')+'|^'+this.weekdaysMin(c, ''), this._weekdaysParse[b]=new RegExp(d.replace('.', ''), 'i')), this._weekdaysParse[b].test(a)) return b;\n} function Pb(a) {\nvar b=this._isUTC?this._d.getUTCDay():this._d.getDay(); return null!=a?(a=Kb(a, this.localeData()), this.add(a-b, 'd')):b;\n} function Qb(a) {\nvar b=(this.day()+7-this.localeData()._week.dow)%7; return null==a?b:this.add(a-b, 'd');\n} function Rb(a) {\nreturn null==a?this.day()||7:this.day(this.day()%7?a:a-7);\n} function Sb(a, b) {\nH(a, 0, 0, function() {\nreturn this.localeData().meridiem(this.hours(), this.minutes(), b);\n});\n} function Tb(a, b) {\nreturn b._meridiemParse;\n} function Ub(a) {\nreturn 'p'===(a+'').toLowerCase().charAt(0);\n} function Vb(a, b, c) {\nreturn a>11?c?'pm':'PM':c?'am':'AM';\n} function Wb(a, b) {\nb[ld]=q(1e3*('0.'+a));\n} function Xb() {\nreturn this._isUTC?'UTC':'';\n} function Yb() {\nreturn this._isUTC?'Coordinated Universal Time':'';\n} function Zb(a) {\nreturn Da(1e3*a);\n} function $b() {\nreturn Da(...arguments).parseZone();\n} function _b(a, b, c) {\nvar d=this._calendar[a]; return 'function'==typeof d?d.call(b, c):d;\n} function ac(a) {\nvar b=this._longDateFormat[a], c=this._longDateFormat[a.toUpperCase()]; return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g, function(a) {\nreturn a.slice(1);\n}), this._longDateFormat[a]);\n} function bc() {\nreturn this._invalidDate;\n} function cc(a) {\nreturn this._ordinal.replace('%d', a);\n} function dc(a) {\nreturn a;\n} function ec(a, b, c, d) {\nvar e=this._relativeTime[c]; return 'function'==typeof e?e(a, b, c, d):e.replace(/%d/i, a);\n} function fc(a, b) {\nvar c=this._relativeTime[a>0?'future':'past']; return 'function'==typeof c?c(b):c.replace(/%s/i, b);\n} function gc(a) {\nvar b, c; for (c in a)b=a[c], 'function'==typeof b?this[c]=b:this['_'+c]=b; this._ordinalParseLenient=new RegExp(this._ordinalParse.source+'|'+/\\d{1,2}/.source);\n} function hc(a, b, c, d) {\nvar e=y(), f=h().set(d, b); return e[c](f, a);\n} function ic(a, b, c, d, e) {\nif ('number'==typeof a&&(b=a, a=void 0), a=a||'', null!=b) return hc(a, b, c, e); var f, g=[]; for (f=0; d>f; f++)g[f]=hc(a, f, c, e); return g;\n} function jc(a, b) {\nreturn ic(a, b, 'months', 12, 'month');\n} function kc(a, b) {\nreturn ic(a, b, 'monthsShort', 12, 'month');\n} function lc(a, b) {\nreturn ic(a, b, 'weekdays', 7, 'day');\n} function mc(a, b) {\nreturn ic(a, b, 'weekdaysShort', 7, 'day');\n} function nc(a, b) {\nreturn ic(a, b, 'weekdaysMin', 7, 'day');\n} function oc() {\nvar a=this._data; return this._milliseconds=Wd(this._milliseconds), this._days=Wd(this._days), this._months=Wd(this._months), a.milliseconds=Wd(a.milliseconds), a.seconds=Wd(a.seconds), a.minutes=Wd(a.minutes), a.hours=Wd(a.hours), a.months=Wd(a.months), a.years=Wd(a.years), this;\n} function pc(a, b, c, d) {\nvar e=Ya(b, c); return a._milliseconds+=d*e._milliseconds, a._days+=d*e._days, a._months+=d*e._months, a._bubble();\n} function qc(a, b) {\nreturn pc(this, a, b, 1);\n} function rc(a, b) {\nreturn pc(this, a, b, -1);\n} function sc(a) {\nreturn 0>a?Math.floor(a):Math.ceil(a);\n} function tc() {\nvar a, b, c, d, e, f=this._milliseconds, g=this._days, h=this._months, i=this._data; return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*sc(vc(h)+g), g=0, h=0), i.milliseconds=f%1e3, a=p(f/1e3), i.seconds=a%60, b=p(a/60), i.minutes=b%60, c=p(b/60), i.hours=c%24, g+=p(c/24), e=p(uc(g)), h+=e, g-=sc(vc(e)), d=p(h/12), h%=12, i.days=g, i.months=h, i.years=d, this;\n} function uc(a) {\nreturn 4800*a/146097;\n} function vc(a) {\nreturn 146097*a/4800;\n} function wc(a) {\nvar b, c, d=this._milliseconds; if (a=A(a), 'month'===a||'year'===a) return b=this._days+d/864e5, c=this._months+uc(b), 'month'===a?c:c/12; switch (b=this._days+Math.round(vc(this._months)), a) {\ncase 'week': return b/7+d/6048e5; case 'day': return b+d/864e5; case 'hour': return 24*b+d/36e5; case 'minute': return 1440*b+d/6e4; case 'second': return 86400*b+d/1e3; case 'millisecond': return Math.floor(864e5*b)+d; default: throw new Error('Unknown unit '+a);\n}\n} function xc() {\nreturn this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*q(this._months/12);\n} function yc(a) {\nreturn function() {\nreturn this.as(a);\n};\n} function zc(a) {\nreturn a=A(a), this[a+'s']();\n} function Ac(a) {\nreturn function() {\nreturn this._data[a];\n};\n} function Bc() {\nreturn p(this.days()/7);\n} function Cc(a, b, c, d, e) {\nreturn e.relativeTime(b||1, !!c, a, d);\n} function Dc(a, b, c) {\nvar d=Ya(a).abs(), e=ke(d.as('s')), f=ke(d.as('m')), g=ke(d.as('h')), h=ke(d.as('d')), i=ke(d.as('M')), j=ke(d.as('y')), k=e<le.s&&['s', e]||1===f&&['m']||f<le.m&&['mm', f]||1===g&&['h']||g<le.h&&['hh', g]||1===h&&['d']||h<le.d&&['dd', h]||1===i&&['M']||i<le.M&&['MM', i]||1===j&&['y']||['yy', j]; return k[2]=b, k[3]=+a>0, k[4]=c, Cc(...k);\n} function Ec(a, b) {\nreturn void 0===le[a]?!1:void 0===b?le[a]:(le[a]=b, !0);\n} function Fc(a) {\nvar b=this.localeData(), c=Dc(this, !a, b); return a&&(c=b.pastFuture(+this, c)), b.postformat(c);\n} function Gc() {\nvar a, b, c, d=me(this._milliseconds)/1e3, e=me(this._days), f=me(this._months); a=p(d/60), b=p(a/60), d%=60, a%=60, c=p(f/12), f%=12; var g=c, h=f, i=e, j=b, k=a, l=d, m=this.asSeconds(); return m?(0>m?'-':'')+'P'+(g?g+'Y':'')+(h?h+'M':'')+(i?i+'D':'')+(j||k||l?'T':'')+(j?j+'H':'')+(k?k+'M':'')+(l?l+'S':''):'P0D';\n} var Hc, Ic, Jc=a.momentProperties=[], Kc=!1, Lc={}, Mc={}, Nc=/(\\[[^\\[]*\\])|(\\\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, Oc=/(\\[[^\\[]*\\])|(\\\\)?(LTS|LT|LL?L?L?|l{1,4})/g, Pc={}, Qc={}, Rc=/\\d/, Sc=/\\d\\d/, Tc=/\\d{3}/, Uc=/\\d{4}/, Vc=/[+-]?\\d{6}/, Wc=/\\d\\d?/, Xc=/\\d{1,3}/, Yc=/\\d{1,4}/, Zc=/[+-]?\\d{1,6}/, $c=/\\d+/, _c=/[+-]?\\d+/, ad=/Z|[+-]\\d\\d:?\\d\\d/gi, bd=/[+-]?\\d+(\\.\\d{1,3})?/, cd=/[0-9]*['a-z\\u00A0-\\u05FF\\u0700-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]+|[\\u0600-\\u06FF\\/]+(\\s*?[\\u0600-\\u06FF]+){1,2}/i, dd={}, ed={}, fd=0, gd=1, hd=2, id=3, jd=4, kd=5, ld=6; H('M', ['MM', 2], 'Mo', function() {\nreturn this.month()+1;\n}), H('MMM', 0, 0, function(a) {\nreturn this.localeData().monthsShort(this, a);\n}), H('MMMM', 0, 0, function(a) {\nreturn this.localeData().months(this, a);\n}), z('month', 'M'), N('M', Wc), N('MM', Wc, Sc), N('MMM', cd), N('MMMM', cd), Q(['M', 'MM'], function(a, b) {\nb[gd]=q(a)-1;\n}), Q(['MMM', 'MMMM'], function(a, b, c, d) {\nvar e=c._locale.monthsParse(a, d, c._strict); null!=e?b[gd]=e:j(c).invalidMonth=a;\n}); var md='January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), nd='Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), od={}; a.suppressDeprecationWarnings=!1; var pd=/^\\s*(?:[+-]\\d{6}|\\d{4})-(?:(\\d\\d-\\d\\d)|(W\\d\\d$)|(W\\d\\d-\\d)|(\\d\\d\\d))((T| )(\\d\\d(:\\d\\d(:\\d\\d(\\.\\d+)?)?)?)?([\\+\\-]\\d\\d(?::?\\d\\d)?|\\s*Z)?)?$/, qd=[['YYYYYY-MM-DD', /[+-]\\d{6}-\\d{2}-\\d{2}/], ['YYYY-MM-DD', /\\d{4}-\\d{2}-\\d{2}/], ['GGGG-[W]WW-E', /\\d{4}-W\\d{2}-\\d/], ['GGGG-[W]WW', /\\d{4}-W\\d{2}/], ['YYYY-DDD', /\\d{4}-\\d{3}/]], rd=[['HH:mm:ss.SSSS', /(T| )\\d\\d:\\d\\d:\\d\\d\\.\\d+/], ['HH:mm:ss', /(T| )\\d\\d:\\d\\d:\\d\\d/], ['HH:mm', /(T| )\\d\\d:\\d\\d/], ['HH', /(T| )\\d\\d/]], sd=/^\\/?Date\\((\\-?\\d+)/i; a.createFromInputFallback=aa('moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.', function(a) {\na._d=new Date(a._i+(a._useUTC?' UTC':''));\n}), H(0, ['YY', 2], 0, function() {\nreturn this.year()%100;\n}), H(0, ['YYYY', 4], 0, 'year'), H(0, ['YYYYY', 5], 0, 'year'), H(0, ['YYYYYY', 6, !0], 0, 'year'), z('year', 'y'), N('Y', _c), N('YY', Wc, Sc), N('YYYY', Yc, Uc), N('YYYYY', Zc, Vc), N('YYYYYY', Zc, Vc), Q(['YYYYY', 'YYYYYY'], fd), Q('YYYY', function(b, c) {\nc[fd]=2===b.length?a.parseTwoDigitYear(b):q(b);\n}), Q('YY', function(b, c) {\nc[fd]=a.parseTwoDigitYear(b);\n}), a.parseTwoDigitYear=function(a) {\nreturn q(a)+(q(a)>68?1900:2e3);\n}; var td=C('FullYear', !1); H('w', ['ww', 2], 'wo', 'week'), H('W', ['WW', 2], 'Wo', 'isoWeek'), z('week', 'w'), z('isoWeek', 'W'), N('w', Wc), N('ww', Wc, Sc), N('W', Wc), N('WW', Wc, Sc), R(['w', 'ww', 'W', 'WW'], function(a, b, c, d) {\nb[d.substr(0, 1)]=q(a);\n}); var ud={dow: 0, doy: 6}; H('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'), z('dayOfYear', 'DDD'), N('DDD', Xc), N('DDDD', Tc), Q(['DDD', 'DDDD'], function(a, b, c) {\nc._dayOfYear=q(a);\n}), a.ISO_8601=function() {}; var vd=aa('moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', function() {\nvar a=Da(...arguments); return this>a?this:a;\n}), wd=aa('moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', function() {\nvar a=Da(...arguments); return a> this?this:a;\n}); Ja('Z', ':'), Ja('ZZ', ''), N('Z', ad), N('ZZ', ad), Q(['Z', 'ZZ'], function(a, b, c) {\nc._useUTC=!0, c._tzm=Ka(a);\n}); var xd=/([\\+\\-]|\\d\\d)/gi; a.updateOffset=function() {}; var yd=/(\\-)?(?:(\\d*)\\.)?(\\d+)\\:(\\d+)(?:\\:(\\d+)\\.?(\\d{3})?)?/, zd=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; Ya.fn=Ha.prototype; var Ad=ab(1, 'add'), Bd=ab(-1, 'subtract'); a.defaultFormat='YYYY-MM-DDTHH:mm:ssZ'; var Cd=aa('moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', function(a) {\nreturn void 0===a?this.localeData():this.locale(a);\n}); H(0, ['gg', 2], 0, function() {\nreturn this.weekYear()%100;\n}), H(0, ['GG', 2], 0, function() {\nreturn this.isoWeekYear()%100;\n}), Db('gggg', 'weekYear'), Db('ggggg', 'weekYear'), Db('GGGG', 'isoWeekYear'), Db('GGGGG', 'isoWeekYear'), z('weekYear', 'gg'), z('isoWeekYear', 'GG'), N('G', _c), N('g', _c), N('GG', Wc, Sc), N('gg', Wc, Sc), N('GGGG', Yc, Uc), N('gggg', Yc, Uc), N('GGGGG', Zc, Vc), N('ggggg', Zc, Vc), R(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function(a, b, c, d) {\nb[d.substr(0, 2)]=q(a);\n}), R(['gg', 'GG'], function(b, c, d, e) {\nc[e]=a.parseTwoDigitYear(b);\n}), H('Q', 0, 0, 'quarter'), z('quarter', 'Q'), N('Q', Rc), Q('Q', function(a, b) {\nb[gd]=3*(q(a)-1);\n}), H('D', ['DD', 2], 'Do', 'date'), z('date', 'D'), N('D', Wc), N('DD', Wc, Sc), N('Do', function(a, b) {\nreturn a?b._ordinalParse:b._ordinalParseLenient;\n}), Q(['D', 'DD'], hd), Q('Do', function(a, b) {\nb[hd]=q(a.match(Wc)[0], 10);\n}); var Dd=C('Date', !0); H('d', 0, 'do', 'day'), H('dd', 0, 0, function(a) {\nreturn this.localeData().weekdaysMin(this, a);\n}), H('ddd', 0, 0, function(a) {\nreturn this.localeData().weekdaysShort(this, a);\n}), H('dddd', 0, 0, function(a) {\nreturn this.localeData().weekdays(this, a);\n}), H('e', 0, 0, 'weekday'), H('E', 0, 0, 'isoWeekday'), z('day', 'd'), z('weekday', 'e'), z('isoWeekday', 'E'), N('d', Wc), N('e', Wc), N('E', Wc), N('dd', cd), N('ddd', cd), N('dddd', cd), R(['dd', 'ddd', 'dddd'], function(a, b, c) {\nvar d=c._locale.weekdaysParse(a); null!=d?b.d=d:j(c).invalidWeekday=a;\n}), R(['d', 'e', 'E'], function(a, b, c, d) {\nb[d]=q(a);\n}); var Ed='Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), Fd='Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), Gd='Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); H('H', ['HH', 2], 0, 'hour'), H('h', ['hh', 2], 0, function() {\nreturn this.hours()%12||12;\n}), Sb('a', !0), Sb('A', !1), z('hour', 'h'), N('a', Tb), N('A', Tb), N('H', Wc), N('h', Wc), N('HH', Wc, Sc), N('hh', Wc, Sc), Q(['H', 'HH'], id), Q(['a', 'A'], function(a, b, c) {\nc._isPm=c._locale.isPM(a), c._meridiem=a;\n}), Q(['h', 'hh'], function(a, b, c) {\nb[id]=q(a), j(c).bigHour=!0;\n}); var Hd=/[ap]\\.?m?\\.?/i, Id=C('Hours', !0); H('m', ['mm', 2], 0, 'minute'), z('minute', 'm'), N('m', Wc), N('mm', Wc, Sc), Q(['m', 'mm'], jd); var Jd=C('Minutes', !1); H('s', ['ss', 2], 0, 'second'), z('second', 's'), N('s', Wc), N('ss', Wc, Sc), Q(['s', 'ss'], kd); var Kd=C('Seconds', !1); H('S', 0, 0, function() {\nreturn ~~(this.millisecond()/100);\n}), H(0, ['SS', 2], 0, function() {\nreturn ~~(this.millisecond()/10);\n}), H(0, ['SSS', 3], 0, 'millisecond'), H(0, ['SSSS', 4], 0, function() {\nreturn 10*this.millisecond();\n}), H(0, ['SSSSS', 5], 0, function() {\nreturn 100*this.millisecond();\n}), H(0, ['SSSSSS', 6], 0, function() {\nreturn 1e3*this.millisecond();\n}), H(0, ['SSSSSSS', 7], 0, function() {\nreturn 1e4*this.millisecond();\n}), H(0, ['SSSSSSSS', 8], 0, function() {\nreturn 1e5*this.millisecond();\n}), H(0, ['SSSSSSSSS', 9], 0, function() {\nreturn 1e6*this.millisecond();\n}), z('millisecond', 'ms'), N('S', Xc, Rc), N('SS', Xc, Sc), N('SSS', Xc, Tc); var Ld; for (Ld='SSSS'; Ld.length<=9; Ld+='S')N(Ld, $c); for (Ld='S'; Ld.length<=9; Ld+='S')Q(Ld, Wb); var Md=C('Milliseconds', !1); H('z', 0, 0, 'zoneAbbr'), H('zz', 0, 0, 'zoneName'); var Nd=n.prototype; Nd.add=Ad, Nd.calendar=cb, Nd.clone=db, Nd.diff=ib, Nd.endOf=ub, Nd.format=mb, Nd.from=nb, Nd.fromNow=ob, Nd.to=pb, Nd.toNow=qb, Nd.get=F, Nd.invalidAt=Cb, Nd.isAfter=eb, Nd.isBefore=fb, Nd.isBetween=gb, Nd.isSame=hb, Nd.isValid=Ab, Nd.lang=Cd, Nd.locale=rb, Nd.localeData=sb, Nd.max=wd, Nd.min=vd, Nd.parsingFlags=Bb, Nd.set=F, Nd.startOf=tb, Nd.subtract=Bd, Nd.toArray=yb, Nd.toObject=zb, Nd.toDate=xb, Nd.toISOString=lb, Nd.toJSON=lb, Nd.toString=kb, Nd.unix=wb, Nd.valueOf=vb, Nd.year=td, Nd.isLeapYear=ia, Nd.weekYear=Fb, Nd.isoWeekYear=Gb, Nd.quarter=Nd.quarters=Jb, Nd.month=Y, Nd.daysInMonth=Z, Nd.week=Nd.weeks=na, Nd.isoWeek=Nd.isoWeeks=oa, Nd.weeksInYear=Ib, Nd.isoWeeksInYear=Hb, Nd.date=Dd, Nd.day=Nd.days=Pb, Nd.weekday=Qb, Nd.isoWeekday=Rb, Nd.dayOfYear=qa, Nd.hour=Nd.hours=Id, Nd.minute=Nd.minutes=Jd, Nd.second=Nd.seconds=Kd,\nNd.millisecond=Nd.milliseconds=Md, Nd.utcOffset=Na, Nd.utc=Pa, Nd.local=Qa, Nd.parseZone=Ra, Nd.hasAlignedHourOffset=Sa, Nd.isDST=Ta, Nd.isDSTShifted=Ua, Nd.isLocal=Va, Nd.isUtcOffset=Wa, Nd.isUtc=Xa, Nd.isUTC=Xa, Nd.zoneAbbr=Xb, Nd.zoneName=Yb, Nd.dates=aa('dates accessor is deprecated. Use date instead.', Dd), Nd.months=aa('months accessor is deprecated. Use month instead', Y), Nd.years=aa('years accessor is deprecated. Use year instead', td), Nd.zone=aa('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', Oa); var Od=Nd, Pd={sameDay: '[Today at] LT', nextDay: '[Tomorrow at] LT', nextWeek: 'dddd [at] LT', lastDay: '[Yesterday at] LT', lastWeek: '[Last] dddd [at] LT', sameElse: 'L'}, Qd={LTS: 'h:mm:ss A', LT: 'h:mm A', L: 'MM/DD/YYYY', LL: 'MMMM D, YYYY', LLL: 'MMMM D, YYYY h:mm A', LLLL: 'dddd, MMMM D, YYYY h:mm A'}, Rd='Invalid date', Sd='%d', Td=/\\d{1,2}/, Ud={future: 'in %s', past: '%s ago', s: 'a few seconds', m: 'a minute', mm: '%d minutes', h: 'an hour', hh: '%d hours', d: 'a day', dd: '%d days', M: 'a month', MM: '%d months', y: 'a year', yy: '%d years'}, Vd=s.prototype; Vd._calendar=Pd, Vd.calendar=_b, Vd._longDateFormat=Qd, Vd.longDateFormat=ac, Vd._invalidDate=Rd, Vd.invalidDate=bc, Vd._ordinal=Sd, Vd.ordinal=cc, Vd._ordinalParse=Td, Vd.preparse=dc, Vd.postformat=dc, Vd._relativeTime=Ud, Vd.relativeTime=ec, Vd.pastFuture=fc, Vd.set=gc, Vd.months=U, Vd._months=md, Vd.monthsShort=V, Vd._monthsShort=nd, Vd.monthsParse=W, Vd.week=ka, Vd._week=ud, Vd.firstDayOfYear=ma, Vd.firstDayOfWeek=la, Vd.weekdays=Lb, Vd._weekdays=Ed, Vd.weekdaysMin=Nb, Vd._weekdaysMin=Gd, Vd.weekdaysShort=Mb, Vd._weekdaysShort=Fd, Vd.weekdaysParse=Ob, Vd.isPM=Ub, Vd._meridiemParse=Hd, Vd.meridiem=Vb, w('en', {ordinalParse: /\\d{1,2}(th|st|nd|rd)/, ordinal: function(a) {\nvar b=a%10, c=1===q(a%100/10)?'th':1===b?'st':2===b?'nd':3===b?'rd':'th'; return a+c;\n}}), a.lang=aa('moment.lang is deprecated. Use moment.locale instead.', w), a.langData=aa('moment.langData is deprecated. Use moment.localeData instead.', y); var Wd=Math.abs, Xd=yc('ms'), Yd=yc('s'), Zd=yc('m'), $d=yc('h'), _d=yc('d'), ae=yc('w'), be=yc('M'), ce=yc('y'), de=Ac('milliseconds'), ee=Ac('seconds'), fe=Ac('minutes'), ge=Ac('hours'), he=Ac('days'), ie=Ac('months'), je=Ac('years'), ke=Math.round, le={s: 45, m: 45, h: 22, d: 26, M: 11}, me=Math.abs, ne=Ha.prototype; ne.abs=oc, ne.add=qc, ne.subtract=rc, ne.as=wc, ne.asMilliseconds=Xd, ne.asSeconds=Yd, ne.asMinutes=Zd, ne.asHours=$d, ne.asDays=_d, ne.asWeeks=ae, ne.asMonths=be, ne.asYears=ce, ne.valueOf=xc, ne._bubble=tc, ne.get=zc, ne.milliseconds=de, ne.seconds=ee, ne.minutes=fe, ne.hours=ge, ne.days=he, ne.weeks=Bc, ne.months=ie, ne.years=je, ne.humanize=Fc, ne.toISOString=Gc, ne.toString=Gc, ne.toJSON=Gc, ne.locale=rb, ne.localeData=sb, ne.toIsoString=aa('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', Gc), ne.lang=Cd, H('X', 0, 0, 'unix'), H('x', 0, 0, 'valueOf'), N('x', _c), N('X', bd), Q('X', function(a, b, c) {\nc._d=new Date(1e3*parseFloat(a, 10));\n}), Q('x', function(a, b, c) {\nc._d=new Date(q(a));\n}), a.version='2.10.6', b(Da), a.fn=Od, a.min=Fa, a.max=Ga, a.utc=h, a.unix=Zb, a.months=jc, a.isDate=d, a.locale=w, a.invalid=l, a.duration=Ya, a.isMoment=o, a.weekdays=lc, a.parseZone=$b, a.localeData=y, a.isDuration=Ia, a.monthsShort=kc, a.weekdaysMin=nc, a.defineLocale=x, a.weekdaysShort=mc, a.normalizeUnits=A, a.relativeTimeThreshold=Ec; var oe=a; return oe;\n});\n"
  },
  {
    "path": "sheets/forms/forms.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_sheets_custom_form_responses_quickstart]\n/**\n * A special function that inserts a custom menu when the spreadsheet opens.\n */\nfunction onOpen() {\n  const menu = [\n    { name: \"Set up conference\", functionName: \"setUpConference_\" },\n  ];\n  try {\n    SpreadsheetApp.getActive().addMenu(\"Conference\", menu);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * A set-up function that uses the conference data in the spreadsheet to create\n * Google Calendar events, a Google Form, and a trigger that allows the script\n * to react to form responses.\n */\nfunction setUpConference_() {\n  if (ScriptProperties.getProperty(\"calId\")) {\n    Browser.msgBox(\"Your conference is already set up. Look in Google Drive!\");\n  }\n\n  try {\n    const ss = SpreadsheetApp.getActive();\n    const sheet = ss.getSheetByName(\"Conference Setup\");\n    const range = sheet.getDataRange();\n    const values = range.getValues();\n    setUpCalendar_(values, range);\n    setUpForm_(ss, values);\n    ScriptApp.newTrigger(\"onFormSubmit\")\n      .forSpreadsheet(ss)\n      .onFormSubmit()\n      .create();\n    ss.removeMenu(\"Conference\");\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Creates a Google Calendar with events for each conference session in the\n * spreadsheet, then writes the event IDs to the spreadsheet for future use.\n * @param {Array<string[]>} values Cell values for the spreadsheet range.\n * @param {Range} range A spreadsheet range that contains conference data.\n */\nfunction setUpCalendar_(values, range) {\n  try {\n    const cal = CalendarApp.createCalendar(\"Conference Calendar\");\n    for (let i = 1; i < values.length; i++) {\n      const session = values[i];\n      const title = session[0];\n      const start = joinDateAndTime_(session[1], session[2]);\n      const end = joinDateAndTime_(session[1], session[3]);\n      const options = { location: session[4], sendInvites: true };\n      const event = cal\n        .createEvent(title, start, end, options)\n        .setGuestsCanSeeGuests(false);\n      session[5] = event.getId();\n    }\n    range.setValues(values);\n\n    // Store the ID for the Calendar, which is needed to retrieve events by ID.\n    ScriptProperties.setProperty(\"calId\", cal.getId());\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Creates a single Date object from separate date and time cells.\n *\n * @param {Date} date A Date object from which to extract the date.\n * @param {Date} time A Date object from which to extract the time.\n * @return {Date} A Date object representing the combined date and time.\n */\nfunction joinDateAndTime_(date, time) {\n  const newDate = new Date(date);\n  newDate.setHours(time.getHours());\n  newDate.setMinutes(time.getMinutes());\n  return newDate;\n}\n\n/**\n * Creates a Google Form that allows respondents to select which conference\n * sessions they would like to attend, grouped by date and start time.\n *\n * @param {Spreadsheet} ss The spreadsheet that contains the conference data.\n * @param {Array<String[]>} values Cell values for the spreadsheet range.\n */\nfunction setUpForm_(ss, values) {\n  // Group the sessions by date and time so that they can be passed to the form.\n  const schedule = {};\n  for (let i = 1; i < values.length; i++) {\n    const session = values[i];\n    const day = session[1].toLocaleDateString();\n    const time = session[2].toLocaleTimeString();\n    if (!schedule[day]) {\n      schedule[day] = {};\n    }\n    if (!schedule[day][time]) {\n      schedule[day][time] = [];\n    }\n    schedule[day][time].push(session[0]);\n  }\n\n  try {\n    // Create the form and add a multiple-choice question for each timeslot.\n    const form = FormApp.create(\"Conference Form\");\n    form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId());\n    form.addTextItem().setTitle(\"Name\").setRequired(true);\n    form.addTextItem().setTitle(\"Email\").setRequired(true);\n    for (const day of schedule) {\n      const header = form\n        .addSectionHeaderItem()\n        .setTitle(`Sessions for ${day}`);\n      for (const time of schedule[day]) {\n        const item = form\n          .addMultipleChoiceItem()\n          .setTitle(`${time} ${day}`)\n          .setChoiceValues(schedule[day][time]);\n      }\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * A trigger-driven function that sends out calendar invitations and a\n * personalized Google Docs itinerary after a user responds to the form.\n *\n * @param {Object} e The event parameter for form submission to a spreadsheet;\n *     see https://developers.google.com/apps-script/understanding_events\n */\nfunction onFormSubmit(e) {\n  const user = {\n    name: e.namedValues.Name[0],\n    email: e.namedValues.Email[0],\n  };\n\n  // Grab the session data again so that we can match it to the user's choices.\n  const response = [];\n  try {\n    values = SpreadsheetApp.getActive()\n      .getSheetByName(\"Conference Setup\")\n      .getDataRange()\n      .getValues();\n    for (let i = 1; i < values.length; i++) {\n      const session = values[i];\n      const title = session[0];\n      const day = session[1].toLocaleDateString();\n      const time = session[2].toLocaleTimeString();\n      const timeslot = `${time} ${day}`;\n\n      // For every selection in the response, find the matching timeslot and\n      // title in the spreadsheet and add the session data to the response array.\n      if (e.namedValues[timeslot] && e.namedValues[timeslot] === title) {\n        response.push(session);\n      }\n    }\n    sendInvites_(user, response);\n    sendDoc_(user, response);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Add the user as a guest for every session he or she selected.\n * @param {object} user An object that contains the user's name and email.\n * @param {Array<String[]>} response An array of data for the user's session choices.\n */\nfunction sendInvites_(user, response) {\n  try {\n    const id = ScriptProperties.getProperty(\"calId\");\n    const cal = CalendarApp.getCalendarById(id);\n    for (let i = 0; i < response.length; i++) {\n      cal.getEventSeriesById(response[i][5]).addGuest(user.email);\n    }\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Create and share a personalized Google Doc that shows the user's itinerary.\n * @param {object} user An object that contains the user's name and email.\n * @param {Array<string[]>} response An array of data for the user's session choices.\n */\nfunction sendDoc_(user, response) {\n  try {\n    const doc = DocumentApp.create(\n      `Conference Itinerary for ${user.name}`,\n    ).addEditor(user.email);\n    const body = doc.getBody();\n    let table = [[\"Session\", \"Date\", \"Time\", \"Location\"]];\n    for (let i = 0; i < response.length; i++) {\n      table.push([\n        response[i][0],\n        response[i][1].toLocaleDateString(),\n        response[i][2].toLocaleTimeString(),\n        response[i][4],\n      ]);\n    }\n    body\n      .insertParagraph(0, doc.getName())\n      .setHeading(DocumentApp.ParagraphHeading.HEADING1);\n    table = body.appendTable(table);\n    table.getRow(0).editAsText().setBold(true);\n    doc.saveAndClose();\n\n    // Email a link to the Doc as well as a PDF copy.\n    MailApp.sendEmail({\n      to: user.email,\n      subject: doc.getName(),\n      body: `Thanks for registering! Here's your itinerary: ${doc.getUrl()}`,\n      attachments: doc.getAs(MimeType.PDF),\n    });\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n// [END apps_script_sheets_custom_form_responses_quickstart]\n"
  },
  {
    "path": "sheets/maps/maps.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_sheets_restaurant_locations_map]\n/**\n * Returns restaurant locations on a map.\n */\nfunction restaurantLocationsMap() {\n  // Get the sheet named 'restaurants'\n  const sheet =\n    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(\"restaurants\");\n\n  // Store the restaurant name and address data in a 2-dimensional array called\n  // restaurantInfo. This is the data in cells A2:B4\n  const restaurantInfo = sheet\n    .getRange(2, 1, sheet.getLastRow() - 1, 2)\n    .getValues();\n\n  // Create a new StaticMap\n  const restaurantMap = Maps.newStaticMap();\n\n  // Create a new UI Application, which we use to display the map\n  const ui = UiApp.createApplication();\n\n  // Create a grid widget to use for displaying the text of the restaurant names\n  // and addresses. Start by populating the header row in the grid.\n  const grid = ui.createGrid(restaurantInfo.length + 1, 3);\n  grid.setWidget(\n    0,\n    0,\n    ui.createLabel(\"Store #\").setStyleAttribute(\"fontWeight\", \"bold\"),\n  );\n  grid.setWidget(\n    0,\n    1,\n    ui.createLabel(\"Store Name\").setStyleAttribute(\"fontWeight\", \"bold\"),\n  );\n  grid.setWidget(\n    0,\n    2,\n    ui.createLabel(\"Address\").setStyleAttribute(\"fontWeight\", \"bold\"),\n  );\n\n  // For each entry in restaurantInfo, create a map marker with the address and\n  // the style we want. Also add the address info for this restaurant to the\n  // grid widget.\n  for (let i = 0; i < restaurantInfo.length; i++) {\n    restaurantMap.setMarkerStyle(\n      Maps.StaticMap.MarkerSize.MID,\n      Maps.StaticMap.Color.GREEN,\n      i + 1,\n    );\n    restaurantMap.addMarker(restaurantInfo[i][1]);\n\n    grid.setWidget(i + 1, 0, ui.createLabel((i + 1).toString()));\n    grid.setWidget(i + 1, 1, ui.createLabel(restaurantInfo[i][0]));\n    grid.setWidget(i + 1, 2, ui.createLabel(restaurantInfo[i][1]));\n  }\n\n  // Create a Flow Panel widget. We add the map and the grid to this panel.\n  // The height needs to be able to accomodate the number of restaurants, so we\n  // use a calculation to scale it based on the number of restaurants.\n  const panel = ui\n    .createFlowPanel()\n    .setSize(\"500px\", `${515 + restaurantInfo.length * 25}px`);\n\n  // Get the URL of the restaurant map and use that to create an image and add\n  // it to the panel. Next add the grid to the panel.\n  panel.add(ui.createImage(restaurantMap.getMapUrl()));\n  panel.add(grid);\n\n  // Finally, add the panel widget to our UI instance, and set its height,\n  // width, and title.\n  ui.add(panel);\n  ui.setHeight(515 + restaurantInfo.length * 25);\n  ui.setWidth(500);\n  ui.setTitle(\"Restaurant Locations\");\n\n  // Make the UI visible in the spreadsheet.\n  SpreadsheetApp.getActiveSpreadsheet().show(ui);\n}\n// [END apps_script_sheets_restaurant_locations_map]\n\n// [START apps_script_sheets_driving_directions]\n/**\n * Gets driving directions from Mountain View to San Francisco.\n * Displays a map inside Google Spreadsheets.\n */\nfunction getDrivingDirections() {\n  // Set starting and ending addresses\n  const start = \"1600 Amphitheatre Pkwy, Mountain View, CA 94043\";\n  const end = \"345 Spear St, San Francisco, CA 94105\";\n\n  // These regular expressions will be used to strip out\n  // unneeded HTML tags\n  const r1 = /<b>/g;\n  const r2 = /<\\/b>/g;\n  const r3 = /<div style=\"font-size:0.9em\">/g;\n  const r4 = /<\\/div>/g;\n\n  // points is used for storing the points in the step-by-step directions\n  let points = [];\n\n  // currentLabel is used for number the steps in the directions\n  let currentLabel = 0;\n\n  // This will be the map on which we display the path\n  const map = Maps.newStaticMap().setSize(500, 350);\n\n  // Create a new UI Application, which we use to display the map\n  const ui = UiApp.createApplication();\n  // Create a Flow Panel widget, which we use for the directions text\n  const directionsPanel = ui.createFlowPanel();\n\n  // Create a new DirectionFinder with our start and end addresses, and request the directions\n  // The response is a JSON object, which contains the directions\n  const directions = Maps.newDirectionFinder()\n    .setOrigin(start)\n    .setDestination(end)\n    .getDirections();\n\n  // Much of this code is based on the template referenced in\n  // http://googleappsdeveloper.blogspot.com/2010/06/automatically-generate-maps-and.html\n  for (const i in directions.routes) {\n    for (const j in directions.routes[i].legs) {\n      for (const k in directions.routes[i].legs[j].steps) {\n        // Parse out the current step in the directions\n        const step = directions.routes[i].legs[j].steps[k];\n\n        // Call Maps.decodePolyline() to decode the polyline for\n        // this step into an array of latitudes and longitudes\n        const path = Maps.decodePolyline(step.polyline.points);\n        points = points.concat(path);\n\n        // Pull out the direction information from step.html_instructions\n        // Because we only want to display text, we will strip out the\n        // HTML tags that are present in the html_instructions\n        let text = step.html_instructions;\n        text = text.replace(r1, \" \");\n        text = text.replace(r2, \" \");\n        text = text.replace(r3, \" \");\n        text = text.replace(r4, \" \");\n\n        // Add each step in the directions to the directionsPanel\n        directionsPanel.add(ui.createLabel(`${++currentLabel} - ${text}`));\n      }\n    }\n  }\n\n  // be conservative and only sample 100 times to create our polyline path\n  let lpoints = [];\n  if (points.length < 200) {\n    lpoints = points;\n  } else {\n    const pCount = points.length / 2;\n    const step = Number.parseInt(pCount / 100);\n    for (let i = 0; i < 100; ++i) {\n      lpoints.push(points[i * step * 2]);\n      lpoints.push(points[i * step * 2 + 1]);\n    }\n  }\n\n  // make the polyline\n  if (lpoints.length > 0) {\n    // Maps.encodePolyline turns an array of latitudes and longitudes\n    // into an encoded polyline\n    const pline = Maps.encodePolyline(lpoints);\n\n    // Once we have the encoded polyline, add that path to the map\n    map.addPath(pline);\n  }\n\n  // Create a FlowPanel to hold the map\n  const panel = ui.createFlowPanel().setSize(\"500px\", \"350px\");\n\n  // Get the URL of the map and use that to create an image and add\n  // it to the panel.\n  panel.add(ui.createImage(map.getMapUrl()));\n\n  // Add both the map panel and the directions panel to the UI instance\n  ui.add(panel);\n  ui.add(directionsPanel);\n\n  // Next set the title, height, and width of the UI instance\n  ui.setTitle(\"Driving Directions\");\n  ui.setHeight(525);\n  ui.setWidth(500);\n\n  // Finally, display the UI within the spreadsheet\n  SpreadsheetApp.getActiveSpreadsheet().show(ui);\n}\n// [END apps_script_sheets_driving_directions]\n\n// [START apps_script_sheets_analyze_locations]\n/**\n * Analyzes locations of Google offices.\n */\nfunction analyzeLocations() {\n  // Select the sheet named 'geocoder and elevation'\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(\n    \"geocoder and elevation\",\n  );\n\n  // Store the address data in an array called\n  // locationInfo. This is the data in cells A2:A20\n  const locationInfo = sheet\n    .getRange(2, 1, sheet.getLastRow() - 1, 1)\n    .getValues();\n\n  // Set up some values to use for comparisons.\n  // latitudes run from -90 to 90, so we start with a max of -90 for comparison\n  let maxLatitude = -90;\n  let indexOfMaxLatitude = 0;\n\n  // Set the starting max elevation to 0, or sea level\n  let maxElevation = 0;\n  let indexOfMaxElevation = 0;\n\n  // geoResults will hold the JSON results array that we get when calling geocode()\n  let geoResults;\n\n  // elevationResults will hold the results object that we get when calling sampleLocation()\n  let elevationResults;\n\n  // lat and lng will temporarily hold the latitude and longitude of each\n  // address\n  let lat;\n  let lng;\n\n  for (let i = 0; i < locationInfo.length; i++) {\n    // Get the latitude and longitude for an address. For more details on\n    // the JSON results array, geoResults, see\n    // http://code.google.com/apis/maps/documentation/geocoding/#Results\n    geoResults = Maps.newGeocoder().geocode(locationInfo[i]);\n\n    // Get the latitude and longitude\n    lat = geoResults.results[0].geometry.location.lat;\n    lng = geoResults.results[0].geometry.location.lng;\n\n    // Use the latitude and longitude to call sampleLocation and get the\n    // elevation. For more details on the JSON-formatted results object,\n    // elevationResults, see\n    // http://code.google.com/apis/maps/documentation/elevation/#ElevationResponses\n    elevationResults = Maps.newElevationSampler().sampleLocation(\n      Number.parseFloat(lat),\n      Number.parseFloat(lng),\n    );\n\n    // Check to see if the current latitude is greater than our max latitude\n    // so far. If so, set maxLatitude and indexOfMaxLatitude\n    if (lat > maxLatitude) {\n      maxLatitude = lat;\n      indexOfMaxLatitude = i;\n    }\n\n    // Check if elevationResults has a good status and also if the current\n    // elevation is greater than the max elevation so far. If so, set\n    // maxElevation and indexOfMaxElevation\n    if (\n      elevationResults.status === \"OK\" &&\n      elevationResults.results[0].elevation > maxElevation\n    ) {\n      maxElevation = elevationResults.results[0].elevation;\n      indexOfMaxElevation = i;\n    }\n  }\n\n  // User Browser.msgBox as a simple way to display the info about highest\n  // elevation and northernmost office.\n  Browser.msgBox(\n    `The US Google office with the highest elevation is: ${locationInfo[indexOfMaxElevation]}. The northernmost US Google office is: ${locationInfo[indexOfMaxLatitude]}`,\n  );\n}\n// [END apps_script_sheets_analyze_locations]\n"
  },
  {
    "path": "sheets/next18/.claspignore",
    "content": "README.md\n"
  },
  {
    "path": "sheets/next18/Constants.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/* Salesforce config */\n\n// Salesforce OAuth configuration, which you get by creating a developer project\n// with OAuth authentication on Salesforce.\nconst SALESFORCE_CLIENT_ID = \"<FILL IN WITH YOUR CLIENT ID>\";\nconst SALESFORCE_CLIENT_SECRET = \"<FILL IN WITH YOUR CLIENT SECRET>\";\n\n// The Salesforce instance to talk to.\nconst SALESFORCE_INSTANCE = \"na1\";\n\n/* Invoice generation config */\n\n// The ID of a Google Doc that is used as a template. Defaults to\n// https://docs.google.com/document/d/1awKvXXMOQomdD68PGMpP5j1kNZwk_2Z0wBbwUgjKKws/view\nconst INVOICE_TEMPLATE = \"1awKvXXMOQomdD68PGMpP5j1kNZwk_2Z0wBbwUgjKKws\";\n\n// The ID of a Drive folder that the generated invoices are created in. Create\n// a new folder that your Google account has edit access to.\nconst INVOICES_FOLDER = \"\";\n"
  },
  {
    "path": "sheets/next18/Invoice.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Generates invoices based on the selected rows in the spreadsheet. Assumes\n * that the Salesforce accountId is in the first selected column and the\n * amount owed is the 4th selected column.\n */\nfunction generateInvoices() {\n  const range = SpreadsheetApp.getActiveRange();\n  const values = range.getDisplayValues();\n  const sheet = SpreadsheetApp.getActiveSheet();\n\n  for (let i = 0; i < values.length; i++) {\n    const row = values[i];\n    const accountId = row[0];\n    const amount = row[3];\n    const invoiceUrl = generateInvoice(accountId, amount);\n    sheet\n      .getRange(range.getRow() + i, range.getLastColumn() + 1)\n      .setValue(invoiceUrl);\n  }\n}\n\n/**\n * Generates a single invoice in Google Docs for a given Salesforce account and\n * an owed amount.\n *\n * @param {string} accountId The Salesforce account Id to invoice\n * @param {string} amount The owed amount to invoice\n * @return {string} the url of the created invoice\n */\nfunction generateInvoice(accountId, amount) {\n  const folder = DriveApp.getFolderById(INVOICES_FOLDER);\n  const copied = DriveApp.getFileById(INVOICE_TEMPLATE).makeCopy(\n    `Invoice for ${accountId}`,\n    folder,\n  );\n  const invoice = DocumentApp.openById(copied.getId());\n  const results = fetchSoqlResults(\n    `select Name, BillingAddress from Account where Id = '${accountId}'`,\n  );\n  const account = results.records[0];\n\n  invoice.getBody().replaceText(\"{{account name}}\", account.Name);\n  invoice\n    .getBody()\n    .replaceText(\"{{account address}}\", account.BillingAddress.street);\n  invoice\n    .getBody()\n    .replaceText(\n      \"{{date}}\",\n      Utilities.formatDate(new Date(), \"GMT\", \"yyyy-MM-dd\"),\n    );\n  invoice.getBody().replaceText(\"{{amount}}\", amount);\n  invoice.saveAndClose();\n  return invoice.getUrl();\n}\n\n/**\n * Generates a report in Google Slides with a chart generated from the sheet.\n */\nfunction generateReport() {\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const chart = sheet\n    .newChart()\n    .asColumnChart()\n    .addRange(sheet.getRange(\"A:A\"))\n    .addRange(sheet.getRange(\"C:D\"))\n    .setNumHeaders(1)\n    .setMergeStrategy(Charts.ChartMergeStrategy.MERGE_COLUMNS)\n    .setOption(\"useFirstColumnAsDomain\", true)\n    .setOption(\"isStacked\", \"absolute\")\n    .setOption(\"title\", \"Expected Payments\")\n    .setOption(\"treatLabelsAsText\", false)\n    .setXAxisTitle(\"AccountId\")\n    .setPosition(3, 1, 114, 138)\n    .build();\n\n  sheet.insertChart(chart);\n\n  // Force the chart to be created before adding it to the presentation\n  SpreadsheetApp.flush();\n\n  const preso = SlidesApp.create(\"Invoicing Report\");\n  const titleSlide = preso.getSlides()[0];\n\n  const titleShape = titleSlide\n    .getPlaceholder(SlidesApp.PlaceholderType.CENTERED_TITLE)\n    .asShape();\n  titleShape.getText().setText(\"Invoicing Report\");\n\n  const newSlide = preso.appendSlide(SlidesApp.PredefinedLayout.BLANK);\n  newSlide.insertSheetsChart(chart);\n\n  showLinkDialog(preso.getUrl(), \"Open report\", \"Report created\");\n}\n"
  },
  {
    "path": "sheets/next18/LinkDialog.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n  </head>\n  <body>\n    <div>\n      <a href=\"<?= url ?>\" target=\"_blank\"><?= message ?></a>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "sheets/next18/README.md",
    "content": "# Invoicing Demo for Google Sheets\n\nThis sample was created for a talk for Google Cloud NEXT'18 entitled \"Building\non the Docs Editors: APIs and Apps Script\". It is an implementation of a\nGoogle Sheets add-on that:\n\n* Authenticates with Salesforce via OAuth2, using the\n[Apps Script OAuth2 library](https://github.com/googleworkspace/apps-script-oauth2).\n* Runs [SOQL](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_sosl_intro.htm)\n  queries against Salesforce and outputs the results into a new sheet\n* Creates invoices in Google Docs and a sample presentation in Google Slides\n  using the imported data.\n\n![Demo gif](demo.gif?raw=true \"Demo\")\n\n\n## Getting started\n\n* Install [clasp](https://github.com/google/clasp)\n* Run `clasp create <script name>` to create a new script\n* Follow Salesforce's [instructions](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/quickstart.htm)\n  to sign up as a developer and create an OAuth2 application\n  * Set your callback URL to `https://script.google.com/macros/d/{SCRIPT ID}/usercallback`\n    where `{SCRIPT ID}` is taken from the URL outputted by `clasp create`.\n* Update `Constants.gs` with your Salesforce client ID and client secret\n* Run `clasp push` to upload the contents of this folder to Apps Script\n* Run `clasp open` to open the project in the Apps Script IDE\n* Follow the [Test as Add-on](https://developers.google.com/apps-script/add-ons/test)\n  instructions to run the add-on in a spreadsheet\n  * On your test spreadsheet's menu, visit Add-ons -> &lt;script name&gt; ->\n    Login to Salesforce to sign in to Salesforce.\n"
  },
  {
    "path": "sheets/next18/Salesforce.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Creates an add-on menu, the main entry point for this add-on\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Login To Salesforce\", \"login\")\n    .addItem(\"Run SOQL Query\", \"promptQuery\")\n    .addSeparator()\n    .addItem(\"Logout From Salesforce\", \"logout\")\n    .addSeparator()\n    .addItem(\"Generate Invoices\", \"generateInvoices\")\n    .addItem(\"Generate Report\", \"generateReport\")\n    .addToUi();\n}\n\n/** Ensure the menu is created when the add-on is installed */\nfunction onInstall() {\n  onOpen();\n}\n\n/**\n * If we dont have a Salesforce OAuth token, starts the OAuth flow with\n * Salesforce.\n */\nfunction login() {\n  const salesforce = getSalesforceService();\n  if (!salesforce.hasAccess()) {\n    showLinkDialog(\n      salesforce.getAuthorizationUrl(),\n      \"Sign-in to Salesforce\",\n      \"Sign-in\",\n    );\n  }\n}\n\n/**\n * Displays a modal dialog with a simple HTML link that opens in a new tab.\n *\n * @param {string} url the URL to link to\n * @param {string} message the message to display to the user as a link\n * @param {string} title the title of the dialog\n */\nfunction showLinkDialog(url, message, title) {\n  const template = HtmlService.createTemplateFromFile(\"LinkDialog\");\n  template.url = url;\n  template.message = message;\n  SpreadsheetApp.getUi().showModalDialog(template.evaluate(), title);\n}\n\n/**\n * Creates a Salesforce OAuth2 service, using the Apps Script OAuth2 library:\n * https://github.com/googleworkspace/apps-script-oauth2\n *\n * @return {Object} a Salesforce OAuth2 service\n */\nfunction getSalesforceService() {\n  return OAuth2.createService(\"salesforce\")\n    .setAuthorizationBaseUrl(\n      \"https://login.salesforce.com/services/oauth2/authorize\",\n    )\n    .setTokenUrl(\"https://login.salesforce.com/services/oauth2/token\")\n    .setClientId(SALESFORCE_CLIENT_ID)\n    .setClientSecret(SALESFORCE_CLIENT_SECRET)\n    .setCallbackFunction(\"authCallback\")\n    .setPropertyStore(PropertiesService.getUserProperties());\n}\n\n/**\n * Authentication callback for OAuth2: called when Salesforce redirects back to\n * Apps Script after sign-in.\n *\n * @param {Object} request the HTTP request, provided by Apps Script\n * @return {Object} HTMLOutput to render the callback as a web page\n */\nfunction authCallback(request) {\n  const salesforce = getSalesforceService();\n  const isAuthorized = salesforce.handleCallback(request);\n  const message = isAuthorized\n    ? \"Success! You can close this tab and the dialog in Sheets.\"\n    : \"Denied. You can close this tab and the dialog in Sheets.\";\n\n  return HtmlService.createHtmlOutput(message);\n}\n\n/**\n * Prompts the user to enter a SOQL (Salesforce Object Query Language) query\n * to execute. If given, the query is run and its results are added as a new\n * sheet.\n */\nfunction promptQuery() {\n  const ui = SpreadsheetApp.getUi();\n  const response = ui.prompt(\n    \"Run SOQL Query\",\n    'Enter your query, ex: \"select Id from Opportunity\"',\n    ui.ButtonSet.OK_CANCEL,\n  );\n  const query = response.getResponseText();\n  if (response.getSelectedButton() === ui.Button.OK) {\n    executeQuery(query);\n  }\n}\n\n/**\n * Executes the given SOQL query and copies its results to a new sheet.\n *\n * @param {string} query the SOQL to execute\n */\nfunction executeQuery(query) {\n  const response = fetchSoqlResults(query);\n  const outputSheet = SpreadsheetApp.getActive().insertSheet();\n  const records = response.records;\n  const fields = getFields(records[0]);\n\n  // Builds the new sheet's contents as a 2D array that can be passed in\n  // to setValues() at once. This gives better performance than updating\n  // a single cell at a time.\n  const outputValues = [];\n  outputValues.push(fields);\n  for (let i = 0; i < records.length; i++) {\n    const row = [];\n    const record = records[i];\n    for (let j = 0; j < fields.length; j++) {\n      const fieldName = fields[j];\n      row.push(record[fieldName]);\n    }\n    outputValues.push(row);\n  }\n\n  outputSheet\n    .getRange(1, 1, outputValues.length, fields.length)\n    .setValues(outputValues);\n}\n\n/**\n * Makes an API call to Salesforce to execute a given SOQL query.\n *\n * @param {string} query the SOQL query to execute\n * @return {Object} the API response from Salesforce, as a parsed JSON object.\n */\nfunction fetchSoqlResults(query) {\n  const salesforce = getSalesforceService();\n  if (!salesforce.hasAccess()) {\n    throw new Error(\"Please login first\");\n  }\n  const params = {\n    headers: {\n      Authorization: `Bearer ${salesforce.getAccessToken()}`,\n      \"Content-Type\": \"application/json\",\n    },\n  };\n  const url = `https://${SALESFORCE_INSTANCE}.salesforce.com/services/data/v30.0/query`;\n  const response = UrlFetchApp.fetch(\n    `${url}?q=${encodeURIComponent(query)}`,\n    params,\n  );\n  return JSON.parse(response.getContentText());\n}\n\n/**\n * Parses the Salesforce response and extracts the list of field names in the\n * result set.\n *\n * @param {Object} record a single Salesforce response record\n * @return {Array<string>} an array of string keys of that record\n */\nfunction getFields(record) {\n  const fields = [];\n  for (const field in record) {\n    if (Object.hasOwn(record, field) && field !== \"attributes\") {\n      fields.push(field);\n    }\n  }\n  return fields;\n}\n\n/** Resets the Salesforce service, removing any saved OAuth tokens. */\nfunction logout() {\n  getSalesforceService().reset();\n}\n"
  },
  {
    "path": "sheets/next18/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"26\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/script.container.ui\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/spreadsheets.currentonly\",\n    \"https://www.googleapis.com/auth/drive\",\n    \"https://www.googleapis.com/auth/documents\",\n    \"https://www.googleapis.com/auth/presentations\"\n  ]\n}\n"
  },
  {
    "path": "sheets/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START sheets_quickstart]\n/**\n * Creates a Sheets API service object and prints the names and majors of\n * students in a sample spreadsheet:\n * https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit\n * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get\n */\nfunction logNamesAndMajors() {\n  const spreadsheetId = \"1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms\";\n  const rangeName = \"Class Data!A2:E\";\n  try {\n    // Get the values from the spreadsheet using spreadsheetId and range.\n    const values = Sheets.Spreadsheets.Values.get(\n      spreadsheetId,\n      rangeName,\n    ).values;\n    //  Print the values from spreadsheet if values are available.\n    if (!values) {\n      console.log(\"No data found.\");\n      return;\n    }\n    console.log(\"Name, Major:\");\n    for (const row in values) {\n      // Print columns A and E, which correspond to indices 0 and 4.\n      console.log(\" - %s, %s\", values[row][0], values[row][4]);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle Values.get() exception from Sheet API\n    console.log(err.message);\n  }\n}\n// [END sheets_quickstart]\n"
  },
  {
    "path": "sheets/removingDuplicates/removingDuplicates.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_sheets_remove_duplicates]\n/**\n * Removes duplicate rows from the current sheet.\n */\nfunction removeDuplicates() {\n  // [START apps_script_sheets_sheet]\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const data = sheet.getDataRange().getValues();\n  // [END apps_script_sheets_sheet]\n  const uniqueData = {};\n  for (const row of data) {\n    const key = row.join();\n    // [START apps_script_sheets_duplicate]\n    uniqueData[key] = uniqueData[key] || row;\n    // [END apps_script_sheets_duplicate]\n  }\n  // [START apps_script_sheets_clear]\n  sheet.clearContents();\n  // [START apps_script_sheets_new_data]\n  const newData = Object.values(uniqueData);\n  // [END apps_script_sheets_new_data]\n  sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);\n  // [END apps_script_sheets_clear]\n}\n// [END apps_script_sheets_remove_duplicates]\n"
  },
  {
    "path": "slides/README.md",
    "content": "# Quickstarts: Add-ons for Google Slides\n\nSample Google Apps Script add-ons for Google Slides.\n\n## [Translate](https://developers.google.com/apps-script/guides/slides/samples/translate)\n\nThis add-on translates selected text from one language to another.\n\n![Translate](https://user-images.githubusercontent.com/380123/45050204-9f383a00-b04e-11e8-9dc8-30fcc5e9fdd7.png)\n\n## [Progress Bars](https://developers.google.com/apps-script/guides/slides/samples/progress-bar)\n\nThis add-on adds a progress bar to your presentation.\n\n![Progress Bars](https://user-images.githubusercontent.com/380123/45050203-9f383a00-b04e-11e8-9abf-042ce463a149.png)\n\n## Speaker Notes Script\n\nThis add-on extracts all the Speaker Notes from your presentation and creates a Document in your\nDrive directory with a formatted \"script\".\n\n![Script](https://user-images.githubusercontent.com/380123/45051769-022bd000-b053-11e8-9700-7a67e89cc4c9.png)\n"
  },
  {
    "path": "slides/SpeakerNotesScript/README.md",
    "content": "# Speaker Notes Script\n\nThis add-on will extract all your Speaker Notes and creates a Google Doc with your 'formatted' script.\nTo run this add-on, first go to your Google slides with Speaker Notes.\n\n![scriptscreenshot](https://user-images.githubusercontent.com/380123/45267455-878bf780-b43a-11e8-9aeb-9c909feb9613.jpg)\n\n## Set Up\n\n1. From within your new presentation, select the menu item\n   **Tools > Script editor**. If you are presented with a welcome screen, click **Blank Project**.\n1. Delete any code in the script editor and rename `Code.gs` to `scriptGen.gs`. Copy and paste the contents of `scriptGen.gs` into this file.\n1. Then select the menu item **View > Show manifest file** in your Script Editor screen. Copy and paste the contents of `appsscript.json` in here. You need 2 scopes to run this sample:\n    * To create and write a document: `https://www.googleapis.com/auth/documents`\n    * To read the current presentation: `https://www.googleapis.com/auth/presentations.currentonly`\n\n## Try It Out\n\n1. Switch back to your presentation and reload the page.\n1. After a few seconds, a **Speaker Notes Script** sub-menu appears under the\n   **Add-ons** menu. Click **Add-ons > Speaker Notes Script > Generate Script Document**.\n1. A dialog box indicates that the script requires authorization.\n   Click **Continue**. A second dialog box requests authorization for\n   specific Google services. Click **Allow**.\n1. Check your Drive folder for script!\n"
  },
  {
    "path": "slides/SpeakerNotesScript/appscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/documents\",\n    \"https://www.googleapis.com/auth/presentations.currentonly\"\n  ],\n  \"exceptionLogging\": \"STACKDRIVER\"\n}\n"
  },
  {
    "path": "slides/SpeakerNotesScript/scriptGen.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_speaker_notes_script]\n/**\n * Runs when the add-on is installed.\n * @param {object} e The event parameter for a simple onInstall trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode. (In practice, onInstall triggers always\n *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or\n *     AuthMode.NONE.)\n */\nfunction onInstall(e) {\n  onOpen();\n}\n\n/**\n * Trigger for opening a presentation.\n * @param {object} e The onOpen event.\n */\nfunction onOpen(e) {\n  SlidesApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Generate Script Document\", \"generateSlidesScript\")\n    .addToUi();\n}\n\n/**\n * Creates a 'script' for the presentation user in a document\n * with the speaker notes for each slide.\n */\nfunction generateSlidesScript() {\n  const presentation = SlidesApp.getActivePresentation();\n  const docTitle = `${presentation.getName()} Script`;\n  const slides = presentation.getSlides();\n\n  // Creates a document in the user's home Drive directory.\n  const speakerNotesDoc = DocumentApp.create(docTitle);\n  console.log(\"Created document with id %s\", speakerNotesDoc.getId());\n\n  const docBody = speakerNotesDoc.getBody();\n  const header = docBody.appendParagraph(docTitle);\n  header.setHeading(DocumentApp.ParagraphHeading.HEADING1);\n\n  // Iterate through each slide and extract the speaker notes\n  // into the document body.\n  for (let i = 0; i < slides.length; i++) {\n    const section = docBody\n      .appendParagraph(`Slide ${i + 1}`)\n      .setHeading(DocumentApp.ParagraphHeading.HEADING2);\n\n    const notes = slides[i]\n      .getNotesPage()\n      .getSpeakerNotesShape()\n      .getText()\n      .asString();\n    docBody.appendParagraph(notes).appendHorizontalRule();\n  }\n\n  SlidesApp.getUi().alert(\n    `${speakerNotesDoc.getName()} has been created in your Drive files.`,\n  );\n}\n// [END apps_script_slides_speaker_notes_script]\n"
  },
  {
    "path": "slides/api/Helpers.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Helper functions.\n */\nfunction Helpers() {\n  this.filesToDelete = [];\n}\n\nHelpers.prototype.reset = function () {\n  this.filesToDelete = [];\n};\n\nHelpers.prototype.deleteFileOnCleanup = function (id) {\n  this.filesToDelete.push(id);\n};\n\nHelpers.prototype.cleanup = function () {\n  this.filesToDelete.forEach(Drive.Files.remove);\n};\n\nHelpers.prototype.createTestPresentation = function () {\n  const presentation = Slides.Presentations.create({\n    title: \"Test Preso\",\n  });\n  this.deleteFileOnCleanup(presentation.presentationId);\n  return presentation.presentationId;\n};\n\nHelpers.prototype.addSlides = (presentationId, num, layout) => {\n  const requests = [];\n  const slideIds = [];\n  for (let i = 0; i < num; ++i) {\n    slideIds.push(`slide_${i}`);\n    requests.push({\n      createSlide: {\n        objectId: slideIds[i],\n        slideLayoutReference: {\n          predefinedLayout: layout,\n        },\n      },\n    });\n  }\n  Slides.Presentations.batchUpdate({ requests: requests }, presentationId);\n  return slideIds;\n};\n\nHelpers.prototype.createTestTextbox = (presentationId, pageId, callback) => {\n  const boxId = \"MyTextBox_01\";\n  const pt350 = {\n    magnitude: 350,\n    unit: \"PT\",\n  };\n  const requests = [\n    {\n      createShape: {\n        objectId: boxId,\n        shapeType: \"TEXT_BOX\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            height: pt350,\n            width: pt350,\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 350,\n            translateY: 100,\n            unit: \"PT\",\n          },\n        },\n      },\n    },\n    {\n      insertText: {\n        objectId: boxId,\n        insertionIndex: 0,\n        text: \"New Box Text Inserted\",\n      },\n    },\n  ];\n  const createTextboxResponse = Slides.Presentations.batchUpdate(\n    {\n      requests: requests,\n    },\n    presentationId,\n  );\n  return createTextboxResponse.replies[0].createShape.objectId;\n};\n\nHelpers.prototype.createTestSheetsChart = (\n  presentationId,\n  pageId,\n  spreadsheetId,\n  sheetChartId,\n  callback,\n) => {\n  const chartId = \"MyChart_01\";\n  const emu4M = {\n    magnitude: 4000000,\n    unit: \"EMU\",\n  };\n  const requests = [\n    {\n      createSheetsChart: {\n        objectId: chartId,\n        spreadsheetId: spreadsheetId,\n        chartId: sheetChartId,\n        linkingMode: \"LINKED\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            height: emu4M,\n            width: emu4M,\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 100000,\n            translateY: 100000,\n            unit: \"EMU\",\n          },\n        },\n      },\n    },\n  ];\n  const createSheetsChartResponse = Slides.Presentations.batchUpdate(\n    {\n      requests: requests,\n    },\n    presentationId,\n  );\n  return createSheetsChartResponse.replies[0].createSheetsChart.objectId;\n};\n"
  },
  {
    "path": "slides/api/Snippets.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nconst title = \"my title\";\n\n// [START slides_create_presentation]\n/**\n * Creates a presentation\n * @returns {*} the created presentation\n */\nfunction createPresentation() {\n  try {\n    const presentation = Slides.Presentations.create({\n      title: title,\n    });\n    console.log(\n      \"Created presentation with ID: %s\",\n      presentation.presentationId,\n    );\n\n    return presentation;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_presentation]\n\n// [START slides_copy_presentation]\n/**\n * create a presentation and copy it\n * @param {string} presentationId - ID of presentation to copy\n * @returns {*} the copy's presentation id\n */\nfunction copyPresentation(presentationId) {\n  const copyTitle = \"Copy Title\";\n\n  let copyFile = {\n    title: copyTitle,\n    parents: [{ id: \"root\" }],\n  };\n  try {\n    copyFile = Drive.Files.copy(copyFile, presentationId);\n    // (optional) copyFile.id can be returned directly\n    const presentationCopyId = copyFile.id;\n\n    return presentationCopyId;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_copy_presentation]\n\n// [START slides_create_slide]\n/**\n * Creates a slide\n * @param {string} presentationId\n * @param {string} pageId\n * @returns {*}\n */\nfunction createSlide(presentationId, pageId) {\n  // See Presentation.insertSlide(...) to learn how to add a slide using SlidesApp.\n  // http://developers.google.com/apps-script/reference/slides/presentation#appendslidelayout\n  const requests = [\n    {\n      createSlide: {\n        objectId: pageId,\n        insertionIndex: \"1\",\n        slideLayoutReference: {\n          predefinedLayout: \"TITLE_AND_TWO_COLUMNS\",\n        },\n      },\n    },\n  ];\n\n  // If you wish to populate the slide with elements, add element create requests here,\n  // using the pageId.\n\n  // Execute the request.\n  try {\n    const createSlideResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\n      \"Created slide with ID: %s\",\n      createSlideResponse.replies[0].createSlide.objectId,\n    );\n    return createSlideResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_slide]\n\n// [START slides_create_textbox_with_text]\n/**\n * Create a new square textbox, using the supplied element ID.\n * @param {string} presentationId\n * @param {string} pageId\n * @returns {*}\n */\nfunction createTextboxWithText(presentationId, pageId) {\n  const elementId = \"MyTextBox_01\";\n  const pt350 = {\n    magnitude: 350,\n    unit: \"PT\",\n  };\n  const requests = [\n    {\n      createShape: {\n        objectId: elementId,\n        shapeType: \"TEXT_BOX\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            height: pt350,\n            width: pt350,\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 350,\n            translateY: 100,\n            unit: \"PT\",\n          },\n        },\n      },\n    },\n    // Insert text into the box, using the supplied element ID.\n    {\n      insertText: {\n        objectId: elementId,\n        insertionIndex: 0,\n        text: \"New Box Text Inserted!\",\n      },\n    },\n  ];\n\n  // Execute the request.\n  try {\n    const createTextboxWithTextResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    const createShapeResponse =\n      createTextboxWithTextResponse.replies[0].createShape;\n    console.log(\"Created textbox with ID: %s\", createShapeResponse.objectId);\n\n    return createTextboxWithTextResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_textbox_with_text]\n\n// [START slides_create_image]\n/**\n * Create a new image, using the supplied object ID, with content downloaded from imageUrl.\n * @param {string} presentationId\n * @param {string} pageId\n * @returns {*}\n */\nfunction createImage(presentationId, pageId) {\n  const requests = [];\n  const imageId = \"MyImage_01\";\n  const imageUrl =\n    \"https://www.google.com/images/branding/googlelogo/2x/\" +\n    \"googlelogo_color_272x92dp.png\";\n  const emu4M = {\n    magnitude: 4000000,\n    unit: \"EMU\",\n  };\n  requests.push({\n    createImage: {\n      objectId: imageId,\n      url: imageUrl,\n      elementProperties: {\n        pageObjectId: pageId,\n        size: {\n          height: emu4M,\n          width: emu4M,\n        },\n        transform: {\n          scaleX: 1,\n          scaleY: 1,\n          translateX: 100000,\n          translateY: 100000,\n          unit: \"EMU\",\n        },\n      },\n    },\n  });\n\n  // Execute the request.\n  try {\n    const response = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n\n    const createImageResponse = response.replies;\n    console.log(\n      \"Created image with ID: %s\",\n      createImageResponse[0].createImage.objectId,\n    );\n\n    return createImageResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_image]\n\n// [START slides_text_merging]\n/**\n * Use the Sheets API to load data, one record per row.\n * @param {string} templatePresentationId\n * @param {string} dataSpreadsheetId\n * @returns {*[]}\n */\nfunction textMerging(templatePresentationId, dataSpreadsheetId) {\n  const responses = [];\n  const dataRangeNotation = \"Customers!A2:M6\";\n  try {\n    const values = SpreadsheetApp.openById(dataSpreadsheetId)\n      .getRange(dataRangeNotation)\n      .getValues();\n\n    // For each record, create a new merged presentation.\n    for (let i = 0; i < values.length; ++i) {\n      const row = values[i];\n      const customerName = row[2]; // name in column 3\n      const caseDescription = row[5]; // case description in column 6\n      const totalPortfolio = row[11]; // total portfolio in column 12\n\n      // Duplicate the template presentation using the Drive API.\n      const copyTitle = `${customerName} presentation`;\n      let copyFile = {\n        title: copyTitle,\n        parents: [{ id: \"root\" }],\n      };\n      copyFile = Drive.Files.copy(copyFile, templatePresentationId);\n      const presentationCopyId = copyFile.id;\n\n      // Create the text merge (replaceAllText) requests for this presentation.\n      const requests = [\n        {\n          replaceAllText: {\n            containsText: {\n              text: \"{{customer-name}}\",\n              matchCase: true,\n            },\n            replaceText: customerName,\n          },\n        },\n        {\n          replaceAllText: {\n            containsText: {\n              text: \"{{case-description}}\",\n              matchCase: true,\n            },\n            replaceText: caseDescription,\n          },\n        },\n        {\n          replaceAllText: {\n            containsText: {\n              text: \"{{total-portfolio}}\",\n              matchCase: true,\n            },\n            replaceText: `${totalPortfolio}`,\n          },\n        },\n      ];\n\n      // Execute the requests for this presentation.\n      const result = Slides.Presentations.batchUpdate(\n        {\n          requests: requests,\n        },\n        presentationCopyId,\n      );\n      // Count the total number of replacements made.\n      let numReplacements = 0;\n      for (const reply of result.replies) {\n        numReplacements += reply.replaceAllText.occurrencesChanged;\n      }\n      console.log(\n        \"Created presentation for %s with ID: %s\",\n        customerName,\n        presentationCopyId,\n      );\n      console.log(\"Replaced %s text instances\", numReplacements);\n      // [START_EXCLUDE silent]\n      responses.push(result.replies);\n      if (responses.length === values.length) {\n        // return for the last value\n        return responses;\n      }\n      // [END_EXCLUDE]\n    }\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_text_merging]\n\n// [START slides_image_merging]\n/**\n * Duplicate the template presentation using the Drive API.\n * @param {string} templatePresentationId\n * @param {string} imageUrl\n * @param {string} customerName\n * @returns {*}\n */\nfunction imageMerging(templatePresentationId, imageUrl, customerName) {\n  const logoUrl = imageUrl;\n  const customerGraphicUrl = imageUrl;\n\n  const copyTitle = `${customerName} presentation`;\n  let copyFile = {\n    title: copyTitle,\n    parents: [{ id: \"root\" }],\n  };\n\n  try {\n    copyFile = Drive.Files.copy(copyFile, templatePresentationId);\n    const presentationCopyId = copyFile.id;\n\n    // Create the image merge (replaceAllShapesWithImage) requests.\n    const requests = [\n      {\n        replaceAllShapesWithImage: {\n          imageUrl: logoUrl,\n          imageReplaceMethod: \"CENTER_INSIDE\",\n          containsText: {\n            text: \"{{company-logo}}\",\n            matchCase: true,\n          },\n        },\n      },\n      {\n        replaceAllShapesWithImage: {\n          imageUrl: customerGraphicUrl,\n          imageReplaceMethod: \"CENTER_INSIDE\",\n          containsText: {\n            text: \"{{customer-graphic}}\",\n            matchCase: true,\n          },\n        },\n      },\n    ];\n\n    // Execute the requests for this presentation.\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationCopyId,\n    );\n    let numReplacements = 0;\n    for (const reply of batchUpdateResponse.replies) {\n      numReplacements += reply.replaceAllShapesWithImage.occurrencesChanged;\n    }\n    console.log(\"Created merged presentation with ID: %s\", presentationCopyId);\n    console.log(\"Replaced %s shapes with images.\", numReplacements);\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_image_merging]\n\n// [START slides_simple_text_replace]\n/**\n * Remove existing text in the shape, then insert new text.\n * @param {string} presentationId\n * @param {string?} shapeId\n * @param {string} replacementText\n * @returns {*}\n */\nfunction simpleTextReplace(presentationId, shapeId, replacementText) {\n  const requests = [\n    {\n      deleteText: {\n        objectId: shapeId,\n        textRange: {\n          type: \"ALL\",\n        },\n      },\n    },\n    {\n      insertText: {\n        objectId: shapeId,\n        insertionIndex: 0,\n        text: replacementText,\n      },\n    },\n  ];\n\n  // Execute the requests.\n  try {\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\"Replaced text in shape with ID: %s\", shapeId);\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_simple_text_replace]\n\n// [START slides_text_style_update]\n/**\n * Update the text style so that the first 5 characters are bolded\n * and italicized, the next 5 are displayed in blue 14 pt Times\n * New Roman font, and the next 5 are hyperlinked.\n * @param {string} presentationId\n * @param {string} shapeId\n * @returns {*}\n */\nfunction textStyleUpdate(presentationId, shapeId) {\n  const requests = [\n    {\n      updateTextStyle: {\n        objectId: shapeId,\n        textRange: {\n          type: \"FIXED_RANGE\",\n          startIndex: 0,\n          endIndex: 5,\n        },\n        style: {\n          bold: true,\n          italic: true,\n        },\n        fields: \"bold,italic\",\n      },\n    },\n    {\n      updateTextStyle: {\n        objectId: shapeId,\n        textRange: {\n          type: \"FIXED_RANGE\",\n          startIndex: 5,\n          endIndex: 10,\n        },\n        style: {\n          fontFamily: \"Times New Roman\",\n          fontSize: {\n            magnitude: 14,\n            unit: \"PT\",\n          },\n          foregroundColor: {\n            opaqueColor: {\n              rgbColor: {\n                blue: 1.0,\n                green: 0.0,\n                red: 0.0,\n              },\n            },\n          },\n        },\n        fields: \"foregroundColor,fontFamily,fontSize\",\n      },\n    },\n    {\n      updateTextStyle: {\n        objectId: shapeId,\n        textRange: {\n          type: \"FIXED_RANGE\",\n          startIndex: 10,\n          endIndex: 15,\n        },\n        style: {\n          link: {\n            url: \"www.example.com\",\n          },\n        },\n        fields: \"link\",\n      },\n    },\n  ];\n\n  // Execute the requests.\n  try {\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\"Updated the text style for shape with ID: %s\", shapeId);\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_text_style_update]\n\n// [START slides_create_bulleted_text]\n/**\n * Add arrow-diamond-disc bullets to all text in the shape.\n */\nfunction createBulletedText(presentationId, shapeId) {\n  const requests = [\n    {\n      createParagraphBullets: {\n        objectId: shapeId,\n        textRange: {\n          type: \"ALL\",\n        },\n        bulletPreset: \"BULLET_ARROW_DIAMOND_DISC\",\n      },\n    },\n  ];\n\n  // Execute the requests.\n  try {\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\"Added bullets to text in shape with ID: %s\", shapeId);\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_bulleted_text]\n\n// [START slides_create_sheets_chart]\n/**\n * Embed a Sheets chart (indicated by the spreadsheetId and sheetChartId) onto\n *   a page in the presentation. Setting the linking mode as 'LINKED' allows the\n *   chart to be refreshed if the Sheets version is updated.\n * @param {string} presentationId\n * @param {string} pageId\n * @param {string} shapeId\n * @param {string} sheetChartId\n * @returns {*}\n */\nfunction createSheetsChart(presentationId, pageId, shapeId, sheetChartId) {\n  const emu4M = {\n    magnitude: 4000000,\n    unit: \"EMU\",\n  };\n  const presentationChartId = \"MyEmbeddedChart\";\n  const requests = [\n    {\n      createSheetsChart: {\n        objectId: presentationChartId,\n        spreadsheetId: shapeId,\n        chartId: sheetChartId,\n        linkingMode: \"LINKED\",\n        elementProperties: {\n          pageObjectId: pageId,\n          size: {\n            height: emu4M,\n            width: emu4M,\n          },\n          transform: {\n            scaleX: 1,\n            scaleY: 1,\n            translateX: 100000,\n            translateY: 100000,\n            unit: \"EMU\",\n          },\n        },\n      },\n    },\n  ];\n\n  // Execute the request.\n  try {\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\"Added a linked Sheets chart with ID: %s\", presentationChartId);\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_create_sheets_chart]\n\n// [START slides_refresh_sheets_chart]\n/**\n * Refresh the sheets charts\n * @param {string} presentationId\n * @param {string} presentationChartId\n * @returns {*}\n */\nfunction refreshSheetsChart(presentationId, presentationChartId) {\n  const requests = [\n    {\n      refreshSheetsChart: {\n        objectId: presentationChartId,\n      },\n    },\n  ];\n\n  // Execute the request.\n  try {\n    const batchUpdateResponse = Slides.Presentations.batchUpdate(\n      {\n        requests: requests,\n      },\n      presentationId,\n    );\n    console.log(\n      \"Refreshed a linked Sheets chart with ID: %s\",\n      presentationChartId,\n    );\n\n    return batchUpdateResponse;\n  } catch (err) {\n    // TODO (Developer) - Handle exception\n    console.log(\"Failed with error: %s\", err.error);\n  }\n}\n// [END slides_refresh_sheets_chart]\n"
  },
  {
    "path": "slides/api/Tests.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nconst helpers = new Helpers();\n\n// Constants\nconst IMAGE_URL =\n  \"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png\";\nconst TEMPLATE_PRESENTATION_ID = \"1iwq83aR9SIQbqVY-3ozLkJjKhdXErfS_m3zD8VZhtFA\";\nconst DATA_SPREADSHEET_ID = \"1Y3GVGdJHDzlyMB9aLDWV2o_e2RstzUHK1iLDaBgbMwc\";\nconst CHART_ID = 1107320627;\nconst CUSTOMER_NAME = \"Fake Customer\";\n\n/**\n * A simple existance assertion. Logs if the value is falsy.\n * @param {object} value The value we expect to exist.\n */\nfunction expectToExist(value) {\n  if (!value) {\n    console.log(\"DNE\");\n  }\n}\n\n/**\n * A simple equality assertion. Logs if there is a mismatch.\n * @param {object} expected The expected value.\n * @param {object} actual The actual value.\n */\nfunction expectToEqual(expected, actual) {\n  if (actual !== expected) {\n    console.log(\"actual: %s expected: %s\", actual, expected);\n  }\n}\n\n/**\n * Runs all tests.\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateAPresentation();\n  itShouldCopyAPresentation();\n  itShouldCreateASlide();\n  itShouldCreateATextboxWithText();\n  itShouldCreateAnImage();\n  itShouldMergeText();\n  itShouldImageMerge();\n  itShouldSimpleTextReplace();\n  itShouldTextStyleUpdate();\n  itShouldCreateBulletedText();\n  itShouldCreateSheetsChart();\n  itShouldRefreshSheetsChart();\n}\n\n/**\n * Creates a presentation.\n */\nfunction itShouldCreateAPresentation() {\n  console.log(\"> itShouldCreateAPresentation\");\n  const presentation = createPresentation();\n  expectToExist(presentation.presentationId);\n  helpers.deleteFileOnCleanup(presentation.presentationId);\n}\n\n/**\n * Copies a presentation.\n */\nfunction itShouldCopyAPresentation() {\n  console.log(\"> itShouldCopyAPresentation\");\n  const presentationId = helpers.createTestPresentation();\n  const copyId = copyPresentation(presentationId, \"My Duplicate, Presentation\");\n  expectToExist(copyId);\n  helpers.deleteFileOnCleanup(copyId);\n}\n\n/**\n * Creates a new slide.\n */\nfunction itShouldCreateASlide() {\n  console.log(\"> itShouldCreateASlide\");\n  const presentationId = helpers.createTestPresentation();\n  helpers.addSlides(presentationId, 3, \"TITLE_AND_TWO_COLUMNS\");\n  const pageId = \"my_page_id\";\n  const response = createSlide(presentationId, pageId);\n  expectToExist(response.replies[0].createSlide.objectId);\n}\n\n/**\n * Creates a slide with text.\n */\nfunction itShouldCreateATextboxWithText() {\n  console.log(\"> itShouldCreateATextboxWithText\");\n  const presentationId = helpers.createTestPresentation();\n  const ids = helpers.addSlides(presentationId, 3, \"TITLE_AND_TWO_COLUMNS\");\n  const pageId = ids[0];\n  const response = createTextboxWithText(presentationId, pageId);\n  expectToEqual(2, response.replies.length);\n  const boxId = response.replies[0].createShape.objectId;\n  expectToExist(boxId);\n}\n\n/**\n * Adds an image to a slide.\n */\nfunction itShouldCreateAnImage() {\n  console.log(\"> itShouldCreateAnImage\");\n  const presentationId = helpers.createTestPresentation();\n  const ids = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = ids[0];\n  const response = createImage(presentationId, pageId);\n  expectToEqual(1, response.length);\n  const imageId = response[0].createImage.objectId;\n  expectToExist(imageId);\n}\n\n/**\n * Merges presentation text from a spreadsheet.\n */\nfunction itShouldMergeText() {\n  console.log(\"> itShouldMergeText\");\n  const responses = textMerging(TEMPLATE_PRESENTATION_ID, DATA_SPREADSHEET_ID);\n  expectToEqual(5, responses.length);\n  for (const response of responses) {\n    let numReplacements = 0;\n    for (const res of response) {\n      numReplacements += res.replaceAllText.occurrencesChanged;\n    }\n    expectToEqual(4, numReplacements);\n  }\n}\n\n/**\n * Merges images into a spreadsheet.\n */\nfunction itShouldImageMerge() {\n  console.log(\"> itShouldImageMerge\");\n  const response = imageMerging(\n    TEMPLATE_PRESENTATION_ID,\n    IMAGE_URL,\n    CUSTOMER_NAME,\n  );\n  expectToEqual(2, response.replies.length);\n  let numReplacements = 0;\n  for (const reply of response.replies) {\n    numReplacements += reply.replaceAllShapesWithImage.occurrencesChanged;\n  }\n  expectToEqual(2, numReplacements);\n}\n\n/**\n * Replaces a text box with some text.\n */\nfunction itShouldSimpleTextReplace() {\n  console.log(\"> itShouldSimpleTextReplace\");\n  const presentationId = helpers.createTestPresentation();\n  const pageIds = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = pageIds[0];\n  const boxId = helpers.createTestTextbox(presentationId, pageId);\n  const response = simpleTextReplace(presentationId, boxId, \"MY NEW TEXT\");\n  expectToEqual(2, response.replies.length);\n}\n\n/**\n * Updates style for text.\n */\nfunction itShouldTextStyleUpdate() {\n  console.log(\"> itShouldTextStyleUpdate\");\n  const presentationId = helpers.createTestPresentation();\n  const pageIds = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = pageIds[0];\n  const boxId = helpers.createTestTextbox(presentationId, pageId);\n  const response = textStyleUpdate(presentationId, boxId);\n  expectToEqual(3, response.replies.length);\n}\n\n/**\n * Creates bulleted text.\n */\nfunction itShouldCreateBulletedText() {\n  console.log(\"> itShouldCreateBulletedText\");\n  const presentationId = helpers.createTestPresentation();\n  const pageIds = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = pageIds[0];\n  const boxId = helpers.createTestTextbox(presentationId, pageId);\n  const response = createBulletedText(presentationId, boxId);\n  expectToEqual(1, response.replies.length);\n}\n\n/**\n * Adds a sheets chart in a presentation.\n */\nfunction itShouldCreateSheetsChart() {\n  console.log(\"> itShouldCreateSheetsChart\");\n  const presentationId = helpers.createTestPresentation();\n  const pageIds = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = pageIds[0];\n  const response = createSheetsChart(\n    presentationId,\n    pageId,\n    DATA_SPREADSHEET_ID,\n    CHART_ID,\n  );\n  expectToEqual(1, response.replies.length);\n  const chartId = response.replies[0].createSheetsChart.objectId;\n  expectToExist(chartId);\n}\n\n/**\n * Refreshes a sheets chart in a presentation.\n */\nfunction itShouldRefreshSheetsChart() {\n  console.log(\"> itShouldRefreshSheetsChart\");\n  const presentationId = helpers.createTestPresentation();\n  const pageIds = helpers.addSlides(presentationId, 1, \"BLANK\");\n  const pageId = pageIds[0];\n  const sheetChartId = helpers.createTestSheetsChart(\n    presentationId,\n    pageId,\n    DATA_SPREADSHEET_ID,\n    CHART_ID,\n  );\n  const response = refreshSheetsChart(presentationId, sheetChartId);\n  expectToEqual(1, response.replies.length);\n}\n"
  },
  {
    "path": "slides/imageSlides/add_image/add_image.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_image_add_image]\n/**\n * Adds an image to a presentation at a given slide index.\n * @param {string} imageUrl The image URL\n * @param {number} index The slide index to add the image to\n */\nfunction addImageSlide(imageUrl, index) {\n  const slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK);\n  const image = slide.insertImage(imageUrl);\n}\n// [END apps_script_slides_image_add_image]\n"
  },
  {
    "path": "slides/imageSlides/add_image_slide/add_image_slide.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_image_add_image_slide]\n/**\n * Creates a single slide using the image from the given link;\n * used directly by foreach(), hence the parameters are fixed.\n * @param {string} imageUrl A String object representing an image URL\n * @param {number} index The index into the array; unused (req'd by forEach)\n */\nfunction addImageSlide(imageUrl, index) {\n  const slide = deck.appendSlide(SlidesApp.PredefinedLayout.BLANK);\n  const image = slide.insertImage(imageUrl);\n  const imgWidth = image.getWidth();\n  const imgHeight = image.getHeight();\n  const pageWidth = deck.getPageWidth();\n  const pageHeight = deck.getPageHeight();\n  const newX = pageWidth / 2 - imgWidth / 2;\n  const newY = pageHeight / 2 - imgHeight / 2;\n  image.setLeft(newX).setTop(newY);\n}\n// [END apps_script_slides_image_add_image_slide]\n"
  },
  {
    "path": "slides/imageSlides/create/create.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_image_create]\nconst NAME = \"My favorite images\";\nconst deck = SlidesApp.create(NAME);\n// [END apps_script_slides_image_create]\n"
  },
  {
    "path": "slides/imageSlides/full/full.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_image_full_script]\nconst NAME = \"My favorite images\";\nconst presentation = SlidesApp.create(NAME);\n\n/**\n * Creates a single slide using the image from the given link;\n * used directly by foreach(), hence the parameters are fixed.\n * @param {string} imageUrl A String object representing an image URL\n * @param {number} index The index into the array; unused (req'd by forEach)\n */\nfunction addImageSlide(imageUrl, index) {\n  const slide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK);\n  const image = slide.insertImage(imageUrl);\n  const imgWidth = image.getWidth();\n  const imgHeight = image.getHeight();\n  const pageWidth = presentation.getPageWidth();\n  const pageHeight = presentation.getPageHeight();\n  const newX = pageWidth / 2 - imgWidth / 2;\n  const newY = pageHeight / 2 - imgHeight / 2;\n  image.setLeft(newX).setTop(newY);\n}\n\n/**\n * The driver application features an array of image URLs, setting of the\n * main title & subtitle, and creation of individual slides for each image.\n */\nfunction main() {\n  const images = [\n    \"http://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png\",\n    \"http://www.google.com/services/images/phone-animation-results_2x.png\",\n    \"http://www.google.com/services/images/section-work-card-img_2x.jpg\",\n    \"http://gsuite.google.com/img/icons/product-lockup.png\",\n    \"http://gsuite.google.com/img/home-hero_2x.jpg\",\n  ];\n  const [title, subtitle] = presentation.getSlides()[0].getPageElements();\n  title.asShape().getText().setText(NAME);\n  subtitle\n    .asShape()\n    .getText()\n    .setText(\"Google Apps Script\\nSlides Service demo\");\n  for (const imageUrl of images) {\n    addImageSlide(imageUrl);\n  }\n}\n// [END apps_script_slides_image_full_script]\n"
  },
  {
    "path": "slides/imageSlides/main/main.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_image_main]\n/**\n * Adds images to a slides presentation.\n */\nfunction main() {\n  const images = [\n    \"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png\",\n    \"http://www.google.com/services/images/phone-animation-results_2x.png\",\n    \"http://www.google.com/services/images/section-work-card-img_2x.jpg\",\n    \"http://gsuite.google.com/img/icons/product-lockup.png\",\n    \"http://gsuite.google.com/img/home-hero_2x.jpg\",\n  ];\n  const [title, subtitle] = deck.getSlides()[0].getPageElements();\n  title.asShape().getText().setText(NAME);\n  subtitle\n    .asShape()\n    .getText()\n    .setText(\"Google Apps Script\\nSlides Service demo\");\n  for (const imageUrl of images) {\n    addImageSlide(imageUrl);\n  }\n}\n// [END apps_script_slides_image_main]\n"
  },
  {
    "path": "slides/progress/progress.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_slides_progress]\n/**\n * @OnlyCurrentDoc Adds progress bars to a presentation.\n */\nconst BAR_ID = \"PROGRESS_BAR_ID\";\nconst BAR_HEIGHT = 10; // px\n\n/**\n * Runs when the add-on is installed.\n * @param {object} e The event parameter for a simple onInstall trigger. To\n *     determine which authorization mode (ScriptApp.AuthMode) the trigger is\n *     running in, inspect e.authMode. (In practice, onInstall triggers always\n *     run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or\n *     AuthMode.NONE.)\n */\nfunction onInstall(e) {\n  onOpen();\n}\n\n/**\n * Trigger for opening a presentation.\n * @param {object} e The onOpen event.\n */\nfunction onOpen(e) {\n  SlidesApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Show progress bar\", \"createBars\")\n    .addItem(\"Hide progress bar\", \"deleteBars\")\n    .addToUi();\n}\n\n/**\n * Create a rectangle on every slide with different bar widths.\n */\nfunction createBars() {\n  deleteBars(); // Delete any existing progress bars\n  const presentation = SlidesApp.getActivePresentation();\n  const slides = presentation.getSlides();\n  for (let i = 0; i < slides.length; ++i) {\n    const ratioComplete = i / (slides.length - 1);\n    const x = 0;\n    const y = presentation.getPageHeight() - BAR_HEIGHT;\n    const barWidth = presentation.getPageWidth() * ratioComplete;\n    if (barWidth > 0) {\n      const bar = slides[i].insertShape(\n        SlidesApp.ShapeType.RECTANGLE,\n        x,\n        y,\n        barWidth,\n        BAR_HEIGHT,\n      );\n      bar.getBorder().setTransparent();\n      bar.setLinkUrl(BAR_ID);\n    }\n  }\n}\n\n/**\n * Deletes all progress bar rectangles.\n */\nfunction deleteBars() {\n  const presentation = SlidesApp.getActivePresentation();\n  const slides = presentation.getSlides();\n  for (let i = 0; i < slides.length; ++i) {\n    const elements = slides[i].getPageElements();\n    for (const el of elements) {\n      if (\n        el.getPageElementType() === SlidesApp.PageElementType.SHAPE &&\n        el.asShape().getLink() &&\n        el.asShape().getLink().getUrl() === BAR_ID\n      ) {\n        el.remove();\n      }\n    }\n  }\n}\n// [END apps_script_slides_progress]\n"
  },
  {
    "path": "slides/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START slides_quickstart]\n/**\n * Creates a Slides API service object and logs the number of slides and\n * elements in a sample presentation:\n * https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit\n */\nfunction logSlidesAndElements() {\n  const presentationId = \"1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc\";\n  try {\n    // Gets the specified presentation using presentationId\n    const presentation = Slides.Presentations.get(presentationId);\n    const slides = presentation.slides;\n    // Print the number of slides and elements in presentation\n    console.log(\"The presentation contains %s slides:\", slides.length);\n    for (let i = 0; i < slides.length; i++) {\n      console.log(\n        \"- Slide # %s contains %s elements.\",\n        i + 1,\n        slides[i].pageElements.length,\n      );\n    }\n  } catch (err) {\n    // TODO (developer) - Handle  Presentation.get() exception from Slides API\n    console.log(\"Failed to found Presentation with error %s\", err.message);\n  }\n}\n// [END slides_quickstart]\n"
  },
  {
    "path": "slides/selection/selection.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_slides_get_selection]\nconst selection = SlidesApp.getActivePresentation().getSelection();\n// [END apps_script_slides_get_selection]\n\n// [START apps_script_slides_get_current_page]\nconst currentPage = SlidesApp.getActivePresentation()\n  .getSelection()\n  .getCurrentPage();\n// [END apps_script_slides_get_current_page]\n\n/**\n * Selection type to read the current selection in a type-appropriate way.\n */\nfunction slidesSelectionTypes() {\n  // [START apps_script_slides_selection_type]\n  const selection = SlidesApp.getActivePresentation().getSelection();\n  const selectionType = selection.getSelectionType();\n  let currentPage;\n  switch (selectionType) {\n    case SlidesApp.SelectionType.NONE:\n      console.log(\"Nothing selected\");\n      break;\n    case SlidesApp.SelectionType.CURRENT_PAGE:\n      currentPage = selection.getCurrentPage();\n      console.log(`Selection is a page with ID: ${currentPage.getObjectId()}`);\n      break;\n    case SlidesApp.SelectionType.PAGE_ELEMENT: {\n      const pageElements = selection.getPageElementRange().getPageElements();\n      console.log(`There are ${pageElements.length} page elements selected.`);\n      break;\n    }\n    case SlidesApp.SelectionType.TEXT: {\n      const tableCellRange = selection.getTableCellRange();\n      if (tableCellRange !== null) {\n        const tableCell = tableCellRange.getTableCells()[0];\n        console.log(\n          `Selected text is in a table at row ${tableCell.getRowIndex()}, column ${tableCell.getColumnIndex()}`,\n        );\n      }\n      const textRange = selection.getTextRange();\n      if (textRange.getStartIndex() === textRange.getEndIndex()) {\n        console.log(`Text cursor position: ${textRange.getStartIndex()}`);\n      } else {\n        console.log(\n          `Selection is a text range from: ${textRange.getStartIndex()} to: ${textRange.getEndIndex()} is selected`,\n        );\n      }\n      break;\n    }\n    case SlidesApp.SelectionType.TABLE_CELL: {\n      const tableCells = selection.getTableCellRange().getTableCells();\n      const table = tableCells[0].getParentTable();\n      console.log(`There are ${tableCells.length} table cells selected.`);\n      break;\n    }\n    case SlidesApp.SelectionType.PAGE: {\n      const pages = selection.getPageRange().getPages();\n      console.log(`There are ${pages.length} pages selected.`);\n      break;\n    }\n    default:\n      break;\n  }\n  // [END apps_script_slides_selection_type]\n}\n/**\n * Selecting the current page\n */\nfunction slideSelect() {\n  // [START apps_script_slides_select]\n  // Select the first slide as the current page selection and remove any previous selection.\n  const selection = SlidesApp.getActivePresentation().getSelection();\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  slide.selectAsCurrentPage();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.CURRENT_PAGE\n  // selection.getCurrentPage() = slide\n  //\n  // [END apps_script_slides_select]\n}\n/**\n * Selecting a page element.\n */\nfunction selectPageElement() {\n  // [START apps_script_slides_select_page_element]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const pageElement = slide.getPageElements()[0];\n  // Only select this page element and remove any previous selection.\n  pageElement.select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = pageElement\n  //\n  // [END apps_script_slides_select_page_element]\n}\n/**\n * Selecting multiple page elements\n */\nfunction selectMultiplePageElement() {\n  // [START apps_script_slides_select_multiple_page_elements]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  // First select the slide page, as the current page selection.\n  slide.selectAsCurrentPage();\n  // Then select all the page elements in the selected slide page.\n  const pageElements = slide.getPageElements();\n  for (let i = 0; i < pageElements.length; i++) {\n    pageElements[i].select(false);\n  }\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements() = pageElements\n  //\n  // [END apps_script_slides_select_multiple_page_elements]\n}\n/**\n *This shows how selection can be transformed by manipulating\n * selected page elements.\n */\nfunction slideTransformSelection() {\n  // [START apps_script_slides_transform_selection]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const shape1 = slide.getPageElements()[0].asShape();\n  const shape2 = slide.getPageElements()[1].asShape();\n  // Select both the shapes.\n  shape1.select();\n  shape2.select(false);\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements() = [shape1, shape2]\n  //\n  // Remove one shape.\n  shape2.remove();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements() = [shape1]\n  //\n  // [END apps_script_slides_transform_selection]\n}\n/**\n * Range selection within text contained in a shape.\n */\nfunction slidesRangeSelection() {\n  // [START apps_script_slides_range_selection_in_shape]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const shape = slide.getPageElements()[0].asShape();\n  shape.getText().setText(\"Hello\");\n  // Range selection: Select the text range 'He'.\n  shape.getText().getRange(0, 2).select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = shape\n  // selection.getTextRange().getStartIndex() = 0\n  // selection.getTextRange().getEndIndex() = 2\n  //\n  // [END apps_script_slides_range_selection_in_shape]\n}\n/**\n * Cursor selection within text contained in a shape.\n */\nfunction slidesCursorSelection() {\n  // [START apps_script_slides_cursor_selection_in_shape]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const shape = slide.getPageElements()[0].asShape();\n  shape.getText().setText(\"Hello\");\n  // Cursor selection: Place the cursor after 'H' like 'H|ello'.\n  shape.getText().getRange(1, 1).select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = shape\n  // selection.getTextRange().getStartIndex() = 1\n  // selection.getTextRange().getEndIndex() = 1\n  //\n  // [END apps_script_slides_cursor_selection_in_shape]\n}\n/**\n * Range selection in table cell.\n */\nfunction slideRangeSelection() {\n  // [START apps_script_slides_range_selection_in_table]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const table = slide.getPageElements()[0].asTable();\n  const tableCell = table.getCell(0, 1);\n  tableCell.getText().setText(\"Hello\");\n  // Range selection: Select the text range 'He'.\n  tableCell.getText().getRange(0, 2).select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = table\n  // selection.getTableCellRange().getTableCells()[0] = tableCell\n  // selection.getTextRange().getStartIndex() = 0\n  // selection.getTextRange().getEndIndex() = 2\n  //\n  // [END apps_script_slides_range_selection_in_table]\n}\n/**\n * Cursor selection in table cell.\n */\nfunction cursorSelection() {\n  // [START apps_script_slides_cursor_selection_in_table]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const table = slide.getPageElements()[0].asTable();\n  const tableCell = table.getCell(0, 1);\n  tableCell.getText().setText(\"Hello\");\n  // Cursor selection: Place the cursor after 'H' like 'H|ello'.\n  tableCell.getText().getRange(1, 1).select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = table\n  // selection.getTableCellRange().getTableCells()[0] = tableCell\n  // selection.getTextRange().getStartIndex() = 1\n  // selection.getTextRange().getEndIndex() = 1\n  //\n  // [END apps_script_slides_cursor_selection_in_table]\n}\n/**\n * This shows how the selection can be transformed by editing the selected text.\n */\nfunction selectTransformation() {\n  // [START apps_script_slides_selection_transformation]\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  const shape = slide.getPageElements()[0].asShape();\n  const textRange = shape.getText();\n  textRange.setText(\"World\");\n  // Select all the text 'World'.\n  textRange.select();\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = shape\n  // selection.getTextRange().getStartIndex() = 0\n  // selection.getTextRange().getEndIndex() = 6\n  //\n  // Add some text to the shape, and the selection will be transformed.\n  textRange.insertText(0, \"Hello \");\n\n  // State of selection\n  //\n  // selection.getSelectionType() = SlidesApp.SelectionType.TEXT\n  // selection.getCurrentPage() = slide\n  // selection.getPageElementRange().getPageElements()[0] = shape\n  // selection.getTextRange().getStartIndex() = 0\n  // selection.getTextRange().getEndIndex() = 12\n  //\n  // [END apps_script_slides_selection_transformation]\n}\n/**\n * The following example shows how to unselect any current selections on a page\n * by setting that page as the current page.\n */\nfunction slidesUnselectingCurrentPage() {\n  // [START apps_script_slides_unselecting]\n  // Unselect one or more page elements already selected.\n  //\n  // In case one or more page elements in the first slide are selected, setting the\n  // same (or any other) slide page as the current page would do the unselect.\n  //\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  slide.selectAsCurrentPage();\n  // [END apps_script_slides_unselecting]\n}\n/**\n * The following example shows how to unselect any current selections on a page\n * by selecting one page element, thus removing all other items from the selection.\n */\nfunction slideUnselectingPageElements() {\n  // [START apps_script_slides_selecting]\n  // Unselect one or more page elements already selected.\n  //\n  // In case one or more page elements in the first slide are selected,\n  // selecting any pageElement in the first slide (or any other pageElement) would\n  // do the unselect and select that pageElement.\n  //\n  const slide = SlidesApp.getActivePresentation().getSlides()[0];\n  slide.getPageElements()[0].select();\n  // [END apps_script_slides_selecting]\n}\n"
  },
  {
    "path": "slides/style/style.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nfunction setTextHelloWorld() {\n  // [START apps_script_slides_hello]\n  try {\n    // Get the first slide of active presentation\n    const slide = SlidesApp.getActivePresentation().getSlides()[0];\n    // Insert shape in the slide with dimensions\n    const shape = slide.insertShape(\n      SlidesApp.ShapeType.TEXT_BOX,\n      100,\n      200,\n      300,\n      60,\n    );\n    const textRange = shape.getText();\n    // Set text in TEXT_BOX\n    textRange.setText(\"Hello world!\");\n    console.log(\n      `Start: ${textRange.getStartIndex()}; End: ${textRange.getEndIndex()}; Content: ${textRange.asString()}`,\n    );\n    const subRange = textRange.getRange(0, 5);\n    console.log(\n      `Sub-range Start: ${subRange.getStartIndex()}; Sub-range End: ${subRange.getEndIndex()}; Sub-range Content: ${subRange.asString()}`,\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with an error %s \", err.message);\n  }\n  // [END apps_script_slides_hello]\n}\n/**\n * Insert Text in shape.\n */\nfunction insertText() {\n  // [START apps_script_slides_insert_text]\n  try {\n    // Get the first slide of active presentation\n    const slide = SlidesApp.getActivePresentation().getSlides()[0];\n    // Insert shape in the slide with dimensions\n    const shape = slide.insertShape(\n      SlidesApp.ShapeType.TEXT_BOX,\n      100,\n      200,\n      300,\n      60,\n    );\n    const textRange = shape.getText();\n    textRange.setText(\"Hello world!\");\n    textRange.clear(6, 11);\n    // Insert text in TEXT_BOX\n    textRange.insertText(6, \"galaxy\");\n    console.log(\n      `Start: ${textRange.getStartIndex()}; End: ${textRange.getEndIndex()}; Content: ${textRange.asString()}`,\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with an error %s \", err.message);\n  }\n  // [END apps_script_slides_insert_text]\n}\n/**\n * Style the text\n */\nfunction styleText() {\n  // [START apps_script_slides_style_text]\n  try {\n    // Get the first slide of active presentation\n    const slide = SlidesApp.getActivePresentation().getSlides()[0];\n    // Insert shape in the slide with dimensions\n    const shape = slide.insertShape(\n      SlidesApp.ShapeType.TEXT_BOX,\n      100,\n      200,\n      300,\n      60,\n    );\n    const textRange = shape.getText();\n    // Set text in TEXT_BOX\n    textRange.setText(\"Hello \");\n    // Append text in TEXT_BOX\n    const insertedText = textRange.appendText(\"world!\");\n    // Style the text with url,bold\n    insertedText\n      .getTextStyle()\n      .setBold(true)\n      .setLinkUrl(\"www.example.com\")\n      .setForegroundColor(\"#ff0000\");\n    const helloRange = textRange.getRange(0, 5);\n    console.log(\n      `Text: ${helloRange.asString()}; Bold: ${helloRange.getTextStyle().isBold()}`,\n    );\n    console.log(\n      `Text: ${insertedText.asString()}; Bold: ${insertedText.getTextStyle().isBold()}`,\n    );\n    console.log(\n      `Text: ${textRange.asString()}; Bold: ${textRange.getTextStyle().isBold()}`,\n    );\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with an error %s \", err.message);\n  }\n  // [END apps_script_slides_style_text]\n}\n\n/**\n * Style the paragraph\n */\nfunction paragraphStyling() {\n  // [START apps_script_slides_paragraph_styling]\n  try {\n    // Get the first slide of active presentation\n    const slide = SlidesApp.getActivePresentation().getSlides()[0];\n    // Insert shape in the slide with dimensions\n    const shape = slide.insertShape(\n      SlidesApp.ShapeType.TEXT_BOX,\n      50,\n      50,\n      300,\n      300,\n    );\n    const textRange = shape.getText();\n    // Set the text in the shape/TEXT_BOX\n    textRange.setText(\"Paragraph 1\\nParagraph2\\nParagraph 3\\nParagraph 4\");\n    const paragraphs = textRange.getParagraphs();\n    // Style the paragraph alignment center.\n    for (let i = 0; i <= 3; i++) {\n      const paragraphStyle = paragraphs[i].getRange().getParagraphStyle();\n      paragraphStyle.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with an error %s \", err.message);\n  }\n  // [END apps_script_slides_paragraph_styling]\n}\n/**\n * Style the list\n */\nfunction listStyling() {\n  // [START apps_script_slides_list_styling]\n  try {\n    // Get the first slide of active presentation\n    const slide = SlidesApp.getActivePresentation().getSlides()[0];\n    // Insert shape in the slide with dimensions\n    const shape = slide.insertShape(\n      SlidesApp.ShapeType.TEXT_BOX,\n      50,\n      50,\n      300,\n      300,\n    );\n    // Add and style the list\n    const textRange = shape.getText();\n    textRange\n      .appendText(\"Item 1\\n\")\n      .appendText(\"\\tItem 2\\n\")\n      .appendText(\"\\t\\tItem 3\\n\")\n      .appendText(\"Item 4\");\n    // Preset patterns of glyphs for lists in text.\n    textRange\n      .getListStyle()\n      .applyListPreset(SlidesApp.ListPreset.DIGIT_ALPHA_ROMAN);\n    const paragraphs = textRange.getParagraphs();\n    for (let i = 0; i < paragraphs.length; i++) {\n      const listStyle = paragraphs[i].getRange().getListStyle();\n      console.log(\n        `Paragraph ${i + 1}'s nesting level: ${listStyle.getNestingLevel()}`,\n      );\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with an error %s \", err.message);\n  }\n  // [END apps_script_slides_list_styling]\n}\n"
  },
  {
    "path": "slides/style/test_style.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * runs all the test\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> itShouldSetTextHelloWorld\");\n  setTextHelloWorld();\n  console.log(\"> itShouldInsertText\");\n  insertText();\n  console.log(\"> itShouldStyleText\");\n  styleText();\n  console.log(\"> itShouldStyleParagraph\");\n  paragraphStyling();\n  console.log(\"> itShouldListStyling\");\n  listStyling();\n}\n"
  },
  {
    "path": "slides/translate/sidebar.html",
    "content": "<!--\nCopyright 2018 Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START apps_script_slides_translate_quickstart] -->\n<html>\n<head>\n  <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n  <style>\n    .logo { vertical-align: middle; }\n    ul { list-style-type: none; padding: 0; }\n    h4 { margin: 0; }\n  </style>\n</head>\n<body>\n<form class=\"sidebar branding-below\">\n  <h4>Translate selected slides into:</h4>\n  <ul id=\"languages\"></ul>\n  <div class=\"block\" id=\"button-bar\">\n    <button class=\"blue\" id=\"run-translation\">Translate</button>\n  </div>\n  <h5 class=\"error\" id=\"error\"></h5>\n</form>\n<div class=\"sidebar bottom\">\n  <img alt=\"Add-on logo\" class=\"logo\"\n       src=\"https://www.gstatic.com/images/branding/product/1x/translate_48dp.png\" width=\"27\" height=\"27\">\n  <span class=\"gray branding-text\">Translate sample by Google</span>\n</div>\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  $(function() {\n    // Add an input radio button for every language.\n    const languages = {\n      ar: 'Arabic',\n      zh: 'Chinese',\n      en: 'English',\n      fr: 'French',\n      de: 'German',\n      hi: 'Hindi',\n      ja: 'Japanese',\n      pt: 'Portuguese',\n      es: 'Spanish'\n    };\n    const languageList = Object.keys(languages).map((id)=> {\n      return $('<li>').html([\n        $('<input>')\n                .attr('type', 'radio')\n                .attr('name', 'dest')\n                .attr('id', 'radio-dest-' + id)\n                .attr('value', id),\n        $('<label>')\n                .attr('for', 'radio-dest-' + id)\n                .html(languages[id])\n      ]);\n    });\n\n    $('#run-translation').click(runTranslation);\n    $('#languages').html(languageList);\n  });\n\n  /**\n   * Runs a server-side function to translate the text on all slides.\n   */\n  function runTranslation() {\n    this.disabled = true;\n    $('#error').text('');\n    google.script.run\n            .withSuccessHandler((numTranslatedElements, element) =>{\n              element.disabled = false;\n              if (numTranslatedElements === 0) {\n                $('#error').empty()\n                        .append('Did you select elements to translate?')\n                        .append('<br/>')\n                        .append('Please select slides or individual elements.');\n              }\n              return false;\n            })\n            .withFailureHandler((msg, element)=> {\n              element.disabled = false;\n              $('#error').text('Something went wrong. Please check the add-on logs.');\n              return false;\n            })\n            .withUserObject(this)\n            .translateSelectedElements($('input[name=dest]:checked').val());\n  }\n</script>\n</body>\n</html>\n<!-- [END apps_script_slides_translate_quickstart] -->\n"
  },
  {
    "path": "slides/translate/translate.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_slides_translate_quickstart]\n/**\n * @OnlyCurrentDoc Limits the script to only accessing the current presentation.\n */\n\n/**\n * Create a open translate menu item.\n * @param {Event} event The open event.\n */\nfunction onOpen(event) {\n  SlidesApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Open Translate\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Open the Add-on upon install.\n * @param {Event} event The install event.\n */\nfunction onInstall(event) {\n  onOpen(event);\n}\n\n/**\n * Opens a sidebar in the document containing the add-on's user interface.\n */\nfunction showSidebar() {\n  const ui =\n    HtmlService.createHtmlOutputFromFile(\"sidebar\").setTitle(\"Translate\");\n  SlidesApp.getUi().showSidebar(ui);\n}\n\n/**\n * Recursively gets child text elements a list of elements.\n * @param {PageElement[]} elements The elements to get text from.\n * @return {Text[]} An array of text elements.\n */\nfunction getElementTexts(elements) {\n  let texts = [];\n  for (const element of elements) {\n    switch (element.getPageElementType()) {\n      case SlidesApp.PageElementType.GROUP:\n        for (const child of element.asGroup().getChildren()) {\n          texts = texts.concat(getElementTexts(child));\n        }\n        break;\n      case SlidesApp.PageElementType.TABLE: {\n        const table = element.asTable();\n        for (let r = 0; r < table.getNumRows(); ++r) {\n          for (let c = 0; c < table.getNumColumns(); ++c) {\n            texts.push(table.getCell(r, c).getText());\n          }\n        }\n        break;\n      }\n      case SlidesApp.PageElementType.SHAPE:\n        texts.push(element.asShape().getText());\n        break;\n    }\n  }\n  return texts;\n}\n\n/**\n * Translates selected slide elements to the target language using Apps Script's Language service.\n *\n * @param {string} targetLanguage The two-letter short form for the target language. (ISO 639-1)\n * @return {number} The number of elements translated.\n */\nfunction translateSelectedElements(targetLanguage) {\n  // Get selected elements.\n  const selection = SlidesApp.getActivePresentation().getSelection();\n  const selectionType = selection.getSelectionType();\n  let texts = [];\n  switch (selectionType) {\n    case SlidesApp.SelectionType.PAGE:\n      for (const page of selection.getPageRange().getPages()) {\n        texts = texts.concat(getElementTexts(page.getPageElements()));\n      }\n      break;\n    case SlidesApp.SelectionType.PAGE_ELEMENT: {\n      const pageElements = selection.getPageElementRange().getPageElements();\n      texts = texts.concat(getElementTexts(pageElements));\n      break;\n    }\n    case SlidesApp.SelectionType.TABLE_CELL:\n      for (const cell of selection.getTableCellRange().getTableCells()) {\n        texts.push(cell.getText());\n      }\n      break;\n    case SlidesApp.SelectionType.TEXT:\n      for (const element of selection.getPageElementRange().getPageElements()) {\n        texts.push(element.asShape().getText());\n      }\n      break;\n  }\n\n  // Translate all elements in-place.\n  for (const text of texts) {\n    text.setText(\n      LanguageApp.translate(text.asRenderedString(), \"\", targetLanguage),\n    );\n  }\n\n  return texts.length;\n}\n// [END apps_script_slides_translate_quickstart]\n"
  },
  {
    "path": "solutions/add-on/book-smartchip/.clasp.json",
    "content": "{ \"scriptId\": \"14tK6PD4C624ivRyGk-S6eYCbYJnDfA24xeP0Jhb1U8sPgAvZXeZm5gpb\" }\n"
  },
  {
    "path": "solutions/add-on/book-smartchip/Code.js",
    "content": "/**\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction getBook(id) {\n  const apiKey = \"YOUR_API_KEY\"; // Replace with your API key\n  const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`;\n  const response = UrlFetchApp.fetch(apiEndpoint);\n  return JSON.parse(response);\n}\n\nfunction bookLinkPreview(event) {\n  if (event.docs.matchedUrl.url) {\n    const segments = event.docs.matchedUrl.url.split(\"/\");\n    const volumeID = segments[segments.length - 1];\n\n    const bookData = getBook(volumeID);\n    const bookTitle = bookData.volumeInfo.title;\n    const bookDescription = bookData.volumeInfo.description;\n    const bookImage = bookData.volumeInfo.imageLinks.small;\n    const bookAuthors = bookData.volumeInfo.authors;\n    const bookPageCount = bookData.volumeInfo.pageCount;\n\n    const previewHeader = CardService.newCardHeader()\n      .setSubtitle(`By ${bookAuthors}`)\n      .setTitle(bookTitle);\n\n    const previewPages = CardService.newDecoratedText()\n      .setTopLabel(\"Page count\")\n      .setText(bookPageCount);\n\n    const previewDescription = CardService.newDecoratedText()\n      .setTopLabel(\"About this book\")\n      .setText(bookDescription)\n      .setWrapText(true);\n\n    const previewImage = CardService.newImage()\n      .setAltText(\"Image of book cover\")\n      .setImageUrl(bookImage);\n\n    const buttonBook = CardService.newTextButton()\n      .setText(\"View book\")\n      .setOpenLink(CardService.newOpenLink().setUrl(event.docs.matchedUrl.url));\n\n    const cardSectionBook = CardService.newCardSection()\n      .addWidget(previewImage)\n      .addWidget(previewPages)\n      .addWidget(CardService.newDivider())\n      .addWidget(previewDescription)\n      .addWidget(buttonBook);\n\n    return CardService.newCardBuilder()\n      .setHeader(previewHeader)\n      .addSection(cardSectionBook)\n      .build();\n  }\n}\n"
  },
  {
    "path": "solutions/add-on/book-smartchip/README.md",
    "content": "# Preview links from Google Books with smart chips\n\nSee \nhttps://developers.google.com/workspace/add-ons/samples/preview-links-google-books\nfor additional details.\n"
  },
  {
    "path": "solutions/add-on/book-smartchip/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/workspace.linkpreview\",\n    \"https://www.googleapis.com/auth/script.external_request\"\n  ],\n  \"urlFetchWhitelist\": [\"https://www.googleapis.com/books/v1/volumes/\"],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Preview Books Add-on\",\n      \"logoUrl\": \"https://developers.google.com/workspace/add-ons/images/library-icon.png\",\n      \"layoutProperties\": {\n        \"primaryColor\": \"#dd4b39\"\n      }\n    },\n    \"docs\": {\n      \"linkPreviewTriggers\": [\n        {\n          \"runFunction\": \"bookLinkPreview\",\n          \"patterns\": [\n            {\n              \"hostPattern\": \"*.google.*\",\n              \"pathPrefix\": \"books\"\n            },\n            {\n              \"hostPattern\": \"*.google.*\",\n              \"pathPrefix\": \"books/edition\"\n            }\n          ],\n          \"labelText\": \"Book\",\n          \"logoUrl\": \"https://developers.google.com/workspace/add-ons/images/book-icon.png\",\n          \"localizedLabelText\": {\n            \"es\": \"Libros\"\n          }\n        }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "solutions/add-on/share-macro/.clasp.json",
    "content": "{ \"scriptId\": \"1BsbWOAbLADGoLtp5P9oqctZMiqT5EFh_R-CufxAV9y1hvVSAMO35Azu9\" }\n"
  },
  {
    "path": "solutions/add-on/share-macro/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.devsite.corp.google.com/apps-script/add-ons/share-macro\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Uses Apps Script API to copy source Apps Script project\n * to destination Google Spreadsheet container.\n *\n * @param {string} sourceScriptId - Script ID of the source project.\n * @param {string} targetSpreadsheetUrl - URL if the target spreadsheet.\n */\nfunction shareMacro_(sourceScriptId, targetSpreadsheetUrl) {\n  // Gets the source project content using the Apps Script API.\n  const sourceProject = APPS_SCRIPT_API.get(sourceScriptId);\n  const sourceFiles = APPS_SCRIPT_API.getContent(sourceScriptId);\n\n  // Opens the target spreadsheet and gets its ID.\n  const parentSSId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId();\n\n  // Creates an Apps Script project that's bound to the target spreadsheet.\n  const targetProjectObj = APPS_SCRIPT_API.create(\n    sourceProject.title,\n    parentSSId,\n  );\n\n  // Updates the Apps Script project with the source project content.\n  APPS_SCRIPT_API.updateContent(targetProjectObj.scriptId, sourceFiles);\n}\n\n/**\n * Function that encapsulates Apps Script API project manipulation.\n */\nconst APPS_SCRIPT_API = {\n  accessToken: ScriptApp.getOAuthToken(),\n\n  /* APPS_SCRIPT_API.get\n   * Gets Apps Script source project.\n   * @param {string} scriptId - Script ID of the source project.\n   * @return {Object} - JSON representation of source project.\n   */\n  get: function (scriptId) {\n    const url = `https://script.googleapis.com/v1/projects/${scriptId}`;\n    const options = {\n      method: \"get\",\n      headers: {\n        Authorization: `Bearer ${this.accessToken}`,\n      },\n      muteHttpExceptions: true,\n    };\n    const res = UrlFetchApp.fetch(url, options);\n    if (res.getResponseCode() === 200) {\n      return JSON.parse(res);\n    }\n    console.log(\"An error occurred gettting the project details\");\n    console.log(res.getResponseCode());\n    console.log(res.getContentText());\n    console.log(res);\n    return false;\n  },\n\n  /* APPS_SCRIPT_API.create\n   * Creates new Apps Script project in the target spreadsheet.\n   * @param {string} title - Name of Apps Script project.\n   * @param {string} parentId - Internal ID of target spreadsheet.\n   * @return {Object} - JSON representation completed project creation.\n   */\n  create: function (title, parentId) {\n    const url = \"https://script.googleapis.com/v1/projects\";\n    const options = {\n      headers: {\n        Authorization: `Bearer ${this.accessToken}`,\n        \"Content-Type\": \"application/json\",\n      },\n      muteHttpExceptions: true,\n      method: \"POST\",\n      payload: { title: title },\n    };\n    if (parentId) {\n      options.payload.parentId = parentId;\n    }\n    options.payload = JSON.stringify(options.payload);\n    let res = UrlFetchApp.fetch(url, options);\n    if (res.getResponseCode() === 200) {\n      res = JSON.parse(res);\n      return res;\n    }\n    console.log(\"An error occurred while creating the project\");\n    console.log(res.getResponseCode());\n    console.log(res.getContentText());\n    console.log(res);\n    return false;\n  },\n  /* APPS_SCRIPT_API.getContent\n   * Gets the content of the source Apps Script project.\n   * @param {string} scriptId - Script ID of the source project.\n   * @return {Object} - JSON representation of Apps Script project content.\n   */\n  getContent: function (scriptId) {\n    const url = `https://script.googleapis.com/v1/projects/${scriptId}/content`;\n    const options = {\n      method: \"get\",\n      headers: {\n        Authorization: `Bearer ${this.accessToken}`,\n      },\n      muteHttpExceptions: true,\n    };\n    let res = UrlFetchApp.fetch(url, options);\n    if (res.getResponseCode() === 200) {\n      res = JSON.parse(res);\n      return res.files;\n    }\n    console.log(\n      \"An error occurred obtaining the content from the source script\",\n    );\n    console.log(res.getResponseCode());\n    console.log(res.getContentText());\n    console.log(res);\n    return false;\n  },\n\n  /* APPS_SCRIPT_API.updateContent\n   * Updates (copies) content from source to target Apps Script project.\n   * @param {string} scriptId - Script ID of the source project.\n   * @param {Object} files - JSON representation of Apps Script project content.\n   * @return {boolean} - Result status of the function.\n   */\n  updateContent: function (scriptId, files) {\n    const url = `https://script.googleapis.com/v1/projects/${scriptId}/content`;\n    const options = {\n      method: \"put\",\n      headers: {\n        Authorization: `Bearer ${this.accessToken}`,\n      },\n      contentType: \"application/json\",\n      payload: JSON.stringify({ files: files }),\n      muteHttpExceptions: true,\n    };\n    const res = UrlFetchApp.fetch(url, options);\n    if (res.getResponseCode() === 200) {\n      return true;\n    }\n    console.log(`An error occurred updating content of script ${scriptId}`);\n    console.log(res.getResponseCode());\n    console.log(res.getContentText());\n    console.log(res);\n    return false;\n  },\n};\n"
  },
  {
    "path": "solutions/add-on/share-macro/README.md",
    "content": "# Copy macros to other spreadsheets\n\nSee [developers.google.com](https://developers.google.com/apps-script/add-ons/share-macro) for additional details.\n\n"
  },
  {
    "path": "solutions/add-on/share-macro/UI.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Change application logo here (and in manifest) as desired.\nconst ADDON_LOGO =\n  \"https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png\";\n\n/**\n * Callback function for rendering the main card.\n * @return {CardService.Card} The card to show the user.\n */\nfunction onHomepage(e) {\n  return createSelectionCard(e);\n}\n\n/**\n * Builds the primary card interface used to collect user inputs.\n *\n * @param {Object} e - Add-on event object.\n * @param {string} sourceScriptId - Script ID of the source project.\n * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet.\n * @param {string[]} errors - Array of error messages.\n *\n * @return {CardService.Card} The card to show to the user for inputs.\n */\nfunction createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) {\n  // Configures card header.\n  const cardHeader = CardService.newCardHeader()\n    .setTitle(\"Share macros with other spreadheets!\")\n    .setImageUrl(ADDON_LOGO)\n    .setImageStyle(CardService.ImageStyle.SQUARE);\n\n  // If form errors exist, configures section with error messages.\n  let showErrors = false;\n\n  if (errors?.length) {\n    showErrors = true;\n    let msg = errors.reduce((str, err) => `${str}• ${err}<br>`, \"\");\n    msg = `<b>Form submission errors:</b><br><font color=\"#ba0000\">${msg}</font>`;\n\n    // Builds error message section.\n    sectionErrors = CardService.newCardSection().addWidget(\n      CardService.newDecoratedText().setText(msg).setWrapText(true),\n    );\n  }\n\n  // Configures source project section.\n  const sectionSource = CardService.newCardSection()\n    .addWidget(\n      CardService.newDecoratedText().setText(\n        \"<b>Source macro</b><br>The Apps Script project to copy\",\n      ),\n    )\n\n    .addWidget(\n      CardService.newTextInput()\n        .setFieldName(\"sourceScriptId\")\n        .setValue(sourceScriptId || \"\")\n        .setTitle(\"Script ID of the source macro\")\n        .setHint(\n          \"You must have at least edit permission for the source spreadsheet to access its script project\",\n        ),\n    )\n\n    .addWidget(\n      CardService.newTextButton()\n        .setText(\"Find the script ID\")\n        .setOpenLink(\n          CardService.newOpenLink()\n            .setUrl(\n              \"https://developers.google.com/apps-script/api/samples/execute\",\n            )\n            .setOpenAs(CardService.OpenAs.FULL_SIZE)\n            .setOnClose(CardService.OnClose.NOTHING),\n        ),\n    );\n\n  // Configures target spreadsheet section.\n  const sectionTarget = CardService.newCardSection()\n    .addWidget(\n      CardService.newDecoratedText().setText(\"<b>Target spreadsheet</b>\"),\n    )\n\n    .addWidget(\n      CardService.newTextInput()\n        .setFieldName(\"targetSpreadsheetUrl\")\n        .setValue(targetSpreadsheetUrl || \"\")\n        .setHint(\n          \"You must have at least edit permission for the target spreadsheet\",\n        )\n        .setTitle(\"Target spreadsheet URL\"),\n    );\n\n  // Configures help section.\n  const sectionHelp = CardService.newCardSection()\n    .addWidget(\n      CardService.newDecoratedText()\n        .setText(\n          \"<b><font color=#c80000>NOTE: </font></b>\" +\n            \"The Apps Script API must be turned on.\",\n        )\n        .setWrapText(true),\n    )\n\n    .addWidget(\n      CardService.newTextButton()\n        .setText(\"Turn on Apps Script API\")\n        .setOpenLink(\n          CardService.newOpenLink()\n            .setUrl(\"https://script.google.com/home/usersettings\")\n            .setOpenAs(CardService.OpenAs.FULL_SIZE)\n            .setOnClose(CardService.OnClose.NOTHING),\n        ),\n    );\n\n  // Configures card footer with action to copy the macro.\n  const cardFooter = CardService.newFixedFooter().setPrimaryButton(\n    CardService.newTextButton()\n      .setText(\"Share macro\")\n      .setOnClickAction(\n        CardService.newAction().setFunctionName(\"onClickFunction_\"),\n      ),\n  );\n\n  // Begins building the card.\n  const builder = CardService.newCardBuilder().setHeader(cardHeader);\n\n  // Adds error section if applicable.\n  if (showErrors) {\n    builder.addSection(sectionErrors);\n  }\n\n  // Adds final sections & footer.\n  builder\n    .addSection(sectionSource)\n    .addSection(sectionTarget)\n    .addSection(sectionHelp)\n    .setFixedFooter(cardFooter);\n\n  return builder.build();\n}\n\n/**\n * Action handler that validates user inputs and calls shareMacro_\n * function to copy Apps Script project to target spreadsheet.\n *\n * @param {Object} e - Add-on event object.\n *\n * @return {CardService.Card} Responds with either a success or error card.\n */\nfunction onClickFunction_(e) {\n  const sourceScriptId = e.formInput.sourceScriptId;\n  const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl;\n\n  // Validates inputs for errors.\n  const errors = [];\n\n  // Pushes an error message if the Script ID parameter is missing.\n  if (!sourceScriptId) {\n    errors.push(\"Missing script ID\");\n  } else {\n    // Gets the Apps Script project if the Script ID parameter is valid.\n    const sourceProject = APPS_SCRIPT_API.get(sourceScriptId);\n    if (!sourceProject) {\n      // Pushes an error message if the Script ID parameter isn't valid.\n      errors.push(\"Invalid script ID\");\n    }\n  }\n\n  // Pushes an error message if the spreadsheet URL is missing.\n  if (!targetSpreadsheetUrl) {\n    errors.push(\"Missing Spreadsheet URL\");\n  } else\n    try {\n      // Tests for valid spreadsheet URL to get the spreadsheet ID.\n      const ssId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId();\n    } catch (err) {\n      // Pushes an error message if the spreadsheet URL parameter isn't valid.\n      errors.push(\"Invalid spreadsheet URL\");\n    }\n\n  if (errors?.length) {\n    // Redisplays form if inputs are missing or invalid.\n    return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors);\n  }\n  // Calls shareMacro function to copy the project.\n  shareMacro_(sourceScriptId, targetSpreadsheetUrl);\n\n  // Creates a success card to display to users.\n  return buildSuccessCard(e, targetSpreadsheetUrl);\n}\n\n/**\n * Builds success card to inform user & let them open the spreadsheet.\n *\n * @param {Object} e - Add-on event object.\n * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet.\n *\n * @return {CardService.Card} Returns success card.\n */ function buildSuccessCard(e, targetSpreadsheetUrl) {\n  // Configures card header.\n  const cardHeader = CardService.newCardHeader()\n    .setTitle(\"Share macros with other spreadsheets!\")\n    .setImageUrl(ADDON_LOGO)\n    .setImageStyle(CardService.ImageStyle.SQUARE);\n\n  // Configures card body section with success message and open button.\n  const sectionBody1 = CardService.newCardSection()\n    .addWidget(\n      CardService.newTextParagraph().setText(\"Sharing process is complete!\"),\n    )\n    .addWidget(\n      CardService.newTextButton()\n        .setText(\"Open spreadsheet\")\n        .setOpenLink(\n          CardService.newOpenLink()\n            .setUrl(targetSpreadsheetUrl)\n            .setOpenAs(CardService.OpenAs.FULL_SIZE)\n            .setOnClose(CardService.OnClose.RELOAD_ADD_ON),\n        ),\n    );\n  const sectionBody2 = CardService.newCardSection()\n    .addWidget(\n      CardService.newTextParagraph().setText(\n        \"If you don't see the copied project in your target spreadsheet,\" +\n          \" make sure you turned on the Apps Script API in the Apps Script dashboard.\",\n      ),\n    )\n    .addWidget(\n      CardService.newTextButton()\n        .setText(\"Check API\")\n        .setOpenLink(\n          CardService.newOpenLink()\n            .setUrl(\"https://script.google.com/home/usersettings\")\n            .setOpenAs(CardService.OpenAs.FULL_SIZE)\n            .setOnClose(CardService.OnClose.RELOAD_ADD_ON),\n        ),\n    );\n\n  // Configures the card footer with action to start new process.\n  const cardFooter = CardService.newFixedFooter().setPrimaryButton(\n    CardService.newTextButton()\n      .setText(\"Share another\")\n      .setOnClickAction(CardService.newAction().setFunctionName(\"onHomepage\")),\n  );\n\n  const builder = CardService.newCardBuilder()\n    .setHeader(cardHeader)\n    .addSection(sectionBody1)\n    .addSection(sectionBody2)\n    .setFixedFooter(cardFooter);\n\n  return builder.build();\n}\n"
  },
  {
    "path": "solutions/add-on/share-macro/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/spreadsheets\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/drive.readonly\",\n    \"https://www.googleapis.com/auth/script.projects\"\n  ],\n  \"urlFetchWhitelist\": [\"https://script.googleapis.com/\"],\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"Share Macro\",\n      \"logoUrl\": \"https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png\",\n      \"layoutProperties\": {\n        \"primaryColor\": \"#188038\",\n        \"secondaryColor\": \"#34a853\"\n      },\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepage\"\n      }\n    },\n    \"sheets\": {}\n  }\n}\n"
  },
  {
    "path": "solutions/automations/agenda-maker/.clasp.json",
    "content": "{ \"scriptId\": \"147xVWUWmw8b010zbiDMIa3eeKATo3P2q5rJCZmY3meirC-yA_XucdZlp\" }\n"
  },
  {
    "path": "solutions/automations/agenda-maker/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/agenda-maker\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Checks if the folder for Agenda docs exists, and creates it if it doesn't.\n *\n * @return {*} Drive folder ID for the app.\n */\nfunction checkFolder() {\n  const folders = DriveApp.getFoldersByName(\"Agenda Maker - App\");\n  // Finds the folder if it exists\n  while (folders.hasNext()) {\n    const folder = folders.next();\n    if (\n      folder.getDescription() ===\n        \"Apps Script App - Do not change this description\" &&\n      folder.getOwner().getEmail() === Session.getActiveUser().getEmail()\n    ) {\n      return folder.getId();\n    }\n  }\n  // If the folder doesn't exist, creates one\n  const folder = DriveApp.createFolder(\"Agenda Maker - App\");\n  folder.setDescription(\"Apps Script App - Do not change this description\");\n  return folder.getId();\n}\n\n/**\n * Finds the template agenda doc, or creates one if it doesn't exist.\n */\nfunction getTemplateId(folderId) {\n  const folder = DriveApp.getFolderById(folderId);\n  const files = folder.getFilesByName(\"Agenda TEMPLATE##\");\n\n  // If there is a file, returns the ID.\n  while (files.hasNext()) {\n    const file = files.next();\n    return file.getId();\n  }\n\n  // Otherwise, creates the agenda template.\n  // You can adjust the default template here\n  const doc = DocumentApp.create(\"Agenda TEMPLATE##\");\n  const body = doc.getBody();\n\n  body\n    .appendParagraph(\"##Attendees##\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1)\n    .editAsText()\n    .setBold(true);\n  body.appendParagraph(\" \").editAsText().setBold(false);\n\n  body\n    .appendParagraph(\"Overview\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1)\n    .editAsText()\n    .setBold(true);\n  body.appendParagraph(\" \");\n  body.appendParagraph(\"- Topic 1: \").editAsText().setBold(true);\n  body.appendParagraph(\" \").editAsText().setBold(false);\n  body.appendParagraph(\"- Topic 2: \").editAsText().setBold(true);\n  body.appendParagraph(\" \").editAsText().setBold(false);\n  body.appendParagraph(\"- Topic 3: \").editAsText().setBold(true);\n  body.appendParagraph(\" \").editAsText().setBold(false);\n\n  body\n    .appendParagraph(\"Next Steps\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1)\n    .editAsText()\n    .setBold(true);\n  body.appendParagraph(\"- Takeaway 1: \").editAsText().setBold(true);\n  body.appendParagraph(\"- Responsible: \").editAsText().setBold(false);\n  body.appendParagraph(\"- Accountable: \");\n  body.appendParagraph(\"- Consult: \");\n  body.appendParagraph(\"- Inform: \");\n  body.appendParagraph(\" \");\n  body.appendParagraph(\"- Takeaway 2: \").editAsText().setBold(true);\n  body.appendParagraph(\"- Responsible: \").editAsText().setBold(false);\n  body.appendParagraph(\"- Accountable: \");\n  body.appendParagraph(\"- Consult: \");\n  body.appendParagraph(\"- Inform: \");\n  body.appendParagraph(\" \");\n  body.appendParagraph(\"- Takeaway 3: \").editAsText().setBold(true);\n  body.appendParagraph(\"- Responsible: \").editAsText().setBold(false);\n  body.appendParagraph(\"- Accountable: \");\n  body.appendParagraph(\"- Consult: \");\n  body.appendParagraph(\"- Inform: \");\n\n  doc.saveAndClose();\n\n  folder.addFile(DriveApp.getFileById(doc.getId()));\n\n  return doc.getId();\n}\n\n/**\n * When there is a change to the calendar, searches for events that include \"#agenda\"\n * in the decrisption.\n *\n */\nfunction onCalendarChange() {\n  // Gets recent events with the #agenda tag\n  const now = new Date();\n  const events = CalendarApp.getEvents(\n    now,\n    new Date(now.getTime() + 2 * 60 * 60 * 1000000),\n    { search: \"#agenda\" },\n  );\n\n  const folderId = checkFolder();\n  const templateId = getTemplateId(folderId);\n\n  const folder = DriveApp.getFolderById(folderId);\n\n  // Loops through any events found\n  for (i = 0; i < events.length; i++) {\n    const event = events[i];\n\n    // Confirms whether the event has the #agenda tag\n    let description = event.getDescription();\n    if (description.search(\"#agenda\") === -1) continue;\n\n    // Only works with events created by the owner of this calendar\n    if (event.isOwnedByMe()) {\n      // Creates a new document from the template for an agenda for this event\n      const newDoc = DriveApp.getFileById(templateId).makeCopy();\n      newDoc.setName(`Agenda for ${event.getTitle()}`);\n\n      const file = DriveApp.getFileById(newDoc.getId());\n      folder.addFile(file);\n\n      const doc = DocumentApp.openById(newDoc.getId());\n      const body = doc.getBody();\n\n      // Fills in the template with information about the attendees from the\n      // calendar event\n      const conf = body.findText(\"##Attendees##\");\n      if (conf) {\n        const ref = conf.getStartOffset();\n\n        for (const i in event.getGuestList()) {\n          const guest = event.getGuestList()[i];\n\n          body.insertParagraph(ref + 2, guest.getEmail());\n        }\n        body.replaceText(\"##Attendees##\", \"Attendees\");\n      }\n\n      // Replaces the tag with a link to the agenda document\n      const agendaUrl = `https://docs.google.com/document/d/${newDoc.getId()}`;\n      description = description.replace(\n        \"#agenda\",\n        `<a href=${agendaUrl}>Agenda Doc</a>`,\n      );\n      event.setDescription(description);\n\n      // Invites attendees to the Google doc so they automatically receive access to the agenda\n      newDoc.addEditor(newDoc.getOwner());\n\n      for (const i in event.getGuestList()) {\n        const guest = event.getGuestList()[i];\n\n        newDoc.addEditor(guest.getEmail());\n      }\n    }\n  }\n  return;\n}\n\n/**\n * Creates an event-driven trigger that fires whenever there's a change to the calendar.\n */\nfunction setUp() {\n  const email = Session.getActiveUser().getEmail();\n  ScriptApp.newTrigger(\"onCalendarChange\")\n    .forUserCalendar(email)\n    .onEventUpdated()\n    .create();\n}\n"
  },
  {
    "path": "solutions/automations/agenda-maker/README.md",
    "content": "# Make an agenda for meetings\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/agenda-maker) for additional details.\n"
  },
  {
    "path": "solutions/automations/agenda-maker/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/.clasp.json",
    "content": "{ \"scriptId\": \"1YGstQLxmTcAQlSHfm0yke12Y2UgT8eVfCxrG_jGpG1dHDmFdOaHQfQZJ\" }\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/aggregate-document-content\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This file containts the main application functions that import data from\n * summary documents into the body of the main document.\n */\n\n// Application constants\nconst APP_TITLE = \"Document summary importer\"; // Application name\nconst PROJECT_FOLDER_NAME = \"Project statuses\"; // Drive folder for the source files.\n\n// Below are the parameters used to identify which content to import from the source documents\n// and which content has already been imported.\nconst FIND_TEXT_KEYWORDS = \"Summary\"; // String that must be found in the heading above the table (case insensitive).\nconst APP_STYLE = DocumentApp.ParagraphHeading.HEADING3; // Style that must be applied to heading above the table.\nconst TEXT_COLOR = \"#2e7d32\"; // Color applied to heading after import to avoid duplication.\n\n/**\n * Updates the main document, importing content from the source files.\n * Uses the above parameters to locate content to be imported.\n *\n * Called from menu option.\n */\nfunction performImport() {\n  // Gets the folder in Drive associated with this application.\n  const folder = getFolderByName_(PROJECT_FOLDER_NAME);\n  // Gets the Google Docs files found in the folder.\n  const files = getFiles(folder);\n\n  // Warns the user if the folder is empty.\n  const ui = DocumentApp.getUi();\n  if (files.length === 0) {\n    const msg = `No files found in the folder '${PROJECT_FOLDER_NAME}'.\n      Run '${MENU.SETUP}' | '${MENU.SAMPLES}' from the menu\n      if you'd like to create samples files.`;\n    ui.alert(APP_TITLE, msg, ui.ButtonSet.OK);\n    return;\n  }\n\n  /** Processes main document */\n  // Gets the active document and body section.\n  const docTarget = DocumentApp.getActiveDocument();\n  const docTargetBody = docTarget.getBody();\n\n  // Appends import summary section to the end of the target document.\n  // Adds a horizontal line and a header with today's date and a title string.\n  docTargetBody.appendHorizontalRule();\n  const dateString = Utilities.formatDate(\n    new Date(),\n    Session.getScriptTimeZone(),\n    \"MMMM dd, yyyy\",\n  );\n  const headingText = `Imported: ${dateString}`;\n  docTargetBody.appendParagraph(headingText).setHeading(APP_STYLE);\n  // Appends a blank paragraph for spacing.\n  docTargetBody.appendParagraph(\" \");\n\n  /** Process source documents */\n  // Iterates through each source document in the folder.\n  // Copies and pastes new updates to the main document.\n  const noContentList = [];\n  let numUpdates = 0;\n  for (const id of files) {\n    // Opens source document; get info and body.\n    const docOpen = DocumentApp.openById(id);\n    const docName = docOpen.getName();\n    const docHtml = docOpen.getUrl();\n    const docBody = docOpen.getBody();\n\n    // Gets summary content from document and returns as object {content:content}\n    const content = getContent(docBody);\n\n    // Logs if document doesn't contain content to be imported.\n    if (!content) {\n      noContentList.push(docName);\n      continue;\n    }\n    numUpdates++;\n    // Inserts content into the main document.\n    // Appends a title/url reference link back to source document.\n    docTargetBody\n      .appendParagraph(\"\")\n      .appendText(`${docName}`)\n      .setLinkUrl(docHtml);\n    // Appends a single-cell table and pastes the content.\n    docTargetBody.appendTable(content);\n    docOpen.saveAndClose();\n  }\n  /** Provides an import summary */\n  docTarget.saveAndClose();\n  let msg = `Number of documents updated: ${numUpdates}`;\n  if (noContentList.length !== 0) {\n    msg += \"\\n\\nThe following documents had no updates:\";\n    for (const file of noContentList) {\n      msg += `\\n ${file}`;\n    }\n  }\n  ui.alert(APP_TITLE, msg, ui.ButtonSet.OK);\n}\n\n/**\n * Updates the main document drawing content from source files.\n * Uses the parameters at the top of this file to locate content to import.\n *\n * Called from performImport().\n */\nfunction getContent(body) {\n  // Finds the heading paragraph with matching style, keywords and !color.\n  let parValidHeading;\n  const searchType = DocumentApp.ElementType.PARAGRAPH;\n  const searchHeading = APP_STYLE;\n  let searchResult = null;\n\n  // Gets and loops through all paragraphs that match the style of APP_STYLE.\n  while (true) {\n    searchResult = body.findElement(searchType, searchResult);\n    if (!searchResult) {\n      break;\n    }\n\n    const par = searchResult.getElement().asParagraph();\n    if (par.getHeading() === searchHeading) {\n      // If heading style matches, searches for text string (case insensitive).\n      const findPos = par.findText(`(?i)${FIND_TEXT_KEYWORDS}`);\n      if (findPos !== null) {\n        // If text color is green, then the paragraph isn't a new summary to copy.\n        if (par.editAsText().getForegroundColor() !== TEXT_COLOR) {\n          parValidHeading = par;\n        }\n      }\n    }\n  }\n\n  if (!parValidHeading) {\n    return;\n  }\n  // Updates the heading color to indicate that the summary has been imported.\n  const style = {};\n  style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR;\n  parValidHeading.setAttributes(style);\n  parValidHeading.appendText(\" [Exported]\");\n\n  // Gets the content from the table following the valid heading.\n  const elemObj = parValidHeading.getNextSibling().asTable();\n  const content = elemObj.copy();\n\n  return content;\n}\n\n/**\n * Gets the IDs of the Docs files within the folder that contains source files.\n *\n * Called from function performImport().\n */\nfunction getFiles(folder) {\n  // Only gets Docs files.\n  const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);\n  const docIDs = [];\n  while (files.hasNext()) {\n    const file = files.next();\n    docIDs.push(file.getId());\n  }\n  return docIDs;\n}\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/Menu.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains the functions that build the custom menu.\n */\n// Menu constants for easy access to update.\nconst MENU = {\n  NAME: \"Import summaries\",\n  IMPORT: \"Import summaries\",\n  SETUP: \"Configure\",\n  NEW_INSTANCE: \"Setup new instance\",\n  TEMPLATE: \"Create starter template\",\n  SAMPLES: \"Run demo setup with sample documents\",\n};\n\n/**\n * Creates custom menu when the document is opened.\n */\nfunction onOpen() {\n  const ui = DocumentApp.getUi();\n  ui.createMenu(MENU.NAME)\n    .addItem(MENU.IMPORT, \"performImport\")\n    .addSeparator()\n    .addSubMenu(\n      ui\n        .createMenu(MENU.SETUP)\n        .addItem(MENU.NEW_INSTANCE, \"setupConfig\")\n        .addItem(MENU.TEMPLATE, \"createSampleFile\")\n        .addSeparator()\n        .addItem(MENU.SAMPLES, \"setupWithSamples\"),\n    )\n    .addItem(\"About\", \"aboutApp\")\n    .addToUi();\n}\n\n/**\n * About box for context and contact.\n * TODO: Personalize\n */\nfunction aboutApp() {\n  const msg = `\n  ${APP_TITLE}\n  Version: 1.0\n  Contact: <Developer Email goes here>`;\n\n  const ui = DocumentApp.getUi();\n  ui.alert(\"About this application\", msg, ui.ButtonSet.OK);\n}\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/README.md",
    "content": "# Aggregate content from multiple documents\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/aggregate-document-content) for additional details.\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/Setup.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains functions that create the template and sample documents.\n */\n\n/**\n * Runs full setup configuration, with option to include samples.\n *\n * Called from menu & setupWithSamples()\n *\n * @param {boolean} includeSamples - Optional, if true creates samples files. *\n */\nfunction setupConfig(includeSamples) {\n  // Gets folder to store documents in.\n  const folder = getFolderByName_(PROJECT_FOLDER_NAME);\n\n  let msg = `\\nDrive Folder for Documents: '${PROJECT_FOLDER_NAME}'\n   \\nURL: \\n${folder.getUrl()}`;\n\n  // Creates sample documents for testing.\n  // Remove sample document creation and add your own process as needed.\n  if (includeSamples) {\n    let filesCreated = 0;\n    for (const doc of samples.documents) {\n      filesCreated += createGoogleDoc(doc, folder, true);\n    }\n    msg += `\\n\\nFiles Created: ${filesCreated}`;\n  }\n  const ui = DocumentApp.getUi();\n  ui.alert(`${APP_TITLE} [Setup]`, msg, ui.ButtonSet.OK);\n}\n\n/**\n * Creates a single document instance in the application folder.\n * Includes import settings already created [Heading | Keywords | Table]\n *\n * Called from menu.\n */\nfunction createSampleFile() {\n  // Creates a new Google Docs document.\n  const templateName = `[Template] ${APP_TITLE}`;\n  const doc = DocumentApp.create(templateName);\n  const docId = doc.getId();\n\n  const msg = `\\nDocument created: '${templateName}'\n  \\nURL: \\n${doc.getUrl()}`;\n\n  // Adds template content to the body.\n  const body = doc.getBody();\n\n  body.setText(templateName);\n  body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);\n  body\n    .appendParagraph(\"Description\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1);\n  body.appendParagraph(\"\");\n\n  const dateString = Utilities.formatDate(\n    new Date(),\n    Session.getScriptTimeZone(),\n    \"MMMM dd, yyyy\",\n  );\n  body\n    .appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`)\n    .setHeading(APP_STYLE);\n  body.appendTable().appendTableRow().appendTableCell(\"TL;DR\");\n  body.appendParagraph(\"\");\n\n  // Gets folder to store documents in.\n  const folder = getFolderByName_(PROJECT_FOLDER_NAME);\n\n  // Moves document to application folder.\n  DriveApp.getFileById(docId).moveTo(folder);\n\n  const ui = DocumentApp.getUi();\n  ui.alert(`${APP_TITLE} [Template]`, msg, ui.ButtonSet.OK);\n}\n\n/**\n * Configures application for demonstration by setting it up with sample documents.\n *\n * Called from menu | Calls setupConfig with option set to true.\n */\nfunction setupWithSamples() {\n  setupConfig(true);\n}\n\n/**\n * Sample document names and demo content.\n * {object} samples[]\n */\nconst samples = {\n  documents: [\n    {\n      name: \"Project GHI\",\n      description: \"Google Workspace Add-on inventory review.\",\n      content:\n        \"Reviewed all of the currently in-use and proposed Google Workspace Add-ons. Will perform an assessment on how we can reduce overlap, reduce licensing costs, and limit security exposures. \\n\\nNext week's goal is to report findings back to the Corp Ops team.\",\n    },\n    {\n      name: \"Project DEF\",\n      description: \"Improve IT networks within the main corporate building.\",\n      content:\n        \"Primarily focused on 2nd thru 5th floors in the main corporate building evaluating the network infrastructure. Benchmarking tests were performed and results are being analyzed. \\n\\nWill submit all findings, analysis, and recommendations next week for committee review.\",\n    },\n    {\n      name: \"Project ABC\",\n      description:\n        \"Assess existing Google Chromebook inventory and recommend upgrades where necessary.\",\n      content:\n        \"Concluded a pilot program with the Customer Service department to perform inventory and update inventory records with Chromebook hardware, Chrome OS versions, and installed apps. \\n\\nScheduling a work plan and seeking necessary go-forward approvals for next week.\",\n    },\n  ],\n  common:\n    'This sample document is configured to work with the Import summaries custom menu. For the import to work, the source documents used must contain a specific keyword (currently set to \"Summary\"). The keyword must reside in a paragraph with a set style (currently set to \"Heading 3\") that is directly followed by a single-cell table. The table contains the contents to be imported into the primary document.\\n\\nWhile those rules might seem precise, it\\'s how the application programmatically determines what content is meant to be imported and what can be ignored. Once a summary has been imported, the script updates the heading font to a new color (currently set to Green, hex \\'#2e7d32\\') to ensure the app ignores it in future imports. You can change these settings in the Apps Script code.',\n};\n\n/**\n * Creates a sample document in application folder.\n * Includes import settings already created [Heading | Keywords | Table].\n * Inserts demo data from samples[].\n *\n * Called from menu.\n */\nfunction createGoogleDoc(document, folder, duplicate) {\n  // Checks for duplicates.\n  if (!duplicate) {\n    // Doesn't create file of same name if one already exists.\n    if (folder.getFilesByName(document.name).hasNext()) {\n      return 0; // File not created.\n    }\n  }\n\n  // Creates a new Google Docs document.\n  const doc = DocumentApp.create(document.name).setName(document.name);\n  const docId = doc.getId();\n\n  // Adds boilerplate content to the body.\n  const body = doc.getBody();\n\n  body.setText(document.name);\n  body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);\n  body\n    .appendParagraph(\"Description\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1);\n  body.appendParagraph(document.description);\n  body\n    .appendParagraph(\"Usage Instructions\")\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1);\n  body.appendParagraph(samples.common);\n\n  const dateString = Utilities.formatDate(\n    new Date(),\n    Session.getScriptTimeZone(),\n    \"MMMM dd, yyyy\",\n  );\n  body\n    .appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`)\n    .setHeading(APP_STYLE);\n  body.appendTable().appendTableRow().appendTableCell(document.content);\n  body.appendParagraph(\"\");\n\n  // Moves document to application folder.\n  DriveApp.getFileById(docId).moveTo(folder);\n\n  // Returns if successfully created.\n  return 1;\n}\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/Utilities.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains common utility functions.\n */\n\n/**\n * Returns a Drive folder located in same folder that the application document is located.\n * Checks if the folder exists and returns that folder, or creates new one if not found.\n *\n * @param {string} folderName - Name of the Drive folder.\n * @return {object} Google Drive folder\n */\nfunction getFolderByName_(folderName) {\n  // Gets the Drive folder where the current document is located.\n  const docId = DocumentApp.getActiveDocument().getId();\n  const parentFolder = DriveApp.getFileById(docId).getParents().next();\n\n  // Iterates subfolders to check if folder already exists.\n  const subFolders = parentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === folderName) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one doesn't already exist.\n  return parentFolder\n    .createFolder(folderName)\n    .setDescription(\n      `Created by ${APP_TITLE} application to store documents to process`,\n    );\n}\n\n/**\n * Test function to run getFolderByName_.\n * @logs details of created Google Drive folder.\n */\nfunction test_getFolderByName() {\n  // Gets the folder in Drive associated with this application.\n  const folder = getFolderByName_(PROJECT_FOLDER_NAME);\n\n  console.log(\n    `Name: ${folder.getName()}\\rID: ${folder.getId()}\\rURL:${folder.getUrl()}\\rDescription: ${folder.getDescription()}`,\n  );\n  // Uncomment the following to automatically delete the test folder.\n  // folder.setTrashed(true);\n}\n"
  },
  {
    "path": "solutions/automations/aggregate-document-content/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/bracket-maker/.clasp.json",
    "content": "{ \"scriptId\": \"1LkY5nKFdBg2Q9-oIUcZsxRuESvgIcFHGobveNeQ5CpTgV6GgpTUQeOIB\" }\n"
  },
  {
    "path": "solutions/automations/bracket-maker/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/bracket-maker\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst RANGE_PLAYER1 = \"FirstPlayer\";\nconst SHEET_PLAYERS = \"Players\";\nconst SHEET_BRACKET = \"Bracket\";\nconst CONNECTOR_WIDTH = 15;\n\n/**\n * Adds a custom menu item to run the script.\n */\nfunction onOpen() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  ss.addMenu(\"Bracket maker\", [\n    { name: \"Create bracket\", functionName: \"createBracket\" },\n  ]);\n}\n\n/**\n * Creates the brackets based on the data provided on the players.\n */\nfunction createBracket() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  let rangePlayers = ss.getRangeByName(RANGE_PLAYER1);\n  const sheetControl = ss.getSheetByName(SHEET_PLAYERS);\n  const sheetResults = ss.getSheetByName(SHEET_BRACKET);\n\n  // Gets the players from column A.  Assumes the entire column is filled.\n  rangePlayers = rangePlayers.offset(\n    0,\n    0,\n    sheetControl.getMaxRows() - rangePlayers.getRowIndex() + 1,\n    1,\n  );\n  let players = rangePlayers.getValues();\n\n  // Figures out how many players there are by skipping the empty cells.\n  let numPlayers = 0;\n  for (let i = 0; i < players.length; i++) {\n    if (!players[i][0] || players[i][0].length === 0) {\n      break;\n    }\n    numPlayers++;\n  }\n  players = players.slice(0, numPlayers);\n\n  // Provides some error checking in case there are too many or too few players/teams.\n  if (numPlayers > 64) {\n    Browser.msgBox(\n      \"Sorry, this script can only create brackets for 64 or fewer players.\",\n    );\n    return; // Early exit\n  }\n\n  if (numPlayers < 3) {\n    Browser.msgBox(\"Sorry, you must have at least 3 players.\");\n    return; // Early exit\n  }\n\n  // Clears the 'Bracket' sheet and all formatting.\n  sheetResults.clear();\n\n  let upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2));\n\n  // Calculates the number that is a power of 2 and lower than numPlayers.\n  const countNodesUpperBound = 2 ** upperPower;\n\n  // Calculates the number that is a power of 2 and higher than numPlayers.\n  const countNodesLowerBound = countNodesUpperBound / 2;\n\n  // Determines the number of nodes that will not show in the 1st level.\n  const countNodesHidden = numPlayers - countNodesLowerBound;\n\n  // Enters the players for the 1st round.\n  const currentPlayer = 0;\n  for (let i = 0; i < countNodesLowerBound; i++) {\n    if (i < countNodesHidden) {\n      // Must be on the first level\n      const rng = sheetResults.getRange(i * 4 + 1, 1);\n      setBracketItem_(rng, players);\n      setBracketItem_(rng.offset(2, 0, 1, 1), players);\n      setConnector_(sheetResults, rng.offset(0, 1, 3, 1));\n      setBracketItem_(rng.offset(1, 2, 1, 1));\n    } else {\n      // This player gets a bye.\n      setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players);\n    }\n  }\n\n  // Fills in the rest of the bracket.\n  upperPower--;\n  for (let i = 0; i < upperPower; i++) {\n    const pow1 = 2 ** (i + 1);\n    const pow2 = 2 ** (i + 2);\n    const pow3 = 2 ** (i + 3);\n    for (let j = 0; j < 2 ** (upperPower - i - 1); j++) {\n      setBracketItem_(sheetResults.getRange(j * pow3 + pow2, i * 2 + 5));\n      setConnector_(\n        sheetResults,\n        sheetResults.getRange(j * pow3 + pow1, i * 2 + 4, pow2 + 1, 1),\n      );\n    }\n  }\n}\n\n/**\n * Sets the value of an item in the bracket and the color.\n * @param {Range} rng The Spreadsheet Range.\n * @param {string[]} players The list of players.\n */\nfunction setBracketItem_(rng, players) {\n  if (players) {\n    const rand = Math.ceil(Math.random() * players.length);\n    rng.setValue(players.splice(rand - 1, 1)[0][0]);\n  }\n  rng.setBackgroundColor(\"yellow\");\n}\n\n/**\n * Sets the color and width for connector cells.\n * @param {Sheet} sheet The spreadsheet to setup.\n * @param {Range} rng The spreadsheet range.\n */\nfunction setConnector_(sheet, rng) {\n  sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH);\n  rng.setBackgroundColor(\"green\");\n}\n"
  },
  {
    "path": "solutions/automations/bracket-maker/README.md",
    "content": "# Create a tournament bracket\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/bracket-maker) for additional details.\n"
  },
  {
    "path": "solutions/automations/bracket-maker/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/calendar-timesheet/.clasp.json",
    "content": "{ \"scriptId\": \"1WL3-mzC219UHqy_vqI1gEeoFy5Y8eeiKCZjiiPsWmVmQfVVedN5Vt7rK\" }\n"
  },
  {
    "path": "solutions/automations/calendar-timesheet/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/calendar-timesheet\n\n/*\nCopyright 2022 Jasper Duizendstra\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Runs when the spreadsheet is opened and adds the menu options\n * to the spreadsheet menu\n */\nconst onOpen = () => {\n  SpreadsheetApp.getUi()\n    .createMenu(\"myTime\")\n    .addItem(\"Sync calendar events\", \"run\")\n    .addItem(\"Settings\", \"settings\")\n    .addToUi();\n};\n\n/**\n * Opens the sidebar\n */\nconst settings = () => {\n  const html =\n    HtmlService.createHtmlOutputFromFile(\"Page\").setTitle(\"Settings\");\n\n  SpreadsheetApp.getUi().showSidebar(html);\n};\n\n/**\n * returns the settings from the script properties\n */\nconst getSettings = () => {\n  const settings = {};\n\n  // get the current settings\n  const savedCalendarSettings = JSON.parse(\n    PropertiesService.getScriptProperties().getProperty(\"calendar\") || \"[]\",\n  );\n\n  // get the primary calendar\n  const primaryCalendar = CalendarApp.getAllCalendars()\n    .filter((cal) => cal.isMyPrimaryCalendar())\n    .map((cal) => ({\n      name: \"Primary calendar\",\n      id: cal.getId(),\n    }));\n\n  // get the secondary calendars\n  const secundaryCalendars = CalendarApp.getAllCalendars()\n    .filter((cal) => cal.isOwnedByMe() && !cal.isMyPrimaryCalendar())\n    .map((cal) => ({\n      name: cal.getName(),\n      id: cal.getId(),\n    }));\n\n  // the current available calendars\n  const availableCalendars = primaryCalendar.concat(secundaryCalendars);\n\n  // find any calendars that were removed\n  const unavailebleCalendars = [];\n  for (const savedCalendarSetting of savedCalendarSettings) {\n    if (\n      !availableCalendars.find(\n        (availableCalendar) => availableCalendar.id === savedCalendarSetting.id,\n      )\n    ) {\n      unavailebleCalendars.push(savedCalendarSetting);\n    }\n  }\n\n  // map the current settings to the available calendars\n  const calendarSettings = availableCalendars.map((availableCalendar) => {\n    if (\n      savedCalendarSettings.find(\n        (savedCalendar) => savedCalendar.id === availableCalendar.id,\n      )\n    ) {\n      availableCalendar.sync = true;\n    }\n    return availableCalendar;\n  });\n\n  // add the calendar settings to the settings\n  settings.calendarSettings = calendarSettings;\n\n  const savedFrom =\n    PropertiesService.getScriptProperties().getProperty(\"syncFrom\");\n  settings.syncFrom = savedFrom;\n\n  const savedTo = PropertiesService.getScriptProperties().getProperty(\"syncTo\");\n  settings.syncTo = savedTo;\n\n  const savedIsUpdateTitle =\n    PropertiesService.getScriptProperties().getProperty(\"isUpdateTitle\") ===\n    \"true\";\n  settings.isUpdateCalendarItemTitle = savedIsUpdateTitle;\n\n  const savedIsUseCategoriesAsCalendarItemTitle =\n    PropertiesService.getScriptProperties().getProperty(\n      \"isUseCategoriesAsCalendarItemTitle\",\n    ) === \"true\";\n  settings.isUseCategoriesAsCalendarItemTitle =\n    savedIsUseCategoriesAsCalendarItemTitle;\n\n  const savedIsUpdateDescription =\n    PropertiesService.getScriptProperties().getProperty(\n      \"isUpdateDescription\",\n    ) === \"true\";\n  settings.isUpdateCalendarItemDescription = savedIsUpdateDescription;\n\n  return settings;\n};\n\n/**\n * Saves the settings from the sidebar\n */\nconst saveSettings = (settings) => {\n  PropertiesService.getScriptProperties().setProperty(\n    \"calendar\",\n    JSON.stringify(settings.calendarSettings),\n  );\n  PropertiesService.getScriptProperties().setProperty(\n    \"syncFrom\",\n    settings.syncFrom,\n  );\n  PropertiesService.getScriptProperties().setProperty(\n    \"syncTo\",\n    settings.syncTo,\n  );\n  PropertiesService.getScriptProperties().setProperty(\n    \"isUpdateTitle\",\n    settings.isUpdateCalendarItemTitle,\n  );\n  PropertiesService.getScriptProperties().setProperty(\n    \"isUseCategoriesAsCalendarItemTitle\",\n    settings.isUseCategoriesAsCalendarItemTitle,\n  );\n  PropertiesService.getScriptProperties().setProperty(\n    \"isUpdateDescription\",\n    settings.isUpdateCalendarItemDescription,\n  );\n  return \"Settings saved\";\n};\n\n/**\n * Builds the myTime object and runs the synchronisation\n */\nconst run = () => {\n  myTime({\n    mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(),\n  }).run();\n};\n\n/**\n * The main function used for the synchronisation\n * @param {Object} par The main parameter object.\n * @return {Object} The myTime Object.\n */\nconst myTime = (par) => {\n  /**\n   * Format the sheet\n   */\n  const formatSheet = () => {\n    // sort decending on start date\n    hourSheet.sort(3, false);\n\n    // hide the technical columns\n    hourSheet.hideColumns(1, 2);\n\n    // remove any extra rows\n    if (\n      hourSheet.getLastRow() > 1 &&\n      hourSheet.getLastRow() < hourSheet.getMaxRows()\n    ) {\n      hourSheet.deleteRows(\n        hourSheet.getLastRow() + 1,\n        hourSheet.getMaxRows() - hourSheet.getLastRow(),\n      );\n    }\n\n    // set the validation for the customers\n    let rule = SpreadsheetApp.newDataValidation()\n      .requireValueInRange(categoriesSheet.getRange(\"A2:A\"), true)\n      .setAllowInvalid(true)\n      .build();\n    hourSheet.getRange(\"I2:I\").setDataValidation(rule);\n\n    // set the validation for the projects\n    rule = SpreadsheetApp.newDataValidation()\n      .requireValueInRange(categoriesSheet.getRange(\"B2:B\"), true)\n      .setAllowInvalid(true)\n      .build();\n    hourSheet.getRange(\"J2:J\").setDataValidation(rule);\n\n    // set the validation for the tsaks\n    rule = SpreadsheetApp.newDataValidation()\n      .requireValueInRange(categoriesSheet.getRange(\"C2:C\"), true)\n      .setAllowInvalid(true)\n      .build();\n    hourSheet.getRange(\"K2:K\").setDataValidation(rule);\n\n    if (isUseCategoriesAsCalendarItemTitle) {\n      hourSheet\n        .getRange(\"L2:L\")\n        .setFormulaR1C1(\n          'IF(OR(R[0]C[-3]=\"tbd\";R[0]C[-2]=\"tbd\";R[0]C[-1]=\"tbd\");\"\"; CONCATENATE(R[0]C[-3];\"|\";R[0]C[-2];\"|\";R[0]C[-1];\"|\"))',\n        );\n    }\n    // set the hours, month, week and number collumns\n    hourSheet\n      .getRange(\"P2:P\")\n      .setFormulaR1C1('=IF(R[0]C[-12]=\"\";\"\";R[0]C[-12]-R[0]C[-13])');\n    hourSheet\n      .getRange(\"Q2:Q\")\n      .setFormulaR1C1('=IF(R[0]C[-13]=\"\";\"\";month(R[0]C[-13]))');\n    hourSheet\n      .getRange(\"R2:R\")\n      .setFormulaR1C1('=IF(R[0]C[-14]=\"\";\"\";WEEKNUM(R[0]C[-14];2))');\n    hourSheet.getRange(\"S2:S\").setFormulaR1C1(\"=R[0]C[-3]\");\n  };\n\n  /**\n   * Activate the synchronisation\n   */\n  function run() {\n    console.log(\"Started processing hours.\");\n\n    const processCalendar = (setting) => {\n      SpreadsheetApp.flush();\n\n      // current calendar info\n      const calendarName = setting.name;\n      const calendarId = setting.id;\n\n      console.log(\n        `processing ${calendarName} with the id ${calendarId} from ${syncStartDate} to ${syncEndDate}`,\n      );\n\n      // get the calendar\n      const calendar = CalendarApp.getCalendarById(calendarId);\n\n      // get the calendar events and create lookups\n      const events = calendar.getEvents(syncStartDate, syncEndDate);\n      const eventsLookup = events.reduce((jsn, event) => {\n        jsn[event.getId()] = event;\n        return jsn;\n      }, {});\n\n      // get the sheet events and create lookups\n      const existingEvents = hourSheet.getDataRange().getValues().slice(1);\n      const existingEventsLookUp = existingEvents.reduce((jsn, row, index) => {\n        if (row[0] !== calendarId) {\n          return jsn;\n        }\n        jsn[row[1]] = {\n          event: row,\n          row: index + 2,\n        };\n        return jsn;\n      }, {});\n\n      // handle a calendar event\n      const handleEvent = (event) => {\n        const eventId = event.getId();\n\n        // new event\n        if (!existingEventsLookUp[eventId]) {\n          hourSheet.appendRow([\n            calendarId,\n            eventId,\n            event.getStartTime(),\n            event.getEndTime(),\n            calendarName,\n            event.getCreators().join(\",\"),\n            event.getTitle(),\n            event.getDescription(),\n            event.getTag(\"Client\") || \"tbd\",\n            event.getTag(\"Project\") || \"tbd\",\n            event.getTag(\"Task\") || \"tbd\",\n            isUpdateCalendarItemTitle ? \"\" : event.getTitle(),\n            isUpdateCalendarItemDescription ? \"\" : event.getDescription(),\n            event\n              .getGuestList()\n              .map((guest) => guest.getEmail())\n              .join(\",\"),\n            event.getLocation(),\n            undefined,\n            undefined,\n            undefined,\n            undefined,\n          ]);\n          return true;\n        }\n\n        // existing event\n        const exisitingEvent = existingEventsLookUp[eventId].event;\n        const exisitingEventRow = existingEventsLookUp[eventId].row;\n\n        if (event.getStartTime() - exisitingEvent[startTimeColumn - 1] !== 0) {\n          hourSheet\n            .getRange(exisitingEventRow, startTimeColumn)\n            .setValue(event.getStartTime());\n        }\n\n        if (event.getEndTime() - exisitingEvent[endTimeColumn - 1] !== 0) {\n          hourSheet\n            .getRange(exisitingEventRow, endTimeColumn)\n            .setValue(event.getEndTime());\n        }\n\n        if (\n          event.getCreators().join(\",\") !== exisitingEvent[creatorsColumn - 1]\n        ) {\n          hourSheet\n            .getRange(exisitingEventRow, creatorsColumn)\n            .setValue(event.getCreators()[0]);\n        }\n\n        if (\n          event\n            .getGuestList()\n            .map((guest) => guest.getEmail())\n            .join(\",\") !== exisitingEvent[guestListColumn - 1]\n        ) {\n          hourSheet.getRange(exisitingEventRow, guestListColumn).setValue(\n            event\n              .getGuestList()\n              .map((guest) => guest.getEmail())\n              .join(\",\"),\n          );\n        }\n\n        if (event.getLocation() !== exisitingEvent[locationColumn - 1]) {\n          hourSheet\n            .getRange(exisitingEventRow, locationColumn)\n            .setValue(event.getLocation());\n        }\n\n        if (event.getTitle() !== exisitingEvent[titleColumn - 1]) {\n          if (!isUpdateCalendarItemTitle) {\n            hourSheet\n              .getRange(exisitingEventRow, titleColumn)\n              .setValue(event.getTitle());\n          }\n          if (isUpdateCalendarItemTitle) {\n            event.setTitle(exisitingEvent[titleColumn - 1]);\n          }\n        }\n\n        if (event.getDescription() !== exisitingEvent[descriptionColumn - 1]) {\n          if (!isUpdateCalendarItemDescription) {\n            hourSheet\n              .getRange(exisitingEventRow, descriptionColumn)\n              .setValue(event.getDescription());\n          }\n          if (isUpdateCalendarItemDescription) {\n            event.setDescription(exisitingEvent[descriptionColumn - 1]);\n          }\n        }\n\n        return true;\n      };\n\n      // process each event for the calendar\n      events.every(handleEvent);\n\n      // remove any events in the sheet that are not in de calendar\n      existingEvents.every((event, index) => {\n        if (event[0] !== calendarId) {\n          return true;\n        }\n\n        if (eventsLookup[event[1]]) {\n          return true;\n        }\n\n        if (event[3] < syncStartDate) {\n          return true;\n        }\n\n        hourSheet.getRange(index + 2, 1, 1, 20).clear();\n        return true;\n      });\n\n      return true;\n    };\n\n    // process the calendars\n    settings.calendarSettings\n      .filter((calenderSetting) => calenderSetting.sync === true)\n      .every(processCalendar);\n\n    formatSheet();\n    SpreadsheetApp.setActiveSheet(hourSheet);\n\n    console.log(\"Finished processing hours.\");\n  }\n\n  const mainSpreadSheetId = par.mainSpreadsheetId;\n  const mainSpreadsheet = SpreadsheetApp.openById(mainSpreadSheetId);\n  const hourSheet = mainSpreadsheet.getSheetByName(\"Hours\");\n  const categoriesSheet = mainSpreadsheet.getSheetByName(\"Categories\");\n  const settings = getSettings();\n\n  const syncStartDate = new Date();\n  syncStartDate.setDate(syncStartDate.getDate() - Number(settings.syncFrom));\n\n  const syncEndDate = new Date();\n  syncEndDate.setDate(syncEndDate.getDate() + Number(settings.syncTo));\n\n  const isUpdateCalendarItemTitle = settings.isUpdateCalendarItemTitle;\n  const isUseCategoriesAsCalendarItemTitle =\n    settings.isUseCategoriesAsCalendarItemTitle;\n  const isUpdateCalendarItemDescription =\n    settings.isUpdateCalendarItemDescription;\n\n  const startTimeColumn = 3;\n  const endTimeColumn = 4;\n  const creatorsColumn = 6;\n  const originalTitleColumn = 7;\n  const originalDescriptionColumn = 8;\n  const clientColumn = 9;\n  const projectColumn = 10;\n  const taskColumn = 11;\n  const titleColumn = 12;\n  const descriptionColumn = 13;\n  const guestListColumn = 14;\n  const locationColumn = 15;\n\n  return Object.freeze({\n    run: run,\n  });\n};\n"
  },
  {
    "path": "solutions/automations/calendar-timesheet/Page.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n\n<head>\n    <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons1.css\">\n    <style>\n        #main {\n            display: none\n        }\n\n        #categories-as-item-title {\n            display: none\n        }\n\n        #show_title_warning {\n            display: none\n        }\n\n        #show_description_warning {\n            display: none\n        }\n\n        .red {\n            color: red;\n        }\n\n        .branding-below {\n            bottom: 56px;\n            top: 0;\n        }\n\n        input[type=number] {\n            width: 50px;\n            height: 15px;\n        }\n    </style>\n</head>\n\n<body>\n    <div class=\"sidebar branding-below\" id=\"wait\">\n        Please wait...\n    </div>\n    <div class=\"sidebar branding-below\" id=\"main\">\n        <div class=\"block\" id=\"checks\">\n            <b>Synchronise calendars</b>\n            <div>\n                <span class=\"error\" id=\"calendar-message\"></span>\n            </div>\n        </div>\n\n        <div class=\"block\">\n            <b>Synchronisation period</b>\n            <br>Synchronise from the last <input type=\"number\" name=\"sync-from\" id=\"sync-from\"> days\n            <br>Synchronise up to the coming <input type=\"number\" name=\"sync-to\" id=\"sync-to\"> days\n        </div>\n\n        <div class=\"block\">\n            <b>Update the calendar items</b><br>\n            <input type=\"checkbox\" id=\"is-update-calendar-item-title\">\n            <label for=\"is-update-calendar-item-title\">Overwrite the calendar item title</label>\n            <span class=\"secondary\" id=\"show_title_warning\">The calendar title will be overwritten with the values in\n                title\n                column of the sheet</span>\n        </div>\n        <div id=\"categories-as-item-title\">\n            <input type=\"checkbox\" id=\"is-use-categories-as-item-title\">\n            <label for=\"is-use-categories-as-item-title\">Use categories as the calendar item title</label>\n        </div>\n        <div class=\"block\">\n            <input type=\"checkbox\" id=\"is-update-calendar-item-description\">\n            <label for=\"is-update-calendar-item-description\">Overwrite the calendar item description</label>\n            <span class=\"secondary\" id=\"show_description_warning\">The calendar description will be overwritten with the\n                values in description column of the sheet</span>\n        </div>\n        <div class=\"block\">\n            <button class=\"blue\" onClick=\"saveSettings()\">Save</button>\n        </div>\n        <div class=\"block\">\n            <span class=\"error\" id=\"generic-error\"></span>\n            <span class=\"gray\" id=\"generic-message\"></span>\n        </div>\n\n    </div>\n    <div class=\"sidebar bottom\">\n        <span class=\"gray\">\n            myTime v1.2.0</span>\n    </div>\n</body>\n<script>\n    // event handler for categrories\n    document.getElementById('is-update-calendar-item-title').addEventListener('change', (event) => {\n        if (event.target.checked) {\n            document.getElementById('categories-as-item-title').style.display = \"block\";\n            document.getElementById('show_title_warning').style.display = \"block\";\n        } else {\n            document.getElementById('categories-as-item-title').style.display = \"none\";\n            document.getElementById('is-use-categories-as-item-title').checked = false;\n            document.getElementById('show_title_warning').style.display = \"none\";\n        }\n    })\n\n    document.getElementById('is-update-calendar-item-description').addEventListener('change', (event) => {\n        if (event.target.checked) {\n            document.getElementById('show_description_warning').style.display = \"block\";\n        } else {\n            document.getElementById('show_description_warning').style.display = \"none\";\n        }\n    })\n\n    // generic error handler\n    const onFailure = (error) => {\n        console.debug(error);\n        document.getElementById('generic-error').innerHTML = error.message;\n    }\n\n    // receiving the settings\n    const onSuccessGetSettings = (settings) => {\n        console.debug(settings);\n\n        settings.calendarSettings.forEach((calendar, index) => {\n            const div = document.createElement('div');\n\n            const check = document.createElement('input');\n            check.className = 'calendar-check';\n            check.className = 'calendar-check red';\n            check.type = 'checkbox';\n            check.id = 'calendar' + index;\n            check.value = (calendar.id);\n            check.name = (calendar.name);\n            check.checked = (calendar.sync);\n\n            const label = document.createElement('label')\n            label.htmlFor = \"calendar\" + index;\n            label.appendChild(document.createTextNode(calendar.name));\n            if (index == 0) {\n                label.className = 'red';\n            }\n\n            div.appendChild(check);\n            div.appendChild(label);\n\n            document.getElementById('checks').appendChild(div);\n        });\n\n        document.getElementById('sync-from').value = settings.syncFrom || 31;\n        document.getElementById('sync-to').value = settings.syncTo || 31;\n        document.getElementById('is-update-calendar-item-title').checked = settings.isUpdateCalendarItemTitle;\n\n        if (settings.isUpdateCalendarItemTitle) {\n            document.getElementById('categories-as-item-title').style.display = \"block\";\n            document.getElementById('is-use-categories-as-item-title').checked = settings.isUseCategoriesAsCalendarItemTitle;\n            document.getElementById('show_title_warning').style.display = \"block\";\n        }\n\n        if (settings.isUpdateCalendarItemDescription) {\n            document.getElementById('is-update-calendar-item-description').checked = settings.isUpdateCalendarItemDescription;\n            document.getElementById('show_description_warning').style.display = \"block\";\n        }\n        document.getElementById('wait').style.display = \"none\";\n        document.getElementById('main').style.display = \"block\";\n\n\n    }\n\n    // receiving the settings saved confirmation\n    const onSuccessSaveSettings = (msg) => {\n        console.debug(msg);\n        document.getElementById('generic-message').innerHTML = msg;\n    }\n\n    // save the settings\n    const saveSettings = () => {\n        document.getElementById('generic-message').innerHTML = '';\n        const checks = document.getElementsByClassName('calendar-check');\n        const calendarSettings = [];\n        for (let check of checks) {\n            if (!check.checked) {\n                continue;\n            }\n            calendarSettings.push({\n                name: check.name,\n                id: check.value,\n                sync: check.checked\n            });\n        }\n\n        const settings = {};\n        settings.calendarSettings = calendarSettings;\n        settings.syncFrom = document.getElementById('sync-from').value;\n        settings.syncTo = document.getElementById('sync-to').value;\n        settings.isUpdateCalendarItemTitle = document.getElementById('is-update-calendar-item-title').checked;\n        if (settings.isUpdateCalendarItemTitle) {\n            settings.isUseCategoriesAsCalendarItemTitle = document.getElementById('is-use-categories-as-item-title').checked;\n        }\n        if (!settings.isUpdateCalendarItemTitle) {\n            settings.isUseCategoriesAsCalendarItemTitle = false;\n        }\n\n        settings.isUpdateCalendarItemDescription = document.getElementById('is-update-calendar-item-description').checked;\n        console.debug(settings)\n\n        google.script.run\n            .withFailureHandler(onFailure)\n            .withSuccessHandler(onSuccessSaveSettings)\n            .saveSettings(settings);\n    }\n\n    // get the initial settings\n    google.script.run\n        .withFailureHandler(onFailure)\n        .withSuccessHandler(onSuccessGetSettings)\n        .getSettings();\n</script>\n\n</html>"
  },
  {
    "path": "solutions/automations/calendar-timesheet/README.md",
    "content": "# Record time and activities in Calendar and Sheets\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/calendar-timesheet) for additional details.\n"
  },
  {
    "path": "solutions/automations/calendar-timesheet/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/content-signup/.clasp.json",
    "content": "{ \"scriptId\": \"1G8TfU6Rfcl76Uo4gKig7jFMYKai-V_fiUNbO12pAb25pA4_uyxN5PSvd\" }\n"
  },
  {
    "path": "solutions/automations/content-signup/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/content-signup\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// To use your own template doc, update the below variable with the URL of your own Google Doc template.\n// Make sure you update the sharing settings so that 'anyone'  or 'anyone in your organization' can view.\nconst EMAIL_TEMPLATE_DOC_URL =\n  \"https://docs.google.com/document/d/1enes74gWsMG3dkK3SFO08apXkr0rcYBd3JHKOb2Nksk/edit?usp=sharing\";\n// Update this variable to customize the email subject.\nconst EMAIL_SUBJECT = \"Hello, here is the content you requested\";\n\n// Update this variable to the content titles and URLs you want to offer. Make sure you update the form so that the content titles listed here match the content titles you list in the form.\nconst topicUrls = {\n  \"Google Calendar how-to videos\":\n    \"https://www.youtube.com/playlist?list=PLU8ezI8GYqs7IPb_UdmUNKyUCqjzGO9PJ\",\n  \"Google Drive how-to videos\":\n    \"https://www.youtube.com/playlist?list=PLU8ezI8GYqs7Y5d1cgZm2Obq7leVtLkT4\",\n  \"Google Docs how-to videos\":\n    \"https://www.youtube.com/playlist?list=PLU8ezI8GYqs4JKwZ-fpBP-zSoWPL8Sit7\",\n  \"Google Sheets how-to videos\":\n    \"https://www.youtube.com/playlist?list=PLU8ezI8GYqs61ciKpXf_KkV7ZRbRHVG38\",\n};\n\n/**\n * Installs a trigger on the spreadsheet for when someone submits a form.\n */\nfunction installTrigger() {\n  ScriptApp.newTrigger(\"onFormSubmit\")\n    .forSpreadsheet(SpreadsheetApp.getActive())\n    .onFormSubmit()\n    .create();\n}\n\n/**\n * Sends a customized email for every form response.\n *\n * @param {Object} event - Form submit event\n */\nfunction onFormSubmit(e) {\n  const responses = e.namedValues;\n\n  // If the question title is a label, it can be accessed as an object field.\n  // If it has spaces or other characters, it can be accessed as a dictionary.\n  const timestamp = responses.Timestamp[0];\n  const email = responses[\"Email address\"][0].trim();\n  const name = responses.Name[0].trim();\n  const topicsString = responses.Topics[0].toLowerCase();\n\n  // Parse topics of interest into a list (since there are multiple items\n  // that are saved in the row as blob of text).\n  const topics = Object.keys(topicUrls).filter((topic) => {\n    // indexOf searches for the topic in topicsString and returns a non-negative\n    // index if the topic is found, or it will return -1 if it's not found.\n    return topicsString.indexOf(topic.toLowerCase()) !== -1;\n  });\n\n  // If there is at least one topic selected, send an email to the recipient.\n  let status = \"\";\n  if (topics.length > 0) {\n    MailApp.sendEmail({\n      to: email,\n      subject: EMAIL_SUBJECT,\n      htmlBody: createEmailBody(name, topics),\n    });\n    status = \"Sent\";\n  } else {\n    status = \"No topics selected\";\n  }\n\n  // Append the status on the spreadsheet to the responses' row.\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const row = sheet.getActiveRange().getRow();\n  const column = e.values.length + 1;\n  sheet.getRange(row, column).setValue(status);\n\n  console.log(`status=${status}; responses=${JSON.stringify(responses)}`);\n}\n\n/**\n * Creates email body and includes the links based on topic.\n *\n * @param {string} recipient - The recipient's email address.\n * @param {string[]} topics - List of topics to include in the email body.\n * @return {string} - The email body as an HTML string.\n */\nfunction createEmailBody(name, topics) {\n  let topicsHtml = topics\n    .map((topic) => {\n      const url = topicUrls[topic];\n      return `<li><a href=\"${url}\">${topic}</a></li>`;\n    })\n    .join(\"\");\n  topicsHtml = `<ul>${topicsHtml}</ul>`;\n\n  // Make sure to update the emailTemplateDocId at the top.\n  const docId = DocumentApp.openByUrl(EMAIL_TEMPLATE_DOC_URL).getId();\n  let emailBody = docToHtml(docId);\n  emailBody = emailBody.replace(/{{NAME}}/g, name);\n  emailBody = emailBody.replace(/{{TOPICS}}/g, topicsHtml);\n  return emailBody;\n}\n\n/**\n * Downloads a Google Doc as an HTML string.\n *\n * @param {string} docId - The ID of a Google Doc to fetch content from.\n * @return {string} The Google Doc rendered as an HTML string.\n */\nfunction docToHtml(docId) {\n  // Downloads a Google Doc as an HTML string.\n  const url = `https://docs.google.com/feeds/download/documents/export/Export?id=${docId}&exportFormat=html`;\n  const param = {\n    method: \"get\",\n    headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` },\n    muteHttpExceptions: true,\n  };\n  return UrlFetchApp.fetch(url, param).getContentText();\n}\n"
  },
  {
    "path": "solutions/automations/content-signup/README.md",
    "content": "# Send curated content\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/content-signup) for additional details.\n"
  },
  {
    "path": "solutions/automations/content-signup/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/documents\",\n    \"https://www.googleapis.com/auth/drive.readonly\",\n    \"https://www.googleapis.com/auth/script.external_request\",\n    \"https://www.googleapis.com/auth/script.scriptapp\",\n    \"https://www.googleapis.com/auth/script.send_mail\",\n    \"https://www.googleapis.com/auth/spreadsheets.currentonly\"\n  ],\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/course-feedback-response/.clasp.json",
    "content": "{ \"scriptId\": \"1k75E4EdC3TcJEGGIupBANjm5duvs35ORAU1Mg2_6DNXENo827dFzmFeC\" }\n"
  },
  {
    "path": "solutions/automations/course-feedback-response/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/course-feedback-response\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Creates custom menu for user to run scripts.\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  ui.createMenu(\"Form Reply Tool\")\n    .addItem(\"Enable auto draft replies\", \"installTrigger\")\n    .addToUi();\n}\n\n/**\n * Installs a trigger on the Spreadsheet for when a Form response is submitted.\n */\nfunction installTrigger() {\n  ScriptApp.newTrigger(\"onFormSubmit\")\n    .forSpreadsheet(SpreadsheetApp.getActive())\n    .onFormSubmit()\n    .create();\n}\n\n/**\n * Creates a draft email for every response on a form\n *\n * @param {Object} event - Form submit event\n */\nfunction onFormSubmit(e) {\n  const responses = e.namedValues;\n\n  // parse form response data\n  const timestamp = responses.Timestamp[0];\n  const email = responses[\"Email address\"][0].trim();\n\n  // create email body\n  const emailBody = createEmailBody(responses);\n\n  // create draft email\n  createDraft(timestamp, email, emailBody);\n}\n\n/**\n * Creates email body and includes feedback from Google Form.\n *\n * @param {string} responses - The form response data\n * @return {string} - The email body as an HTML string\n */\nfunction createEmailBody(responses) {\n  // parse form response data\n  const name = responses.Name[0].trim();\n  const industry = responses[\"What industry do you work in?\"][0];\n  const source = responses[\"How did you find out about this course?\"][0];\n  const rating =\n    responses[\"On a scale of 1 - 5 how would you rate this course?\"][0];\n  const productFeedback =\n    responses[\"What could be different to make it a 5 rating?\"][0];\n  const otherFeedback = responses[\"Any other feedback?\"][0];\n\n  // create email body\n  const htmlBody = `Hi ${name},<br><br>Thanks for responding to our course feedback questionnaire.<br><br>It's really useful to us to help improve this course.<br><br>Have a great day!<br><br>Thanks,<br>Course Team<br><br>****************************************************************<br><br><i>Your feedback:<br><br>What industry do you work in?<br><br>${industry}<br><br>How did you find out about this course?<br><br>${source}<br><br>On a scale of 1 - 5 how would you rate this course?<br><br>${rating}<br><br>What could be different to make it a 5 rating?<br><br>${productFeedback}<br><br>Any other feedback?<br><br>${otherFeedback}<br><br></i>`;\n\n  return htmlBody;\n}\n\n/**\n * Create a draft email with the feedback\n *\n * @param {string} timestamp Timestamp for the form response\n * @param {string} email Email address from the form response\n * @param {string} emailBody The email body as an HTML string\n */\nfunction createDraft(timestamp, email, emailBody) {\n  console.log(\"draft email create process started\");\n\n  // create subject line\n  const subjectLine = `Thanks for your course feedback! ${timestamp}`;\n\n  // create draft email\n  GmailApp.createDraft(email, subjectLine, \"\", {\n    htmlBody: emailBody,\n  });\n}\n"
  },
  {
    "path": "solutions/automations/course-feedback-response/README.md",
    "content": "# Respond to feedback\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/course-feedback-response) for additional details.\n"
  },
  {
    "path": "solutions/automations/course-feedback-response/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/employee-certificate/.clasp.json",
    "content": "{ \"scriptId\": \"1f0EhMh_a2Jtq3DS96ZWeG2XviJ-XHSStB2B3mVXODPz3KyojS7nFRzV-\" }\n"
  },
  {
    "path": "solutions/automations/employee-certificate/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/employee-certificate\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst slideTemplateId = \"PRESENTATION_ID\";\nconst tempFolderId = \"FOLDER_ID\"; // Create an empty folder in Google Drive\n\n/**\n * Creates a custom menu \"Appreciation\" in the spreadsheet\n * with drop-down options to create and send certificates\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  ui.createMenu(\"Appreciation\")\n    .addItem(\"Create certificates\", \"createCertificates\")\n    .addSeparator()\n    .addItem(\"Send certificates\", \"sendCertificates\")\n    .addToUi();\n}\n\n/**\n * Creates a personalized certificate for each employee\n * and stores every individual Slides doc on Google Drive\n */\nfunction createCertificates() {\n  // Load the Google Slide template file\n  const template = DriveApp.getFileById(slideTemplateId);\n\n  // Get all employee data from the spreadsheet and identify the headers\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();\n  const values = sheet.getDataRange().getValues();\n  const headers = values[0];\n  const empNameIndex = headers.indexOf(\"Employee Name\");\n  const dateIndex = headers.indexOf(\"Date\");\n  const managerNameIndex = headers.indexOf(\"Manager Name\");\n  const titleIndex = headers.indexOf(\"Title\");\n  const compNameIndex = headers.indexOf(\"Company Name\");\n  const empEmailIndex = headers.indexOf(\"Employee Email\");\n  const empSlideIndex = headers.indexOf(\"Employee Slide\");\n  const statusIndex = headers.indexOf(\"Status\");\n\n  // Iterate through each row to capture individual details\n  for (let i = 1; i < values.length; i++) {\n    const rowData = values[i];\n    const empName = rowData[empNameIndex];\n    const date = rowData[dateIndex];\n    const managerName = rowData[managerNameIndex];\n    const title = rowData[titleIndex];\n    const compName = rowData[compNameIndex];\n\n    // Make a copy of the Slide template and rename it with employee name\n    const tempFolder = DriveApp.getFolderById(tempFolderId);\n    const empSlideId = template.makeCopy(tempFolder).setName(empName).getId();\n    const empSlide = SlidesApp.openById(empSlideId).getSlides()[0];\n\n    // Replace placeholder values with actual employee related details\n    empSlide.replaceAllText(\"Employee Name\", empName);\n    empSlide.replaceAllText(\n      \"Date\",\n      `Date: ${Utilities.formatDate(\n        date,\n        Session.getScriptTimeZone(),\n        \"MMMM dd, yyyy\",\n      )}`,\n    );\n    empSlide.replaceAllText(\"Your Name\", managerName);\n    empSlide.replaceAllText(\"Title\", title);\n    empSlide.replaceAllText(\"Company Name\", compName);\n\n    // Update the spreadsheet with the new Slide Id and status\n    sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId);\n    sheet.getRange(i + 1, statusIndex + 1).setValue(\"CREATED\");\n    SpreadsheetApp.flush();\n  }\n}\n\n/**\n * Send an email to each individual employee\n * with a PDF attachment of their appreciation certificate\n */\nfunction sendCertificates() {\n  // Get all employee data from the spreadsheet and identify the headers\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();\n  const values = sheet.getDataRange().getValues();\n  const headers = values[0];\n  const empNameIndex = headers.indexOf(\"Employee Name\");\n  const dateIndex = headers.indexOf(\"Date\");\n  const managerNameIndex = headers.indexOf(\"Manager Name\");\n  const titleIndex = headers.indexOf(\"Title\");\n  const compNameIndex = headers.indexOf(\"Company Name\");\n  const empEmailIndex = headers.indexOf(\"Employee Email\");\n  const empSlideIndex = headers.indexOf(\"Employee Slide\");\n  const statusIndex = headers.indexOf(\"Status\");\n\n  // Iterate through each row to capture individual details\n  for (let i = 1; i < values.length; i++) {\n    const rowData = values[i];\n    const empName = rowData[empNameIndex];\n    const date = rowData[dateIndex];\n    const managerName = rowData[managerNameIndex];\n    const title = rowData[titleIndex];\n    const compName = rowData[compNameIndex];\n    const empSlideId = rowData[empSlideIndex];\n    const empEmail = rowData[empEmailIndex];\n\n    // Load the employee's personalized Google Slide file\n    const attachment = DriveApp.getFileById(empSlideId);\n\n    // Setup the required parameters and send them the email\n    const senderName = \"CertBot\";\n    const subject = `${empName}, you're awesome!`;\n    const body = `Please find your employee appreciation certificate attached.\\n\\n${compName} team`;\n    GmailApp.sendEmail(empEmail, subject, body, {\n      attachments: [attachment.getAs(MimeType.PDF)],\n      name: senderName,\n    });\n\n    // Update the spreadsheet with email status\n    sheet.getRange(i + 1, statusIndex + 1).setValue(\"SENT\");\n    SpreadsheetApp.flush();\n  }\n}\n"
  },
  {
    "path": "solutions/automations/employee-certificate/README.md",
    "content": "# Send personalized appreciation certificates to employees\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/employee-certificate) for additional details.\n"
  },
  {
    "path": "solutions/automations/employee-certificate/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/equipment-requests/.clasp.json",
    "content": "{ \"scriptId\": \"1T0G2Qr0QkHfqOK8dqjdiMRGuX2UVzkQU3BGfl2lC3wsNwkSmISbp2q6t\" }\n"
  },
  {
    "path": "solutions/automations/equipment-requests/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/equipment-requests\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Update this variable with the email address you want to send equipment requests to.\nconst REQUEST_NOTIFICATION_EMAIL = \"request_intake@example.com\";\n\n// Update the following variables with your own equipment options.\nconst AVAILABLE_LAPTOPS = [\n  '15\" high Performance Laptop (OS X)',\n  '15\" high Performance Laptop (Windows)',\n  '15\" high performance Laptop (Linux)',\n  '13\" lightweight laptop (Windows)',\n];\nconst AVAILABLE_DESKTOPS = [\n  \"Standard workstation (Windows)\",\n  \"Standard workstation (Linux)\",\n  \"High performance workstation (Windows)\",\n  \"High performance workstation (Linux)\",\n  \"Mac Pro (OS X)\",\n];\nconst AVAILABLE_MONITORS = ['Single 27\"', 'Single 32\"', 'Dual 24\"'];\n\n// Form field titles, used for creating the form and as keys when handling\n// responses.\n/**\n * Adds a custom menu to the spreadsheet.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createMenu(\"Equipment requests\")\n    .addItem(\"Set up\", \"setup_\")\n    .addItem(\"Clean up\", \"cleanup_\")\n    .addToUi();\n}\n\n/**\n * Creates the form and triggers for the workflow.\n */\nfunction setup_() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  if (ss.getFormUrl()) {\n    const msg = \"Form already exists. Unlink the form and try again.\";\n    SpreadsheetApp.getUi().alert(msg);\n    return;\n  }\n  const form = FormApp.create(\"Equipment Requests\")\n    .setCollectEmail(true)\n    .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId())\n    .setLimitOneResponsePerUser(false);\n  form.addTextItem().setTitle(\"Employee name\").setRequired(true);\n  form.addTextItem().setTitle(\"Desk location\").setRequired(true);\n  form.addDateItem().setTitle(\"Due date\").setRequired(true);\n  form.addListItem().setTitle(\"Laptop\").setChoiceValues(AVAILABLE_LAPTOPS);\n  form.addListItem().setTitle(\"Desktop\").setChoiceValues(AVAILABLE_DESKTOPS);\n  form.addListItem().setTitle(\"Monitor\").setChoiceValues(AVAILABLE_MONITORS);\n\n  // Hide the raw form responses.\n  for (const sheet of ss.getSheets()) {\n    if (sheet.getFormUrl() === ss.getFormUrl()) {\n      sheet.hideSheet();\n    }\n  }\n  // Start workflow on each form submit\n  ScriptApp.newTrigger(\"onFormSubmit_\").forForm(form).onFormSubmit().create();\n  // Archive completed items every 5m.\n  ScriptApp.newTrigger(\"processCompletedItems_\")\n    .timeBased()\n    .everyMinutes(5)\n    .create();\n}\n\n/**\n * Cleans up the project (stop triggers, form submission, etc.)\n */\nfunction cleanup_() {\n  const formUrl = SpreadsheetApp.getActiveSpreadsheet().getFormUrl();\n  if (!formUrl) {\n    return;\n  }\n  for (const trigger of ScriptApp.getProjectTriggers()) {\n    ScriptApp.deleteTrigger(trigger);\n  }\n  FormApp.openByUrl(formUrl).deleteAllResponses().setAcceptingResponses(false);\n}\n\n/**\n * Handles new form submissions to trigger the workflow.\n *\n * @param {Object} event - Form submit event\n */\nfunction onFormSubmit_(event) {\n  const response = mapResponse_(event.response);\n  sendNewEquipmentRequestEmail_(response);\n  const equipmentDetails = Utilities.formatString(\n    \"%s\\n%s\\n%s\",\n    response.Laptop,\n    response.Desktop,\n    response.Monitor,\n  );\n  const row = [\n    \"New\",\n    \"\",\n    response[\"Due date\"],\n    response[\"Employee name\"],\n    response[\"Desk location\"],\n    equipmentDetails,\n    response.email,\n  ];\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheet = ss.getSheetByName(\"Pending requests\");\n  sheet.appendRow(row);\n}\n\n/**\n * Sweeps completed and cancelled requests, notifying the requestors and archiving them\n * to the completed sheet.\n *\n * @param {Object} event\n */\nfunction processCompletedItems_() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const pending = ss.getSheetByName(\"Pending requests\");\n  const completed = ss.getSheetByName(\"Completed requests\");\n  const rows = pending.getDataRange().getValues();\n  for (let i = rows.length; i >= 2; i--) {\n    const row = rows[i - 1];\n    const status = row[0];\n    if (status === \"Completed\" || status === \"Cancelled\") {\n      pending.deleteRow(i);\n      completed.appendRow(row);\n      console.log(`Deleted row: ${i}`);\n      sendEquipmentRequestCompletedEmail_({\n        \"Employee name\": row[3],\n        \"Desk location\": row[4],\n        email: row[6],\n      });\n    }\n  }\n}\n\n/**\n * Sends an email notification that a new equipment request has been submitted.\n *\n * @param {Object} request - Request details\n */\nfunction sendNewEquipmentRequestEmail_(request) {\n  const template = HtmlService.createTemplateFromFile(\n    \"new-equipment-request.html\",\n  );\n  template.request = request;\n  template.sheetUrl = SpreadsheetApp.getActiveSpreadsheet().getUrl();\n  const msg = template.evaluate();\n  MailApp.sendEmail({\n    to: REQUEST_NOTIFICATION_EMAIL,\n    subject: \"New equipment request\",\n    htmlBody: msg.getContent(),\n  });\n}\n\n/**\n * Sends an email notifying the requestor that the request is complete.\n *\n * @param {Object} request - Request details\n */\nfunction sendEquipmentRequestCompletedEmail_(request) {\n  const template = HtmlService.createTemplateFromFile(\"request-complete.html\");\n  template.request = request;\n  const msg = template.evaluate();\n  MailApp.sendEmail({\n    to: request.email,\n    subject: \"Equipment request completed\",\n    htmlBody: msg.getContent(),\n  });\n}\n\n/**\n * Converts a form response to an object keyed by the item titles. Allows easier\n * access to response values.\n *\n * @param {FormResponse} response\n * @return {Object} Form values keyed by question title\n */\nfunction mapResponse_(response) {\n  const initialValue = {\n    email: response.getRespondentEmail(),\n    timestamp: response.getTimestamp(),\n  };\n  return response.getItemResponses().reduce((obj, itemResponse) => {\n    const key = itemResponse.getItem().getTitle();\n    obj[key] = itemResponse.getResponse();\n    return obj;\n  }, initialValue);\n}\n"
  },
  {
    "path": "solutions/automations/equipment-requests/README.md",
    "content": "# Manage new employee equipment requests\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/equipment-requests) for additional details.\n"
  },
  {
    "path": "solutions/automations/equipment-requests/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/equipment-requests/new-equipment-request.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <body>\n    <p>\n    A new equipment request has been made by <?= request.email ?>.\n    </p>\n    \n    <p>\n    Employee name: <?= request['Employee name'] ?><br/>\n    Desk location name: <?= request['Desk location'] ?><br/>\n    Due date: <?= request['Due date'] ?><br/>\n    Laptop model: <?= request['Laptop'] ?><br/>\n    Desktop model: <?= request['Desktop'] ?><br/>\n    Monitor(s): <?= request['Monitor'] ?><br/>\n    </p>\n    \n    See <a href=\"<?= sheetUrl ?>\">the spreadsheet</a> to take or assign this item.\n  </body>\n</html>\n"
  },
  {
    "path": "solutions/automations/equipment-requests/request-complete.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <body>\n    <p>\n    An equipment request has been completed.\n    </p>\n    \n    <p>\n    Employee name: <?= request['Employee name'] ?><br/>\n    Desk location name: <?= request['Desk location'] ?><br/>\n    </p>\n  </body>\n</html>\n"
  },
  {
    "path": "solutions/automations/event-session-signup/.clasp.json",
    "content": "{ \"scriptId\": \"1RTfpaBw-RYW8PTJsidiqXHRrqaKnwMWAK_nq4LnWk9xXKGJWi_bhexRj\" }\n"
  },
  {
    "path": "solutions/automations/event-session-signup/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/event-session-signup\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Inserts a custom menu when the spreadsheet opens.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createMenu(\"Conference\")\n    .addItem(\"Set up conference\", \"setUpConference_\")\n    .addToUi();\n}\n\n/**\n * Uses the conference data in the spreadsheet to create\n * Google Calendar events, a Google Form, and a trigger that allows the script\n * to react to form responses.\n */\nfunction setUpConference_() {\n  const scriptProperties = PropertiesService.getScriptProperties();\n  if (scriptProperties.getProperty(\"calId\")) {\n    Browser.msgBox(\n      \"Your conference is already set up. Look in Google Drive for your\" +\n        \" sign-up form!\",\n    );\n    return;\n  }\n  const ss = SpreadsheetApp.getActive();\n  const sheet = ss.getSheetByName(\"Conference Setup\");\n  const range = sheet.getDataRange();\n  const values = range.getValues();\n  setUpCalendar_(values, range);\n  setUpForm_(ss, values);\n  ScriptApp.newTrigger(\"onFormSubmit\")\n    .forSpreadsheet(ss)\n    .onFormSubmit()\n    .create();\n}\n\n/**\n * Creates a Google Calendar with events for each conference session in the\n * spreadsheet, then writes the event IDs to the spreadsheet for future use.\n * @param {Array<string[]>} values Cell values for the spreadsheet range.\n * @param {Range} range A spreadsheet range that contains conference data.\n */\nfunction setUpCalendar_(values, range) {\n  const cal = CalendarApp.createCalendar(\"Conference Calendar\");\n  // Start at 1 to skip the header row.\n  for (let i = 1; i < values.length; i++) {\n    const session = values[i];\n    const title = session[0];\n    const start = joinDateAndTime_(session[1], session[2]);\n    const end = joinDateAndTime_(session[1], session[3]);\n    const options = { location: session[4], sendInvites: true };\n    const event = cal\n      .createEvent(title, start, end, options)\n      .setGuestsCanSeeGuests(false);\n    session[5] = event.getId();\n  }\n  range.setValues(values);\n\n  // Stores the ID for the Calendar, which is needed to retrieve events by ID.\n  const scriptProperties = PropertiesService.getScriptProperties();\n  scriptProperties.setProperty(\"calId\", cal.getId());\n}\n\n/**\n * Creates a single Date object from separate date and time cells.\n *\n * @param {Date} date A Date object from which to extract the date.\n * @param {Date} time A Date object from which to extract the time.\n * @return {Date} A Date object representing the combined date and time.\n */\nfunction joinDateAndTime_(date_, time) {\n  const processedDate = new Date(date_);\n  processedDate.setHours(time.getHours());\n  processedDate.setMinutes(time.getMinutes());\n  return processedDate;\n}\n\n/**\n * Creates a Google Form that allows respondents to select which conference\n * sessions they would like to attend, grouped by date and start time in the\n * caller's time zone.\n *\n * @param {Spreadsheet} ss The spreadsheet that contains the conference data.\n * @param {Array<String[]>} values Cell values for the spreadsheet range.\n */\nfunction setUpForm_(ss, values) {\n  // Group the sessions by date and time so that they can be passed to the form.\n  const schedule = {};\n  // Start at 1 to skip the header row.\n  for (let i = 1; i < values.length; i++) {\n    const session = values[i];\n    const day = session[1].toLocaleDateString();\n    const time = session[2].toLocaleTimeString();\n    if (!schedule[day]) {\n      schedule[day] = {};\n    }\n    if (!schedule[day][time]) {\n      schedule[day][time] = [];\n    }\n    schedule[day][time].push(session[0]);\n  }\n\n  // Creates the form and adds a multiple-choice question for each timeslot.\n  const form = FormApp.create(\"Conference Form\");\n  form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId());\n  form.addTextItem().setTitle(\"Name\").setRequired(true);\n  form.addTextItem().setTitle(\"Email\").setRequired(true);\n  for (const day of Object.keys(schedule)) {\n    form.addSectionHeaderItem().setTitle(`Sessions for ${day}`);\n    for (const time of Object.keys(schedule[day])) {\n      form\n        .addMultipleChoiceItem()\n        .setTitle(`${time} ${day}`)\n        .setChoiceValues(schedule[day][time]);\n    }\n  }\n}\n\n/**\n * Sends out calendar invitations and a\n * personalized Google Docs itinerary after a user responds to the form.\n *\n * @param {Object} e The event parameter for form submission to a spreadsheet;\n *     see https://developers.google.com/apps-script/understanding_events\n */\nfunction onFormSubmit(e) {\n  const user = {\n    name: e.namedValues.Name[0],\n    email: e.namedValues.Email[0],\n  };\n\n  // Grab the session data again so that we can match it to the user's choices.\n  const response = [];\n  const values = SpreadsheetApp.getActive()\n    .getSheetByName(\"Conference Setup\")\n    .getDataRange()\n    .getValues();\n  for (let i = 1; i < values.length; i++) {\n    const session = values[i];\n    const title = session[0];\n    const day = session[1].toLocaleDateString();\n    const time = session[2].toLocaleTimeString();\n    const timeslot = `${time} ${day}`;\n\n    // For every selection in the response, find the matching timeslot and title\n    // in the spreadsheet and add the session data to the response array.\n    if (e.namedValues[timeslot] && e.namedValues[timeslot] === title) {\n      response.push(session);\n    }\n  }\n  sendInvites_(user, response);\n  sendDoc_(user, response);\n}\n\n/**\n * Add the user as a guest for every session he or she selected.\n * @param {object} user An object that contains the user's name and email.\n * @param {Array<String[]>} response An array of data for the user's session choices.\n */\nfunction sendInvites_(user, response) {\n  const id = ScriptProperties.getProperty(\"calId\");\n  const cal = CalendarApp.getCalendarById(id);\n  for (let i = 0; i < response.length; i++) {\n    cal.getEventSeriesById(response[i][5]).addGuest(user.email);\n  }\n}\n\n/**\n * Creates and shares a personalized Google Doc that shows the user's itinerary.\n * @param {object} user An object that contains the user's name and email.\n * @param {Array<string[]>} response An array of data for the user's session choices.\n */\nfunction sendDoc_(user, response) {\n  const doc = DocumentApp.create(\n    `Conference Itinerary for ${user.name}`,\n  ).addEditor(user.email);\n  const body = doc.getBody();\n  let table = [[\"Session\", \"Date\", \"Time\", \"Location\"]];\n  for (let i = 0; i < response.length; i++) {\n    table.push([\n      response[i][0],\n      response[i][1].toLocaleDateString(),\n      response[i][2].toLocaleTimeString(),\n      response[i][4],\n    ]);\n  }\n  body\n    .insertParagraph(0, doc.getName())\n    .setHeading(DocumentApp.ParagraphHeading.HEADING1);\n  table = body.appendTable(table);\n  table.getRow(0).editAsText().setBold(true);\n  doc.saveAndClose();\n\n  // Emails a link to the Doc as well as a PDF copy.\n  MailApp.sendEmail({\n    to: user.email,\n    subject: doc.getName(),\n    body: `Thanks for registering! Here's your itinerary: ${doc.getUrl()}`,\n    attachments: doc.getAs(MimeType.PDF),\n  });\n}\n\n/**\n * Removes the calId script property so that the 'setUpConference_()' can be run again.\n */\nfunction resetProperties() {\n  const scriptProperties = PropertiesService.getScriptProperties();\n  scriptProperties.deleteAllProperties();\n}\n"
  },
  {
    "path": "solutions/automations/event-session-signup/README.md",
    "content": "# Create a sign-up for sessions at a conference\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/event-session-signup) for additional details.\n"
  },
  {
    "path": "solutions/automations/event-session-signup/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/feedback-sentiment-analysis/.clasp.json",
    "content": "{ \"scriptId\": \"1LOheMLQDlSkvmlt8EQOGGETewdt8tKWyzxspCwqzfianqxTXjBGpAc8c\" }\n"
  },
  {
    "path": "solutions/automations/feedback-sentiment-analysis/README.md",
    "content": "# Analyze sentiment of open-ended feedback\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/feedback-sentiment-analysis) for additional details."
  },
  {
    "path": "solutions/automations/feedback-sentiment-analysis/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {\n    \"libraries\": [\n      {\n        \"userSymbol\": \"OAuth2\",\n        \"libraryId\": \"1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF\",\n        \"version\": \"24\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/feedback-sentiment-analysis/code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/feedback-sentiment-analysis\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Sets API key for accessing Cloud Natural Language API.\nconst myApiKey = \"YOUR_API_KEY\"; // Replace with your API key.\n\n// Matches column names in Review Data sheet to variables.\nconst COLUMN_NAME = {\n  COMMENTS: \"comments\",\n  ENTITY: \"entity_sentiment\",\n  ID: \"id\",\n};\n\n/**\n * Creates a Demo menu in Google Spreadsheets.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi()\n    .createMenu(\"Sentiment Tools\")\n    .addItem(\"Mark entities and sentiment\", \"markEntitySentiment\")\n    .addToUi();\n}\n\n/**\n * Analyzes entities and sentiment for each comment in\n * Review Data sheet and copies results into the\n * Entity Sentiment Data sheet.\n */\nfunction markEntitySentiment() {\n  // Sets variables for \"Review Data\" sheet\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const dataSheet = ss.getSheetByName(\"Review Data\");\n  const rows = dataSheet.getDataRange();\n  const numRows = rows.getNumRows();\n  const values = rows.getValues();\n  const headerRow = values[0];\n\n  // Checks to see if \"Entity Sentiment Data\" sheet is present, and\n  // if not, creates a new sheet and sets the header row.\n  const entitySheet = ss.getSheetByName(\"Entity Sentiment Data\");\n  if (entitySheet == null) {\n    ss.insertSheet(\"Entity Sentiment Data\");\n    const entitySheet = ss.getSheetByName(\"Entity Sentiment Data\");\n    const esHeaderRange = entitySheet.getRange(1, 1, 1, 6);\n    const esHeader = [\n      [\n        \"Review ID\",\n        \"Entity\",\n        \"Salience\",\n        \"Sentiment Score\",\n        \"Sentiment Magnitude\",\n        \"Number of mentions\",\n      ],\n    ];\n    esHeaderRange.setValues(esHeader);\n  }\n\n  // Finds the column index for comments, language_detected,\n  // and comments_english columns.\n  const textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS);\n  const entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY);\n  const idColumnIdx = headerRow.indexOf(COLUMN_NAME.ID);\n  if (entityColumnIdx === -1) {\n    Browser.msgBox(\n      `Error: Could not find the column named ${COLUMN_NAME.ENTITY}. Please create an empty column with header \"entity_sentiment\" on the Review Data tab.`,\n    );\n    return; // bail\n  }\n\n  ss.toast(\"Analyzing entities and sentiment...\");\n  for (let i = 0; i < numRows; ++i) {\n    const value = values[i];\n    const commentEnCellVal = value[textColumnIdx];\n    const entityCellVal = value[entityColumnIdx];\n    const reviewId = value[idColumnIdx];\n\n    // Calls retrieveEntitySentiment function for each row that has a comment\n    // and also an empty entity_sentiment cell value.\n    if (commentEnCellVal && !entityCellVal) {\n      const nlData = retrieveEntitySentiment(commentEnCellVal);\n      // Pastes each entity and sentiment score into Entity Sentiment Data sheet.\n      const newValues = [];\n      for (let entity in nlData.entities) {\n        entity = nlData.entities[entity];\n        const row = [\n          reviewId,\n          entity.name,\n          entity.salience,\n          entity.sentiment.score,\n          entity.sentiment.magnitude,\n          entity.mentions.length,\n        ];\n        newValues.push(row);\n      }\n      if (newValues.length) {\n        entitySheet\n          .getRange(\n            entitySheet.getLastRow() + 1,\n            1,\n            newValues.length,\n            newValues[0].length,\n          )\n          .setValues(newValues);\n      }\n      // Pastes \"complete\" into entity_sentiment column to denote completion of NL API call.\n      dataSheet.getRange(i + 1, entityColumnIdx + 1).setValue(\"complete\");\n    }\n  }\n}\n\n/**\n * Calls the Cloud Natural Language API with a string of text to analyze\n * entities and sentiment present in the string.\n * @param {String} the string for entity sentiment analysis\n * @return {Object} the entities and related sentiment present in the string\n */\nfunction retrieveEntitySentiment(line) {\n  const apiKey = myApiKey;\n  const apiEndpoint = `https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=${apiKey}`;\n  // Creates a JSON request, with text string, language, type and encoding\n  const nlData = {\n    document: {\n      language: \"en-us\",\n      type: \"PLAIN_TEXT\",\n      content: line,\n    },\n    encodingType: \"UTF8\",\n  };\n  // Packages all of the options and the data together for the API call.\n  const nlOptions = {\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: JSON.stringify(nlData),\n  };\n  // Makes the API call.\n  const response = UrlFetchApp.fetch(apiEndpoint, nlOptions);\n  return JSON.parse(response);\n}\n"
  },
  {
    "path": "solutions/automations/folder-creation/Code.js",
    "content": "/*\nCopyright 2022 Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/* \nThis function will create a new folder in the defined Shard Drive.\nYou define the Shared Drive by adding its ID on line number 26.\nThe parameter 'project' is passed in from the AppSheet app. \nPlease watch this video tutorial to see how to use this script: https://youtu.be/Utl57R7I2Cs\n*/\n\nfunction createNewFolder(project) {\n  const folder = Drive.Files.insert(\n    {\n      parents: [{ id: \"ADD YOUR SHARED DRIVE FOLDER ID HERE\" }],\n      title: project,\n      mimeType: \"application/vnd.google-apps.folder\",\n    },\n    null,\n    { supportsAllDrives: true },\n  );\n\n  return folder.alternateLink;\n}\n"
  },
  {
    "path": "solutions/automations/folder-creation/README.md",
    "content": "# Folder creation\n\nThis code sample is part of a video tutorial on how to combine AppSheet and Apps Script.\n\nYou can watch the video tutorial to find out how to use the sample.\n\n<p align=\"center\">\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/Utl57R7I2Cs\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</p>\n\nSee the [Google Apps Script Documentation](https://developers.google.com/apps-script/advanced/drive) for additional information about the advanced Google Drive services.\n"
  },
  {
    "path": "solutions/automations/folder-creation/appscript.json",
    "content": "{\n  \"timeZone\": \"Europe/Madrid\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"version\": \"v2\",\n        \"serviceId\": \"drive\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/.clasp.json",
    "content": "{ \"scriptId\": \"1k9PjGdQ_G0HKEoS3np_Szfe-flmLw9gUvblQIxOfvTmS-NLeLgVUzvOa\" }\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/generate-pdfs\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// TODO: To test this solution, set EMAIL_OVERRIDE to true and set EMAIL_ADDRESS_OVERRIDE to your email address.\nconst EMAIL_OVERRIDE = false;\nconst EMAIL_ADDRESS_OVERRIDE = \"test@example.com\";\n\n// Application constants\nconst APP_TITLE = \"Generate and send PDFs\";\nconst OUTPUT_FOLDER_NAME = \"Customer PDFs\";\nconst DUE_DATE_NUM_DAYS = 15;\n\n// Sheet name constants. Update if you change the names of the sheets.\nconst CUSTOMERS_SHEET_NAME = \"Customers\";\nconst PRODUCTS_SHEET_NAME = \"Products\";\nconst TRANSACTIONS_SHEET_NAME = \"Transactions\";\nconst INVOICES_SHEET_NAME = \"Invoices\";\nconst INVOICE_TEMPLATE_SHEET_NAME = \"Invoice Template\";\n\n// Email constants\nconst EMAIL_SUBJECT = \"Invoice Notification\";\nconst EMAIL_BODY = \"Hello!\\rPlease see the attached PDF document.\";\n\n/**\n * Iterates through the worksheet data populating the template sheet with\n * customer data, then saves each instance as a PDF document.\n *\n * Called by user via custom menu item.\n */\nfunction processDocuments() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const customersSheet = ss.getSheetByName(CUSTOMERS_SHEET_NAME);\n  const productsSheet = ss.getSheetByName(PRODUCTS_SHEET_NAME);\n  const transactionsSheet = ss.getSheetByName(TRANSACTIONS_SHEET_NAME);\n  const invoicesSheet = ss.getSheetByName(INVOICES_SHEET_NAME);\n  const invoiceTemplateSheet = ss.getSheetByName(INVOICE_TEMPLATE_SHEET_NAME);\n\n  // Gets data from the storage sheets as objects.\n  const customers = dataRangeToObject(customersSheet);\n  const products = dataRangeToObject(productsSheet);\n  const transactions = dataRangeToObject(transactionsSheet);\n\n  ss.toast(\"Creating Invoices\", APP_TITLE, 1);\n  const invoices = [];\n\n  // Iterates for each customer calling createInvoiceForCustomer routine.\n  for (const customer of customers) {\n    ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1);\n    const invoice = createInvoiceForCustomer(\n      customer,\n      products,\n      transactions,\n      invoiceTemplateSheet,\n      ss.getId(),\n    );\n    invoices.push(invoice);\n  }\n  // Writes invoices data to the sheet.\n  invoicesSheet\n    .getRange(2, 1, invoices.length, invoices[0].length)\n    .setValues(invoices);\n}\n\n/**\n * Processes each customer instance with passed in data parameters.\n *\n * @param {object} customer - Object for the customer\n * @param {object} products - Object for all the products\n * @param {object} transactions - Object for all the transactions\n * @param {object} invoiceTemplateSheet - Object for the invoice template sheet\n * @param {string} ssId - Google Sheet ID\n * Return {array} of instance customer invoice data\n */\nfunction createInvoiceForCustomer(\n  customer,\n  products,\n  transactions,\n  templateSheet,\n  ssId,\n) {\n  const customerTransactions = transactions.filter(\n    (transaction) => transaction.customer_name === customer.customer_name,\n  );\n\n  // Clears existing data from the template.\n  clearTemplateSheet();\n\n  const lineItems = [];\n  let totalAmount = 0;\n  for (const lineItem of customerTransactions) {\n    const lineItemProduct = products.filter(\n      (product) => product.sku_name === lineItem.sku,\n    )[0];\n    const qty = Number.parseInt(lineItem.licenses);\n    const price = Number.parseFloat(lineItemProduct.price).toFixed(2);\n    const amount = Number.parseFloat(qty * price).toFixed(2);\n    lineItems.push([\n      lineItemProduct.sku_name,\n      lineItemProduct.sku_description,\n      \"\",\n      qty,\n      price,\n      amount,\n    ]);\n    totalAmount += Number.parseFloat(amount);\n  }\n\n  // Generates a random invoice number. You can replace with your own document ID method.\n  const invoiceNumber = Math.floor(100000 + Math.random() * 900000);\n\n  // Calulates dates.\n  const todaysDate = new Date().toDateString();\n  const dueDate = new Date(\n    Date.now() + 1000 * 60 * 60 * 24 * DUE_DATE_NUM_DAYS,\n  ).toDateString();\n\n  // Sets values in the template.\n  templateSheet.getRange(\"B10\").setValue(customer.customer_name);\n  templateSheet.getRange(\"B11\").setValue(customer.address);\n  templateSheet.getRange(\"F10\").setValue(invoiceNumber);\n  templateSheet.getRange(\"F12\").setValue(todaysDate);\n  templateSheet.getRange(\"F14\").setValue(dueDate);\n  templateSheet.getRange(18, 2, lineItems.length, 6).setValues(lineItems);\n\n  // Cleans up and creates PDF.\n  SpreadsheetApp.flush();\n  Utilities.sleep(500); // Using to offset any potential latency in creating .pdf\n  const pdf = createPDF(\n    ssId,\n    templateSheet,\n    `Invoice#${invoiceNumber}-${customer.customer_name}`,\n  );\n  return [\n    invoiceNumber,\n    todaysDate,\n    customer.customer_name,\n    customer.email,\n    \"\",\n    totalAmount,\n    dueDate,\n    pdf.getUrl(),\n    \"No\",\n  ];\n}\n\n/**\n * Resets the template sheet by clearing out customer data.\n * You use this to prepare for the next iteration or to view blank\n * the template for design.\n *\n * Called by createInvoiceForCustomer() or by the user via custom menu item.\n */\nfunction clearTemplateSheet() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const templateSheet = ss.getSheetByName(INVOICE_TEMPLATE_SHEET_NAME);\n  // Clears existing data from the template.\n  const rngClear = templateSheet\n    .getRangeList([\"B10:B11\", \"F10\", \"F12\", \"F14\"])\n    .getRanges();\n  for (const cell of rngClear) {\n    cell.clearContent();\n  }\n  // This sample only accounts for six rows of data 'B18:G24'. You can extend or make dynamic as necessary.\n  templateSheet.getRange(18, 2, 7, 6).clearContent();\n}\n\n/**\n * Creates a PDF for the customer given sheet.\n * @param {string} ssId - Id of the Google Spreadsheet\n * @param {object} sheet - Sheet to be converted as PDF\n * @param {string} pdfName - File name of the PDF being created\n * @return {file object} PDF file as a blob\n */\nfunction createPDF(ssId, sheet, pdfName) {\n  const fr = 0;\n  const fc = 0;\n  const lc = 9;\n  const lr = 27;\n  const url = `https://docs.google.com/spreadsheets/d/${ssId}/export?format=pdf&size=7&fzr=true&portrait=true&fitw=true&gridlines=false&printtitle=false&top_margin=0.5&bottom_margin=0.25&left_margin=0.5&right_margin=0.5&sheetnames=false&pagenum=UNDEFINED&attachment=true&gid=${sheet.getSheetId()}&r1=${fr}&c1=${fc}&r2=${lr}&c2=${lc}`;\n\n  const params = {\n    method: \"GET\",\n    headers: { authorization: `Bearer ${ScriptApp.getOAuthToken()}` },\n  };\n  const blob = UrlFetchApp.fetch(url, params)\n    .getBlob()\n    .setName(`${pdfName}.pdf`);\n\n  // Gets the folder in Drive where the PDFs are stored.\n  const folder = getFolderByName_(OUTPUT_FOLDER_NAME);\n\n  const pdfFile = folder.createFile(blob);\n  return pdfFile;\n}\n\n/**\n * Sends emails with PDF as an attachment.\n * Checks/Sets 'Email Sent' column to 'Yes' to avoid resending.\n *\n * Called by user via custom menu item.\n */\nfunction sendEmails() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const invoicesSheet = ss.getSheetByName(INVOICES_SHEET_NAME);\n  const invoicesData = invoicesSheet\n    .getRange(1, 1, invoicesSheet.getLastRow(), invoicesSheet.getLastColumn())\n    .getValues();\n  const keysI = invoicesData.splice(0, 1)[0];\n  const invoices = getObjects(invoicesData, createObjectKeys(keysI));\n  ss.toast(\"Emailing Invoices\", APP_TITLE, 1);\n  invoices.forEach((invoice, index) => {\n    if (invoice.email_sent !== \"Yes\") {\n      ss.toast(`Emailing Invoice for ${invoice.customer}`, APP_TITLE, 1);\n\n      const fileId = invoice.invoice_link.match(/[-\\w]{25,}(?!.*[-\\w]{25,})/);\n      const attachment = DriveApp.getFileById(fileId);\n\n      let recipient = invoice.email;\n      if (EMAIL_OVERRIDE) {\n        recipient = EMAIL_ADDRESS_OVERRIDE;\n      }\n\n      GmailApp.sendEmail(recipient, EMAIL_SUBJECT, EMAIL_BODY, {\n        attachments: [attachment.getAs(MimeType.PDF)],\n        name: APP_TITLE,\n      });\n      invoicesSheet.getRange(index + 2, 9).setValue(\"Yes\");\n    }\n  });\n}\n\n/**\n * Helper function that turns sheet data range into an object.\n *\n * @param {SpreadsheetApp.Sheet} sheet - Sheet to process\n * Return {object} of a sheet's datarange as an object\n */\nfunction dataRangeToObject(sheet) {\n  const dataRange = sheet\n    .getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn())\n    .getValues();\n  const keys = dataRange.splice(0, 1)[0];\n  return getObjects(dataRange, createObjectKeys(keys));\n}\n\n/**\n * Utility function for mapping sheet data to objects.\n */\nfunction getObjects(data, keys) {\n  const objects = [];\n  for (let i = 0; i < data.length; ++i) {\n    const object = {};\n    let hasData = false;\n    for (let j = 0; j < data[i].length; ++j) {\n      const cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n// Creates object keys for column headers.\nfunction createObjectKeys(keys) {\n  return keys.map((key) => key.replace(/\\W+/g, \"_\").toLowerCase());\n}\n// Returns true if the cell where cellData was read from is empty.\nfunction isCellEmpty(cellData) {\n  return typeof cellData === \"string\" && cellData === \"\";\n}\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/Menu.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc\n *\n * The above comment specifies that this automation will only\n * attempt to read or modify the spreadsheet this script is bound to.\n * The authorization request message presented to users reflects the\n * limited scope.\n */\n\n/**\n * Creates a custom menu in the Google Sheets UI when the document is opened.\n *\n * @param {object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  const menu = SpreadsheetApp.getUi().createMenu(APP_TITLE);\n  menu\n    .addItem(\"Process invoices\", \"processDocuments\")\n    .addItem(\"Send emails\", \"sendEmails\")\n    .addSeparator()\n    .addItem(\"Reset template\", \"clearTemplateSheet\")\n    .addToUi();\n}\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/README.md",
    "content": "# Generate and send PDFs from Google Sheets\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/generate-pdfs) for additional details.\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/Utilities.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Returns a Google Drive folder in the same location\n * in Drive where the spreadsheet is located. First, it checks if the folder\n * already exists and returns that folder. If the folder doesn't already\n * exist, the script creates a new one. The folder's name is set by the\n * \"OUTPUT_FOLDER_NAME\" variable from the Code.gs file.\n *\n * @param {string} folderName - Name of the Drive folder.\n * @return {object} Google Drive Folder\n */\nfunction getFolderByName_(folderName) {\n  // Gets the Drive Folder of where the current spreadsheet is located.\n  const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();\n  const parentFolder = DriveApp.getFileById(ssId).getParents().next();\n\n  // Iterates the subfolders to check if the PDF folder already exists.\n  const subFolders = parentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === folderName) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one does not already exist.\n  return parentFolder\n    .createFolder(folderName)\n    .setDescription(\n      `Created by ${APP_TITLE} application to store PDF output files`,\n    );\n}\n\n/**\n * Test function to run getFolderByName_.\n * @prints a Google Drive FolderId.\n */\nfunction test_getFolderByName() {\n  // Gets the PDF folder in Drive.\n  const folder = getFolderByName_(OUTPUT_FOLDER_NAME);\n\n  console.log(\n    `Name: ${folder.getName()}\\rID: ${folder.getId()}\\rDescription: ${folder.getDescription()}`,\n  );\n  // To automatically delete test folder, uncomment the following code:\n  // folder.setTrashed(true);\n}\n"
  },
  {
    "path": "solutions/automations/generate-pdfs/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/.clasp.json",
    "content": "{ \"scriptId\": \"1ANsCqbcTeepCzPpAKRUSxavm-2bTtKhp6I-G530ddH315H-59LGofc6m\" }\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/Code.js",
    "content": "// To learn more about this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/import-csv-sheets\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This file contains the main functions that import data from CSV files into a Google Spreadsheet.\n */\n\n// Application constants\nconst APP_TITLE = \"Trigger-driven CSV import [App Script Sample]\"; // Application name\nconst APP_FOLDER = \"[App Script sample] Import CSVs\"; // Application primary folder\nconst SOURCE_FOLDER = \"Inbound CSV Files\"; // Folder for the update files.\nconst PROCESSED_FOLDER = \"Processed CSV Files\"; // Folder to hold processed files.\nconst SHEET_REPORT_NAME = \"Import CSVs\"; // Name of destination spreadsheet.\n\n// Application settings\nconst CSV_HEADER_EXIST = true; // Set to true if CSV files have a header row, false if not.\nconst HANDLER_FUNCTION = \"updateApplicationSheet\"; // Function called by installable trigger to run data processing.\n\n/**\n * Installs a time-driven trigger that runs daily to import CSVs into the main application spreadsheet.\n * Prior to creating a new instance, removes any existing triggers to avoid duplication.\n *\n * Called by setupSample() or run directly setting up the application.\n */\nfunction installTrigger() {\n  // Checks for an existing trigger to avoid creating duplicate instances.\n  // Removes existing if found.\n  const projectTriggers = ScriptApp.getProjectTriggers();\n  for (let i = 0; i < projectTriggers.length; i++) {\n    if (projectTriggers[i].getHandlerFunction() === HANDLER_FUNCTION) {\n      console.log(\n        `Existing trigger with Handler Function of '${HANDLER_FUNCTION}' removed.`,\n      );\n      ScriptApp.deleteTrigger(projectTriggers[i]);\n    }\n  }\n  // Creates the new trigger.\n  const newTrigger = ScriptApp.newTrigger(HANDLER_FUNCTION)\n    .timeBased()\n    .atHour(23) // Runs at 11 PM in the time zone of this script.\n    .everyDays(1) // Runs once per day.\n    .create();\n  console.log(\n    `New trigger with Handler Function of '${HANDLER_FUNCTION}' created.`,\n  );\n}\n\n/**\n * Handler function called by the trigger created with the \"installTrigger\" function.\n * Run this directly to execute the entire automation process of the application with a trigger.\n *\n * Process: Iterates through CSV files located in the source folder (SOURCE_FOLDER),\n * and appends them to the end of destination spreadsheet (SHEET_REPORT_NAME).\n * Successfully processed CSV files are moved to the processed folder (PROCESSED_FOLDER) to avoid duplication.\n * Sends summary email with status of the import.\n */\nfunction updateApplicationSheet() {\n  // Gets application & supporting folders.\n  const folderAppPrimary = getApplicationFolder_(APP_FOLDER);\n  const folderSource = getFolder_(SOURCE_FOLDER);\n  const folderProcessed = getFolder_(PROCESSED_FOLDER);\n\n  // Gets the application's destination spreadsheet {Spreadsheet object}\n  const objSpreadSheet = getSpreadSheet_(SHEET_REPORT_NAME, folderAppPrimary);\n\n  // Creates arrays to track every CSV file, categorized as processed sucessfully or not.\n  const filesProcessed = [];\n  const filesNotProcessed = [];\n\n  // Gets all CSV files found in the source folder.\n  const cvsFiles = folderSource.getFilesByType(MimeType.CSV);\n\n  // Iterates through each CSV file.\n  while (cvsFiles.hasNext()) {\n    const csvFile = cvsFiles.next();\n    const isSuccess = processCsv_(objSpreadSheet, csvFile);\n\n    if (isSuccess) {\n      // Moves the processed file to the processed folder to prevent future duplicate data imports.\n      csvFile.moveTo(folderProcessed);\n      // Logs the successfully processed file to the filesProcessed array.\n      filesProcessed.push(csvFile.getName());\n      console.log(`Successfully processed: ${csvFile.getName()}`);\n    } else {\n      // Doesn't move the unsuccesfully processed file so that it can be corrected and reprocessed later.\n      // Logs the unsuccessfully processed file to the filesNotProcessed array.\n      filesNotProcessed.push(csvFile.getName());\n      console.log(`Not processed: ${csvFile.getName()}`);\n    }\n  }\n\n  // Prepares summary email.\n  // Gets variables to link to this Apps Script project.\n  const scriptId = ScriptApp.getScriptId();\n  const scriptUrl = DriveApp.getFileById(scriptId).getUrl();\n  const scriptName = DriveApp.getFileById(scriptId).getName();\n\n  // Gets variables to link to the main application spreadsheet.\n  const sheetUrl = objSpreadSheet.getUrl();\n  const sheetName = objSpreadSheet.getName();\n\n  // Gets user email and timestamp.\n  const emailTo = Session.getEffectiveUser().getEmail();\n  const timestamp = Utilities.formatDate(\n    new Date(),\n    Session.getScriptTimeZone(),\n    \"yyyy-MM-dd HH:mm:ss zzzz\",\n  );\n\n  // Prepares lists and counts of processed CSV files.\n  let processedList = \"\";\n  const processedCount = filesProcessed.length;\n  for (const processed of filesProcessed) {\n    processedList += `${processed}<br>`;\n  }\n\n  const unProcessedCount = filesNotProcessed.length;\n  let unProcessedList = \"\";\n  for (const unProcessed of filesNotProcessed) {\n    unProcessedList += `${unProcessed}\\n`;\n  }\n\n  // Assembles email body as html.\n  const eMailBody = `${APP_TITLE} ran an automated process at ${timestamp}.<br><br><b>Files successfully updated:</b> ${processedCount}<br>${processedList}<br><b>Files not updated:</b> ${unProcessedCount}<br>${unProcessedList}<br><br>View all updates in the Google Sheets spreadsheet <b><a href= \"${sheetUrl}\" target=\\\"_blank\\\">${sheetName}</a></b>.<br><br>*************<br><br>This email was generated by Google Apps Script. To learn more about this application or make changes, open the script project below: <br><a href= \"${scriptUrl}\" target=\\\"_blank\\\">${scriptName}</a>`;\n\n  MailApp.sendEmail({\n    to: emailTo,\n    subject: `Automated email from ${APP_TITLE}`,\n    htmlBody: eMailBody,\n  });\n  console.log(`Email sent to ${emailTo}`);\n}\n\n/**\n * Parses CSV data into an array and appends it after the last row in the destination spreadsheet.\n *\n * @return {boolean} true if the update is successful, false if unexpected errors occur.\n */\nfunction processCsv_(objSpreadSheet, csvFile) {\n  try {\n    // Gets the first sheet of the destination spreadsheet.\n    const sheet = objSpreadSheet.getSheets()[0];\n\n    // Parses CSV file into data array.\n    const data = Utilities.parseCsv(csvFile.getBlob().getDataAsString());\n\n    // Omits header row if application variable CSV_HEADER_EXIST is set to 'true'.\n    if (CSV_HEADER_EXIST) {\n      data.splice(0, 1);\n    }\n    // Gets the row and column coordinates for next available range in the spreadsheet.\n    const startRow = sheet.getLastRow() + 1;\n    const startCol = 1;\n    // Determines the incoming data size.\n    const numRows = data.length;\n    const numColumns = data[0].length;\n\n    // Appends data into the sheet.\n    sheet.getRange(startRow, startCol, numRows, numColumns).setValues(data);\n    return true; // Success.\n  } catch {\n    return false; // Failure. Checks for CSV data file error.\n  }\n}\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/README.md",
    "content": "# Import CSV data to a spreadsheet\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/import-csv-sheets) for additional details.\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/SampleData.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains functions to access headings and data for sample files.\n *\n * Sample data is stored in the variable SAMPLE_DATA.\n */\n\n// Fictitious sample data.\nconst SAMPLE_DATA = {\n  headings: [\n    \"PropertyName\",\n    \"LeaseID\",\n    \"LeaseLocation\",\n    \"OwnerName\",\n    \"SquareFootage\",\n    \"RenewDate\",\n    \"LastAmount\",\n    \"LastPaymentDate\",\n    \"Revenue\",\n  ],\n  csvFiles: [\n    {\n      name: \"Sample One.CSV\",\n      rows: [\n        {\n          PropertyName: \"The Modern Building\",\n          LeaseID: \"271312\",\n          LeaseLocation: \"Mountain View CA 94045\",\n          OwnerName: \"Yuri\",\n          SquareFootage: \"17500\",\n          RenewDate: \"12/15/2022\",\n          LastAmount: \"100000\",\n          LastPaymentDate: \"3/01/2022\",\n          Revenue: \"12000\",\n        },\n        {\n          PropertyName: \"Garage @ 45\",\n          LeaseID: \"271320\",\n          LeaseLocation: \"Mountain View CA 94045\",\n          OwnerName: \"Luka\",\n          SquareFootage: \"1000\",\n          RenewDate: \"6/2/2022\",\n          LastAmount: \"50000\",\n          LastPaymentDate: \"4/01/2022\",\n          Revenue: \"20000\",\n        },\n        {\n          PropertyName: \"Office Park Deluxe\",\n          LeaseID: \"271301\",\n          LeaseLocation: \"Mountain View CA 94045\",\n          OwnerName: \"Sasha\",\n          SquareFootage: \"5000\",\n          RenewDate: \"6/2/2022\",\n          LastAmount: \"25000\",\n          LastPaymentDate: \"4/01/2022\",\n          Revenue: \"1200\",\n        },\n      ],\n    },\n    {\n      name: \"Sample Two.CSV\",\n      rows: [\n        {\n          PropertyName: \"Tours Jumelles Minuscules\",\n          LeaseID: \"271260\",\n          LeaseLocation: \"8 Rue du Nom Fictif 341 Paris\",\n          OwnerName: \"Lucian\",\n          SquareFootage: \"1000000\",\n          RenewDate: \"7/14/2022\",\n          LastAmount: \"1250000\",\n          LastPaymentDate: \"5/01/2022\",\n          Revenue: \"77777\",\n        },\n        {\n          PropertyName: \"Barraca da Praia\",\n          LeaseID: \"271281\",\n          LeaseLocation: \"Avenida da Pastelaria 1903 Lisbon 1229-076\",\n          OwnerName: \"Raha\",\n          SquareFootage: \"1000\",\n          RenewDate: \"6/2/2022\",\n          LastAmount: \"50000\",\n          LastPaymentDate: \"4/01/2022\",\n          Revenue: \"20000\",\n        },\n      ],\n    },\n    {\n      name: \"Sample Three.CSV\",\n      rows: [\n        {\n          PropertyName: \"Round Building in the Square\",\n          LeaseID: \"371260\",\n          LeaseLocation: \"8 Rue du Nom Fictif 341 Paris\",\n          OwnerName: \"Charlie\",\n          SquareFootage: \"75000\",\n          RenewDate: \"8/1/2022\",\n          LastAmount: \"250000\",\n          LastPaymentDate: \"6/01/2022\",\n          Revenue: \"22222\",\n        },\n        {\n          PropertyName: \"Square Building in the Round\",\n          LeaseID: \"371281\",\n          LeaseLocation: \"Avenida da Pastelaria 1903 Lisbon 1229-076\",\n          OwnerName: \"Lee\",\n          SquareFootage: \"10000\",\n          RenewDate: \"6/2/2022\",\n          LastAmount: \"5000\",\n          LastPaymentDate: \"4/01/2022\",\n          Revenue: \"1800\",\n        },\n      ],\n    },\n  ],\n};\n\n/**\n * Returns headings for use in destination spreadsheet and CSV files.\n * @return {string[][]} array of each column heading as string.\n */\nfunction getHeadings() {\n  const headings = [[]];\n  for (const i in SAMPLE_DATA.headings)\n    headings[0].push(SAMPLE_DATA.headings[i]);\n  return headings;\n}\n\n/**\n * Returns CSV file names and content to create sample CSV files.\n * @return {object[]} {\"file\": [\"name\",\"csv\"]}\n */\nfunction getCSVFilesData() {\n  const files = [];\n\n  // Gets headings once - same for all files/rows.\n  let csvHeadings = \"\";\n  for (const i in SAMPLE_DATA.headings)\n    csvHeadings += `${SAMPLE_DATA.headings[i]},`;\n\n  // Gets data for each file by rows.\n  for (const i in SAMPLE_DATA.csvFiles) {\n    let sampleCSV = \"\";\n    sampleCSV += csvHeadings;\n    const fileName = SAMPLE_DATA.csvFiles[i].name;\n    for (const j in SAMPLE_DATA.csvFiles[i].rows) {\n      sampleCSV += \"\\n\";\n      for (const k in SAMPLE_DATA.csvFiles[i].rows[j]) {\n        sampleCSV += `${SAMPLE_DATA.csvFiles[i].rows[j][k]},`;\n      }\n    }\n    files.push({ name: fileName, csv: sampleCSV });\n  }\n  return files;\n}\n\n/*\n * Checks data functions are working as necessary.\n */\nfunction test_getHeadings() {\n  const h = getHeadings();\n  console.log(h);\n  console.log(h[0].length);\n}\n\nfunction test_getCSVFilesData() {\n  const csvFiles = getCSVFilesData();\n  console.log(csvFiles);\n\n  for (const file of csvFiles) {\n    console.log(file.name);\n    console.log(file.csv);\n  }\n}\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/SetupSample.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains functions that set up the folders and sample files used to demo the application.\n *\n * Sample data for the application is stored in the SampleData.gs file.\n */\n\n// Global variables for sample setup.\nconst INCLUDE_SAMPLE_DATA_FILES = true; // Set to true to create sample data files, false to skip.\n\n/**\n * Runs the setup for the sample.\n * 1) Creates the application folder and subfolders for unprocessed/processed CSV files.\n *    from global variables APP_FOLDER | SOURCE_FOLDER | PROCESSED_FOLDER\n * 2) Creates the sample Sheets spreadsheet in the application folder.\n *    from global variable SHEET_REPORT_NAME\n * 3) Creates CSV files from sample data in the unprocessed files folder.\n *    from variable SAMPLE_DATA in SampleData.gs.\n * 4) Creates an installable trigger to run process automatically at a specified time interval.\n */\nfunction setupSample() {\n  console.log(`Application setup for: ${APP_TITLE}`);\n\n  // Creates application folder.\n  const folderAppPrimary = getApplicationFolder_(APP_FOLDER);\n  // Creates supporting folders.\n  const folderSource = getFolder_(SOURCE_FOLDER);\n  const folderProcessed = getFolder_(PROCESSED_FOLDER);\n\n  console.log(\n    `Application folders: ${folderAppPrimary.getName()}, ${folderSource.getName()}, ${folderProcessed.getName()}`,\n  );\n\n  if (INCLUDE_SAMPLE_DATA_FILES) {\n    // Sets up primary destination spreadsheet\n    const sheet = setupPrimarySpreadsheet_(folderAppPrimary);\n\n    // Gets the CSV files data - refer to the SampleData.gs file to view.\n    const csvFiles = getCSVFilesData();\n\n    // Processes each CSV file.\n    for (const file of csvFiles) {\n      // Creates CSV file in source folder if it doesn't exist.\n      if (!fileExists_(file.name, folderSource)) {\n        const csvFileId = DriveApp.createFile(\n          file.name,\n          file.csv,\n          MimeType.CSV,\n        );\n        console.log(`Created Sample CSV: ${file.name}`);\n        csvFileId.moveTo(folderSource);\n      }\n    }\n  }\n  // Installs (or recreates) project trigger\n  installTrigger();\n\n  console.log(`Setup completed for: ${APP_TITLE}`);\n}\n\n/**\n *\n */\nfunction setupPrimarySpreadsheet_(folderAppPrimary) {\n  // Creates the report destination spreadsheet if doesn't exist.\n  if (!fileExists_(SHEET_REPORT_NAME, folderAppPrimary)) {\n    // Creates new destination spreadsheet (report) with cell size of 20 x 10.\n    const sheet = SpreadsheetApp.create(SHEET_REPORT_NAME, 20, 10);\n\n    // Adds the sample data headings.\n    const sheetHeadings = getHeadings();\n    sheet\n      .getSheets()[0]\n      .getRange(1, 1, 1, sheetHeadings[0].length)\n      .setValues(sheetHeadings);\n    SpreadsheetApp.flush();\n    // Moves to primary application root folder.\n    DriveApp.getFileById(sheet.getId()).moveTo(folderAppPrimary);\n\n    console.log(\n      `Created file: ${SHEET_REPORT_NAME} In folder: ${folderAppPrimary.getName()}.`,\n    );\n    return sheet;\n  }\n}\n\n/**\n * Moves sample content to Drive trash & uninstalls trigger.\n * This function removes all folders and content related to this application.\n */\nfunction removeSample() {\n  getApplicationFolder_(APP_FOLDER).setTrashed(true);\n  console.log(\n    `'${APP_FOLDER}' contents have been moved to Drive Trash folder.`,\n  );\n\n  // Removes existing trigger if found.\n  const projectTriggers = ScriptApp.getProjectTriggers();\n  for (let i = 0; i < projectTriggers.length; i++) {\n    if (projectTriggers[i].getHandlerFunction() === HANDLER_FUNCTION) {\n      console.log(\n        `Existing trigger with handler function of '${HANDLER_FUNCTION}' removed.`,\n      );\n      ScriptApp.deleteTrigger(projectTriggers[i]);\n    }\n  }\n}\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/Utilities.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains utility functions that work with application's folder and files.\n */\n\n/**\n * Gets application destination spreadsheet from a given folder\n * Returns new sample version if orignal is not found.\n *\n * @param {string} fileName - Name of the file to test for.\n * @param {object} objFolder - Folder object in which to search.\n * @return {object} Spreadsheet object.\n */\nfunction getSpreadSheet_(fileName, objFolder) {\n  const files = objFolder.getFilesByName(fileName);\n\n  while (files.hasNext()) {\n    const file = files.next();\n    const fileId = file.getId();\n\n    const existingSpreadsheet = SpreadsheetApp.openById(fileId);\n    return existingSpreadsheet;\n  }\n\n  // If application destination spreadsheet is missing, creates a new sample version.\n  const folderAppPrimary = getApplicationFolder_(APP_FOLDER);\n  const sampleSheet = setupPrimarySpreadsheet_(folderAppPrimary);\n  return sampleSheet;\n}\n\n/**\n * Tests if a file exists within a given folder.\n *\n * @param {string} fileName - Name of the file to test for.\n * @param {object} objFolder - Folder object in which to search.\n * @return {boolean} true if found in folder, false if not.\n */\nfunction fileExists_(fileName, objFolder) {\n  const files = objFolder.getFilesByName(fileName);\n\n  while (files.hasNext()) {\n    const file = files.next();\n    console.log(`${file.getName()} already exists.`);\n    return true;\n  }\n  return false;\n}\n\n/**\n * Returns folder named in folderName parameter.\n * Checks if folder already exists,  creates it if it doesn't.\n *\n * @param {string} folderName - Name of the Drive folder.\n * @return {object} Google Drive Folder\n */\nfunction getFolder_(folderName) {\n  // Gets the primary folder for the application.\n  const parentFolder = getApplicationFolder_();\n\n  // Iterates subfolders to check if folder already exists.\n  const subFolders = parentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === folderName) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one doesn't already exist.\n  return parentFolder\n    .createFolder(folderName)\n    .setDescription(`Supporting folder created by ${APP_TITLE}.`);\n}\n\n/**\n * Returns the primary folder as named by the APP_FOLDER variable in the Code.gs file.\n * Checks if folder already exists to avoid duplication.\n * Creates new instance if existing folder not found.\n *\n * @return {object} Google Drive Folder\n */\nfunction getApplicationFolder_() {\n  // Gets root folder, currently set to 'My Drive'\n  const parentFolder = DriveApp.getRootFolder();\n\n  // Iterates through the subfolders to check if folder already exists.\n  const subFolders = parentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === APP_FOLDER) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one doesn't already exist.\n  return parentFolder\n    .createFolder(APP_FOLDER)\n    .setDescription(`Main application folder created by ${APP_TITLE}.`);\n}\n\n/**\n * Tests getApplicationFolder_ and getFolder_\n * @logs details of created Google Drive folder.\n */\nfunction test_getFolderByName() {\n  let folder = getApplicationFolder_();\n  console.log(\n    `Name: ${folder.getName()}\\rID: ${folder.getId()}\\rURL:${folder.getUrl()}\\rDescription: ${folder.getDescription()}`,\n  );\n  // Uncomment the following to automatically delete test folder.\n  // folder.setTrashed(true);\n\n  folder = getFolder_(SOURCE_FOLDER);\n  console.log(\n    `Name: ${folder.getName()}\\rID: ${folder.getId()}\\rURL:${folder.getUrl()}\\rDescription: ${folder.getDescription()}`,\n  );\n  // Uncomment the following to automatically delete test folder.\n  // folder.setTrashed(true);\n\n  folder = getFolder_(PROCESSED_FOLDER);\n  console.log(\n    `Name: ${folder.getName()}\\rID: ${folder.getId()}\\rURL:${folder.getUrl()}\\rDescription: ${folder.getDescription()}`,\n  );\n  // Uncomment the following to automatically delete test folder.\n  // folder.setTrashed(true);\n}\n"
  },
  {
    "path": "solutions/automations/import-csv-sheets/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/mail-merge/.clasp.json",
    "content": "{ \"scriptId\": \"1evL25lW9fLN43j6gGBJWtLq4GncLkdgoxxSVCawc8dWNoLoravNebAih\" }\n"
  },
  {
    "path": "solutions/automations/mail-merge/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/mail-merge\n\n/*\nCopyright 2022 Martin Hawksey\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * @OnlyCurrentDoc\n */\n\n/**\n * Change these to match the column names you are using for email\n * recipient addresses and email sent column.\n */\nconst RECIPIENT_COL = \"Recipient\";\nconst EMAIL_SENT_COL = \"Email Sent\";\n\n/**\n * Creates the menu item \"Mail Merge\" for user to run scripts on drop-down.\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  ui.createMenu(\"Mail Merge\").addItem(\"Send Emails\", \"sendEmails\").addToUi();\n}\n\n/**\n * Sends emails from sheet data.\n * @param {string} subjectLine (optional) for the email draft message\n * @param {Sheet} sheet to read data from\n */\nfunction sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) {\n  // option to skip browser prompt if you want to use this code in other projects\n  let processedSubjectLine = subjectLine;\n  if (!processedSubjectLine) {\n    processedSubjectLine = Browser.inputBox(\n      \"Mail Merge\",\n      \"Type or copy/paste the subject line of the Gmail \" +\n        \"draft message you would like to mail merge with:\",\n      Browser.Buttons.OK_CANCEL,\n    );\n\n    if (processedSubjectLine === \"cancel\" || processedSubjectLine === \"\") {\n      // If no subject line, finishes up\n      return;\n    }\n  }\n\n  // Gets the draft Gmail message to use as a template\n  const emailTemplate = getGmailTemplateFromDrafts_(processedSubjectLine);\n\n  // Gets the data from the passed sheet\n  const dataRange = sheet.getDataRange();\n  // Fetches displayed values for each row in the Range HT Andrew Roberts\n  // https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490\n  // @see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues\n  const data = dataRange.getDisplayValues();\n\n  // Assumes row 1 contains our column headings\n  const heads = data.shift();\n\n  // Gets the index of the column named 'Email Status' (Assumes header names are unique)\n  // @see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions\n  const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL);\n\n  // Converts 2d array into an object array\n  // See https://stackoverflow.com/a/22917499/1027723\n  // For a pretty version, see https://mashe.hawksey.info/?p=17869/#comment-184945\n  const obj = data.map((r) =>\n    heads.reduce((o, k, i) => {\n      o[k] = r[i] || \"\";\n      return o;\n    }, {}),\n  );\n\n  // Creates an array to record sent emails\n  const out = [];\n\n  // Loops through all the rows of data\n  obj.forEach((row, rowIdx) => {\n    // Only sends emails if email_sent cell is blank and not hidden by a filter\n    if (row[EMAIL_SENT_COL] === \"\") {\n      try {\n        const msgObj = fillInTemplateFromObject_(emailTemplate.message, row);\n\n        // See https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)\n        // If you need to send emails with unicode/emoji characters change GmailApp for MailApp\n        // Uncomment advanced parameters as needed (see docs for limitations)\n        GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, {\n          htmlBody: msgObj.html,\n          // bcc: 'a.bcc@email.com',\n          // cc: 'a.cc@email.com',\n          // from: 'an.alias@email.com',\n          // name: 'name of the sender',\n          // replyTo: 'a.reply@email.com',\n          // noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users)\n          attachments: emailTemplate.attachments,\n          inlineImages: emailTemplate.inlineImages,\n        });\n        // Edits cell to record email sent date\n        out.push([new Date()]);\n      } catch (e) {\n        // modify cell to record error\n        out.push([e.message]);\n      }\n    } else {\n      out.push([row[EMAIL_SENT_COL]]);\n    }\n  });\n\n  // Updates the sheet with new data\n  sheet.getRange(2, emailSentColIdx + 1, out.length).setValues(out);\n\n  /**\n   * Get a Gmail draft message by matching the subject line.\n   * @param {string} subject_line to search for draft message\n   * @return {object} containing the subject, plain and html message body and attachments\n   */\n  function getGmailTemplateFromDrafts_(subject_line) {\n    try {\n      // get drafts\n      const drafts = GmailApp.getDrafts();\n      // filter the drafts that match subject line\n      const draft = drafts.filter(subjectFilter_(subject_line))[0];\n      // get the message object\n      const msg = draft.getMessage();\n\n      // Handles inline images and attachments so they can be included in the merge\n      // Based on https://stackoverflow.com/a/65813881/1027723\n      // Gets all attachments and inline image attachments\n      const allInlineImages = draft.getMessage().getAttachments({\n        includeInlineImages: true,\n        includeAttachments: false,\n      });\n      const attachments = draft\n        .getMessage()\n        .getAttachments({ includeInlineImages: false });\n      const htmlBody = msg.getBody();\n\n      // Creates an inline image object with the image name as key\n      // (can't rely on image index as array based on insert order)\n      const img_obj = allInlineImages.reduce((obj, i) => {\n        obj[i.getName()] = i;\n        return obj;\n      }, {});\n\n      //Regexp searches for all img string positions with cid\n      const imgexp = /<img.*?src=\"cid:(.*?)\".*?alt=\"(.*?)\"[^\\>]+>/g;\n      const matches = [...htmlBody.matchAll(imgexp)];\n\n      //Initiates the allInlineImages object\n      const inlineImagesObj = {};\n      for (const match of matches) {\n        inlineImagesObj[match[1]] = img_obj[match[2]];\n      }\n\n      return {\n        message: {\n          subject: subject_line,\n          text: msg.getPlainBody(),\n          html: htmlBody,\n        },\n        attachments: attachments,\n        inlineImages: inlineImagesObj,\n      };\n    } catch (e) {\n      throw new Error(\"Oops - can't find Gmail draft\");\n    }\n\n    /**\n     * Filter draft objects with the matching subject linemessage by matching the subject line.\n     * @param {string} subject_line to search for draft message\n     * @return {object} GmailDraft object\n     */\n    function subjectFilter_(subject_line) {\n      return (element) => {\n        if (element.getMessage().getSubject() === subject_line) {\n          return element;\n        }\n      };\n    }\n  }\n\n  /**\n   * Fill template string with data object\n   * @see https://stackoverflow.com/a/378000/1027723\n   * @param {string} template string containing {{}} markers which are replaced with data\n   * @param {object} data object used to replace {{}} markers\n   * @return {object} message replaced with data\n   */\n  function fillInTemplateFromObject_(template, data) {\n    // We have two templates one for plain text and the html body\n    // Stringifing the object means we can do a global replace\n    let template_string = JSON.stringify(template);\n\n    // Token replacement\n    template_string = template_string.replace(/{{[^{}]+}}/g, (key) => {\n      return escapeData_(data[key.replace(/[{}]+/g, \"\")] || \"\");\n    });\n    return JSON.parse(template_string);\n  }\n\n  /**\n   * Escape cell data to make JSON safe\n   * @see https://stackoverflow.com/a/9204218/1027723\n   * @param {string} str to escape JSON special characters from\n   * @return {string} escaped string\n   */\n  function escapeData_(str) {\n    return str\n      .replace(/[\\\\]/g, \"\\\\\\\\\")\n      .replace(/[\\\"]/g, '\\\\\"')\n      .replace(/[\\/]/g, \"\\\\/\")\n      .replace(/[\\b]/g, \"\\\\b\")\n      .replace(/[\\f]/g, \"\\\\f\")\n      .replace(/[\\n]/g, \"\\\\n\")\n      .replace(/[\\r]/g, \"\\\\r\")\n      .replace(/[\\t]/g, \"\\\\t\");\n  }\n}\n"
  },
  {
    "path": "solutions/automations/mail-merge/README.md",
    "content": "# Create a mail merge with Gmail & Google Sheets\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/mail-merge) for additional details.\n"
  },
  {
    "path": "solutions/automations/mail-merge/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/news-sentiment/.clasp.json",
    "content": "{ \"scriptId\": \"1KHPvTOwE2pd2myZmvX0mbsp8SPlhJBFotNCwflZiP01xmTasNfibG4zl\" }\n"
  },
  {
    "path": "solutions/automations/news-sentiment/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/news-sentiment\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Global variables\nconst googleAPIKey = \"YOUR_GOOGLE_API_KEY\";\nconst newsApiKey = \"YOUR_NEWS_API_KEY\";\nconst apiEndPointHdr = \"https://newsapi.org/v2/everything?q=\";\nconst happyFace =\n  '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635449_1280.png\")';\nconst mehFace =\n  '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635450_1280.png\")';\nconst sadFace =\n  '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/25/smiley-1635454_1280.png\")';\nconst happyColor = \"#44f83d\";\nconst mehColor = \"#f7f6cc\";\nconst sadColor = \"#ff3c3d\";\nconst fullsheet = \"A2:D25\";\nconst sentimentCols = \"B2:D25\";\nconst articleMax = 20;\nconst threshold = 0.3;\n\nlet headlines = [];\nlet rows = null;\nlet rowValues = null;\nlet topic = null;\nlet bottomRow = 0;\nlet ds = null;\nlet ss = null;\nlet headerRow = null;\nlet sentimentCol = null;\nlet headlineCol = null;\nlet scoreCol = null;\n\n/**\n * Creates menu in the Google Sheets spreadsheet when the spreadsheet is opened.\n *\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  ui.createMenu(\"News Headlines Sentiments\")\n    .addItem(\"Analyze News Headlines...\", \"showNewsPrompt\")\n    .addToUi();\n}\n\n/**\n * Prompts user to enter a new headline topic.\n * Calls main function AnalyzeHeadlines with entered topic.\n */\nfunction showNewsPrompt() {\n  //Initializes global variables\n  ss = SpreadsheetApp.getActiveSpreadsheet();\n  ds = ss.getSheetByName(\"Sheet1\");\n  headerRow = ds.getDataRange().getValues()[0];\n  sentimentCol = headerRow.indexOf(\"Sentiment\");\n  headlineCol = headerRow.indexOf(\"Headlines\");\n  scoreCol = headerRow.indexOf(\"Score\");\n\n  // Builds Menu\n  const ui = SpreadsheetApp.getUi();\n  const result = ui.prompt(\"Enter news topic:\", ui.ButtonSet.OK_CANCEL);\n\n  // Processes the user's response.\n  const button = result.getSelectedButton();\n  topic = result.getResponseText();\n  if (button === ui.Button.OK) {\n    analyzeNewsHeadlines();\n  } else if (button === ui.Button.CANCEL) {\n    // Shows alert if user clicked \"Cancel.\"\n    ui.alert(\"News topic not selected!\");\n  }\n}\n\n/**\n * For each headline cell, calls the Natural Language API to get general sentiment and then updates\n * the sentiment response column.\n */\nfunction analyzeNewsHeadlines() {\n  // Clears and reformats the sheet\n  reformatSheet();\n\n  // Gets the headlines array\n  headlines = getHeadlinesArray();\n\n  // Syncs the headlines array to the sheet using a single setValues call\n  if (headlines.length > 0) {\n    ds.getRange(2, 1, headlines.length, headlineCol + 1).setValues(headlines);\n    // Set global rowValues\n    rows = ds.getDataRange();\n    rowValues = rows.getValues();\n    getSentiments();\n  } else {\n    ss.toast(`No headlines returned for topic: ${topic}!`);\n  }\n}\n\n/**\n * Fetches current headlines from the Free News API\n */\nfunction getHeadlinesArray() {\n  // Fetches headlines for a given topic\n  const hdlnsResp = [];\n  const encodedtopic = encodeURIComponent(topic);\n  ss.toast(`Getting headlines for: ${topic}`);\n  const response = UrlFetchApp.fetch(\n    `${apiEndPointHdr + encodedtopic}&apiKey=${newsApiKey}`,\n  );\n  const results = JSON.parse(response);\n  const articles = results.articles;\n\n  for (let i = 0; i < articles.length && i < articleMax; i++) {\n    let newsStory = articles[i].title;\n    if (articles[i].description !== null) {\n      newsStory += `: ${articles[i].description}`;\n    }\n    // Scrubs newsStory of invalid characters\n    newsStory = scrub(newsStory);\n\n    // Constructs hdlnsResp as a 2d array. This simplifies syncing to the sheet.\n    hdlnsResp.push(new Array(newsStory));\n  }\n\n  return hdlnsResp;\n}\n\n/**\n * For each article cell, calls the Natural Language API to get general sentiment and then updates\n * the sentiment response columns.\n */\nfunction getSentiments() {\n  ss.toast(\"Analyzing the headline sentiments...\");\n\n  const articleCount = rows.getNumRows() - 1;\n  let avg = 0;\n\n  // Gets sentiment for each row\n  for (let i = 1; i <= articleCount; i++) {\n    const headlineCell = rowValues[i][headlineCol];\n    if (headlineCell) {\n      const sentimentData = retrieveSentiment(headlineCell);\n      const result = sentimentData.documentSentiment.score;\n      avg += result;\n      ds.getRange(i + 1, sentimentCol + 1).setBackgroundColor(getColor(result));\n      ds.getRange(i + 1, sentimentCol + 1).setValue(getFace(result));\n      ds.getRange(i + 1, scoreCol + 1).setValue(result);\n    }\n  }\n  const avgDecimal = (avg / articleCount).toFixed(2);\n\n  // Shows news topic and average face, color and sentiment value.\n  bottomRow = articleCount + 3;\n  ds.getRange(bottomRow, 1, headlines.length, scoreCol + 1).setFontWeight(\n    \"bold\",\n  );\n  ds.getRange(bottomRow, headlineCol + 1).setValue(`Topic: \"${topic}\"`);\n  ds.getRange(bottomRow, headlineCol + 2).setValue(\"Avg:\");\n  ds.getRange(bottomRow, sentimentCol + 1).setValue(getFace(avgDecimal));\n  ds.getRange(bottomRow, sentimentCol + 1).setBackgroundColor(\n    getColor(avgDecimal),\n  );\n  ds.getRange(bottomRow, scoreCol + 1).setValue(avgDecimal);\n  ss.toast(\"Done!!\");\n}\n\n/**\n * Calls the Natureal Language API to get sentiment response for headline.\n *\n * Important note: Not all languages are supported by Google document\n * sentiment analysis.\n * Unsupported languages generate a \"400\" response: \"INVALID_ARGUMENT\".\n */\nfunction retrieveSentiment(text) {\n  // Sets REST call options\n  const apiEndPoint = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${googleAPIKey}`;\n  const jsonReq = JSON.stringify({\n    document: {\n      type: \"PLAIN_TEXT\",\n      content: text,\n    },\n    encodingType: \"UTF8\",\n  });\n\n  const options = {\n    method: \"post\",\n    contentType: \"application/json\",\n    payload: jsonReq,\n  };\n\n  //  Makes the REST call\n  const response = UrlFetchApp.fetch(apiEndPoint, options);\n  const responseData = JSON.parse(response);\n  return responseData;\n}\n\n// Helper Functions\n\n/**\n * Removes old headlines, sentiments and reset formatting\n */\nfunction reformatSheet() {\n  let range = ds.getRange(fullsheet);\n  range.clearContent();\n  range.clearFormat();\n  range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP);\n\n  range = ds.getRange(sentimentCols); // Center the sentiment cols only\n  range.setHorizontalAlignment(\"center\");\n}\n\n/**\n * Returns a corresponding face based on numeric value.\n */\nfunction getFace(value) {\n  if (value >= threshold) {\n    return happyFace;\n  }\n  if (value < threshold && value > -threshold) {\n    return mehFace;\n  }\n  if (value <= -threshold) {\n    return sadFace;\n  }\n}\n\n/**\n * Returns a corresponding color based on numeric value.\n */\nfunction getColor(value) {\n  if (value >= threshold) {\n    return happyColor;\n  }\n  if (value < threshold && value > -threshold) {\n    return mehColor;\n  }\n  if (value <= -threshold) {\n    return sadColor;\n  }\n}\n\n/**\n * Scrubs invalid characters out of headline text.\n * Can be expanded if needed.\n */\nfunction scrub(text) {\n  return text.replace(/[\\‘\\,\\“\\”\\\"\\'\\’\\-\\n\\â\\]/g, \" \");\n}\n"
  },
  {
    "path": "solutions/automations/news-sentiment/README.md",
    "content": "# Connect to an external API: Analyze news headlines\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/news-sentiment) for additional details."
  },
  {
    "path": "solutions/automations/news-sentiment/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/offsite-activity-signup/.clasp.json",
    "content": "{ \"scriptId\": \"10clpAH4ojSXvTlZaE74rhJ6dDwwkfvi24L_AilGROca5Nds2Jy2oZmvY\" }\n"
  },
  {
    "path": "solutions/automations/offsite-activity-signup/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/offsite-activity-signup\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nconst NUM_ITEMS_TO_RANK = 5;\nconst ACTIVITIES_PER_PERSON = 2;\nconst NUM_TEST_USERS = 150;\n\n/**\n * Adds custom menu items when opening the sheet.\n */\nfunction onOpen() {\n  const menu = SpreadsheetApp.getUi()\n    .createMenu(\"Activities\")\n    .addItem(\"Create form\", \"buildForm_\")\n    .addItem(\"Generate test data\", \"generateTestData_\")\n    .addItem(\"Assign activities\", \"assignActivities_\")\n    .addToUi();\n}\n\n/**\n * Builds a form based on the \"Activity Schedule\" sheet. The form asks attendees to rank their top\n * N choices of activities, where N is defined by NUM_ITEMS_TO_RANK.\n */\nfunction buildForm_() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  if (ss.getFormUrl()) {\n    const msg = \"Form already exists. Unlink the form and try again.\";\n    SpreadsheetApp.getUi().alert(msg);\n    return;\n  }\n  const form = FormApp.create(\"Activity Signup\")\n    .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId())\n    .setAllowResponseEdits(true)\n    .setLimitOneResponsePerUser(true)\n    .setCollectEmail(true);\n  const sectionHelpText = Utilities.formatString(\n    \"Please choose your top %d activities\",\n    NUM_ITEMS_TO_RANK,\n  );\n  form\n    .addSectionHeaderItem()\n    .setTitle(\"Activity choices\")\n    .setHelpText(sectionHelpText);\n\n  // Presents activity ranking as a form grid with each activity as a row and rank as a column.\n  const rows = loadActivitySchedule_(ss).map(\n    (activity) => activity.description,\n  );\n  const columns = range_(1, NUM_ITEMS_TO_RANK).map((value) =>\n    Utilities.formatString(\"%s\", toOrdinal_(value)),\n  );\n  const gridValidation = FormApp.createGridValidation()\n    .setHelpText(\"Select one item per column.\")\n    .requireLimitOneResponsePerColumn()\n    .build();\n  form\n    .addGridItem()\n    .setColumns(columns)\n    .setRows(rows)\n    .setValidation(gridValidation);\n\n  form\n    .addListItem()\n    .setTitle(\"Assign other activities if choices are not available?\")\n    .setChoiceValues([\"Yes\", \"No\"]);\n}\n\n/**\n * Assigns activities using a random priority/random serial dictatorship approach. The results\n * are then populated into two new sheets, one listing activities per person, the other listing\n * the rosters for each activity.\n *\n * See https://en.wikipedia.org/wiki/Random_serial_dictatorship for additional information.\n */\nfunction assignActivities_() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const activities = loadActivitySchedule_(ss);\n  const activityIds = activities.map((activity) => activity.id);\n  const attendees = loadAttendeeResponses_(ss, activityIds);\n  assignWithRandomPriority_(attendees, activities, 2);\n  writeAttendeeAssignments_(ss, attendees);\n  writeActivityRosters_(ss, activities);\n}\n\n/**\n * Selects activities via random priority.\n *\n * @param {object[]} attendees - Array of attendees to assign activities to\n * @param {object[]} activities - Array of all available activities\n * @param {number} numActivitiesPerPerson - Maximum number of activities to assign\n */\nfunction assignWithRandomPriority_(\n  attendees,\n  activities,\n  numActivitiesPerPerson,\n) {\n  const activitiesById = activities.reduce((obj, activity) => {\n    obj[activity.id] = activity;\n    return obj;\n  }, {});\n  for (let i = 0; i < numActivitiesPerPerson; ++i) {\n    const randomizedAttendees = shuffleArray_(attendees);\n    for (const attendee of randomizedAttendees) {\n      makeChoice_(attendee, activitiesById);\n    }\n  }\n}\n\n/**\n * Attempts to assign an activity for an attendee based on their preferences and current schedule.\n *\n * @param {object} attendee - Attendee looking to join an activity\n * @param {object} activitiesById - Map of all available activities\n */\nfunction makeChoice_(attendee, activitiesById) {\n  for (let i = 0; i < attendee.preferences.length; ++i) {\n    const activity = activitiesById[attendee.preferences[i]];\n    if (!activity) {\n      continue;\n    }\n    const canJoin = checkAvailability_(attendee, activity);\n    if (canJoin) {\n      attendee.assigned.push(activity);\n      activity.roster.push(attendee);\n      break;\n    }\n  }\n}\n\n/**\n * Checks that an activity has capacity and doesn't conflict with previously assigned\n * activities.\n *\n * @param {object} attendee - Attendee looking to join the activity\n * @param {object} activity - Proposed activity\n * @return {boolean} - True if attendee can join the activity\n */\nfunction checkAvailability_(attendee, activity) {\n  if (activity.capacity <= activity.roster.length) {\n    return false;\n  }\n  const timesConflict = attendee.assigned.some(\n    (assignedActivity) =>\n      !(\n        assignedActivity.startAt.getTime() > activity.endAt.getTime() ||\n        activity.startAt.getTime() > assignedActivity.endAt.getTime()\n      ),\n  );\n  return !timesConflict;\n}\n\n/**\n * Populates a sheet with the assigned activities for each attendee.\n *\n * @param {Spreadsheet} ss - Spreadsheet to write to.\n * @param {object[]} attendees - Array of attendees with their activity assignments\n */\nfunction writeAttendeeAssignments_(ss, attendees) {\n  const sheet = findOrCreateSheetByName_(ss, \"Activities by person\");\n  sheet.clear();\n  sheet.appendRow([\"Email address\", \"Activities\"]);\n  sheet.getRange(\"B1:1\").merge();\n  const rows = attendees.map((attendee) => {\n    // Prefill row to ensure consistent length otherwise\n    // can't bulk update the sheet with range.setValues()\n    const row = fillArray_([], ACTIVITIES_PER_PERSON + 1, \"\");\n    row[0] = attendee.email;\n    attendee.assigned.forEach((activity, index) => {\n      row[index + 1] = activity.description;\n    });\n    return row;\n  });\n  bulkAppendRows_(sheet, rows);\n  sheet.setFrozenRows(1);\n  sheet.getRange(\"1:1\").setFontWeight(\"bold\");\n  sheet.autoResizeColumns(1, sheet.getLastColumn());\n}\n\n/**\n * Populates a sheet with the rosters for each activity.\n *\n * @param {Spreadsheet} ss - Spreadsheet to write to.\n * @param {object[]} activities - Array of activities with their rosters\n */\nfunction writeActivityRosters_(ss, activities) {\n  const sheet = findOrCreateSheetByName_(ss, \"Activity rosters\");\n  sheet.clear();\n  let rows = activities.map((activity) => {\n    const roster = activity.roster.map((attendee) => attendee.email);\n    return [activity.description].concat(roster);\n  });\n  // Transpose the data so each activity is a column\n  rows = transpose_(rows, \"\");\n  bulkAppendRows_(sheet, rows);\n  sheet.setFrozenRows(1);\n  sheet.getRange(\"1:1\").setFontWeight(\"bold\");\n  sheet.autoResizeColumns(1, sheet.getLastColumn());\n}\n\n/**\n * Loads the activity schedule.\n *\n * @param {Spreadsheet} ss - Spreadsheet to load from\n * @return {object[]} Array of available activities.\n */\nfunction loadActivitySchedule_(ss) {\n  const timeZone = ss.getSpreadsheetTimeZone();\n  const sheet = ss.getSheetByName(\"Activity Schedule\");\n  const rows = sheet.getSheetValues(\n    sheet.getFrozenRows() + 1,\n    1,\n    sheet.getLastRow() - 1,\n    sheet.getLastRow(),\n  );\n  const activities = rows.map((row, index) => {\n    const name = row[0];\n    const startAt = new Date(row[1]);\n    const endAt = new Date(row[2]);\n    const capacity = Number.parseInt(row[3]);\n    const formattedStartAt = Utilities.formatDate(\n      startAt,\n      timeZone,\n      \"EEE hh:mm a\",\n    );\n    const formattedEndAt = Utilities.formatDate(endAt, timeZone, \"hh:mm a\");\n    const description = Utilities.formatString(\n      \"%s (%s-%s)\",\n      name,\n      formattedStartAt,\n      formattedEndAt,\n    );\n    return {\n      id: index,\n      name: name,\n      description: description,\n      capacity: capacity,\n      startAt: startAt,\n      endAt: endAt,\n      roster: [],\n    };\n  });\n  return activities;\n}\n\n/**\n * Loads the attendeee response data.\n *\n * @param {Spreadsheet} ss - Spreadsheet to load from\n * @param {number[]} allActivityIds - Full set of available activity IDs\n * @return {object[]} Array of parsed attendee respones.\n */\nfunction loadAttendeeResponses_(ss, allActivityIds) {\n  const sheet = findResponseSheetForForm_(ss);\n\n  if (!sheet || sheet.getLastRow() === 1) {\n    return undefined;\n  }\n\n  const rows = sheet.getSheetValues(\n    sheet.getFrozenRows() + 1,\n    1,\n    sheet.getLastRow() - 1,\n    sheet.getLastRow(),\n  );\n  const attendees = rows.map((row) => {\n    const _ = row.shift(); // Ignore timestamp\n    const email = row.shift();\n    const autoAssign = row.pop();\n    // Find ranked items in the response data.\n    let preferences = row.reduce((prefs, value, index) => {\n      const match = value.match(/(\\d+).*/);\n      if (!match) {\n        return prefs;\n      }\n      const rank = Number.parseInt(match[1]) - 1; // Convert ordinal to array index\n      prefs[rank] = index;\n      return prefs;\n    }, []);\n    if (autoAssign === \"Yes\") {\n      // If auto assigning additional activites, append a randomized list of all the activities.\n      // These will then be considered as if the attendee ranked them.\n      const additionalChoices = shuffleArray_(allActivityIds);\n      preferences = preferences.concat(additionalChoices);\n    }\n    return {\n      email: email,\n      preferences: preferences,\n      assigned: [],\n    };\n  });\n  return attendees;\n}\n\n/**\n * Simulates a large number of users responding to the form. This enables users to quickly\n * experience the full solution without having to collect sufficient form responses\n * through other means.\n */\nfunction generateTestData_() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheet = findResponseSheetForForm_(ss);\n  if (!sheet) {\n    const msg = \"No response sheet found. Create the form and try again.\";\n    SpreadsheetApp.getUi().alert(msg);\n  }\n  if (sheet.getLastRow() > 1) {\n    const msg =\n      \"Response sheet is not empty, can not generate test data. \" +\n      \"Remove responses and try again.\";\n    SpreadsheetApp.getUi().alert(msg);\n    return;\n  }\n\n  const activities = loadActivitySchedule_(ss);\n  const choices = fillArray_([], activities.length, \"\");\n  for (const value of range_(1, 5)) {\n    choices[value] = toOrdinal_(value);\n  }\n\n  const rows = range_(1, NUM_TEST_USERS).map((value) => {\n    const randomizedChoices = shuffleArray_(choices);\n    const email = Utilities.formatString(\"person%d@example.com\", value);\n    return [new Date(), email].concat(randomizedChoices).concat([\"Yes\"]);\n  });\n  bulkAppendRows_(sheet, rows);\n}\n\n/**\n * Retrieves a sheet by name, creating it if it doesn't yet exist.\n *\n * @param {Spreadsheet} ss - Containing spreadsheet\n * @Param {string} name - Name of sheet to return\n * @return {Sheet} Sheet instance\n */\nfunction findOrCreateSheetByName_(ss, name) {\n  const sheet = ss.getSheetByName(name);\n  if (sheet) {\n    return sheet;\n  }\n  return ss.insertSheet(name);\n}\n\n/**\n * Faster version of appending multiple rows via ranges. Requires all rows are equal length.\n *\n * @param {Sheet} sheet - Sheet to append to\n * @param {Array<Array<object>>} rows - Rows to append\n */\nfunction bulkAppendRows_(sheet, rows) {\n  const startRow = sheet.getLastRow() + 1;\n  const startColumn = 1;\n  const numRows = rows.length;\n  const numColumns = rows[0].length;\n  sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows);\n}\n\n/**\n * Copies and randomizes an array.\n *\n * @param {object[]} array - Array to shuffle\n * @return {object[]} randomized copy of the array\n */\nfunction shuffleArray_(array) {\n  const clone = array.slice(0);\n  for (let i = clone.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    const temp = clone[i];\n    clone[i] = clone[j];\n    clone[j] = temp;\n  }\n  return clone;\n}\n\n/**\n * Formats an number as an ordinal.\n *\n * See: https://stackoverflow.com/questions/13627308/add-st-nd-rd-and-th-ordinal-suffix-to-a-number/13627586\n *\n * @param {number} i - Number to format\n * @return {string} Formatted string\n */\nfunction toOrdinal_(i) {\n  const j = i % 10;\n  const k = i % 100;\n  if (j === 1 && k !== 11) {\n    return `${i}st`;\n  }\n  if (j === 2 && k !== 12) {\n    return `${i}nd`;\n  }\n  if (j === 3 && k !== 13) {\n    return `${i}rd`;\n  }\n  return `${i}th`;\n}\n\n/**\n * Locates the sheet containing the form responses.\n *\n * @param {Spreadsheet} ss - Spreadsheet instance to search\n * @return {Sheet} Sheet with form responses, undefined if not found.\n */\nfunction findResponseSheetForForm_(ss) {\n  const formUrl = ss.getFormUrl();\n  if (!ss || !formUrl) {\n    return undefined;\n  }\n  const sheets = ss.getSheets();\n  for (const i in sheets) {\n    if (sheets[i].getFormUrl() === formUrl) {\n      return sheets[i];\n    }\n  }\n  return undefined;\n}\n\n/**\n * Fills an array with a value ([].fill() not supported in Apps Script).\n *\n * @param {object[]} arr - Array to fill\n * @param {number} length - Number of items to fill.\n * @param {object} value - Value to place at each index.\n * @return {object[]} the array, for chaining purposes\n */\nfunction fillArray_(arr, length, value) {\n  for (let i = 0; i < length; ++i) {\n    arr[i] = value;\n  }\n  return arr;\n}\n\n/**\n * Creates and fills an array with numbers in the range [start, end].\n *\n * @param {number} start - First value in the range, inclusive\n * @param {number} end - Last value in the range, inclusive\n * @return {number[]} Array of values representing the range\n */\nfunction range_(start, end) {\n  const arr = [start];\n  let i = start;\n  while (i < end) {\n    i += 1;\n    arr.push(i);\n  }\n  return arr;\n}\n\n/**\n * Transposes a matrix/2d array. For cases where the rows are not the same length,\n * `fillValue` is used where no other value would otherwise be present.\n *\n * @param {Array<Array<object>>} arr - 2D array to transpose\n * @param {object} fillValue - Placeholder for undefined values created as a result\n *     of the transpose. Only required if rows aren't all of equal length.\n * @return {Array<Array<object>>} New transposed array\n */\nfunction transpose_(arr, fillValue) {\n  const transposed = [];\n  for (const [rowIndex, row] of arr.entries()) {\n    for (const [colIndex, col] of row.entries()) {\n      transposed[colIndex] =\n        transposed[colIndex] || fillArray_([], arr.length, fillValue);\n      transposed[colIndex][rowIndex] = row[colIndex];\n    }\n  }\n  return transposed;\n}\n"
  },
  {
    "path": "solutions/automations/offsite-activity-signup/README.md",
    "content": "# Create a sign-up for an offsite\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/offsite-activity-signup) for additional details.\n"
  },
  {
    "path": "solutions/automations/offsite-activity-signup/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/tax-loss-harvest-alerts/.clasp.json",
    "content": "{ \"scriptId\": \"1SVf_XAGJiwksNTMnAwtlIvkKaDou4RLsmwGTa9ipVHKgwITgwXWqMixB\" }\n"
  },
  {
    "path": "solutions/automations/tax-loss-harvest-alerts/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/tax-loss-harvest-alerts\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Checks for losses in the sheet.\n */\nfunction checkLosses() {\n  // Pulls data from the spreadsheet\n  const sheet =\n    SpreadsheetApp.getActiveSpreadsheet().getSheetByName(\"Calculations\");\n  const source = sheet.getRange(\"A:G\");\n  const data = source.getValues();\n\n  //Prepares the email alert content\n  let message = \"Stocks: <br><br>\";\n\n  let send_message = false;\n\n  console.log(\"starting loop\");\n\n  //Loops through the cells in the spreadsheet to find cells where the stock fell below purchase price\n  let n = 0;\n  for (const i in data) {\n    //Skips the first row\n    if (n++ === 0) continue;\n\n    //Loads the current row\n    const row = data[i];\n\n    console.log(row[1]);\n    console.log(row[6]);\n\n    //Once at the end of the list, exits the loop\n    if (row[1] === \"\") break;\n\n    //If value is below purchase price, adds stock ticker and difference to list of tax loss opportunities\n    if (row[6] < 0) {\n      message += `${row[1]}: ${(Number.parseFloat(row[6].toString()) * 100).toFixed(2).toString()}%<br>`;\n      send_message = true;\n    }\n  }\n  if (!send_message) return;\n\n  MailApp.sendEmail({\n    to: SpreadsheetApp.getActiveSpreadsheet().getOwner().getEmail(),\n    subject: \"Tax-loss harvest\",\n    htmlBody: message,\n  });\n}\n"
  },
  {
    "path": "solutions/automations/tax-loss-harvest-alerts/README.md",
    "content": "# Get stock price drop alerts\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/tax-loss-harvest-alerts) for additional details."
  },
  {
    "path": "solutions/automations/tax-loss-harvest-alerts/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/timesheets/.clasp.json",
    "content": "{ \"scriptId\": \"1uzOldn2RjqdrbDJwxuPlcsb7twKLdW59YPS02rbEg_ajAG9XzrYF1-fH\" }\n"
  },
  {
    "path": "solutions/automations/timesheets/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/timesheets\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Global variables representing the index of certain columns.\nconst COLUMN_NUMBER = {\n  EMAIL: 2,\n  HOURS_START: 4,\n  HOURS_END: 8,\n  HOURLY_PAY: 9,\n  TOTAL_HOURS: 10,\n  CALC_PAY: 11,\n  APPROVAL: 12,\n  NOTIFY: 13,\n};\n\n// Global variables:\nconst APPROVED_EMAIL_SUBJECT = \"Weekly Timesheet APPROVED\";\nconst REJECTED_EMAIL_SUBJECT = \"Weekly Timesheet NOT APPROVED\";\nconst APPROVED_EMAIL_MESSAGE = \"Your timesheet has been approved.\";\nconst REJECTED_EMAIL_MESSAGE = \"Your timesheet has not been approved.\";\n\n/**\n * Creates the menu item \"Timesheets\" for user to run scripts on drop-down.\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  ui.createMenu(\"Timesheets\")\n    .addItem(\"Form setup\", \"setUpForm\")\n    .addItem(\"Column setup\", \"columnSetup\")\n    .addItem(\"Notify employees\", \"checkApprovedStatusToNotify\")\n    .addToUi();\n}\n\n/**\n * Adds \"WEEKLY PAY\" column with calculated values using array formulas.\n * Adds an \"APPROVAL\" column at the end of the sheet, containing\n * drop-down menus to either approve/disapprove employee timesheets.\n * Adds a \"NOTIFIED STATUS\" column indicating whether or not an\n * employee has yet been e mailed.\n */\nfunction columnSetup() {\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const lastCol = sheet.getLastColumn();\n  const lastRow = sheet.getLastRow();\n  const frozenRows = sheet.getFrozenRows();\n  const beginningRow = frozenRows + 1;\n  const numRows = lastRow - frozenRows;\n\n  // Calls helper functions to add new columns.\n  addCalculatePayColumn(sheet, beginningRow);\n  addApprovalColumn(sheet, beginningRow, numRows);\n  addNotifiedColumn(sheet, beginningRow, numRows);\n}\n\n/**\n * Adds TOTAL HOURS and CALCULATE PAY columns and automatically calculates\n * every employee's weekly pay.\n *\n * @param {Object} sheet Spreadsheet object of current sheet.\n * @param {Integer} beginningRow Index of beginning row.\n */\nfunction addCalculatePayColumn(sheet, beginningRow) {\n  sheet.insertColumnAfter(COLUMN_NUMBER.HOURLY_PAY);\n  sheet.getRange(1, COLUMN_NUMBER.TOTAL_HOURS).setValue(\"TOTAL HOURS\");\n  sheet.getRange(1, COLUMN_NUMBER.CALC_PAY).setValue(\"WEEKLY PAY\");\n\n  // Calculates weekly total hours.\n  sheet\n    .getRange(beginningRow, COLUMN_NUMBER.TOTAL_HOURS)\n    .setFormula(\"=ArrayFormula(D2:D+E2:E+F2:F+G2:G+H2:H)\");\n  // Calculates weekly pay.\n  sheet\n    .getRange(beginningRow, COLUMN_NUMBER.CALC_PAY)\n    .setFormula(\"=ArrayFormula(I2:I * J2:J)\");\n}\n\n/**\n * Adds an APPROVAL column allowing managers to approve/\n * disapprove of each employee's timesheet.\n *\n * @param {Object} sheet Spreadsheet object of current sheet.\n * @param {Integer} beginningRow Index of beginning row.\n * @param {Integer} numRows Number of rows currently in use.\n */\nfunction addApprovalColumn(sheet, beginningRow, numRows) {\n  sheet.insertColumnAfter(COLUMN_NUMBER.CALC_PAY);\n  sheet.getRange(1, COLUMN_NUMBER.APPROVAL).setValue(\"APPROVAL\");\n\n  // Make sure approval column is all drop-down menus.\n  const approvalColumnRange = sheet.getRange(\n    beginningRow,\n    COLUMN_NUMBER.APPROVAL,\n    numRows,\n    1,\n  );\n  const dropdownValues = [\"APPROVED\", \"NOT APPROVED\", \"IN PROGRESS\"];\n  const rule = SpreadsheetApp.newDataValidation()\n    .requireValueInList(dropdownValues)\n    .build();\n  approvalColumnRange.setDataValidation(rule);\n  approvalColumnRange.setValue(\"IN PROGRESS\");\n}\n\n/**\n * Adds a NOTIFIED column allowing managers to see which employees\n * have/have not yet been notified of their approval status.\n *\n * @param {Object} sheet Spreadsheet object of current sheet.\n * @param {Integer} beginningRow Index of beginning row.\n * @param {Integer} numRows Number of rows currently in use.\n */\nfunction addNotifiedColumn(sheet, beginningRow, numRows) {\n  sheet.insertColumnAfter(COLUMN_NUMBER.APPROVAL); // global\n  sheet.getRange(1, COLUMN_NUMBER.APPROVAL + 1).setValue(\"NOTIFIED STATUS\");\n\n  // Make sure notified column is all drop-down menus.\n  const notifiedColumnRange = sheet.getRange(\n    beginningRow,\n    COLUMN_NUMBER.APPROVAL + 1,\n    numRows,\n    1,\n  );\n  const dropdownValues = [\"NOTIFIED\", \"PENDING\"];\n  const rule = SpreadsheetApp.newDataValidation()\n    .requireValueInList(dropdownValues)\n    .build();\n  notifiedColumnRange.setDataValidation(rule);\n  notifiedColumnRange.setValue(\"PENDING\");\n}\n\n/**\n * Sets the notification status to NOTIFIED for employees\n * who have received a notification email.\n *\n * @param {Object} sheet Current Spreadsheet.\n * @param {Object} notifiedValues Array of notified values.\n * @param {Integer} i Current status in the for loop.\n * @parma {Integer} beginningRow Row where iterations began.\n */\nfunction updateNotifiedStatus(sheet, notifiedValues, i, beginningRow) {\n  // Update notification status.\n  notifiedValues[i][0] = \"NOTIFIED\";\n  sheet.getRange(i + beginningRow, COLUMN_NUMBER.NOTIFY).setValue(\"NOTIFIED\");\n}\n\n/**\n * Checks the approval status of every employee, and calls helper functions\n * to notify employees via email & update their notification status.\n */\nfunction checkApprovedStatusToNotify() {\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const lastRow = sheet.getLastRow();\n  const lastCol = sheet.getLastColumn();\n  // lastCol here is the NOTIFIED column.\n  const frozenRows = sheet.getFrozenRows();\n  const beginningRow = frozenRows + 1;\n  const numRows = lastRow - frozenRows;\n\n  // Gets ranges of email, approval, and notified values for every employee.\n  const emailValues = sheet\n    .getRange(beginningRow, COLUMN_NUMBER.EMAIL, numRows, 1)\n    .getValues();\n  const approvalValues = sheet\n    .getRange(beginningRow, COLUMN_NUMBER.APPROVAL, lastRow - frozenRows, 1)\n    .getValues();\n  const notifiedValues = sheet\n    .getRange(beginningRow, COLUMN_NUMBER.NOTIFY, numRows, 1)\n    .getValues();\n\n  // Traverses through employee's row.\n  for (let i = 0; i < numRows; i++) {\n    // Do not notify twice.\n    if (notifiedValues[i][0] === \"NOTIFIED\") {\n      continue;\n    }\n    const emailAddress = emailValues[i][0];\n    const approvalValue = approvalValues[i][0];\n\n    // Sends notifying emails & update status.\n    if (approvalValue === \"IN PROGRESS\") {\n    } else if (approvalValue === \"APPROVED\") {\n      MailApp.sendEmail(\n        emailAddress,\n        APPROVED_EMAIL_SUBJECT,\n        APPROVED_EMAIL_MESSAGE,\n      );\n      updateNotifiedStatus(sheet, notifiedValues, i, beginningRow);\n    } else if (approvalValue === \"NOT APPROVED\") {\n      MailApp.sendEmail(\n        emailAddress,\n        REJECTED_EMAIL_SUBJECT,\n        REJECTED_EMAIL_MESSAGE,\n      );\n      updateNotifiedStatus(sheet, notifiedValues, i, beginningRow);\n    }\n  }\n}\n\n/**\n * Set up the Timesheets Responses form, & link the form's trigger to\n * send manager an email when a new request is submitted.\n */\nfunction setUpForm() {\n  const sheet = SpreadsheetApp.getActiveSpreadsheet();\n  if (sheet.getFormUrl()) {\n    const msg = \"Form already exists. Unlink the form and try again.\";\n    SpreadsheetApp.getUi().alert(msg);\n    return;\n  }\n\n  // Create the form.\n  const form = FormApp.create(\"Weekly Timesheets\")\n    .setCollectEmail(true)\n    .setDestination(FormApp.DestinationType.SPREADSHEET, sheet.getId())\n    .setLimitOneResponsePerUser(false);\n  form.addTextItem().setTitle(\"Employee Name:\").setRequired(true);\n  form.addTextItem().setTitle(\"Monday Hours:\").setRequired(true);\n  form.addTextItem().setTitle(\"Tuesday Hours:\").setRequired(true);\n  form.addTextItem().setTitle(\"Wednesday Hours:\").setRequired(true);\n  form.addTextItem().setTitle(\"Thursday Hours:\").setRequired(true);\n  form.addTextItem().setTitle(\"Friday Hours:\").setRequired(true);\n  form.addTextItem().setTitle(\"HourlyWage:\").setRequired(true);\n\n  // Set up on form submit trigger.\n  ScriptApp.newTrigger(\"onFormSubmit\").forForm(form).onFormSubmit().create();\n}\n\n/**\n * Handle new form submissions to trigger the workflow.\n *\n * @param {Object} event Form submit event\n */\nfunction onFormSubmit(event) {\n  const response = getResponsesByName(event.response);\n\n  // Load form responses into a new row.\n  const row = [\n    \"New\",\n    \"\",\n    response[\"Emoloyee Email:\"],\n    response[\"Employee Name:\"],\n    response[\"Monday Hours:\"],\n    response[\"Tuesday Hours:\"],\n    response[\"Wednesday Hours:\"],\n    response[\"Thursday Hours:\"],\n    response[\"Friday Hours:\"],\n    response[\"Hourly Wage:\"],\n  ];\n  const sheet = SpreadsheetApp.getActiveSpreadsheet();\n  sheet.appendRow(row);\n}\n\n/**\n * Converts a form response to an object keyed by the item titles. Allows easier\n * access to response values.\n *\n * @param {FormResponse} response\n * @return {Object} Form values keyed by question title\n */\nfunction getResponsesByName(response) {\n  const initialValue = {\n    email: response.getRespondentEmail(),\n    timestamp: response.getTimestamp(),\n  };\n  return response.getItemResponses().reduce((obj, itemResponse) => {\n    const key = itemResponse.getItem().getTitle();\n    obj[key] = itemResponse.getResponse();\n    return obj;\n  }, initialValue);\n}\n"
  },
  {
    "path": "solutions/automations/timesheets/README.md",
    "content": "# Collect and review timesheets from employees\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/timesheets) for additional details.\n"
  },
  {
    "path": "solutions/automations/timesheets/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/upload-files/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/upload-files\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// [START apps_script_upload_files]\n// TODO Before you start using this sample, you must run the setUp()\n// function in the Setup.gs file.\n\n// Application constants\nconst APP_TITLE = \"Upload files to Drive from Forms\";\nconst APP_FOLDER_NAME = \"Upload files to Drive (File responses)\";\n\n// Identifies the subfolder form item\nconst APP_SUBFOLDER_ITEM = \"Subfolder\";\nconst APP_SUBFOLDER_NONE = \"<None>\";\n\n/**\n * Gets the file uploads from a form response and moves files to the corresponding subfolder.\n *\n * @param {object} event - Form submit.\n */\nfunction onFormSubmit(e) {\n  try {\n    // Gets the application root folder.\n    let destFolder = getFolder_(APP_FOLDER_NAME);\n\n    // Gets all form responses.\n    const itemResponses = e.response.getItemResponses();\n\n    // Determines the subfolder to route the file to, if any.\n    let subFolderName;\n    const dest = itemResponses.filter(\n      (itemResponse) =>\n        itemResponse.getItem().getTitle().toString() === APP_SUBFOLDER_ITEM,\n    );\n\n    // Gets the destination subfolder name, but ignores if APP_SUBFOLDER_NONE was selected;\n    if (dest.length > 0) {\n      if (dest[0].getResponse() !== APP_SUBFOLDER_NONE) {\n        subFolderName = dest[0].getResponse();\n      }\n    }\n    // Gets the subfolder or creates it if it doesn't exist.\n    if (subFolderName !== undefined) {\n      destFolder = getSubFolder_(destFolder, subFolderName);\n    }\n    console.log(`Destination folder to use:\n    Name: ${destFolder.getName()}\n    ID: ${destFolder.getId()}\n    URL: ${destFolder.getUrl()}`);\n\n    // Gets the file upload response as an array to allow for multiple files.\n    const fileUploads = itemResponses\n      .filter(\n        (itemResponse) =>\n          itemResponse.getItem().getType().toString() === \"FILE_UPLOAD\",\n      )\n      .map((itemResponse) => itemResponse.getResponse())\n      .reduce((a, b) => a.concat(b), []);\n\n    // Moves the files to the destination folder.\n    if (fileUploads.length > 0) {\n      for (const fileId of fileUploads) {\n        DriveApp.getFileById(fileId).moveTo(destFolder);\n        console.log(`File Copied: ${fileId}`);\n      }\n    }\n  } catch (err) {\n    console.log(err);\n  }\n}\n\n/**\n * Returns a Drive folder under the passed in objParentFolder parent\n * folder. Checks if folder of same name exists before creating, returning\n * the existing folder or the newly created one if not found.\n *\n * @param {object} objParentFolder - Drive folder as an object.\n * @param {string} subFolderName - Name of subfolder to create/return.\n * @return {object} Drive folder\n */\nfunction getSubFolder_(objParentFolder, subFolderName) {\n  // Iterates subfolders of parent folder to check if folder already exists.\n  const subFolders = objParentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === subFolderName) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one doesn't already exist.\n  return objParentFolder\n    .createFolder(subFolderName)\n    .setDescription(\n      `Created by ${APP_TITLE} application to store uploaded Forms files.`,\n    );\n}\n\n// [END apps_script_upload_files]\n"
  },
  {
    "path": "solutions/automations/upload-files/README.md",
    "content": "# Upload files to Google Drive from Google Forms\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/upload-files) for additional details.\n"
  },
  {
    "path": "solutions/automations/upload-files/Setup.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_upload_files_setup]\n// TODO You must run the setUp() function before you start using this sample.\n\n/**\n * The setUp() function performs the following:\n *  - Creates a Google Drive folder named by the APP_FOLDER_NAME\n *    variable in the Code.gs file.\n *  - Creates a trigger to handle onFormSubmit events.\n */\nfunction setUp() {\n  // Ensures the root destination folder exists.\n  const appFolder = getFolder_(APP_FOLDER_NAME);\n  if (appFolder !== null) {\n    console.log(`Application folder setup.\n    Name: ${appFolder.getName()}\n    ID: ${appFolder.getId()}\n    URL: ${appFolder.getUrl()}`);\n  } else {\n    console.log(\"Could not setup application folder.\");\n  }\n  // Calls the function that creates the Forms onSubmit trigger.\n  installTrigger_();\n}\n\n/**\n * Returns a folder to store uploaded files in the same location\n * in Drive where the form is located. First, it checks if the folder\n * already exists, and creates it if it doesn't.\n *\n * @param {string} folderName - Name of the Drive folder.\n * @return {object} Google Drive Folder\n */\nfunction getFolder_(folderName) {\n  // Gets the Drive folder where the form is located.\n  const ssId = FormApp.getActiveForm().getId();\n  const parentFolder = DriveApp.getFileById(ssId).getParents().next();\n\n  // Iterates through the subfolders to check if folder already exists.\n  // The script checks for the folder name specified in the APP_FOLDER_NAME variable.\n  const subFolders = parentFolder.getFolders();\n  while (subFolders.hasNext()) {\n    const folder = subFolders.next();\n\n    // Returns the existing folder if found.\n    if (folder.getName() === folderName) {\n      return folder;\n    }\n  }\n  // Creates a new folder if one doesn't already exist.\n  return parentFolder\n    .createFolder(folderName)\n    .setDescription(\n      `Created by ${APP_TITLE} application to store uploaded files.`,\n    );\n}\n\n/**\n * Installs trigger to capture onFormSubmit event when a form is submitted.\n * Ensures that the trigger is only installed once.\n * Called by setup().\n */\nfunction installTrigger_() {\n  // Ensures existing trigger doesn't already exist.\n  const propTriggerId =\n    PropertiesService.getScriptProperties().getProperty(\"triggerUniqueId\");\n  if (propTriggerId !== null) {\n    const triggers = ScriptApp.getProjectTriggers();\n    for (const t in triggers) {\n      if (triggers[t].getUniqueId() === propTriggerId) {\n        console.log(\n          `Trigger with the following unique ID already exists: ${propTriggerId}`,\n        );\n        return;\n      }\n    }\n  }\n  // Creates the trigger if one doesn't exist.\n  const triggerUniqueId = ScriptApp.newTrigger(\"onFormSubmit\")\n    .forForm(FormApp.getActiveForm())\n    .onFormSubmit()\n    .create()\n    .getUniqueId();\n  PropertiesService.getScriptProperties().setProperty(\n    \"triggerUniqueId\",\n    triggerUniqueId,\n  );\n  console.log(\n    `Trigger with the following unique ID was created: ${triggerUniqueId}`,\n  );\n}\n\n/**\n * Removes all script properties and triggers for the project.\n * Use primarily to test setup routines.\n */\nfunction removeTriggersAndScriptProperties() {\n  PropertiesService.getScriptProperties().deleteAllProperties();\n  // Removes all triggers associated with project.\n  const triggers = ScriptApp.getProjectTriggers();\n  for (const t in triggers) {\n    ScriptApp.deleteTrigger(triggers[t]);\n  }\n}\n\n/**\n * Removes all form responses to reset the form.\n */\nfunction deleteAllResponses() {\n  FormApp.getActiveForm().deleteAllResponses();\n}\n\n// [END apps_script_upload_files_setup]\n"
  },
  {
    "path": "solutions/automations/upload-files/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/vacation-calendar/.clasp.json",
    "content": "{ \"scriptId\": \"1jvPSSwJcuLzlDLDy2dr-qorjihiTNAW2H6B5k-dJxHjEPX6hMcNghzSh\" }\n"
  },
  {
    "path": "solutions/automations/vacation-calendar/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/vacation-calendar\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Set the ID of the team calendar to add events to. You can find the calendar's\n// ID on the settings page.\nconst TEAM_CALENDAR_ID = \"ENTER_TEAM_CALENDAR_ID_HERE\";\n// Set the email address of the Google Group that contains everyone in the team.\n// Ensure the group has less than 500 members to avoid timeouts.\n// Change to an array in order to add indirect members frrm multiple groups, for example:\n// let GROUP_EMAIL = ['ENTER_GOOGLE_GROUP_EMAIL_HERE', 'ENTER_ANOTHER_GOOGLE_GROUP_EMAIL_HERE'];\nconst GROUP_EMAIL = \"ENTER_GOOGLE_GROUP_EMAIL_HERE\";\n\nconst ONLY_DIRECT_MEMBERS = false;\n\nconst KEYWORDS = [\"vacation\", \"ooo\", \"out of office\", \"offline\"];\nconst MONTHS_IN_ADVANCE = 3;\n\n/**\n * Sets up the script to run automatically every hour.\n */\nfunction setup() {\n  const triggers = ScriptApp.getProjectTriggers();\n  if (triggers.length > 0) {\n    throw new Error(\"Triggers are already setup.\");\n  }\n  ScriptApp.newTrigger(\"sync\").timeBased().everyHours(1).create();\n  // Runs the first sync immediately.\n  sync();\n}\n\n/**\n * Looks through the group members' public calendars and adds any\n * 'vacation' or 'out of office' events to the team calendar.\n */\nfunction sync() {\n  // Defines the calendar event date range to search.\n  const today = new Date();\n  const maxDate = new Date();\n  maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE);\n\n  // Determines the time the the script was last run.\n  let lastRun = PropertiesService.getScriptProperties().getProperty(\"lastRun\");\n  lastRun = lastRun ? new Date(lastRun) : null;\n\n  // Gets the list of users in the Google Group.\n  let users = getAllMembers(GROUP_EMAIL);\n  if (ONLY_DIRECT_MEMBERS) {\n    users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers();\n  } else if (Array.isArray(GROUP_EMAIL)) {\n    users = getUsersFromGroups(GROUP_EMAIL);\n  }\n\n  // For each user, finds events having one or more of the keywords in the event\n  // summary in the specified date range. Imports each of those to the team\n  // calendar.\n  let count = 0;\n  for (const user of users) {\n    const username = user.getEmail().split(\"@\")[0];\n    const events = findEvents(user, today, maxDate, lastRun);\n    for (const event of events) {\n      importEvent(username, event);\n      count++;\n    }\n  }\n\n  PropertiesService.getScriptProperties().setProperty(\"lastRun\", today);\n  console.log(`Imported ${count} events`);\n}\n\n/**\n * Imports the given event from the user's calendar into the shared team\n * calendar.\n * @param {string} username The team member that is attending the event.\n * @param {Calendar.Event} event The event to import.\n */\nfunction importEvent(username, event) {\n  event.summary = `[${username}] ${event.summary}`;\n  event.organizer = {\n    id: TEAM_CALENDAR_ID,\n  };\n  event.attendees = [];\n\n  // If the event is not of type 'default', it can't be imported, so it needs\n  // to be changed.\n  if (event.eventType !== \"default\") {\n    event.eventType = \"default\";\n    event.outOfOfficeProperties = undefined;\n    event.focusTimeProperties = undefined;\n  }\n\n  console.log(\"Importing: %s\", event.summary);\n  try {\n    Calendar.Events.import(event, TEAM_CALENDAR_ID);\n  } catch (e) {\n    console.error(\n      \"Error attempting to import event: %s. Skipping.\",\n      e.toString(),\n    );\n  }\n}\n\n/**\n * In a given user's calendar, looks for occurrences of the given keyword\n * in events within the specified date range and returns any such events\n * found.\n * @param {Session.User} user The user to retrieve events for.\n * @param {string} keyword The keyword to look for.\n * @param {Date} start The starting date of the range to examine.\n * @param {Date} end The ending date of the range to examine.\n * @param {Date} optSince A date indicating the last time this script was run.\n * @return {Calendar.Event[]} An array of calendar events.\n */\nfunction findEvents(user, start, end, optSince) {\n  const params = {\n    eventTypes: \"outOfOffice\",\n    timeMin: formatDateAsRFC3339(start),\n    timeMax: formatDateAsRFC3339(end),\n    showDeleted: true,\n  };\n  if (optSince) {\n    // This prevents the script from examining events that have not been\n    // modified since the specified date (that is, the last time the\n    // script was run).\n    params.updatedMin = formatDateAsRFC3339(optSince);\n  }\n  let pageToken = null;\n  let events = [];\n  do {\n    params.pageToken = pageToken;\n    let response;\n    try {\n      response = Calendar.Events.list(user.getEmail(), params);\n    } catch (e) {\n      console.error(\n        \"Error retriving events for %s, %s: %s; skipping\",\n        user,\n        keyword,\n        e.toString(),\n      );\n      continue;\n    }\n    events = events.concat(response.items);\n    pageToken = response.nextPageToken;\n  } while (pageToken);\n  return events;\n}\n\n/**\n * Returns an RFC3339 formated date String corresponding to the given\n * Date object.\n * @param {Date} date a Date.\n * @return {string} a formatted date string.\n */\nfunction formatDateAsRFC3339(date) {\n  return Utilities.formatDate(date, \"UTC\", \"yyyy-MM-dd'T'HH:mm:ssZ\");\n}\n\n/**\n * Get both direct and indirect members (and delete duplicates).\n * @param {string} the e-mail address of the group.\n * @return {object} direct and indirect members.\n */\nfunction getAllMembers(groupEmail) {\n  const group = GroupsApp.getGroupByEmail(groupEmail);\n  let users = group.getUsers();\n  const childGroups = group.getGroups();\n  for (let i = 0; i < childGroups.length; i++) {\n    const childGroup = childGroups[i];\n    users = users.concat(getAllMembers(childGroup.getEmail()));\n  }\n  // Remove duplicate members\n  const uniqueUsers = [];\n  const userEmails = {};\n  for (let i = 0; i < users.length; i++) {\n    const user = users[i];\n    if (!userEmails[user.getEmail()]) {\n      uniqueUsers.push(user);\n      userEmails[user.getEmail()] = true;\n    }\n  }\n  return uniqueUsers;\n}\n\n/**\n * Get indirect members from multiple groups (and delete duplicates).\n * @param {array} the e-mail addresses of multiple groups.\n * @return {object} indirect members of multiple groups.\n */\nfunction getUsersFromGroups(groupEmails) {\n  const users = [];\n  for (const groupEmail of groupEmails) {\n    const groupUsers = GroupsApp.getGroupByEmail(groupEmail).getUsers();\n    for (const user of groupUsers) {\n      if (!users.some((u) => u.getEmail() === user.getEmail())) {\n        users.push(user);\n      }\n    }\n  }\n  return users;\n}\n"
  },
  {
    "path": "solutions/automations/vacation-calendar/README.md",
    "content": "# Populate a team vacation calendar\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/vacation-calendar) for additional details."
  },
  {
    "path": "solutions/automations/vacation-calendar/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/youtube-tracker/.clasp.json",
    "content": "{ \"scriptId\": \"15WP4FukVYk_4zy21j0_13GftPH7J8lpdtemYcy_168TYKsAQ4x-pAeQz\" }\n"
  },
  {
    "path": "solutions/automations/youtube-tracker/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/automations/youtube-tracker\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Sets preferences for email notification. Choose 'Y' to send emails, 'N' to skip emails.\nconst EMAIL_ON = \"Y\";\n\n// Matches column names in Video sheet to variables. If the column names change, update these variables.\nconst COLUMN_NAME = {\n  VIDEO: \"Video Link\",\n  TITLE: \"Video Title\",\n};\n\n/**\n * Gets YouTube video details and statistics for all\n * video URLs listed in 'Video Link' column in each\n * sheet. Sends email summary, based on preferences above,\n * when videos have new comments or replies.\n */\nfunction markVideos() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();\n\n  // Runs through process for each tab in Spreadsheet.\n  for (const dataSheet of sheets) {\n    const tabName = dataSheet.getName();\n    const range = dataSheet.getDataRange();\n    const numRows = range.getNumRows();\n    const rows = range.getValues();\n    const headerRow = rows[0];\n\n    // Finds the column indices.\n    const videoColumnIdx = headerRow.indexOf(COLUMN_NAME.VIDEO);\n    const titleColumnIdx = headerRow.indexOf(COLUMN_NAME.TITLE);\n\n    // Creates empty array to collect data for email table.\n    const emailContent = [];\n\n    // Processes each row in spreadsheet.\n    for (let i = 1; i < numRows; ++i) {\n      const row = rows[i];\n      // Extracts video ID.\n      const videoId = extractVideoIdFromUrl(row[videoColumnIdx]);\n      // Processes each row that contains a video ID.\n      if (!videoId) {\n        continue;\n      }\n      // Calls getVideoDetails function and extracts target data for the video.\n      const detailsResponse = getVideoDetails(videoId);\n      const title = detailsResponse.items[0].snippet.title;\n      const publishDate = detailsResponse.items[0].snippet.publishedAt;\n      const publishDateFormatted = new Date(publishDate);\n      const views = detailsResponse.items[0].statistics.viewCount;\n      const likes = detailsResponse.items[0].statistics.likeCount;\n      const comments = detailsResponse.items[0].statistics.commentCount;\n      const channel = detailsResponse.items[0].snippet.channelTitle;\n\n      // Collects title, publish date, channel, views, comments, likes details and pastes into tab.\n      const detailsRow = [\n        title,\n        publishDateFormatted,\n        channel,\n        views,\n        comments,\n        likes,\n      ];\n      dataSheet\n        .getRange(i + 1, titleColumnIdx + 1, 1, 6)\n        .setValues([detailsRow]);\n\n      // Determines if new count of comments/replies is greater than old count of comments/replies.\n      const addlCommentCount = comments - row[titleColumnIdx + 4];\n\n      // Adds video title, link, and additional comment count to table if new counts > old counts.\n      if (addlCommentCount > 0) {\n        const emailRow = [title, row[videoColumnIdx], addlCommentCount];\n        emailContent.push(emailRow);\n      }\n    }\n    // Sends notification email if Content is not empty.\n    if (emailContent.length > 0 && EMAIL_ON === \"Y\") {\n      sendEmailNotificationTemplate(emailContent, tabName);\n    }\n  }\n}\n\n/**\n * Gets video details for YouTube videos\n * using YouTube advanced service.\n */\nfunction getVideoDetails(videoId) {\n  const part = \"snippet,statistics\";\n  const response = YouTube.Videos.list(part, { id: videoId });\n  return response;\n}\n\n/**\n * Extracts YouTube video ID from url.\n * (h/t https://stackoverflow.com/a/3452617)\n */\nfunction extractVideoIdFromUrl(url) {\n  let videoId = url.split(\"v=\")[1];\n  const ampersandPosition = videoId.indexOf(\"&\");\n  if (ampersandPosition !== -1) {\n    videoId = videoId.substring(0, ampersandPosition);\n  }\n  return videoId;\n}\n\n/**\n * Assembles notification email with table of video details.\n * (h/t https://stackoverflow.com/questions/37863392/making-table-in-google-apps-script-from-array)\n */\nfunction sendEmailNotificationTemplate(content, emailAddress) {\n  const template = HtmlService.createTemplateFromFile(\"email\");\n  template.content = content;\n  const msg = template.evaluate();\n  MailApp.sendEmail(\n    emailAddress,\n    \"New comments or replies on YouTube\",\n    msg.getContent(),\n    { htmlBody: msg.getContent() },\n  );\n}\n"
  },
  {
    "path": "solutions/automations/youtube-tracker/README.md",
    "content": "# Track YouTube video views and comments\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/automations/youtube-tracker) for additional details."
  },
  {
    "path": "solutions/automations/youtube-tracker/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/automations/youtube-tracker/email.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!-- [START email_template] -->\n<body>\n  Hello,<br><br>You have new comments and/or replies on videos: <br><br>\n  <table border=\"1\">\n    <tr>\n      <th>Video Title</th>\n      <th>Link</th>\n      <th>Number of new replies and comments</th>\n    </tr>\n    <? for (var i = 0; i < content.length; i++) { ?>\n    <tr>\n      <? for (var j = 0; j < content[i].length; j++) { ?>\n      <td align=\"center\"><?= content[i][j] ?></td>\n      <? } ?>\n    </tr>\n    <? } ?>\n  </table>\n</body>\n<!-- [END email_template] -->\n\n"
  },
  {
    "path": "solutions/custom-functions/calculate-driving-distance/.clasp.json",
    "content": "{ \"scriptId\": \"1_cfhZv-VJBekzu1V4mFD1C5ggRaUumWw9rUz0NaLED6XD4_yHB-eJ01a\" }\n"
  },
  {
    "path": "solutions/custom-functions/calculate-driving-distance/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * @OnlyCurrentDoc Limits the script to only accessing the current sheet.\n */\n\n/**\n * A special function that runs when the spreadsheet is open, used to add a\n * custom menu to the spreadsheet.\n */\nfunction onOpen() {\n  try {\n    const spreadsheet = SpreadsheetApp.getActive();\n    const menuItems = [\n      { name: \"Prepare sheet...\", functionName: \"prepareSheet_\" },\n      { name: \"Generate step-by-step...\", functionName: \"generateStepByStep_\" },\n    ];\n    spreadsheet.addMenu(\"Directions\", menuItems);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * A custom function that converts meters to miles.\n *\n * @param {Number} meters The distance in meters.\n * @return {Number} The distance in miles.\n */\nfunction metersToMiles(meters) {\n  if (typeof meters !== \"number\") {\n    return null;\n  }\n  return (meters / 1000) * 0.621371;\n}\n\n/**\n * A custom function that gets the driving distance between two addresses.\n *\n * @param {String} origin The starting address.\n * @param {String} destination The ending address.\n * @return {Number} The distance in meters.\n */\nfunction drivingDistance(origin, destination) {\n  const directions = getDirections_(origin, destination);\n  return directions.routes[0].legs[0].distance.value;\n}\n\n/**\n * A function that adds headers and some initial data to the spreadsheet.\n */\nfunction prepareSheet_() {\n  try {\n    const sheet = SpreadsheetApp.getActiveSheet().setName(\"Settings\");\n    const headers = [\n      \"Start Address\",\n      \"End Address\",\n      \"Driving Distance (meters)\",\n      \"Driving Distance (miles)\",\n    ];\n    const initialData = [\n      \"350 5th Ave, New York, NY 10118\",\n      \"405 Lexington Ave, New York, NY 10174\",\n    ];\n    sheet.getRange(\"A1:D1\").setValues([headers]).setFontWeight(\"bold\");\n    sheet.getRange(\"A2:B2\").setValues([initialData]);\n    sheet.setFrozenRows(1);\n    sheet.autoResizeColumns(1, 4);\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Creates a new sheet containing step-by-step directions between the two\n * addresses on the \"Settings\" sheet that the user selected.\n */\nfunction generateStepByStep_() {\n  try {\n    const spreadsheet = SpreadsheetApp.getActive();\n    const settingsSheet = spreadsheet.getSheetByName(\"Settings\");\n    settingsSheet.activate();\n\n    // Prompt the user for a row number.\n    const selectedRow = Browser.inputBox(\n      \"Generate step-by-step\",\n      \"Please enter the row number of\" +\n        \" the\" +\n        \" addresses to use\" +\n        ' (for example, \"2\"):',\n      Browser.Buttons.OK_CANCEL,\n    );\n    if (selectedRow === \"cancel\") {\n      return;\n    }\n    const rowNumber = Number(selectedRow);\n    if (\n      Number.isNaN(rowNumber) ||\n      rowNumber < 2 ||\n      rowNumber > settingsSheet.getLastRow()\n    ) {\n      Browser.msgBox(\n        \"Error\",\n        Utilities.formatString('Row \"%s\" is not valid.', selectedRow),\n        Browser.Buttons.OK,\n      );\n      return;\n    }\n\n    // Retrieve the addresses in that row.\n    const row = settingsSheet.getRange(rowNumber, 1, 1, 2);\n    const rowValues = row.getValues();\n    const origin = rowValues[0][0];\n    const destination = rowValues[0][1];\n    if (!origin || !destination) {\n      Browser.msgBox(\n        \"Error\",\n        \"Row does not contain two addresses.\",\n        Browser.Buttons.OK,\n      );\n      return;\n    }\n\n    // Get the raw directions information.\n    const directions = getDirections_(origin, destination);\n\n    // Create a new sheet and append the steps in the directions.\n    const sheetName = `Driving Directions for Row ${rowNumber}`;\n    let directionsSheet = spreadsheet.getSheetByName(sheetName);\n    if (directionsSheet) {\n      directionsSheet.clear();\n      directionsSheet.activate();\n    } else {\n      directionsSheet = spreadsheet.insertSheet(\n        sheetName,\n        spreadsheet.getNumSheets(),\n      );\n    }\n    const sheetTitle = Utilities.formatString(\n      \"Driving Directions from %s to %s\",\n      origin,\n      destination,\n    );\n    const headers = [\n      [sheetTitle, \"\", \"\"],\n      [\"Step\", \"Distance (Meters)\", \"Distance (Miles)\"],\n    ];\n    const newRows = [];\n    for (const step of directions.routes[0].legs[0].steps) {\n      // Remove HTML tags from the instructions.\n      const instructions = XmlService.parse(\n        `<root>${step.html_instructions}</root>`,\n      )\n        .getRootElement()\n        .getText();\n      newRows.push([instructions, step.distance.value]);\n    }\n    directionsSheet.getRange(1, 1, headers.length, 3).setValues(headers);\n    directionsSheet\n      .getRange(headers.length + 1, 1, newRows.length, 2)\n      .setValues(newRows);\n    directionsSheet\n      .getRange(headers.length + 1, 3, newRows.length, 1)\n      .setFormulaR1C1(\"=METERSTOMILES(R[0]C[-1])\");\n\n    // Format the new sheet.\n    directionsSheet.getRange(\"A1:C1\").merge().setBackground(\"#ddddee\");\n    directionsSheet.getRange(\"A1:2\").setFontWeight(\"bold\");\n    directionsSheet.setColumnWidth(1, 500);\n    directionsSheet.getRange(\"B2:C\").setVerticalAlignment(\"top\");\n    directionsSheet.getRange(\"C2:C\").setNumberFormat(\"0.00\");\n    const stepsRange = directionsSheet\n      .getDataRange()\n      .offset(2, 0, directionsSheet.getLastRow() - 2);\n    setAlternatingRowBackgroundColors_(stepsRange, \"#ffffff\", \"#eeeeee\");\n    directionsSheet.setFrozenRows(2);\n    SpreadsheetApp.flush();\n  } catch (e) {\n    // TODO (Developer) - Handle Exception\n    console.log(`Failed with error: %s${e.error}`);\n  }\n}\n\n/**\n * Sets the background colors for alternating rows within the range.\n * @param {Range} range The range to change the background colors of.\n * @param {string} oddColor The color to apply to odd rows (relative to the\n *     start of the range).\n * @param {string} evenColor The color to apply to even rows (relative to the\n *     start of the range).\n */\nfunction setAlternatingRowBackgroundColors_(range, oddColor, evenColor) {\n  const backgrounds = [];\n  for (let row = 1; row <= range.getNumRows(); row++) {\n    const rowBackgrounds = [];\n    for (let column = 1; column <= range.getNumColumns(); column++) {\n      if (row % 2 === 0) {\n        rowBackgrounds.push(evenColor);\n      } else {\n        rowBackgrounds.push(oddColor);\n      }\n    }\n    backgrounds.push(rowBackgrounds);\n  }\n  range.setBackgrounds(backgrounds);\n}\n\n/**\n * A shared helper function used to obtain the full set of directions\n * information between two addresses. Uses the Apps Script Maps Service.\n *\n * @param {String} origin The starting address.\n * @param {String} destination The ending address.\n * @return {Object} The directions response object.\n */\nfunction getDirections_(origin, destination) {\n  const directionFinder = Maps.newDirectionFinder();\n  directionFinder.setOrigin(origin);\n  directionFinder.setDestination(destination);\n  const directions = directionFinder.getDirections();\n  if (directions.status !== \"OK\") {\n    throw directions.error_message;\n  }\n  return directions;\n}\n"
  },
  {
    "path": "solutions/custom-functions/calculate-driving-distance/README.md",
    "content": "# Calculate driving distance & convert meters to miles\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance) for additional details.\n\n"
  },
  {
    "path": "solutions/custom-functions/calculate-driving-distance/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/custom-functions/summarize-sheets-data/.clasp.json",
    "content": "{ \"scriptId\": \"1NN-ROSZO3ZsfiVUlCdmNqggpCQuGNtgO_r0nehV0s5mkZJN2bcMTri-7\" }\n"
  },
  {
    "path": "solutions/custom-functions/summarize-sheets-data/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Gets summary data from other sheets. The sheets you want to summarize must have columns with headers that match the names of the columns this function summarizes data from.\n *\n * @return {string} Summary data from other sheets.\n * @customfunction\n */\n\n// The following sheets are ignored. Add additional constants for other sheets that should be ignored.\nconst READ_ME_SHEET_NAME = \"ReadMe\";\nconst PM_SHEET_NAME = \"Summary\";\n\n/**\n * Reads data ranges for each sheet. Filters and counts based on 'Status' columns. To improve performance, the script uses arrays\n * until all summary data is gathered. Then the script writes the summary array starting at the cell of the custom function.\n */\nfunction getSheetsData() {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheets = ss.getSheets();\n  const outputArr = [];\n\n  // For each sheet, summarizes the data and pushes to a temporary array.\n  for (const s in sheets) {\n    // Gets sheet name.\n    const sheetNm = sheets[s].getName();\n    // Skips ReadMe and Summary sheets.\n    if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) {\n      continue;\n    }\n    // Gets sheets data.\n    const values = sheets[s].getDataRange().getValues();\n    // Gets the first row of the sheet which is the header row.\n    const headerRowValues = values[0];\n    // Finds the columns with the heading names 'Owner Name' and 'Status' and gets the index value of each.\n    // Using 'indexOf()' to get the position of each column prevents the script from breaking if the columns change positions in a sheet.\n    const columnOwner = headerRowValues.indexOf(\"Owner Name\");\n    const columnStatus = headerRowValues.indexOf(\"Status\");\n    // Removes header row.\n    values.splice(0, 1);\n    // Gets the 'Owner Name' column value by retrieving the first data row in the array.\n    const owner = values[0][columnOwner];\n    // Counts the total number of tasks.\n    const taskCnt = values.length;\n    // Counts the number of tasks that have the 'Complete' status.\n    // If the options you want to count in your spreadsheet differ, update the strings below to match the text of each option.\n    // To add more options, copy the line below and update the string to the new text.\n    const completeCnt = filterByPosition(\n      values,\n      \"Complete\",\n      columnStatus,\n    ).length;\n    // Counts the number of tasks that have the 'In-Progress' status.\n    const inProgressCnt = filterByPosition(\n      values,\n      \"In-Progress\",\n      columnStatus,\n    ).length;\n    // Counts the number of tasks that have the 'Scheduled' status.\n    const scheduledCnt = filterByPosition(\n      values,\n      \"Scheduled\",\n      columnStatus,\n    ).length;\n    // Counts the number of tasks that have the 'Overdue' status.\n    const overdueCnt = filterByPosition(values, \"Overdue\", columnStatus).length;\n    // Builds the output array.\n    outputArr.push([\n      owner,\n      taskCnt,\n      completeCnt,\n      inProgressCnt,\n      scheduledCnt,\n      overdueCnt,\n      sheetNm,\n    ]);\n  }\n  // Writes the output array.\n  return outputArr;\n}\n\n/**\n * Below is a helper function that filters a 2-dimenstional array.\n */\nfunction filterByPosition(array, find, position) {\n  return array.filter((innerArray) => innerArray[position] === find);\n}\n"
  },
  {
    "path": "solutions/custom-functions/summarize-sheets-data/README.md",
    "content": "# Summarize data from multiple sheets\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data) for additional details.\n\n"
  },
  {
    "path": "solutions/custom-functions/summarize-sheets-data/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/custom-functions/tier-pricing/.clasp.json",
    "content": "{ \"scriptId\": \"1-ql7ECe91XZgWu-hW_UZBx8mhuTtQQj0yNITYh8yQCOuHxLEjxtTngGB\" }\n"
  },
  {
    "path": "solutions/custom-functions/tier-pricing/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/samples/custom-functions/tier-pricing\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Calculates the tiered pricing discount.\n *\n * You must provide a value to calculate its discount. The value can be a string or a reference\n * to a cell that contains a string.\n * You must provide a data table range, for example, $B$4:$D$7, that includes the\n * tier start, end, and percent columns. If your table has headers, don't include\n * the headers in the range.\n *\n * @param {string} value The value to calculate the discount for, which can be a string or a\n * reference to a cell that contains a string.\n * @param {string} table The tier table data range using A1 notation.\n * @return number The total discount amount for the value.\n * @customfunction\n *\n */\nfunction tierPrice(value, table) {\n  let total = 0;\n  // Creates an array for each row of the table and loops through each array.\n  for (const [start, end, percent] of table) {\n    // Checks if the value is less than the starting value of the tier. If it is less, the loop stops.\n    if (value < start) {\n      break;\n    }\n    // Calculates the portion of the value to be multiplied by the tier's percent value.\n    const amount = Math.min(value, end) - start;\n    // Multiplies the amount by the tier's percent value and adds the product to the total.\n    total += amount * percent;\n  }\n  return total;\n}\n"
  },
  {
    "path": "solutions/custom-functions/tier-pricing/README.md",
    "content": "# Calculate a tiered pricing discount\n\nSee [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/tier-pricing) for additional details.\n\n"
  },
  {
    "path": "solutions/custom-functions/tier-pricing/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/editor-add-on/clean-sheet/.clasp.json",
    "content": "{ \"scriptId\": \"10bxhn6eGypm20dgRcTbUCbzP4Bz0dyYR6IZTNEA2gIXXxwoy8Zqs06yr\" }\n"
  },
  {
    "path": "solutions/editor-add-on/clean-sheet/Code.js",
    "content": "// To learn how to use this script, refer to the documentation:\n// https://developers.google.com/apps-script/add-ons/clean-sheet\n\n/*\nCopyright 2022 Google LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Application Constants\nconst APP_TITLE = \"Clean sheet\";\n\n/**\n * Identifies and deletes empty rows in selected range of active sheet.\n *\n * Cells that contain space characters are treated as non-empty.\n * The entire row, including the cells outside of the selected range,\n * must be empty to be deleted.\n *\n * Called from menu option.\n */\nfunction deleteEmptyRows() {\n  const sheet = SpreadsheetApp.getActiveSheet();\n\n  // Gets active selection and dimensions.\n  const activeRange = sheet.getActiveRange();\n  const rowCount = activeRange.getHeight();\n  const firstActiveRow = activeRange.getRow();\n  const columnCount = sheet.getMaxColumns();\n\n  // Tests that the selection is a valid range.\n  if (rowCount < 1) {\n    showMessage(\"Select a valid range.\");\n    return;\n  }\n  // Tests active range isn't too large to process. Enforces limit set to 10k.\n  if (rowCount > 10000) {\n    showMessage(\n      \"Selected range too large. Select up to 10,000 rows at one time.\",\n    );\n    return;\n  }\n\n  // Utilizes an array of values for efficient processing to determine blank rows.\n  const activeRangeValues = sheet\n    .getRange(firstActiveRow, 1, rowCount, columnCount)\n    .getValues();\n\n  // Checks if array is all empty values.\n  const valueFilter = (value) => value !== \"\";\n  const isRowEmpty = (row) => {\n    return row.filter(valueFilter).length === 0;\n  };\n\n  // Maps the range values as an object with value (to test) and corresponding row index (with offset from selection).\n  const rowsToDelete = activeRangeValues\n    .map((row, index) => ({ row, offset: index + activeRange.getRowIndex() }))\n    .filter((item) => isRowEmpty(item.row)) // Test to filter out non-empty rows.\n    .map((item) => item.offset); //Remap to include just the row indexes that will be removed.\n\n  // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges.\n  // Combines sequential empty rows for faster processing.\n  const rangesToDelete = rowsToDelete.reduce((ranges, index) => {\n    const currentRange = ranges[ranges.length - 1];\n    if (currentRange && index === currentRange[1] + 1) {\n      currentRange[1] = index;\n      return ranges;\n    }\n    ranges.push([index, index]);\n    return ranges;\n  }, []);\n\n  // Sends a list of row indexes to be deleted to the console.\n  console.log(rangesToDelete);\n\n  // Deletes the rows using REVERSE order to ensure proper indexing is used.\n  for (const [start, end] of rangesToDelete.reverse()) {\n    sheet.deleteRows(start, end - start + 1);\n  }\n  SpreadsheetApp.flush();\n}\n\n/**\n * Removes blank columns in a selected range.\n *\n * Cells containing Space characters are treated as non-empty.\n * The entire column, including cells outside of the selected range,\n * must be empty to be deleted.\n *\n * Called from menu option.\n */\nfunction deleteEmptyColumns() {\n  const sheet = SpreadsheetApp.getActiveSheet();\n\n  // Gets active selection and dimensions.\n  const activeRange = sheet.getActiveRange();\n  const rowCountMax = sheet.getMaxRows();\n  const columnWidth = activeRange.getWidth();\n  const firstActiveColumn = activeRange.getColumn();\n\n  // Tests that the selection is a valid range.\n  if (columnWidth < 1) {\n    showMessage(\"Select a valid range.\");\n    return;\n  }\n  // Tests active range is not too large to process. Enforces limit set to 1k.\n  if (columnWidth > 1000) {\n    showMessage(\n      \"Selected range too large. Select up to 10,000 rows at one time.\",\n    );\n    return;\n  }\n\n  // Utilizes an array of values for efficient processing to determine blank columns.\n  const activeRangeValues = sheet\n    .getRange(1, firstActiveColumn, rowCountMax, columnWidth)\n    .getValues();\n\n  // Transposes the array of range values so it can be processed in order of columns.\n  const activeRangeValuesTransposed = activeRangeValues[0].map((_, colIndex) =>\n    activeRangeValues.map((row) => row[colIndex]),\n  );\n\n  // Checks if array is all empty values.\n  const valueFilter = (value) => value !== \"\";\n  const isColumnEmpty = (column) => {\n    return column.filter(valueFilter).length === 0;\n  };\n\n  // Maps the range values as an object with value (to test) and corresponding column index (with offset from selection).\n  const columnsToDelete = activeRangeValuesTransposed\n    .map((column, index) => ({ column, offset: index + firstActiveColumn }))\n    .filter((item) => isColumnEmpty(item.column)) // Test to filter out non-empty rows.\n    .map((item) => item.offset); //Remap to include just the column indexes that will be removed.\n\n  // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges.\n  // Combines sequential empty columns for faster processing.\n  const rangesToDelete = columnsToDelete.reduce((ranges, index) => {\n    const currentRange = ranges[ranges.length - 1];\n    if (currentRange && index === currentRange[1] + 1) {\n      currentRange[1] = index;\n      return ranges;\n    }\n    ranges.push([index, index]);\n    return ranges;\n  }, []);\n\n  // Sends a list of column indexes to be deleted to the console.\n  console.log(rangesToDelete);\n\n  // Deletes the columns using REVERSE order to ensure proper indexing is used.\n  for (const [start, end] of rangesToDelete.reverse()) {\n    sheet.deleteColumns(start, end - start + 1);\n  }\n  SpreadsheetApp.flush();\n}\n\n/**\n * Trims all of the unused rows and columns outside of selected data range.\n *\n * Called from menu option.\n */\nfunction cropSheet() {\n  const dataRange = SpreadsheetApp.getActiveSheet().getDataRange();\n  const sheet = dataRange.getSheet();\n\n  let numRows = dataRange.getNumRows();\n  let numColumns = dataRange.getNumColumns();\n\n  const maxRows = sheet.getMaxRows();\n  const maxColumns = sheet.getMaxColumns();\n\n  const numFrozenRows = sheet.getFrozenRows();\n  const numFrozenColumns = sheet.getFrozenColumns();\n\n  // If last data row is less than maximium row, then deletes rows after the last data row.\n  if (numRows < maxRows) {\n    numRows = Math.max(numRows, numFrozenRows + 1); // Don't crop empty frozen rows.\n    sheet.deleteRows(numRows + 1, maxRows - numRows);\n  }\n\n  // If last data column is less than maximium column, then deletes columns after the last data column.\n  if (numColumns < maxColumns) {\n    numColumns = Math.max(numColumns, numFrozenColumns + 1); // Don't crop empty frozen columns.\n    sheet.deleteColumns(numColumns + 1, maxColumns - numColumns);\n  }\n}\n\n/**\n * Copies value of active cell to the blank cells beneath it.\n * Stops at last row of the sheet's data range if only blank cells are encountered.\n *\n * Called from menu option.\n */\nfunction fillDownData() {\n  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();\n\n  // Gets sheet's active cell and confirms it's not empty.\n  const activeCell = sheet.getActiveCell();\n  const activeCellValue = activeCell.getValue();\n\n  if (!activeCellValue) {\n    showMessage(\"The active cell is empty. Nothing to fill.\");\n    return;\n  }\n\n  // Gets coordinates of active cell.\n  const column = activeCell.getColumn();\n  const row = activeCell.getRow();\n\n  // Gets entire data range of the sheet.\n  const dataRange = sheet.getDataRange();\n  const dataRangeRows = dataRange.getNumRows();\n\n  // Gets trimmed range starting from active cell to the end of sheet data range.\n  const searchRange = dataRange.offset(\n    row - 1,\n    column - 1,\n    dataRangeRows - row + 1,\n    1,\n  );\n  const searchValues = searchRange.getDisplayValues();\n\n  // Find the number of empty rows below the active cell.\n  let i = 1; // Start at 1 to skip the ActiveCell.\n  while (searchValues[i] && searchValues[i][0] === \"\") {\n    i++;\n  }\n\n  // If blanks exist, fill the range with values.\n  if (i > 1) {\n    const fillRange = searchRange.offset(0, 0, i, 1).setValue(activeCellValue);\n    //sheet.setActiveRange(fillRange) // Uncomment to test affected range.\n  } else {\n    showMessage(\"There are no empty cells below the Active Cell to fill.\");\n  }\n}\n\n/**\n * A helper function to display messages to user.\n *\n * @param {string} message - Message to be displayed.\n * @param {string} caller - {Optional} text to append to title.\n */\nfunction showMessage(message, caller) {\n  // Sets the title using the APP_TITLE variable; adds optional caller string.\n  let title = APP_TITLE;\n  if (caller != null) {\n    title += ` : ${caller}`;\n  }\n\n  const ui = SpreadsheetApp.getUi();\n  ui.alert(title, message, ui.ButtonSet.OK);\n}\n"
  },
  {
    "path": "solutions/editor-add-on/clean-sheet/Menu.js",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Creates a menu entry in the Google Sheets Extensions menu when the document is opened.\n *\n * @param {object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  // Builds a menu that displays under the Extensions menu in Sheets.\n  const menu = SpreadsheetApp.getUi().createAddonMenu();\n\n  menu\n    .addItem(\"Delete blank rows (from selected rows only)\", \"deleteEmptyRows\")\n    .addItem(\n      \"Delete blank columns (from selected columns only)\",\n      \"deleteEmptyColumns\",\n    )\n    .addItem(\"Crop sheet to data range\", \"cropSheet\")\n    .addSeparator()\n    .addItem(\"Fill in blank rows below\", \"fillDownData\")\n    .addSeparator()\n    .addItem(\"About\", \"aboutApp\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately. This method is only used by\n * the desktop add-on and is never called by the mobile version.\n *\n * @param {object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * About box for context and developer contact information.\n * TODO: Personalize\n */\nfunction aboutApp() {\n  const msg = `\n  Name: ${APP_TITLE}\n  Version: 1.0\n  Contact: <Developer Email Goes Here>`;\n\n  const ui = SpreadsheetApp.getUi();\n  ui.alert(\"About this application\", msg, ui.ButtonSet.OK);\n}\n"
  },
  {
    "path": "solutions/editor-add-on/clean-sheet/README.md",
    "content": "# Clean up data in a spreadsheet\n\nSee [developers.google.com](https://developers.google.com/apps-script/add-ons/clean-sheet) for additional details.\n\n"
  },
  {
    "path": "solutions/editor-add-on/clean-sheet/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "solutions/ooo-assistant/.clasp.json",
    "content": "{ \"scriptId\": \"16L_UmGrkrDKYWrfw9YlnUnnnWOMBEWywyPrZDZIQqKF17Q97RtZeinqn\" }\n"
  },
  {
    "path": "solutions/ooo-assistant/Chat.gs",
    "content": "/**\n * Copyright 2025 Google LLC.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst APP_COMMAND = \"app command\";\n\n/**\n * Responds to an ADDED_TO_SPACE event in Google Chat.\n * @param {Object} event the event object from Google Workspace Add On\n */\nfunction onAddedToSpace(event) {\n  return sendCreateMessageAction(createCardMessage(help(APP_COMMAND)));\n}\n\n/**\n * Responds to a MESSAGE event in Google Chat.\n * @param {Object} event the event object from Google Workspace Add On\n */\nfunction onMessage(event) {\n  return sendCreateMessageAction(createCardMessage(help(APP_COMMAND)));\n}\n\n/**\n * Responds to a APP_COMMAND event in Google Chat.\n * @param {Object} event the event object from Google Workspace Add On\n */\nfunction onAppCommand(event) {\n  switch (event.chat.appCommandPayload.appCommandMetadata.appCommandId) {\n    case 2: // Block out day\n      return sendCreateMessageAction(createCardMessage(blockDayOut()));\n    case 3: // Set auto reply\n      return sendCreateMessageAction(createCardMessage(setAutoReply()));\n    default: // Help, any other\n      return sendCreateMessageAction(createCardMessage(help(APP_COMMAND)));\n  }\n}\n\n/**\n * Responds to a REMOVED_FROM_SPACE event in Google Chat.\n * @param {Object} event the event object from Google Workspace Add On\n */\nfunction onRemovedFromSpace(event) {\n  const space = event.chat.removedFromSpacePayload.space;\n  console.info(`Chat app removed from ${space.name || \"this chat\"}`);\n}\n\n// ----------------------\n// Util functions\n// ----------------------\n\nfunction createTextMessage(text) {\n  return { text: text };\n}\n\nfunction createCardMessage(card) {\n  return { cardsV2: [{ card: card }] };\n}\n\nfunction sendCreateMessageAction(message) {\n  return {\n    hostAppDataAction: {\n      chatDataAction: { createMessageAction: { message: message } },\n    },\n  };\n}\n"
  },
  {
    "path": "solutions/ooo-assistant/Common.gs",
    "content": "/**\n * Copyright 2025 Google LLC.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst UNIVERSAL_ACTION = \"universal action\";\n\n// ----------------------\n// Homepage util functions\n// ----------------------\n\n/**\n * Responds to homepage load request.\n */\nfunction onHomepage() {\n  return help();\n}\n\n// ----------------------\n// Action util functions\n// ----------------------\n\n// Help action: Show add-on details.\nfunction help(featureName = UNIVERSAL_ACTION) {\n  return {\n    header: addOnCardHeader(),\n    sections: [\n      {\n        widgets: [\n          {\n            decoratedText: {\n              text: `Hi! 👋 Feel free to use the following ${featureName}s:`,\n              wrapText: true,\n            },\n          },\n          {\n            decoratedText: {\n              text: \"<b>⛔ Block day out</b>: I will block out your calendar for today.\",\n              wrapText: true,\n            },\n          },\n          {\n            decoratedText: {\n              text: \"<b>↩️ Set auto reply</b>: I will set an OOO auto reply in your Gmail.\",\n              wrapText: true,\n            },\n          },\n        ],\n      },\n    ],\n  };\n}\n\n// Block day out action: Adds an all-day event to the user's Google Calendar.\nfunction blockDayOut() {\n  blockOutCalendar();\n  return createActionResponseCard(\n    \"Your calendar is now blocked out for today.\",\n  );\n}\n\n// Creates an OOO event in the user's Calendar.\nfunction blockOutCalendar() {\n  function getDateAndHours(hour, minutes) {\n    const date = new Date();\n    date.setHours(hour);\n    date.setMinutes(minutes);\n    date.setSeconds(0);\n    date.setMilliseconds(0);\n    return date.toISOString();\n  }\n\n  const event = {\n    start: { dateTime: getDateAndHours(9, 0) },\n    end: { dateTime: getDateAndHours(17, 0) },\n    eventType: \"outOfOffice\",\n    summary: \"OOO\",\n    outOfOfficeProperties: {\n      autoDeclineMode: \"declineOnlyNewConflictingInvitations\",\n      declineMessage: \"Declined because OOO.\",\n    },\n  };\n  Calendar.Events.insert(event, \"primary\");\n}\n\n// Set auto reply action: Set OOO auto reply in the user's Gmail .\nfunction setAutoReply() {\n  turnOnAutoResponder();\n  return createActionResponseCard(\n    \"The out of office auto reply has been turned on.\",\n  );\n}\n\n// Turns on the user's vacation response for today in Gmail.\nfunction turnOnAutoResponder() {\n  const ONE_DAY_MILLIS = 24 * 60 * 60 * 1000;\n  const currentTime = new Date().getTime();\n  Gmail.Users.Settings.updateVacation(\n    {\n      enableAutoReply: true,\n      responseSubject: \"I am OOO today\",\n      responseBodyHtml:\n        \"I am OOO today.<br><br><i>Created by OOO Assistant add-on!</i>\",\n      restrictToContacts: true,\n      restrictToDomain: true,\n      startTime: currentTime,\n      endTime: currentTime + ONE_DAY_MILLIS,\n    },\n    \"me\",\n  );\n}\n\n// ----------------------\n// Card util functions\n// ----------------------\n\nfunction addOnCardHeader() {\n  return {\n    title: \"OOO Assistant\",\n    subtitle: \"Helping manage your OOO\",\n    imageUrl: \"https://goo.gle/3SfMkjb\",\n  };\n}\n\n// Create an action response card\nfunction createActionResponseCard(text) {\n  return {\n    header: addOnCardHeader(),\n    sections: [\n      {\n        widgets: [\n          {\n            decoratedText: {\n              startIcon: {\n                iconUrl:\n                  \"https://fonts.gstatic.com/s/i/short-term/web/system/1x/task_alt_gm_grey_48dp.png\",\n              },\n              text: text,\n              wrapText: true,\n            },\n          },\n        ],\n      },\n    ],\n  };\n}\n"
  },
  {
    "path": "solutions/ooo-assistant/README.md",
    "content": "# Build a Google Workspace add-on extending all UIs\n\nThe add-on extends the following Google Workspace UIs: Chat, Calendar, Gmail, Drive, Docs, Sheets, and Slides.\n\nIt relies on app commands in Chat, and homepage and universal actions in the others.\n\nIt's featured in [this YT video](https://www.youtube.com/watch?v=pDthZ2xssDc).\n"
  },
  {
    "path": "solutions/ooo-assistant/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/New_York\",\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Gmail\",\n        \"version\": \"v1\",\n        \"serviceId\": \"gmail\"\n      },\n      {\n        \"userSymbol\": \"Calendar\",\n        \"version\": \"v3\",\n        \"serviceId\": \"calendar\"\n      }\n    ]\n  },\n  \"addOns\": {\n    \"common\": {\n      \"name\": \"OOO Assistant\",\n      \"logoUrl\": \"https://goo.gle/3SfMkjb\",\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomepage\"\n      },\n      \"universalActions\": [\n        {\n          \"label\": \"Block day out\",\n          \"runFunction\": \"blockDayOut\"\n        },\n        {\n          \"label\": \"Set auto reply\",\n          \"runFunction\": \"setAutoReply\"\n        }\n      ]\n    },\n    \"chat\": {},\n    \"calendar\": {},\n    \"gmail\": {},\n    \"drive\": {},\n    \"docs\": {},\n    \"sheets\": {},\n    \"slides\": {}\n  }\n}\n"
  },
  {
    "path": "solutions/webhook-chat-app/README.md",
    "content": "# Google Chat App Webhook\n\nPlease see related guide on how to\n[send messages to Google Chat with incoming webhooks](https://developers.google.com/workspace/chat/quickstart/webhooks).\n"
  },
  {
    "path": "solutions/webhook-chat-app/thread-reply.gs",
    "content": "/**\n * Copyright 2023 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n// [START chat_webhook_thread]\nfunction webhook() {\n  const url = \"https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD\"\n  const options = {\n    \"method\": \"post\",\n    \"headers\": {\"Content-Type\": \"application/json; charset=UTF-8\"},\n    \"payload\": JSON.stringify({\n      \"text\": \"Hello from Apps Script!\",\n      \"thread\": {\n        \"threadKey\": \"THREAD_KEY_VALUE\"\n      }\n    })\n  };\n  const response = UrlFetchApp.fetch(url, options);\n  console.log(response);\n}\n// [END chat_webhook_thread]\n"
  },
  {
    "path": "solutions/webhook-chat-app/webhook.gs",
    "content": "/**\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n// [START chat_webhook]\nfunction webhook() {\n  const url = \"https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN\"\n  const options = {\n    \"method\": \"post\",\n    \"headers\": {\"Content-Type\": \"application/json; charset=UTF-8\"},\n    \"payload\": JSON.stringify({\n      \"text\": \"Hello from Apps Script!\"\n    })\n  };\n  const response = UrlFetchApp.fetch(url, options);\n  console.log(response);\n}\n// [END chat_webhook]\n"
  },
  {
    "path": "tasks/quickstart/quickstart.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START tasks_quickstart]\n/**\n * Lists the user's tasks.\n * @see https://developers.google.com/tasks/reference/rest/v1/tasklists/list\n */\nfunction listTaskLists() {\n  const optionalArgs = {\n    maxResults: 10,\n  };\n  try {\n    // Returns all the authenticated user's task lists.\n    const response = Tasks.Tasklists.list(optionalArgs);\n    const taskLists = response.items;\n    // Print task list of user if available.\n    if (!taskLists || taskLists.length === 0) {\n      console.log(\"No task lists found.\");\n      return;\n    }\n    for (const taskList of taskLists) {\n      console.log(\"%s (%s)\", taskList.title, taskList.id);\n    }\n  } catch (err) {\n    // TODO (developer) - Handle exception from Task API\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END tasks_quickstart]\n"
  },
  {
    "path": "tasks/simpleTasks/README.md",
    "content": "# Simple Tasks\n\nSimple Tasks is a sample web app built using Apps Script that provides limited\nread and write access to your data in\n[Google Tasks](https://mail.google.com/tasks/canvas). It was created using the\n[HTML Service](https://developers.google.com/apps-script/guides/html-service)\nand demonstrates some common patterns and best practices to use when developing\nuser interfaces.\n\n![Simple tasks screenshot](screenshot.png)\n\n## Try it out\n\nFor your convience we have a\n[deployed instance](https://script.google.com/macros/s/AKfycbx-sB0Lp8JVgfvVoXkFtLsxMzqvOdfjG7VDo8OAeLusUDkFLj8/exec)\nof the script already running. The application supports reading your task lists\nand tasks, marking tasks as complete or incomplete, and adding new tasks to a\ntask list.\n\n## Setup\n\nThe first step is to create your script and copy in the code. The simplest way\nto do this is to\n[make a copy](https://script.google.com/d/1HCsbqH8WNEKFwRZCw8KEhykCGEzfXi-1k5eN-7t8lZoEAAvfqzOOsKtu/edit?newcopy=true)\nof the deployed instance of the script. If you wish to create your project from\nscratch, follow the steps below.\n\n1. Create a new standalone script in your Google Drive\n   ([instructions available here](https://developers.google.com/apps-script/managing_projects#creatingDrive))\n   and add in each of the files in this directory. You should already  have a\n   file named Code.gs in your project, and you can replace its contents with\n   the new code. For the remaining files, ensure you select\n   **File > New > HTML file** when creating the files, and when entering the\n   filename omit the `.html` suffix as it will be added automatically.\n\n2. Enabled the Google Tasks API on the script\n   ([instructions available here](https://developers.google.com/apps-script/built_in_services#advanced_google_services)).\n3. Save a new version of your script and publish it as a web app that runs as\n   the **User acessing the web app**.\n   ([instructions available here](https://developers.google.com/apps-script/execution_web_apps)).\n"
  },
  {
    "path": "tasks/simpleTasks/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Los_Angeles\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Tasks\",\n        \"serviceId\": \"tasks\",\n        \"version\": \"v1\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\"\n}\n"
  },
  {
    "path": "tasks/simpleTasks/javascript.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!-- Load the jQuery and jQuery UI libraries. -->\n<script src=\"https://code.jquery.com/jquery-1.8.3.min.js\"></script>\n<script src=\"https://code.jquery.com/ui/1.10.0/jquery-ui.min.js\"></script>\n\n<!-- Custom client-side JavaScript code. -->\n<script>\n  // When the page loads.\n  $(function() {\n    $('#tasklist').bind('change', loadTasks);\n    $('#new-task').bind('submit', onNewTaskFormSubmit);\n    loadTaskLists();\n  });\n\n  /**\n   * Load the available task lists.\n   */\n  function loadTaskLists() {\n    google.script.run.withSuccessHandler(showTaskLists)\n        .withFailureHandler(showError)\n        .getTaskLists();\n  }\n\n  /**\n   * Show the returned task lists in the dropdown box.\n   * @param {Array.<Object>} taskLists The task lists to show.\n   */\n  function showTaskLists(taskLists) {\n    var select = $('#tasklist');\n    select.empty();\n    taskLists.forEach(function(taskList) {\n      var option = $('<option>')\n          .attr('value', taskList.id)\n          .text(taskList.name);\n      select.append(option);\n    });\n    loadTasks();\n  }\n\n  /**\n   * Load the tasks in the currently selected task list.\n   */\n  function loadTasks() {\n    $('#tasks').text('Loading...');\n    var taskListId = $('#tasklist').val();\n    google.script.run.withSuccessHandler(showTasks)\n        .withFailureHandler(showError)\n        .getTasks(taskListId);\n  }\n\n  /**\n   * Show the returned tasks on the page.\n   * @param {Array.<Object>} tasks The tasks to show.\n   */\n  function showTasks(tasks) {\n    var list = $('#tasks').empty();\n    if (tasks.length > 0) {\n      tasks.forEach(function(task) {\n        var item = $('<li>');\n        var checkbox = $('<input type=\"checkbox\">')\n            .data('taskId', task.id)\n            .bind('change', onCheckBoxChange);\n        item.append(checkbox);\n\n        var title = $('<span>')\n            .text(task.title);\n        item.append(title);\n\n        if (task.completed) {\n          checkbox.prop('checked', true);\n          title.css('text-decoration', 'line-through')\n        }\n\n        list.append(item);\n      });\n    } else {\n      list.text('No tasks');\n    }\n  }\n\n  /**\n   * A callback function that runs when a task is checked or unchecked.\n   */\n  function onCheckBoxChange() {\n    var checkbox = $(this);\n    var title = checkbox.siblings('span');\n    var isChecked = checkbox.prop('checked');\n    var taskListId = $('#tasklist').val();\n    var taskId = checkbox.data('taskId');\n    if (isChecked) {\n      title.css('text-decoration', 'line-through');\n    } else {\n      title.css('text-decoration', 'none');\n    }\n    google.script.run.withSuccessHandler(function() {\n      title.effect(\"highlight\", {\n        duration: 1500\n      });\n    }).withFailureHandler(showError)\n      .setCompleted(taskListId, taskId, isChecked);\n  }\n\n  /**\n   * A callback function that runs when the new task form is submitted.\n   */\n  function onNewTaskFormSubmit() {\n    var taskListId = $('#tasklist').val();\n    var titleTextBox = $('#task-title');\n    var title = titleTextBox.val();\n    google.script.run.withSuccessHandler(function() {\n      titleTextBox.val('');\n      titleTextBox.effect(\"highlight\", {\n        duration: 1500\n      });\n      loadTasks();\n    }).withFailureHandler(showError)\n      .addTask(taskListId, title);\n    return false;\n  }\n\n  /**\n   * Logs an error message and shows an alert to the user.\n   */\n  function showError(error) {\n    console.log(error);\n    window.alert('An error has occurred, please try again.');\n  }\n</script>\n"
  },
  {
    "path": "tasks/simpleTasks/page.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n<head>\n<?!= HtmlService.createHtmlOutputFromFile('stylesheet').getContent(); ?>\n</head>\n<body>\n\n<h1>Simple Tasks</h1>\n<p>\n  This application allows you to view your <a href=\"https://mail.google.com/tasks/canvas\" target=\"_blank\">Google Tasks</a>,\n  mark them as complete or incomplete, and add new tasks.\n</p>\n\n<label for=\"tasklist\">Select a task list: </label>\n<select id=\"tasklist\">\n  <option>Loading...</option>\n</select>\n\n<fieldset id=\"tasks-panel\">\n  <legend>Tasks</legend>\n\n  <form name=\"new-task\" id=\"new-task\">\n    <label for=\"task-title\">Add a new task:</label>\n    <input type=\"text\" name=\"task-title\" id=\"task-title\" />\n    <input type=\"submit\" name=\"add\" id=\"add-button\" value=\"Add\" />\n  </form>\n\n  <ul id=\"tasks\">\n    Loading...\n  </ul>\n</fieldset>\n\n<?!= HtmlService.createHtmlOutputFromFile('javascript').getContent(); ?>\n\n</body>\n</html>\n"
  },
  {
    "path": "tasks/simpleTasks/simpleTasks.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Special function that handles HTTP GET requests to the published web app.\n * @return {HtmlOutput} The HTML page to be served.\n */\nfunction doGet() {\n  return HtmlService.createTemplateFromFile(\"page\")\n    .evaluate()\n    .setTitle(\"Simple Tasks\");\n}\n\n/**\n * Returns the ID and name of every task list in the user's account.\n * @return {Array.<Object>} The task list data.\n */\nfunction getTaskLists() {\n  const taskLists = Tasks.Tasklists.list().getItems();\n  if (!taskLists) {\n    return [];\n  }\n  return taskLists.map((taskList) => ({\n    id: taskList.getId(),\n    name: taskList.getTitle(),\n  }));\n}\n\n/**\n * Returns information about the tasks within a given task list.\n * @param {String} taskListId The ID of the task list.\n * @return {Array.<Object>} The task data.\n */\nfunction getTasks(taskListId) {\n  const tasks = Tasks.Tasks.list(taskListId).getItems();\n  if (!tasks) {\n    return [];\n  }\n  return tasks\n    .map((task) => ({\n      id: task.getId(),\n      title: task.getTitle(),\n      notes: task.getNotes(),\n      completed: Boolean(task.getCompleted()),\n    }))\n    .filter((task) => task.title);\n}\n\n/**\n * Sets the completed status of a given task.\n * @param {String} taskListId The ID of the task list.\n * @param {String} taskId The ID of the task.\n * @param {Boolean} completed True if the task should be marked as complete, false otherwise.\n */\nfunction setCompleted(taskListId, taskId, completed) {\n  const task = Tasks.newTask();\n  if (completed) {\n    task.setStatus(\"completed\");\n  } else {\n    task.setStatus(\"needsAction\");\n    task.setCompleted(null);\n  }\n  Tasks.Tasks.patch(task, taskListId, taskId);\n}\n\n/**\n * Adds a new task to the task list.\n * @param {String} taskListId The ID of the task list.\n * @param {String} title The title of the new task.\n */\nfunction addTask(taskListId, title) {\n  const task = Tasks.newTask().setTitle(title);\n  Tasks.Tasks.insert(task, taskListId);\n}\n"
  },
  {
    "path": "tasks/simpleTasks/stylesheet.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<!-- Load the jQuery UI styles. -->\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://code.jquery.com/ui/1.10.0/themes/base/jquery-ui.css\" />\n\n<!-- Custom styles. -->\n<style>\n  body {\n    width: 600px;\n    font-family: Arial, sans-serif;\n    font-size: 13px;\n    padding: 10px 10px 10px 30px;\n  }\n  #tasks-panel {\n    margin-top: 10px;\n  }\n  #tasks {\n    padding: 0;\n    list-style-type: none;\n  }\n  #task-title {\n    width: 300px;\n  }\n</style>\n"
  },
  {
    "path": "templates/README.md",
    "content": "# Templates for Google Apps Script\n\nTemplates that provide an initial, working framework for Apps Script\nprojects.\n\n## Introduction\n\nGoogle Apps Script allows developers to extend and maniplate Google\nDocs, Sheets and Forms. For those just starting with Apps Script, it\ncan be useful to have a template to work from -- a framework that\ndevelopers can learn from and modify to suit their needs.\n\nThis collection hosts the following templates:\n\n* Custom Functions for Sheets\n* Google Docs Add-on\n* Google Sheets Add-on\n* Google Forms Add-on\n* Script as Web App\n\nWithin these templates the following Google Apps Script concepts are\nillustrated:\n\n* [Dialogs and Sidebars](https://developers.google.com/apps-script/guides/dialogs)\n* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates)\n* Responding to HTTP GET requests with doGet(e)\n* Using IFRAME sandbox mode\n\n## Getting Started\n\nTemplates can be accessed from the Apps Script editor Welcome Screen\n(which is shown when the editor is first opened or by clicking the\n\"Help > Welcome screen\" menu item. Selecting a template from the\nWelcome Screen will create a new project pre-populated with the code\nyou need to get started.\n\nAlternatively, the code provided in this repository can be manually copied\ninto the Apps Script editor. Note that certain templates need to be used\nin a container-bound script (that is, the template is meant to be in a\nscript attached to a Doc, Sheet or Form, rather than a standalone script).\n"
  },
  {
    "path": "templates/custom-functions/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc Limits the script to only accessing the current spreadsheet.\n */\n\n/**\n * A function that takes a single input value and returns a single value.\n * Returns a simple concatenation of Strings.\n *\n * @param {String} name A name to greet.\n * @return {String} A greeting.\n * @customfunction\n */\nfunction SAY_HELLO(name) {\n  return `Hello ${name}`;\n}\n\n/**\n * A function that takes an input cell or range of cells and returns a cell or\n * range of cells.\n * Returns a range with all the input values incremented by one.\n *\n * @param {Array} input The range of numbers to increment.\n * @return {Array} The incremented values.\n * @customfunction\n */\nfunction INCREMENT(input) {\n  if (Array.isArray(input)) {\n    // Recurse to process an array.\n    return input.map(INCREMENT);\n  }\n  if (!(typeof input === \"number\")) {\n    throw new Error(\"Input contains a cell value that is not a number\");\n  }\n  // Otherwise process as a single value.\n  return input + 1;\n}\n\n/**\n * A function that takes an range of values and returns a single value.\n * Returns the sum the corner values in the range; for a single cell,\n * this is equal to (4 * the cell value).\n *\n * @param {Array} input The Range of numbers to sum the corners of.\n * @return {Number} The calculated sum.\n * @customfunction\n */\nfunction CORNER_SUM(input) {\n  if (!Array.isArray(input)) {\n    // Handle non-range inputs by putting them in an array.\n    return CORNER_SUM([[input]]); // eslint-disable-line new-cap\n  }\n  // Range processing here.\n  const maxRowIndex = input.length - 1;\n  const maxColIndex = input[0].length - 1;\n  return (\n    input[0][0] +\n    input[0][maxColIndex] +\n    input[maxRowIndex][0] +\n    input[maxRowIndex][maxColIndex]\n  );\n}\n\n/**\n * A function that takes a single value and returns a range of values.\n * Returns a range consisting of the first 10 powers and roots of that\n * number (with column headers).\n *\n * @param {Number} input The number to calculate from.\n * @return {Array} The first ten powers and roots of that number,\n *     with associated labels.\n * @customfunction\n */\nfunction POWERS_AND_ROOTS(input) {\n  if (Array.isArray(input)) {\n    throw new Error(\"Invalid: Range input not permitted\");\n  }\n  // Value processing and range generation here.\n  const headers = [\"x\", `${input}^x`, `${input}^(1/x)`];\n  const result = [headers];\n  for (let i = 1; i <= 10; i++) {\n    result.push([i, input ** i, input ** (1 / i)]);\n  }\n  return result;\n}\n\n/**\n * A function that takes a single input cell that is Date- or Date time-formatted.\n * Returns the day of the year represented by the provided date.\n *\n * @param {Date} date A Date to examine.\n * @return {Number} The day of year for that date.\n * @customfunction\n */\nfunction GET_DAY_OF_YEAR(date) {\n  if (!(date instanceof Date)) {\n    throw new Error(\"Invalid: Date input required\");\n  }\n  // Date processing here.\n  const firstOfYear = new Date(date.getFullYear(), 0, 0);\n  const diff = date - firstOfYear;\n  const oneDay = 1000 * 60 * 60 * 24;\n  return Math.floor(diff / oneDay);\n}\n\n/**\n * A function that takes a single input cell that is Duration-formatted.\n * Returns the number of seconds measured by that duration.\n *\n * @param {Date} duration A duration to convert.\n * @return {Number} Number of seconds in that duration.\n * @customfunction\n */\nfunction CONVERT_DURATION_TO_SECONDS(duration) {\n  if (!(duration instanceof Date)) {\n    throw new Error(\"Invalid: Duration input required\");\n  }\n\n  // Getting elapsed times from duration-formatted cells in Sheets requires\n  // subtracting the reference date from the cell value (while correcting for\n  // timezones).\n  const spreadsheetTimezone =\n    SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone();\n  const dateString = Utilities.formatDate(\n    duration,\n    spreadsheetTimezone,\n    \"EEE, d MMM yyyy HH:mm:ss\",\n  );\n  const date = new Date(dateString);\n  const epoch = new Date(\"Dec 30, 1899 00:00:00\");\n  const durationInMilliseconds = date.getTime() - epoch.getTime();\n\n  // Duration processing here.\n  return Math.round(durationInMilliseconds / 1000);\n}\n"
  },
  {
    "path": "templates/custom-functions/README.md",
    "content": "Template: Custom Functions for Sheets\n=====================================\n\nThis template provides a framework for creating custom functions\nin Google Sheets. It shows the structure needed to define a\ncustom function and its autocomplete documentation, and provides\na few examples.\n\nThe examples provided here demonstrate:\n\n* How to create custom functions that accept arguments of different types\n* How to document a custom function to generate correct autocomplete\n  information in Sheets\n\nNote that this template must be added to a container-bound script\nattached to a Google Sheet in order to function.\n\n\nFor more information, see [Custom Functions in Google Sheets](https://developers.google.com/apps-script/guides/sheets/functions).\n\nIn addition, developers of Sheets custom functions should be aware of\nthe [known issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets).\n\n"
  },
  {
    "path": "templates/docs-addon/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc  Limits the script to only accessing the current document.\n */\n\nconst DIALOG_TITLE = \"Example Dialog\";\nconst SIDEBAR_TITLE = \"Example Sidebar\";\n\n/**\n * Adds a custom menu with items to show the sidebar and dialog.\n *\n * @param {Object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  DocumentApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Show sidebar\", \"showSidebar\")\n    .addItem(\"Show dialog\", \"showDialog\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately.\n *\n * @param {Object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar. The sidebar structure is described in the Sidebar.html\n * project file.\n */\nfunction showSidebar() {\n  const ui = HtmlService.createTemplateFromFile(\"Sidebar\")\n    .evaluate()\n    .setTitle(SIDEBAR_TITLE);\n  DocumentApp.getUi().showSidebar(ui);\n}\n\n/**\n * Opens a dialog. The dialog structure is described in the Dialog.html\n * project file.\n */\nfunction showDialog() {\n  const ui = HtmlService.createTemplateFromFile(\"Dialog\")\n    .evaluate()\n    .setWidth(400)\n    .setHeight(150);\n  DocumentApp.getUi().showModalDialog(ui, DIALOG_TITLE);\n}\n\n/**\n * Returns the existing footer text (if any).\n *\n * @return {String} existing document footer text (as a plain string).\n */\nfunction getFooterText() {\n  // Retrieve and return the information requested by the sidebar.\n  return DocumentApp.getActiveDocument().getFooter().getText();\n}\n\n/**\n * Replaces the current document footer with the given text.\n *\n * @param {String} footerText text collected from the client-side\n *     sidebar.\n */\nfunction setFooterText(footerText) {\n  // Use data collected from sidebar to manipulate the document.\n  DocumentApp.getActiveDocument().getFooter().setText(footerText);\n}\n\n/**\n * Returns the document title.\n *\n * @return {String} the current document title.\n */\nfunction getDocTitle() {\n  // Retrieve and return the information requested by the dialog.\n  return DocumentApp.getActiveDocument().getName();\n}\n\n/**\n * Changes the document title.\n *\n * @param {String} title the new title to use for the document.\n */\nfunction setDocTitle(title) {\n  // Use data collected from dialog to manipulate the document.\n  DocumentApp.getActiveDocument().setName(title);\n}\n"
  },
  {
    "path": "templates/docs-addon/Dialog.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the dialog element structure. -->\n    <cursor></cursor><form id=\"title-form\">\n      <div class=\"block\" id=\"dialog-elements\">\n        <label for=\"dialog-title\">\n          You can set the document title here:\n        </label>\n        <input class=\"width-100\" id=\"dialog-title\">\n      </div>\n      <div class=\"block\" id=\"dialog-button-bar\">\n        <button type=\"submit\" class=\"action\" id=\"dialog-save-button\">Save</button>\n        <button id=\"dialog-cancel-button\" onclick=\"google.script.host.close()\">Cancel</button>\n      </div>\n      <div id=\"dialog-status\"></div>\n    </form>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript -->\n    <?!= HtmlService.createHtmlOutputFromFile('DialogJavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/docs-addon/DialogJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on dialog load.\n   */\n  $(function() {\n    // Assign handler functions to dialog elements here, if needed.\n    $('#title-form').submit(onTitleSave);\n\n    // Call the server here to retrieve any information needed to build\n    // the dialog, if necessary.\n    google.script.run\n       .withSuccessHandler(function(title) {\n            // Respond to success conditions here.\n            $('#dialog-title').val(title);\n            showStatus('Ready.');\n          })\n       .withFailureHandler(function(msg) {\n            // Respond to failure conditions here.\n            showStatus('Error retrieving title: ' + msg, 'error');\n          })\n       .getDocTitle();\n\n  });\n\n  /**\n   * Calls the server to modify the document.\n   * Changes the document title to match the dialog text.\n   */\n  function onTitleSave() {\n    this.disabled = true;\n    showStatus('Saving...');\n\n    // Gather any information that needs to be sent to the server here.\n    var title = $('#dialog-title').val();\n\n    // Send the value to the server and handle the response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Document title saved.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus('Could not save title: ' + msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .setDocTitle(title);\n    return false;\n  }\n\n  /**\n   * Displays the given status message in the dialog.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#dialog-status').removeClass().html(msg);\n    if (classId) {\n      $('#dialog-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/docs-addon/README.md",
    "content": "Template: Google Docs Add-on\n============================\n\nThis template provides a framework for creating a\n[Google Docs add-on](https://developers.google.com/apps-script/add-ons/).\nIt shows the structure needed to define a UI (including\nmenus, a sidebar and dialog) and how to coordinate communication\nbetween the UI client and the server where the Doc resides. This\ntemplate also covers some basic uses of Apps Script with Google\nDocs, including:\n\n* Reading and writing text to and from a Google Doc\n* Getting and modifying basic file information, such as the file title\n\nNote that add-ons that work with Google Docs will usually need to read\nand manipulate the (sometimes complex)\n[Doc structure](https://developers.google.com/apps-script/guides/docs#structure_of_a_document).\n\nFinally, note that this template must be added to a container-bound\nscript attached to a Google Doc in order to function. Developed\nadd-ons must go through a\n[publishing process](https://developers.google.com/apps-script/add-ons/publish)\nbefore they can be made available publicly.\n"
  },
  {
    "path": "templates/docs-addon/Sidebar.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the sidebar element structure. -->\n    <cursor></cursor><div class=\"sidebar branding-below\">\n      <p>\n        This sidebar allows you to retrieve and edit the document footer text. This\n        does not allow for complex formatting or styles, just plain text.\n      </p>\n      <div class=\"block form-group\">\n        <label for=\"sidebar-footer-text\">Footer Text:</label>\n        <textarea class=\"width-100\" id=\"sidebar-footer-text\" rows=\"10\"></textarea>\n      </div>\n      <div class=\"block\" id=\"sidebar-button-bar\">\n        <button class=\"action\" id=\"sidebar-save-button\">Save</button>\n      </div>\n      <div id=\"sidebar-status\"></div>\n    </div>\n\n    <div class=\"sidebar bottom\">\n      <img alt=\"Add-on logo\" class=\"logo\" width=\"25\"\n          src=\"https://googledrive.com/host/0B0G1UdyJGrY6XzdjQWF4a1JYY1k/apps-script_2x.png\">\n      <span class=\"gray branding-text\">Docs Add-on Template by Google</span>\n    </div>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript -->\n    <?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/docs-addon/SidebarJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on sidebar load.\n   */\n  $(function() {\n    // Assign handler functions to sidebar elements here, if needed.\n    $('#sidebar-save-button').click(onSaveFooterClick);\n\n    // Call the server here to retrieve any information needed to build\n    // the sidebar, if necessary.\n    google.script.run\n       .withSuccessHandler(function(footerText) {\n            // Respond to success conditions here.\n            $('#sidebar-footer-text').val(footerText);\n            showStatus('Ready.');\n          })\n       .withFailureHandler(function(msg) {\n            // Respond to failure conditions here.\n            showStatus(msg, 'error');\n          })\n       .getFooterText();\n  });\n\n  /**\n   * Calls the server to modify the document.\n   * Replaces the document footer text; formatting and styles will\n   * not be preserved.\n   */\n  function onSaveFooterClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n    var sidebarText = $('#sidebar-footer-text').val();\n    showStatus('Saving...');\n\n    // Send the value to the server and listen for a response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Saved.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus(msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .setFooterText(sidebarText);\n  }\n\n  /**\n   * Displays the given status message in the sidebar.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#sidebar-status').removeClass().html(msg);\n    if (classId) {\n      $('#sidebar-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/docs-addon/Stylesheet.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!-- This CSS package applies Google styling; it should always be included. -->\n<link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n\n<style>\nlabel {\n  font-weight: bold;\n}\n\nbutton {\n  min-width: 100px;\n}\n\n.width-100 {\n  width: 100%;\n  box-sizing: border-box;\n  -webkit-box-sizing : border-box;‌\n  -moz-box-sizing : border-box;\n}\n\n.table-label {\n  font-weight: bold;\n  width: 25%\n}\n\n.branding-below {\n  bottom: 54px;\n  top: 0;\n}\n\n.branding-text {\n  left: 7px;\n  position: relative;\n  top: 3px;\n}\n\n.logo {\n  vertical-align: middle;\n}\n\n#dialog-elements {\n  background-color: #eee;\n  border-color: #eee;\n  border-width: 5px;\n  border-style: solid;\n}\n\n#sidebar-button-bar,\n#dialog-button-bar,\n#dialog-elements {\n  margin-bottom: 10px;\n  margin-top: 10px;\n}\n\n</style>\n"
  },
  {
    "path": "templates/forms-addon/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc  Limits the script to only accessing the current form.\n */\n\nconst DIALOG_TITLE = \"Example Dialog\";\nconst SIDEBAR_TITLE = \"Example Sidebar\";\n\n/**\n * Adds a custom menu with items to show the sidebar and dialog.\n *\n * @param {Object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  FormApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Show sidebar\", \"showSidebar\")\n    .addItem(\"Show dialog\", \"showDialog\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately.\n *\n * @param {Object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar. The sidebar structure is described in the Sidebar.html\n * project file.\n */\nfunction showSidebar() {\n  const ui = HtmlService.createTemplateFromFile(\"Sidebar\")\n    .evaluate()\n    .setTitle(SIDEBAR_TITLE);\n  FormApp.getUi().showSidebar(ui);\n}\n\n/**\n * Opens a dialog. The dialog structure is described in the Dialog.html\n * project file.\n */\nfunction showDialog() {\n  const ui = HtmlService.createTemplateFromFile(\"Dialog\")\n    .evaluate()\n    .setWidth(350)\n    .setHeight(180);\n  FormApp.getUi().showModalDialog(ui, DIALOG_TITLE);\n}\n\n/**\n * Appends a new form item to the current form.\n *\n * @param {Object} itemData a collection of String data used to\n *     determine the exact form item created.\n */\nfunction addFormItem(itemData) {\n  // Use data collected from sidebar to manipulate the form.\n  const form = FormApp.getActiveForm();\n  switch (itemData.type) {\n    case \"Date\":\n      form.addDateItem().setTitle(itemData.name);\n      break;\n    case \"Scale\":\n      form.addScaleItem().setTitle(itemData.name);\n      break;\n    case \"Text\":\n      form.addTextItem().setTitle(itemData.name);\n      break;\n  }\n}\n\n/**\n * Queries the form DocumentProperties to determine whether the formResponse\n * trigger is enabled or not.\n *\n * @return {Boolean} True if the form submit trigger is enabled; false\n *     otherwise.\n */\nfunction getTriggerState() {\n  // Retrieve and return the information requested by the dialog.\n  const properties = PropertiesService.getDocumentProperties();\n  return properties.getProperty(\"triggerId\") != null;\n}\n\n/**\n * Turns the form submit trigger on or off based on the given argument.\n *\n * @param {Boolean} enableTrigger whether to turn on the form submit\n *     trigger or not\n */\nfunction adjustFormSubmitTrigger(enableTrigger) {\n  // Use data collected from dialog to manipulate form.\n\n  // Determine existing state of trigger on the server.\n  const form = FormApp.getActiveForm();\n  const properties = PropertiesService.getDocumentProperties();\n  const triggerId = properties.getProperty(\"triggerId\");\n\n  if (!enableTrigger && triggerId != null) {\n    // Delete the existing trigger.\n    const triggers = ScriptApp.getUserTriggers(form);\n    for (let i = 0; i < triggers.length; i++) {\n      if (triggers[i].getUniqueId() === triggerId) {\n        ScriptApp.deleteTrigger(triggers[i]);\n        break;\n      }\n    }\n    properties.deleteProperty(\"triggerId\");\n  } else if (enableTrigger && triggerId == null) {\n    // Create a new trigger.\n    const trigger = ScriptApp.newTrigger(\"respondToFormSubmit\")\n      .forForm(form)\n      .onFormSubmit()\n      .create();\n    properties.setProperty(\"triggerId\", trigger.getUniqueId());\n  }\n}\n\n/**\n * Responds to form submit events if a form summit trigger is enabled.\n * Collects some form information and sends it as an email to the form creator.\n *\n * @param {Object} e The event parameter created by a form\n *      submission; see\n *      https://developers.google.com/apps-script/understanding_events\n */\nfunction respondToFormSubmit(e) {\n  if (MailApp.getRemainingDailyQuota() > 0) {\n    const form = FormApp.getActiveForm();\n    let message = `There have been ${form.getResponses().length} response(s) so far. Latest Response:\\n`;\n    const itemResponses = e.response.getItemResponses();\n    for (let i = 0; i < itemResponses.length; i++) {\n      const itemTitle = itemResponses[i].getItem().getTitle();\n      const itemResponse = JSON.stringify(itemResponses[i].getResponse());\n      message += `${itemTitle}: ${itemResponse}\\n`;\n    }\n    MailApp.sendEmail(\n      Session.getEffectiveUser().getEmail(),\n      `Form response received for form ${form.getTitle()}`,\n      message,\n      { name: \"Forms Add-on Template\" },\n    );\n  }\n}\n"
  },
  {
    "path": "templates/forms-addon/Dialog.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the dialog element structure. -->\n    <cursor></cursor><div>\n      <div class=\"block\" id=\"dialog-elements\">\n        <p>\n          This dialog is used to turn on or off a form submit trigger that will\n          send an email to the form owner whenever a form is responded to.\n        </p>\n        <div class=\"bottom-spacer\" id=\"dialog-trigger-checkbox\">\n          <input type=\"checkbox\" id=\"dialog-trigger-check\">\n          <label for=\"dialog-trigger-check\">Enable form submit trigger</label>\n        </div>\n        <div class=\"block bottom-spacer\" id=\"dialog-button-bar\">\n          <button class=\"action\" id=\"dialog-save-button\">Save</button>\n          <button id=\"dialog-cancel-button\" onclick=\"google.script.host.close()\">Cancel</button>\n        </div>\n        <div id=\"dialog-status\"></div>\n      </div>\n    </div>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript -->\n    <?!= HtmlService.createHtmlOutputFromFile('DialogJavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/forms-addon/DialogJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on dialog load.\n   */\n  $(function() {\n    // Assign handler functions to sidebar elements here, if needed.\n    $('#dialog-save-button').click(onSaveTriggerClick);\n\n    showStatus('Please wait...');\n    // Call the server here to retrieve any information needed to build\n    // the sidebar, if necessary.\n    google.script.run\n       .withSuccessHandler(getTriggerStateCallback)\n       .getTriggerState();\n  });\n\n  /**\n   * Calls the server to modify the form's trigger.\n   * Enables or disables the form submit trigger based on the status of\n   * the dialog checkbox.\n   */\n  function onSaveTriggerClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n    var triggerEnabled = $('#dialog-trigger-check').is(':checked');\n    showStatus('Saving...');\n\n    // Send the value to the server and listen for a response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Saved trigger setting.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus('Error saving: ' + msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .adjustFormSubmitTrigger(triggerEnabled);\n  }\n\n  /**\n   * Responds to information coming from the server to adjust the dialog.\n   * Sets the checkbox state based on the server property.\n   *\n   * @param {Boolean} triggerEnabled indicates if a formResponse trigger has\n   *     been enabled or not.\n   */\n  function getTriggerStateCallback(triggerEnabled) {\n    // Use the information retrieved from the server to adjust the dialog.\n    if (triggerEnabled) {\n      $('#dialog-trigger-check').prop('checked', true);\n    } else {\n      $('#dialog-trigger-check').prop('checked', false);\n    }\n    showStatus('Ready.');\n  }\n\n  /**\n   * Displays the given status message in the dialog.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#dialog-status').removeClass().html(msg);\n    if (classId) {\n      $('#dialog-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/forms-addon/README.md",
    "content": "Template: Google Forms Add-on\n=============================\n\nThis template provides a framework for creating a\n[Google Forms add-on](https://developers.google.com/apps-script/add-ons/).\nIt shows the structure needed to define a UI (including\nmenus, a sidebar and dialog) and how to coordinate communication\nbetween the UI client and the server where the Form resides. This\ntemplate also covers some basic uses of Apps Script with Google\nForms, including:\n\n* Creating new form items programmatically\n* Setting, removing and responding to form submit triggers\n\nNote that this template must be added to a container-bound script\nattached to a Google Form in order to function. Also note that\ndeveloped add-ons must go through a\n[publishing process](https://developers.google.com/apps-script/add-ons/publish)\nbefore they can be made available publicly.\n"
  },
  {
    "path": "templates/forms-addon/Sidebar.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the sidebar element structure. -->\n    <cursor></cursor><div class=\"sidebar branding-below\">\n      <div class=\"block\" id=\"sidebar-elements\">\n        <p>\n          This sidebar allows you to set up and add form items. Choose the type of\n          the item and click 'Add item'. The resulting item will be added to the Form.\n        </p>\n        <label for=\"sidebar-item-type\">\n          Choose item type:\n        </label>\n        <select class=\"width-100\" id=\"sidebar-item-type\">\n          <option value=\"Date\">Date</option>\n          <option value=\"Scale\">Scale</option>\n          <option value=\"Text\">Text</option>\n        </select>\n      </div>\n      <div class=\"block bottom-spacer\" id=\"sidebar-button-bar\">\n          <button class=\"action\" id=\"sidebar-add-button\">Add item</button>\n      </div>\n      <div id=\"sidebar-status\"></div>\n    </div>\n\n    <div class=\"sidebar bottom\">\n      <img alt=\"Add-on logo\" class=\"logo\" width=\"25\"\n          src=\"https://googledrive.com/host/0B0G1UdyJGrY6XzdjQWF4a1JYY1k/apps-script_2x.png\">\n      <span class=\"gray branding-text\">Forms Add-on Template by Google</span>\n    </div>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript -->\n    <?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/forms-addon/SidebarJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on sidebar load.\n   */\n  $(function() {\n    // Assign handler functions to sidebar elements here, if needed.\n    $('#sidebar-add-button').click(onAddItemClick);\n\n    // Call the server here to retrieve any information needed to build\n    // the sidebar, if necessary.\n  });\n\n  /**\n   * Calls the server to modify the form.\n   * Sends the information entered into the sidebar to the server to append\n   * a new form item to the current form.\n   */\n  function onAddItemClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n    var itemData = {};\n    itemData.type = $('#sidebar-item-type').val();\n    itemData.name = itemData.type + ' question';\n\n    // Send the value to the server and listen for a response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Added a ' + itemData.type + ' form item.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus(msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .addFormItem(itemData);\n  }\n\n  /**\n   * Displays the given status message in the sidebar.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#sidebar-status').removeClass().html(msg);\n    if (classId) {\n      $('#sidebar-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/forms-addon/Stylesheet.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n<!-- The CSS package above applies Google styling to buttons and other elements. -->\n\n<style>\nlabel {\n  font-weight: bold;\n}\n\n.bottom-spacer {\n  margin-bottom: 10px;\n}\n\n.branding-below {\n  bottom: 54px;\n  top: 0;\n}\n\n.branding-text {\n  left: 7px;\n  position: relative;\n  top: 3px;\n}\n\n.logo {\n  vertical-align: middle;\n}\n\n.width-100 {\n  width: 100%;\n  box-sizing: border-box;\n  -webkit-box-sizing : border-box;‌\n  -moz-box-sizing : border-box;\n}\n\n#sidebar-item-size {\n  background-color: #eee;\n  border-color: #eee;\n  border-width: 5px;\n  border-style: solid;\n}\n</style>\n"
  },
  {
    "path": "templates/sheets-addon/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc  Limits the script to only accessing the current spreadsheet.\n */\n\nconst DIALOG_TITLE = \"Example Dialog\";\nconst SIDEBAR_TITLE = \"Example Sidebar\";\n\n/**\n * Adds a custom menu with items to show the sidebar and dialog.\n *\n * @param {Object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  SpreadsheetApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Show sidebar\", \"showSidebar\")\n    .addItem(\"Show dialog\", \"showDialog\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately.\n *\n * @param {Object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar. The sidebar structure is described in the Sidebar.html\n * project file.\n */\nfunction showSidebar() {\n  const ui = HtmlService.createTemplateFromFile(\"Sidebar\")\n    .evaluate()\n    .setTitle(SIDEBAR_TITLE);\n  SpreadsheetApp.getUi().showSidebar(ui);\n}\n\n/**\n * Opens a dialog. The dialog structure is described in the Dialog.html\n * project file.\n */\nfunction showDialog() {\n  const ui = HtmlService.createTemplateFromFile(\"Dialog\")\n    .evaluate()\n    .setWidth(400)\n    .setHeight(190);\n  SpreadsheetApp.getUi().showModalDialog(ui, DIALOG_TITLE);\n}\n\n/**\n * Returns the value in the active cell.\n *\n * @return {String} The value of the active cell.\n */\nfunction getActiveValue() {\n  // Retrieve and return the information requested by the sidebar.\n  const cell = SpreadsheetApp.getActiveSheet().getActiveCell();\n  return cell.getValue();\n}\n\n/**\n * Replaces the active cell value with the given value.\n *\n * @param {Number} value A reference number to replace with.\n */\nfunction setActiveValue(value) {\n  // Use data collected from sidebar to manipulate the sheet.\n  const cell = SpreadsheetApp.getActiveSheet().getActiveCell();\n  cell.setValue(value);\n}\n\n/**\n * Executes the specified action (create a new sheet, copy the active sheet, or\n * clear the current sheet).\n *\n * @param {String} action An identifier for the action to take.\n */\nfunction modifySheets(action) {\n  // Use data collected from dialog to manipulate the spreadsheet.\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const currentSheet = ss.getActiveSheet();\n  if (action === \"create\") {\n    ss.insertSheet();\n  } else if (action === \"copy\") {\n    currentSheet.copyTo(ss);\n  } else if (action === \"clear\") {\n    currentSheet.clear();\n  }\n}\n"
  },
  {
    "path": "templates/sheets-addon/Dialog.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the dialog element structure. -->\n    <cursor></cursor><div>\n      <p>This allows you to create, copy and clear sheets.</p>\n      <div class=\"block\" id=\"dialog-elements\">\n        <label for=\"dialog-action-select\">\n          Select an action to perfom:\n        </label>\n        <select class=\"width-100\" id=\"dialog-action-select\">\n          <option value=\"create\">Create a new Sheet</option>\n          <option value=\"copy\">Copy the current Sheet</option>\n          <option value=\"clear\">Clear the current Sheet</option>\n        </select>\n      </div>\n      <div class=\"block\" id=\"dialog-button-bar\">\n          <button class=\"action\" id=\"dialog-execute-button\">Execute</button>\n          <button id=\"dialog-cancel-button\" onclick=\"google.script.host.close()\">Cancel</button>\n      </div>\n      <div id=\"dialog-status\"></div>\n    </div>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript. -->\n    <?!= HtmlService.createHtmlOutputFromFile('DialogJavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/sheets-addon/DialogJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on dialog load.\n   */\n  $(function() {\n    // Assign handler functions to dialog elements here, if needed.\n    $('#dialog-execute-button').click(onExecuteClick);\n\n    // Call the server here to retrieve any information needed to build\n    // the dialog, if necessary.\n  });\n\n  /**\n   * Calls the server to modify the sheet.\n   * Determines the user-specified action (create a sheet, copy the active\n   * sheet, clear the active sheet) and asks the server to execute it. The\n   * dialog is then closed.\n   */\n  function onExecuteClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n    var action = $('#dialog-action-select').val();\n    showStatus('Working...');\n\n    // Send the value to the server and handle the response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Execution successful.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus('Execution failed: ' + msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .modifySheets(action);\n  }\n\n  /**\n   * Displays the given status message in the dialog.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#dialog-status').removeClass().html(msg);\n    if (classId) {\n      $('#dialog-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/sheets-addon/README.md",
    "content": "Template: Google Sheets Add-on\n==============================\n\nThis template provides a framework for creating a\n[Google Sheets add-on](https://developers.google.com/apps-script/add-ons/).\nIt shows the structure needed to define a UI (including menus, a sidebar and\ndialog) and how to coordinate communication between the UI client and the server\nwhere the Sheet resides. This template also covers some basic uses of Apps\nScript with Google Sheets, including:\n\n* Reading and writing data to a Sheet\n* Creating, copying and clearing a sheet\n\nNote that add-ons that work with Google Sheets will usually need to\nread and manipulate the Sheet data, formatting, validation, etc. For\nmore information, see\n[Extending Google Sheets](https://developers.google.com/apps-script/guides/sheets).\n\nIn addition, developers of Sheets add-ons should be aware of the\n[Known Issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets).\n\nFinally, note that this template must be added to a container-bound\nscript attached to a Google Sheet in order to function. Developed\nadd-ons must go through a\n[publishing process](https://developers.google.com/apps-script/add-ons/publish)\nbefore they can be made available publicly.\n\n"
  },
  {
    "path": "templates/sheets-addon/Sidebar.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <!-- Below is the HTML code that defines the sidebar element structure. -->\n    <cursor></cursor><div class=\"sidebar branding-below\">\n      <p>\n        This sidebar allows you to pull a value from a cell into the sidebar\n        and place a value from the sidebar into a cell.\n      </p>\n      <div class=\"block form-group\" id=\"sidebar-value-block\">\n        <label for=\"sidebar-value\">Sidebar value:</label>\n        <input id=\"sidebar-value\" value=\"1\">\n      </div>\n      <div class=\"block\" id=\"sidebar-button-bar\">\n        <button id=\"sidebar-pull-button\">Pull</button>\n        <button id=\"sidebar-put-button\">Put</button>\n      </div>\n      <div id=\"sidebar-status\"></div>\n    </div>\n\n    <!-- Enter sidebar bottom-branding below. -->\n    <div class=\"sidebar bottom\">\n      <img alt=\"Add-on logo\" class=\"logo\" width=\"25\"\n          src=\"https://googledrive.com/host/0B0G1UdyJGrY6XzdjQWF4a1JYY1k/apps-script_2x.png\">\n      <span class=\"gray branding-text\">Spreadsheet Add-on Template by Google</span>\n    </div>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript. -->\n    <?!= HtmlService.createHtmlOutputFromFile('SidebarJavaScript').getContent(); ?>\n  </body>\n</html>"
  },
  {
    "path": "templates/sheets-addon/SidebarJavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on sidebar load.\n   */\n  $(function() {\n    // Assign handler functions to sidebar elements here, if needed.\n    $('#sidebar-pull-button').click(onPullClick);\n    $('#sidebar-put-button').click(onPutClick);\n\n    // Call the server here to retrieve any information needed to build\n    // the dialog, if necessary.\n  });\n\n  /**\n   * Calls the server to retrieve information from the sheet.\n   * Gets the value in the active cell, which is then placed in the\n   * sidebar text field.\n   */\n  function onPullClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n\n    // Send the value to the server and handle the response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            $('#sidebar-value').val(msg);\n            showStatus('Pulled value successfully.');\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus(msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .getActiveValue();\n  }\n\n  /**\n   * Calls the server to modify the sheet.\n   * Replace the currently selected cell value with the value in the\n   * sidebar text field.\n   */\n  function onPutClick() {\n    this.disabled = true;\n\n    // Gather any information that needs to be sent to the server here.\n    var value = $('#sidebar-value').val();\n\n    // Send the value to the server and handle the response.\n    google.script.run\n        .withSuccessHandler(\n          function(msg, element) {\n            // Respond to success conditions here.\n            showStatus('Cell set to reference value: ' + value);\n            element.disabled = false;\n          })\n        .withFailureHandler(\n          function(msg, element) {\n            // Respond to failure conditions here.\n            showStatus(msg, 'error');\n            element.disabled = false;\n          })\n        .withUserObject(this)\n        .setActiveValue(value);\n  }\n\n  /**\n   * Displays the given status message in the sidebar.\n   *\n   * @param {String} msg The status message to display.\n   * @param {String} classId The message type (class id) that the message\n   *   should be displayed as.\n   */\n  function showStatus(msg, classId) {\n    $('#sidebar-status').removeClass().html(msg);\n    if (classId) {\n      $('#sidebar-status').addClass(classId);\n    }\n  }\n\n</script>\n"
  },
  {
    "path": "templates/sheets-addon/Stylesheet.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!-- This CSS package applies Google styling; it should always be included. -->\n<link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n\n<style>\nlabel {\n  font-weight: bold;\n}\n\n.branding-below {\n  bottom: 54px;\n  top: 0;\n}\n\n.branding-text {\n  left: 7px;\n  position: relative;\n  top: 3px;\n}\n\n.logo {\n  vertical-align: middle;\n}\n\n.width-100 {\n  width: 100%;\n  box-sizing: border-box;\n  -webkit-box-sizing : border-box;‌\n  -moz-box-sizing : border-box;\n}\n\n#sidebar-value-block,\n#dialog-elements {\n  background-color: #eee;\n  border-color: #eee;\n  border-width: 5px;\n  border-style: solid;\n}\n\n#sidebar-button-bar,\n#dialog-button-bar {\n  margin-bottom: 10px;\n}\n</style>\n"
  },
  {
    "path": "templates/sheets-import/APICode.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Return an array of potential columns (identifiers to locate them in\n * the data response object and the labels to use as column headers).\n * @return {Array} list of potential columns.\n */\nfunction getColumnOptions() {\n  const columns = [];\n\n  // TODO: Replace this section, adding a column entry for each data of\n  // interest. id should be an identifier that can be used to locate\n  // the data in the data request response, and label should be the name\n  // to associate with that data in the UI.\n  columns.push({ id: \"DATA_ITEM1_ID\", label: \"Data Item 1 label\" });\n  columns.push({ id: \"DATA_ITEM2_ID\", label: \"Data Item 2 label\" });\n  columns.push({ id: \"DATA_ITEM3_ID\", label: \"Data Item 3 label\" });\n\n  return columns;\n}\n\n/**\n * Return a page of results from the data source as a 2D array of\n * values (with columns corresponding to the columns specified). Return\n * null if no data exists for the specified pageNumber.\n * @param {Array} columns an array of Strings specifying the column ids\n *   to include in the output.\n * @param {Number} pageNumber a number indicating what page of data to\n *   retrieve from the data source.\n * @param {Number} pageSize a number indicating the maximum number of\n *   rows to return per call.\n * @param {Object} opt_settings optional object containing any additional\n *   information needed to retrieve data from the data source.\n * @return {object[]|null} Pages of data.\n */\nfunction getDataPage(columns, pageNumber, pageSize, opt_settings) {\n  const data = null;\n  /**\n   * TODO: This function needs to be implemented based on the particular\n   * details of the data source you are extracting data from. For example,\n   * you might request a page of data from an API using OAuth2 credentials\n   * similar to this:\n   *\n   * var service = getService(); // Be sure to configure the Auth.gs code\n   *\n   * // Build the appropriate API URL based on the parameters (pageNumber,\n   * // pageSize, and opt_settings).\n   * var url = '...';\n   * var response = UrlFetchApp.fetch(url, {\n   *   headers: {\n   *     Authorization: 'Bearer ' + service.getAccessToken(),\n   *     // Include any API-required headers needed for the call\n   *   }\n   * });\n   *\n   * // Given the response, construct the appropriate data output. Return\n   * // null if there is no data for the specified page.\n   * if (noData(response)) {\n   *   return null;\n   * }\n   * data = [];\n   *\n   * // Iterate over each relevant data item in the API response and build\n   * // a data row for it containing the data specified by columns\n   * // (in the same column order). Add each data row to data.\n   *\n   */\n\n  return data;\n}\n"
  },
  {
    "path": "templates/sheets-import/Auth.gs",
    "content": "/**\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Return an OAuth service object to handle authorization for a specific\n * data source (such as an API resource). Makes use of the OAuth2 Apps\n * Script library:\n *   https://github.com/googlesamples/apps-script-oauth2\n * @return {Object} a service object associated with the specified\n *   resource.\n */\nfunction getService() {\n  /* TODO: Fill in the following required parameters for your data source. */\n  const service = OAuth2.createService(\"ENTER_SERVICE_NAME_HERE\")\n    .setAuthorizationBaseUrl(\"ENTER_BASE_URL_HERE\")\n    .setTokenUrl(\"ENTER_TOKEN_URL_HERE\")\n    .setClientId(\"ENTER_CLIENT_ID_HERE\")\n    .setClientSecret(\"ENTER_CLIENT_SECRET_HERE\")\n    .setCallbackFunction(\"authCallback\")\n    .setPropertyStore(PropertiesService.getUserProperties());\n\n  /* TODO: Do any app-specific OAuth property setting here.\n   * For details, see:\n   *   https://github.com/googlesamples/apps-script-oauth2\n   */\n\n  return service;\n}\n\n/**\n * Example of a authorization callback function that is called after an\n * authorization attempt. Presents an authorization results window upon\n * completion of the API auth sequence. For additional details, see the\n * OAuth2 Apps Script library:\n *   https://github.com/googlesamples/apps-script-oauth2\n * @param {Object} request results of API auth request.\n * @return {HTML} A auth callback HTML page.\n */\nfunction authCallback(request) {\n  const template = HtmlService.createTemplateFromFile(\"AuthCallbackView\");\n  template.user = Session.getEffectiveUser().getEmail();\n  template.isAuthorized = false;\n  template.error = null;\n  let title;\n  try {\n    const service = getService();\n    const authorized = service.handleCallback(request);\n    template.isAuthorized = authorized;\n    title = authorized ? \"Access Granted\" : \"Access Denied\";\n  } catch (e) {\n    template.error = e;\n    title = \"Access Error\";\n  }\n  template.title = title;\n  return template.evaluate().setTitle(title);\n}\n\n/**\n * Builds and returns the API authorization URL from the service object.\n * @return {String} the API authorization URL.\n */\nfunction getAuthorizationUrl() {\n  return getService().getAuthorizationUrl();\n}\n\n/**\n * Resets the API service, forcing re-authorization before\n * additional authorization-required API calls can be made.\n */\nfunction signout() {\n  getService().reset();\n}\n"
  },
  {
    "path": "templates/sheets-import/AuthCallbackView.html",
    "content": "<!--\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<html>\n<head>\n  <link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n  <?!= include('Stylesheet') ?>\n</head>\n<body>\n  <h1><?= title ?></h1>\n  <? if (error) { ?>\n    <p> <span class=\"error-text\">An error has occurred: <?= error ?>.</span></p>\n    <p>You may close this tab.</p>\n  <? } else if (isAuthorized) { ?>\n    <p> <span class=\"all-clear-text\">Authorization complete!</span>\n      You may close this tab.</p>\n  <? } else { ?>\n    <p> <span class=\"error-text\">Authorization denied.</span>\n      You may close this tab.</p>\n  <? } ?>\n\n  <?!= include('intercom.js') ?>\n  <script>\n    var user = '<?= user ?>';\n    var intercom = Intercom.getInstance();\n    intercom.emit('oauthComplete', {\n      isAuthorized: '<?= isAuthorized ?>' == 'true',\n      user: user\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "templates/sheets-import/AuthorizationEmail.html",
    "content": "<!--\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<p>The Google Sheets add-on <i><?= addonName ?></i> was recently updated\nand it needs you to re-authorize.</p>\n\n<p>The add-on's automatic report updates are temporarily disabled until you\nre-authorize. You can accomplish this by opening one of the sheets\nusing the add-on and running the add-on through the menu. Alternatively, you can\nclick this link to approve authorization directly:</p>\n\n<p><a href=\"<?= url ?>\">Click here</a> to re-authorize the add-on.</p>\n\n<p>This notification email will be sent to you at most once per day until the\nadd-on is re-authorized.</p>\n\n<hr>\n\n<p style=\"font-size:80%\">This automatic message was sent to you via\nthe <i><?= addonName ?></i> add-on for Google Sheets.\n</p>\n"
  },
  {
    "path": "templates/sheets-import/Configurations.gs",
    "content": "/**\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst REPORT_SET_KEY = \"Import.ReportSet\";\nconst SCHEDULE_TRIGGER_ID = \"Import.scheduled.triggerId\";\n\n/**\n * Update type enum used when adding or deleting a report.\n */\nconst UPDATE_TYPE = {\n  ADD: 1,\n  REMOVE: 2,\n};\n\n/**\n * Return the report configuration for the report with the given\n * ID; returns an empty Object if no such report name exists.\n * @param {String} reportId a report ID.\n * @return {Object} a report configuration corresponding to that ID,\n *   or null if no such report exists.\n */\nfunction getReportConfig(reportId) {\n  const config = getObjectFromProperties(reportId);\n  if (!config) {\n    return null;\n  }\n  // Sheet name may have been changed manually, so\n  // get the current one.\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheet = getSheetById(ss, Number.parseInt(config.sheetId));\n  config.sheetName = !sheet ? null : sheet.getName();\n  return config;\n}\n\n/**\n * Given a report configuration, save it.\n * @param {object} config the report configuration.\n * @param {object} the updated report configuration.\n * @return {object} The saved configuration.\n */\nfunction saveReportConfig(config) {\n  const previous = getReportConfig(config.reportId);\n  if (config.reportId === \"new-report\") {\n    config.reportId = newReportId();\n    config.lastRun = null;\n    config.owner = Session.getEffectiveUser().getEmail();\n  }\n  saveObjectToProperties(config.reportId, config);\n  updateReportSet(UPDATE_TYPE.ADD, config.reportId, config.name);\n  if (previous == null) {\n    return config;\n  }\n  return {\n    ...previous,\n    ...config,\n  };\n}\n\n/**\n * Delete the report specified by the given ID.\n * @param {String} reportId indicates the report to delete.\n */\nfunction deleteReportConfig(reportId) {\n  deleteObjectFromProperties(reportId);\n  updateReportSet(UPDATE_TYPE.REMOVE, reportId);\n}\n\n/**\n * Returns true if the current user is allowed to edit the\n * report associated with the given config.\n * @param {Object} config a report configuration.\n * @return {boolean} True if the user can edit the report.\n */\nfunction canEditReport(config) {\n  if (!config) {\n    return false;\n  }\n  return (\n    config.scheduled === false ||\n    Session.getEffectiveUser().getEmail() === config.owner\n  );\n}\n\n/**\n * Given a new report configuration, return true if it saving this report would mean the limit on\n * scheduled reports would be exceeded.\n * @param {Object} config a report configuration to be saved.\n * @return {boolean} If it saving this report would mean the limit on scheduled reports\n * would be exceeded.\n */\nfunction isOverScheduleLimit(config) {\n  const previous = getReportConfig(config.reportId);\n  const currentUser = Session.getEffectiveUser().getEmail();\n  const isScheduled = config == null ? false : config.scheduled;\n  const wasScheduled = previous == null ? false : previous.scheduled;\n  return (\n    isScheduled &&\n    wasScheduled !== true &&\n    getScheduledReports(currentUser).length >= MAX_SCHEDULED_REPORTS\n  );\n}\n\n/**\n * Return a set of all saved reports (reportIds as keys, report\n * names as values).\n * @return {Object}\n */\nfunction getAllReports() {\n  const properties = PropertiesService.getDocumentProperties();\n  return JSON.parse(properties.getProperty(REPORT_SET_KEY));\n}\n\n/**\n * Get a set of report configurations that all have been marked\n * for scheduled imports.\n * @param {String} opt_user optional user email; if provided, returned\n *   results will only include reports that user is the owner of.\n * @return {Object} collection of configuration object for scheduled\n *   reports.\n */\nfunction getScheduledReports(opt_user) {\n  const scheduledReports = [];\n  for (const reportId of Object.keys(getAllReports())) {\n    const config = getReportConfig(reportId);\n    if (config?.scheduled && (!opt_user || opt_user === config.owner)) {\n      scheduledReports.push(config);\n    }\n  }\n  return scheduledReports;\n}\n\n/**\n * Updates the current report list (adding or removing a given\n * report name and id).\n * @param {Number} updateType Enum: either UPDATE_TYPE.ADD or\n *   UPDATE_TYPE.REMOVE.\n * @param {String} reportId report to add or remove.\n * @param {String} reportName report name (only needed for ADD).\n */\nfunction updateReportSet(updateType, reportId, reportName) {\n  const properties = PropertiesService.getDocumentProperties();\n  const lock = LockService.getDocumentLock();\n  lock.waitLock(2000);\n  let reportSet = JSON.parse(properties.getProperty(REPORT_SET_KEY));\n  if (reportSet == null) {\n    reportSet = {};\n  }\n  if (updateType === UPDATE_TYPE.ADD) {\n    reportSet[reportId] = reportName;\n  } else if (updateType === UPDATE_TYPE.REMOVE) {\n    delete reportSet[reportId];\n  }\n  properties.setProperty(REPORT_SET_KEY, JSON.stringify(reportSet));\n  lock.releaseLock();\n}\n\n/**\n * Update a report configuration with a sheetId and last runtime\n * information, save and return it. Include but do not save the\n * sheet name.\n * @param {Object} config the report configuration.\n * @param {Sheet} sheet the report's sheet.\n * @param {String} lastRun the datetime string indicating the last\n *   time the report was run.\n * @return {Object} the updated report configuration.\n */\nfunction updateOnImport(config, sheet, lastRun) {\n  const update = {\n    sheetId: sheet.getSheetId().toString(),\n    lastRun: lastRun,\n  };\n  saveObjectToProperties(config.reportId, update);\n  update.sheetName = sheet.getName();\n  return {\n    ...config,\n    ...update,\n  };\n}\n\n/**\n * Return the array of column IDs used by the given report\n * configuration.\n * @param {Object} config the report configuration.\n * @return {Array} column ID strings.\n */\nfunction getColumnIds(config) {\n  return config.columns.map((col) => col.column);\n}\n\n/**\n * Return the saved trigger ID of the scheduling trigger for this user.\n * @return {string|null} the trigger ID or null if the trigger is not set.\n */\nfunction getTriggerId() {\n  const properties = PropertiesService.getUserProperties();\n  return properties.getProperty(SCHEDULE_TRIGGER_ID);\n}\n\n/**\n * Save the trigger ID of the scheduling trigger for this user.\n * @param {Trigger} trigger the trigger whose ID should be saved.\n */\nfunction saveTriggerId(trigger) {\n  const properties = PropertiesService.getUserProperties();\n  properties.setProperty(SCHEDULE_TRIGGER_ID, trigger.getUniqueId());\n}\n\n/**\n * Remove the saved trigger ID.\n */\nfunction removeTriggerId() {\n  const properties = PropertiesService.getUserProperties();\n  properties.deleteProperty(SCHEDULE_TRIGGER_ID);\n}\n"
  },
  {
    "path": "templates/sheets-import/JavaScript.html",
    "content": "<!--\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script>\n  /**\n   * Error code enum; should be identical to codes defined in\n   * Server.gs.\n   */\n  var ERROR_CODES = {\n    AUTO_UPDATE_LIMIT: 1,\n    ILLEGAL_EDIT: 2,\n    ILLEGAL_DELETE: 3,\n    IMPORT_FAILED: 4\n  }\n\n  /**\n   * UI state enum used to determine how to reset sidebar elements after\n   * user actions.\n   */\n  var UI_STATE = {\n    NEW_REPORT: 1,\n    CONFIG_LOADED: 2,\n    AFTER_SAVE: 3,\n    AFTER_DELETE: 4,\n    TOGGLE_ALL: 5\n  };\n\n  $(function() {\n    // Pull in template parameters and set the initial sidebar view.\n    toggleUiView(isAuthorized);\n\n    // Add UI interaction handlers.\n    $('#authorize').bind('click', onAuthorizeClick);\n    $('#signout').bind('click', onSignoutClick);\n    $('#column-options').click(addSelectedColumnOption);\n    $('#columns-selected').click(removeSelectedColumnOption);\n    $('#report-name').keyup(verifyAndEnableSave);\n    $('#report-select').change(onReportSelect);\n    $('#save-report').click(onSaveClick);\n    $('#run-report').click(onRunClick);\n    $('#delete-report').click(onDeleteClick);\n\n    // Listen for auth complete intercom signal & toggle UI when received.\n    var intercom = Intercom.getInstance();\n    intercom.on('oauthComplete', function(data) {\n      if (data.user === user) {\n        toggleUiView(data.isAuthorized);\n      }\n    });\n\n    // Get existing reports & load them into the report select box.\n    refreshReportAndColumnLists();\n  });\n\n  /**\n   * Switch sidebar UI according to whether auth has\n   * occurred yet.\n   * @param {Boolean} true if auth has finished; false otherwise.\n   */\n  function toggleUiView(isAuthorized) {\n    if (!isAuthorized) {\n      $('#main').hide();\n      $('#authorization').show();\n    } else {\n      $('#main').show();\n      $('#authorization').hide();\n    }\n  }\n\n  /**\n   * Set the state of the UI elements.\n   * @param {Number} state Enum UI_STATE.\n   * @param {Object} opt_config potential parameters to load into\n   *   the sidebar.\n   */\n  function setUiState(state, opt_config) {\n    switch (state) {\n      case UI_STATE.NEW_REPORT:\n        // Refresh to collect any changes other editors have made.\n        refreshReportAndColumnLists();\n\n        // Disable buttons and clear sidebar data.\n        $('#delete-report').prop(\"disabled\", true);\n        $('#run-report').prop(\"disabled\", true);\n        $('#save-report').prop(\"disabled\", true);\n        $('#report-select').val(\"new-report\");\n        $(\"#columns-selected\").find('option').remove();\n        $(\"#report-name\").val('');\n        $('#scheduling-checkbox').prop('checked', false);\n        $('#last-run').html('?');\n        $('#sheet-name').html('?');\n        $('#owner-name').html('?');\n      break;\n    case UI_STATE.CONFIG_LOADED:\n      // Refresh to collect any changes other editors have made.\n      refreshReportAndColumnLists();\n\n      // Switch to existing report data and enable buttons.\n      $('#delete-report').prop(\"disabled\", false);\n      $('#run-report').prop(\"disabled\", false);\n      $('#save-report').prop(\"disabled\", false);\n      if (opt_config) {\n        $('#report-name').val(opt_config.name);\n        $('#scheduling-checkbox').prop('checked', opt_config.scheduled);\n        $('#owner-name').html(opt_config.owner);\n        if (! opt_config.lastRun) {\n          $('#last-run').html('?');\n        } else {\n          $('#last-run').html(opt_config.lastRun);\n        }\n        if (! opt_config.sheetName) {\n          $('#sheet-name').html('?');\n        } else {\n          $('#sheet-name').html(opt_config.sheetName);\n        }\n        $(\"#columns-selected\").find('option').remove();\n        opt_config.columns.forEach(function(col) {\n          addToSelectBox('#columns-selected', col.label, col.column);\n        });\n      }\n      break;\n    case UI_STATE.AFTER_SAVE:\n      // Update some UI elements and enable buttons.\n      if (opt_config) {\n        updateReportNameInList(opt_config.reportId, opt_config.name);\n        $('#report-select').val(opt_config.reportId);\n      }\n      setUiState(UI_STATE.CONFIG_LOADED, opt_config);\n      break;\n    case UI_STATE.AFTER_DELETE:\n      // Remove report from list and clear UI.\n      $(\"#report-select option[value='\" + opt_config.reportId + \"']\").remove();\n      setUiState(UI_STATE.NEW_REPORT);\n      break;\n    case UI_STATE.TOGGLE_ALL:\n      $('#delete-report').prop(\"disabled\", opt_config.disable);\n      $('#run-report').prop(\"disabled\", opt_config.disable);\n      $('#save-report').prop(\"disabled\", opt_config.disable);\n      $('#report-name').prop(\"disabled\", opt_config.disable);\n      $('#column-options').prop(\"disabled\", opt_config.disable);\n      $('#columns-selected').prop(\"disabled\", opt_config.disable);\n      $('#scheduling-checkbox').prop(\"disabled\", opt_config.disable);\n      $('#report-select').prop(\"disabled\", opt_config.disable);\n      break;\n    }\n  }\n\n  /**\n   * Collect the report configuration data currently entered into\n   * the sidebar into an object.\n   * @return {Object} report configuration data.\n   */\n  function collectSidebarData() {\n    var config = {\n      'name': $('#report-name').val().trim(),\n      'reportId': $('#report-select').val(),\n      'scheduled': $('#scheduling-checkbox').is(':checked'),\n      'columns': []\n    };\n    $('#columns-selected option').each(function(i, selected) {\n      config.columns.push({\n        'column': $(selected).val(),\n        'label': $(selected).text()\n      });\n    });\n    return config;\n  }\n\n  /**\n   * Respond to a change in the report select. Calls the server\n   * to retrieve the selected report's configuration information.\n   */\n  function onReportSelect() {\n    var reportId = $('#report-select').val();\n    if (reportId == 'new-report') {\n      setUiState(UI_STATE.NEW_REPORT);\n    } else {\n      setUiState(UI_STATE.TOGGLE_ALL, {disable: true});\n      google.script.run\n        .withSuccessHandler(function(config) {\n          setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n          setUiState(UI_STATE.CONFIG_LOADED, config);\n        })\n        .withFailureHandler(function(error) {\n          setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n          setUiState(UI_STATE.NEW_REPORT);\n          reportError('Unknown error during report retrieval.');\n        })\n        .switchToReport(reportId);\n    }\n  }\n\n  /**\n   * Run the currently selected report import.\n   */\n  function onRunClick() {\n    this.disabled = true;\n    var config = collectSidebarData();\n    if (config.columns.length > 0) {\n      setUiState(UI_STATE.TOGGLE_ALL, {disable: true});\n      google.script.run\n        .withSuccessHandler(function(config) {\n          setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n          setUiState(UI_STATE.CONFIG_LOADED, config);\n        })\n        .withFailureHandler(function(error) {\n          setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n          setUiState(UI_STATE.CONFIG_LOADED);\n          if (error == ERROR_CODES.IMPORT_FAILED) {\n            reportError('Error: data import failed.');\n          } else {\n            reportError('Unknown error during data import.');\n          }\n        })\n        .runImport(config.reportId);\n    } else {\n      this.disabled = false;\n    }\n  }\n\n  /**\n   * Save the current sidebar configuration using the given report name.\n   */\n  function onSaveClick() {\n    this.disabled = true;\n    var config = collectSidebarData();\n    setUiState(UI_STATE.TOGGLE_ALL, {disable: true});\n    google.script.run\n      .withSuccessHandler(function(config) {\n        setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n        setUiState(UI_STATE.AFTER_SAVE, config);\n      })\n      .withFailureHandler(function(error) {\n        setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n        if (error == ERROR_CODES.AUTO_UPDATE_LIMIT) {\n          setUiState(UI_STATE.NEW_REPORT);\n          reportError(\n            'Error: too many auto-updated reports! Configuration not saved.');\n        } else if (error == ERROR_CODES.ILLEGAL_EDIT) {\n          setUiState(UI_STATE.AFTER_SAVE);\n          reportError(\n            'Error: cannot save edits to auto-updated report of another user.');\n          onReportSelect();\n        } else {\n          setUiState(UI_STATE.AFTER_SAVE);\n          reportError('Unknown error during save.');\n        }\n      })\n      .saveReport(config);\n  }\n\n  /**\n   * Delete the currently selected report configuration.\n   */\n  function onDeleteClick() {\n    this.disabled = true;\n    var reportId = $('#report-select').find(\":selected\").val();\n    setUiState(UI_STATE.TOGGLE_ALL, {disable: true});\n    google.script.run\n      .withSuccessHandler(function(id) {\n        setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n        setUiState(UI_STATE.AFTER_DELETE, {'reportId': id});\n      })\n      .withFailureHandler(function(error) {\n        setUiState(UI_STATE.TOGGLE_ALL, {disable: false});\n        if (error == ERROR_CODES.ILLEGAL_DELETE) {\n          reportError(\n          'Error: cannot delete auto-updated report of another user.');\n        } else {\n          reportError('Unknown error during deletion.');\n        }\n      })\n      .removeReport(reportId);\n  }\n\n  /**\n   * Start the authorization flow.\n   */\n  function onAuthorizeClick() {\n    google.script.run\n      .withSuccessHandler(openUrlWindow)\n      .withFailureHandler(reportError)\n      .getAuthorizationUrl();\n  }\n\n  /**\n   * Sign off; further interaction will require re-authorization.\n   */\n  function onSignoutClick() {\n    toggleUiView(false);\n    google.script.run\n      .withFailureHandler(reportError)\n      .signout();\n  }\n\n  /**\n   * Open a new window with the given URL.\n   * @param {String} url the URL to open.\n   */\n  function openUrlWindow(url) {\n    window.open(url);\n  }\n\n  /**\n   * Add a clicked option to the selected column list.\n   */\n  function addSelectedColumnOption() {\n    var value = $('#column-options').val();\n    var text = $('#column-options').find(\":selected\").text();\n    addToSelectBox('#columns-selected', text, value);\n  }\n\n  /**\n   * Remove a clicked option to the selected column list.\n   */\n  function removeSelectedColumnOption() {\n    $(\"#columns-selected\").find('option:selected').remove();\n  }\n\n  /**\n   * Disables the Save button if the report name is blank,\n   * otherwise enables the Save button.\n   */\n  function verifyAndEnableSave() {\n    if ($('#report-name').val().trim() != '') {\n      $('#save-report').prop(\"disabled\",false);\n    } else {\n      $('#save-report').prop(\"disabled\",true);\n    }\n  }\n\n  /**\n   * Call for the currently saved report list and column options\n   * and load them into the sidebar.\n   */\n  function refreshReportAndColumnLists() {\n    google.script.run\n      .withSuccessHandler(loadInitialSidebarLists)\n      .withFailureHandler(reportError)\n      .getInitialDataForSidebar();\n  }\n\n  /**\n   * Load saved report data and column options into the sidebar\n   * select boxes.\n   * @param {Object} savedData a collection of saved report data\n   *   and column options.\n   */\n  function loadInitialSidebarLists(savedData) {\n    // Clear the existing report list, but save the current selection\n    // so that it can be reset after repopulating the list.\n    var currentReport = $('#report-select').val();\n    $(\"#report-select\").find('option').remove();\n    updateReportNameInList('new-report');\n    savedData.reports.forEach(function(report) {\n      updateReportNameInList(report.reportId, report.name);\n    });\n    $('#report-select').val(currentReport);\n    $(\"#column-options\").find('option').remove();\n    savedData.columns.forEach(function(col) {\n      addToSelectBox('#column-options', col.label, col.id);\n    });\n  }\n\n  /**\n   * If the given report exists in the select box list, change its\n   * name. Otherwise create a new option in that list with this name\n   * and report ID.\n   * @param {String} reportId report ID to adjust or add.\n   * @param {String} name new report name.\n   */\n  function updateReportNameInList(reportId, name) {\n    var label = 'Report ' + name;\n    if (reportId == 'new-report') {\n      label = 'Create a new report';\n    }\n    if ($(\"#report-select option[value='\" + reportId + \"']\").length > 0) {\n      $('#report-select').find(\":selected\").text(label);\n    } else {\n      addToSelectBox('#report-select', label, reportId);\n    }\n  }\n\n  /**\n   * Add the given report name and id as a select box option.\n   * @param {String} box reference string indicating which box\n   *   to append the option to.\n   * @param {String} label the label for the option.\n   * @param {String} value the value for the option.\n   */\n  function addToSelectBox(box, label, value) {\n    $(box).append(\n        $(\"<option></option>\").attr(\"value\", value).text(label)\n    );\n  }\n\n  /**\n   * Report error to user.\n   * @param {String} error the error message.\n   */\n  function reportError(error) {\n    window.alert(error);\n  }\n\n</script>\n"
  },
  {
    "path": "templates/sheets-import/README.md",
    "content": "# Template: Importing Data to Sheets\n\nThis template provides a framework for creating a Sheets [add-on](https://developers.google.com/apps-script/add-ons/)\nthat imports data from a third-party source (such as an API).\n\nIt shows the basic structure needed to define a UI and how to coordinate\ncommunication between the client, server, and third-party source.\nThis template also demonstrates some useful aspects of Apps Script, including:\n\n* Writing data to a Google Sheet\n* Using time-based triggers to establish automated sheet updates\n* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates)\n* Using IFRAME sandbox mode\n\n**Note**: The purpose of this template is to show a general add-on structure.\nIt will not run as an add-on in it's current state. To make use of this\ntemplate, you will need to fill in the sections marked **TODO** to customize\nthe template to a specific third-party data source.\n\n## Project manifest\nThe following project files are included in this template:\n\n* `**APICode.gs**` - This file contains all the API-specific code for handling\n  authorization, callbacks, and API calls. It will need to be modified to handle\n  a specific API.\n* `**Auth.gs**` - This file contains code that assists with constructing a\n  OAuth2 service object using the [Apps Script OAuth2 library](https://github.com/googlesamples/apps-script-oauth2).\n* `**AuthCallbackView.html**` - This file is the page that is presented to the\n  user after an authorization attempt, and shows whether the authorization was\n  successful.\n* `**AuthorizationEmail.html**` - This file contains the HTML template of an email\n  that would be sent to the user in the event that a trigger attempts to fire\n  without all the required authorizations.\n* `**Configurations.gs**` - This file contains code that controls the creation,\n  updating and deletion of report configurations that describe what to import\n  to Sheets from the third-party source. By default report configurations are\n  saved to Apps Script's PropertyService, but it would be possible to adapt the\n  code here to store that data elsewhere (for example, in an external database).\n* `**JavaScript.html**` - This file contains the bulk of the control code for the\n  sidebar UI.\n* `**Server.gs**` - This file contains server-side code that responds to user\n  interactions in the sidebar UI. It also sets up the add-on menu.\n* `**Sidebar.html**` - This file contains the HTML structure for that defines\n  the sidebar UI.\n* *`*Stylesheet.html**` - This file contains all the CSS properties defined for\n  the template.\n* `**Utilities.gs**` - This file contains some generic functionalities to support\n  the rest of the code. The functions here are not specific to this template and\n  could be taken for use in other projects without modification.\n* `**intercom.js.html**` - This file contains a copy of\n  [intercom.js](https://github.com/diy/intercom.js),\n  a cross-window message broadcast interface (intercom.js is released under an\n  Apache V2.0 license).\n\n## Setup: Libraries\n\nThis template makes use of the following libraries, which much the added to the\nApps Script project before the template can be used:\n\n* [Apps Script OAuth2 library](https://github.com/googlesamples/apps-script-oauth2)\n* [Underscore](http://underscorejs.org/)\n\nThese libraries are already published as an Apps Script, making it easy to\ninclude in your project. To add it to your script, do the following in the\nApps Script code editor:\n\n1. Click on the menu item \"Resources > Libraries...\"\n1. In the \"Find a Library\" text box, enter the project key\n\"MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48\" and click the \"Select\" button.\n1. Choose the latest version in the dropdown box.\n1. Click the \"Save\" button.\n\nThis will add the [OAuth2 library](https://github.com/googlesamples/apps-script-oauth2)\nto your project. Repeat the above steps with the project key\n\"MGwgKN2Th03tJ5OdmlzB8KPxhMjh3Sh48\" to add Underscore to the project as well.\n\n## Setup: API configuration\n\nThis template requires app-specific configuration before it can used.\nSpecifically, the template will need to be informed of the authorization\ndetails, and certain adjustments made to ensure the correct data can be\nextracted from the responses.\n\nIn the Server.gs, APICode.gs and Auth.gs project files, there are several\ncomments marked as **TODO**. To configure the template, visit each of these\n**TODO** sections and follow the directions found there.\n\nNote that the template design assumes that the template will connect to an API\nservice using OAuth 2.0; however, it would be possible adapt it to connect to\na different kind of service, such as a SQL database.\n\n## Additional information\n\nFor more information, see:\n\n* [Extending Google Sheets](https://developers.google.com/apps-script/guides/sheets)\n* [Known Issues specific to Google Sheets](https://developers.google.com/apps-script/migration/sheets)\n\nNote that this template must be added to a container-bound\nscript attached to a Google Sheet in order to function. Developed\nadd-ons must go through a\n[publishing process](https://developers.google.com/apps-script/add-ons/publish)\nbefore they can be made available publicly.\n"
  },
  {
    "path": "templates/sheets-import/Server.gs",
    "content": "/**\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @OnlyCurrentDoc  Limits the script to only accessing the current spreadsheet.\n */\n\nconst _ = Underscore.load();\n\n/**\n * TODO: Replace the following with the name of the service you are importing\n * from and the name of the add-on you are building, respectively.\n */\nconst DATA_ALIAS = \"MyDataSource\";\nconst ADDON_NAME = \"YOUR_ADDON_NAME_HERE\";\nconst SIDEBAR_TITLE = \"Import Control Center\";\nconst MAX_SCHEDULED_REPORTS = 24;\nconst IMPORT_PAGE_SIZE = 30;\n\n/**\n * Error code enum; this gets passed to the sidebar for use there as well.\n */\nconst ERROR_CODES = {\n  AUTO_UPDATE_LIMIT: 1,\n  ILLEGAL_EDIT: 2,\n  ILLEGAL_DELETE: 3,\n  IMPORT_FAILED: 4,\n};\n\n/**\n * Adds a custom menu with items to show the sidebar.\n * @param {Object} e The event parameter for a simple onOpen trigger.\n */\nfunction onOpen(e) {\n  SpreadsheetApp.getUi()\n    .createAddonMenu()\n    .addItem(\"Import control center\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Runs when the add-on is installed; calls onOpen() to ensure menu creation and\n * any other initializion work is done immediately.\n * @param {Object} e The event parameter for a simple onInstall trigger.\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n\n/**\n * Opens a sidebar. The sidebar structure is described in the Sidebar.html\n * project file.\n */\nfunction showSidebar() {\n  const service = getService();\n  const template = HtmlService.createTemplateFromFile(\"Sidebar\");\n  template.user = Session.getEffectiveUser().getEmail();\n  template.dataSource = DATA_ALIAS;\n  template.isAuthorized = service.hasAccess();\n  template.authorizationUrl = null;\n  if (!template.isAuthorized) {\n    template.authorizationUrl = service.getAuthorizationUrl();\n  }\n  const page = template.evaluate().setTitle(SIDEBAR_TITLE);\n  SpreadsheetApp.getUi().showSidebar(page);\n}\n\n/**\n * Return data needed to build the sidebar UI: a list of the names of the\n * currently saved report configurations and the list of potential\n * column choices.\n * @return {Object} a collection of saved report data and column options.\n */\nfunction getInitialDataForSidebar() {\n  const reportSet = getAllReports();\n  const reportList = [];\n  _.each(reportSet, (val, key) => {\n    reportList.push({ name: val, reportId: key });\n  });\n  reportList.sort((a, b) => {\n    if (a.name > b.name) {\n      return 1;\n    }\n    if (a.name < b.name) {\n      return -1;\n    }\n    return 0;\n  });\n  return { reports: reportList, columns: getColumnOptions() };\n}\n\n/**\n * Get the report configuration for the given report and, if a sheet\n * exists for it, activate that sheet.\n * @param {String} reportId a report ID.\n * @return {object} The report config.\n */\nfunction switchToReport(reportId) {\n  const config = getReportConfig(reportId);\n  activateById(config.sheetId);\n  return config;\n}\n\n/**\n * Import data to the spreadsheet according to the given report\n * configuration.\n * @param {string} reportId the report identifier.\n * @return {object} the (possibly updated) report configuration.\n */\nfunction runImport(reportId) {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  let config = getReportConfig(reportId);\n\n  // Acquire the sheet to place the import results in,\n  // then clear and format it.\n  // Update the saved config with sheet/time information.\n  const sheet = activateReportSheet(config);\n  const columnIds = getColumnIds(config);\n  const lastRun = new Date().toString();\n  config = updateOnImport(config, sheet, lastRun);\n\n  // Call for pages of API information to place in the sheet, one\n  // page at a time.\n  let pageNumber = 0;\n  let firstRow = 2;\n  try {\n    let page;\n    do {\n      page = getDataPage(columnIds, pageNumber, IMPORT_PAGE_SIZE, config);\n      if (page) {\n        sheet\n          .getRange(firstRow, 1, page.length, page[0].length)\n          .setValues(page);\n        firstRow += page.length;\n        pageNumber++;\n        SpreadsheetApp.flush();\n      }\n    } while (page != null);\n  } catch (e) {\n    // Ensure a new sheet Id, if created, is preserved.\n    throw ERROR_CODES.IMPORT_FAILED;\n  }\n\n  for (let i = 1; i <= sheet.getLastColumn(); i++) {\n    sheet.autoResizeColumn(i);\n  }\n  ss.toast(`Report ${config.name} updated.`);\n  return config;\n}\n\n/**\n * Save the given report configuration.\n * @param {Object} config a report configuration to save.\n * @return {Object} the updated report configuration.\n */\nfunction saveReport(config) {\n  const existingConfig = getReportConfig(config.reportId);\n  if (existingConfig != null) {\n    activateById(existingConfig.sheetId);\n    // Check: users are not allowed to save edits to reports\n    // created by other users if those reports have been marked\n    // for auto-update.\n    if (!canEditReport(existingConfig)) {\n      throw ERROR_CODES.ILLEGAL_EDIT;\n    }\n  }\n  // Check against max number of scheduled reports.\n  if (isOverScheduleLimit(config)) {\n    throw ERROR_CODES.AUTO_UPDATE_LIMIT;\n  }\n\n  const result = saveReportConfig(config);\n  adjustScheduleTrigger();\n  return result;\n}\n\n/**\n * Delete the given report configuration.\n * @param {String} reportId indicates the report to delete.\n * @return {String} the report ID deleted.\n */\nfunction removeReport(reportId) {\n  // Check: users are not allowed to delete reports created by\n  // other users if those reports have been marked for auto-update.\n  if (!canEditReport(getReportConfig(reportId))) {\n    throw ERROR_CODES.ILLEGAL_DELETE;\n  }\n  deleteReportConfig(reportId);\n  adjustScheduleTrigger();\n  return reportId;\n}\n\n/**\n * Activate, clear, format and return the sheet associated with the\n * specified report configuration. If the sheet does not exist, create,\n * format and activate it.\n * @param {Object} config the report configuration.\n * @return {Sheet}\n */\nfunction activateReportSheet(config) {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  let sheet = getSheetById(ss, Number.parseInt(config.sheetId));\n  if (sheet == null) {\n    sheet = ss.insertSheet();\n    sheet.setName(getUniqueSheetName(ss, config.name));\n  }\n  sheet.activate();\n\n  const headers = _.map(config.columns, (col) => col.label);\n  sheet.clear();\n  sheet.clearNotes();\n  sheet.setFrozenRows(1);\n  sheet\n    .getRange(\"1:1\")\n    .setFontWeight(\"bold\")\n    .setBackground(\"#000000\")\n    .setFontColor(\"#ffffff\");\n  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);\n  return sheet;\n}\n\n/**\n * On an hourly trigger, search through scheduled reports, find one\n * that hasn't been run in 24 hours or more (or never), and run\n * an import for that one. With <= 24 scheduled reports, this pattern\n * ensures that every scheduled report will be updated once a day.\n */\nfunction respondToHourlyTrigger() {\n  const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);\n  // Check if the actions of the trigger require authorizations that have not\n  // been supplied yet -- if so, warn the active user via email (if possible).\n  // This check is required when using triggers with add-ons to maintain\n  // functional triggers.\n  if (\n    authInfo.getAuthorizationStatus() === ScriptApp.AuthorizationStatus.REQUIRED\n  ) {\n    // Re-authorization is required. In this case, the user needs to be alerted\n    // that they need to reauthorize; the normal trigger action is not\n    // conducted, since it authorization needs to be provided first. Send at\n    // most one 'Authorization Required' email a day, to avoid spamming users\n    // of the add-on.\n    sendReauthorizationRequest();\n  } else {\n    const potentials = getScheduledReports(\n      Session.getEffectiveUser().getEmail(),\n    );\n    for (let i = 0; i < potentials.length; i++) {\n      const lastRun = potentials[i].lastRun;\n      if (!lastRun || isOlderThanADay(lastRun)) {\n        runImport(potentials[i].reportId);\n        return;\n      }\n    }\n  }\n}\n\n/**\n * Called when the user needs to reauthorize. Sends the user of the\n * add-on an email explaining the need to reauthorize and provides\n * a link for the user to do so. Capped to send at most one email\n * a day to prevent spamming the users of the add-on.\n */\nfunction sendReauthorizationRequest() {\n  const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);\n  const properties = PropertiesService.getUserProperties();\n  const LAST_AUTH_EMAIL_KEY = \"Import.reauth.lastAuthEmailDate\";\n  const lastAuthEmailDate = properties.getProperty(LAST_AUTH_EMAIL_KEY);\n  const today = new Date().toDateString();\n  if (lastAuthEmailDate !== today) {\n    if (MailApp.getRemainingDailyQuota() > 0) {\n      const template = HtmlService.createTemplateFromFile(\"AuthorizationEmail\");\n      template.url = authInfo.getAuthorizationUrl();\n      template.addonName = ADDON_NAME;\n      const message = template.evaluate();\n      MailApp.sendEmail(\n        Session.getEffectiveUser().getEmail(),\n        \"Add-on Authorization Required\",\n        message.getContent(),\n        {\n          name: ADDON_NAME,\n          htmlBody: message.getContent(),\n        },\n      );\n    }\n    properties.setProperty(LAST_AUTH_EMAIL_KEY, today);\n  }\n}\n\n/**\n * Turn on the scheduling trigger if scheduled reports owned\n * by the current user are present; turn it off otherwise.\n */\nfunction adjustScheduleTrigger() {\n  const existingTriggerId = getTriggerId();\n  const user = Session.getEffectiveUser().getEmail();\n  const triggerNeeded = getScheduledReports(user).length > 0;\n\n  // Create a new trigger if required; delete existing trigger\n  // if it is not needed.\n  if (triggerNeeded && existingTriggerId == null) {\n    const trigger = ScriptApp.newTrigger(\"respondToHourlyTrigger\")\n      .timeBased()\n      .everyHours(1)\n      .create();\n    saveTriggerId(trigger);\n  } else if (!triggerNeeded && existingTriggerId != null) {\n    const existingTrigger = getUserTriggerById(\n      SpreadsheetApp.getActiveSpreadsheet(),\n      existingTriggerId,\n    );\n    if (existingTrigger != null) {\n      ScriptApp.deleteTrigger(existingTrigger);\n    }\n    removeTriggerId();\n  }\n}\n"
  },
  {
    "path": "templates/sheets-import/Sidebar.html",
    "content": "<!--\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n<!DOCTYPE html>\n<html>\n<head>\n  <base target=\"_top\">\n  <!-- Use a templated HTML printing scriptlet to import common stylesheet. -->\n  <?!= include('Stylesheet') ?>\n</head>\n<body>\n  <div id=\"authorization\" class=\"hidden\">\n    <p>Please authorize access to your <?= dataSource ?> account to continue.</p>\n    <button id=\"authorize\">Authorize</button>\n  </div>\n\n  <div id=\"main\" class=\"hidden\">\n    <select class=\"width-100 block\" id=\"report-select\">\n      <option value=\"new-report\">Create a new report</option>\n    </select>\n\n    <div class=\"width-100 block\">\n      <label for=\"report-name\">Report name:</label>\n      <input class=\"width-100\" id=\"report-name\" value=\"\">\n    </div>\n\n    <div class=\"col-contain block width-100\">\n      <div class=\"col-one\">\n        <label>Column options:</label>\n        <select class=\"col-select\" size=\"14\" id=\"column-options\">\n        </select>\n      </div>\n      <div>\n        <label>Selected Columns:</label>\n        <select class=\"col-select\" size=\"14\" id=\"columns-selected\">\n        </select>\n      </div>\n    </div>\n\n    <div class=\"width-100 block\">\n      <input type=\"checkbox\" id=\"scheduling-checkbox\">\n      <label for=\"scheduling-checkbox\">Auto-update this report daily</label>\n    </div>\n\n    <div class=\"width-100 block\">\n      <label for=\"sheet-name\">Imported to sheet:</label>\n      <label id=\"sheet-name\" class=\"\">?</label>\n    </div>\n\n    <div class=\"width-100 block\">\n      <label for=\"owner-name\">Report created by:</label>\n      <label id=\"owner-name\" class=\"small-label\">?</label>\n    </div>\n\n    <div class=\"width-100 block\">\n      <label for=\"last-run\">Last Run on:</label>\n      <label id=\"last-run\" class=\"small-label\">?</label>\n    </div>\n\n    <div id=\"button-bar\" class=\"block\">\n      <button class=\"create\" id=\"save-report\" disabled>Save</button>\n      <button class=\"action\" id=\"run-report\" disabled>Run</button>\n      <button id=\"delete-report\" disabled>Delete</button>\n    </div>\n\n    <div class=\"sidebar bottom\" id=\"bottom-bar\">\n      <button id=\"signout\">Sign out</button>\n    </div>\n  </div>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js\"></script>\n  <?!= include('intercom.js') ?>\n  <script>\n    var isAuthorized = '<?= isAuthorized ?>' == 'true';\n    var user = '<?= user ?>';\n  </script>\n  <?!= include('JavaScript') ?>\n\n</body>\n</html>\n"
  },
  {
    "path": "templates/sheets-import/Stylesheet.html",
    "content": "<!--\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!-- This CSS package applies Google styling; it should always be included. -->\n<link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n\n<style>\nlabel {\n  font-weight: bold;\n}\n\n.width-100 {\n  width: 100%;\n  box-sizing: border-box;\n  -webkit-box-sizing : border-box;‌\n  -moz-box-sizing : border-box;\n}\n\n.hidden {\n  display: none;\n}\n\n.col-select {\n  min-height: 250px;\n  min-width: 125px;\n}\n\n.col-one {\n  float: left;\n  width: 50%;\n}\n\n.col-contain {\n  overflow: hidden;\n}\n\n.small-label {\n  font-weight: normal;\n  font-size: 75%;\n}\n\n.error-text {\n  font-weight: bold;\n  color: red;\n}\n\n.all-clear-text {\n  font-weight: bold;\n  color: darkgreen;\n}\n\n</style>\n"
  },
  {
    "path": "templates/sheets-import/Utilities.gs",
    "content": "/**\n * Copyright 2015 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Includes the given project HTML file in the current HTML project file.\n * Also used to include JavaScript.\n * @param {string} filename Project file name.\n * @return {string} The content of the rendered HTML.\n */\nfunction include(filename) {\n  return HtmlService.createHtmlOutputFromFile(filename).getContent();\n}\n\n/**\n * Returns true if the given date string represents a date that is\n * more than 24 hours in the past; returns false otherwise.\n * @param {String} dateStr a date string.\n * @return {Boolean}\n */\nfunction isOlderThanADay(dateStr) {\n  const now = new Date().getTime();\n  const then = Date.parse(dateStr);\n  return then + 24 * 60 * 60 * 1000 < now;\n}\n\n/**\n * Given an object and a string prefix, save every value in that object\n * to the Document properties service as a JSONified string. The property\n * key for each object key will be: prefix.<object_key>\n * @param {String} prefix a common string to label each added property.\n * @param {Object} obj a collection of key-values to save as\n *   user properties.\n */\nfunction saveObjectToProperties(prefix, obj) {\n  const properties = PropertiesService.getDocumentProperties();\n  _.each(obj, (val, key) => {\n    const propKey = `${prefix}.${key}`;\n    properties.setProperty(propKey, JSON.stringify(val));\n  });\n}\n\n/**\n * Given a string prefix, fetch from the Document properties service all\n * properties whose keys start with that prefix, and return the (JSON-parsed)\n * values in an object. The keys of the returned object will be the\n * same as the property keys with the leading \"prefix.\" removed.\n * @param {String} prefix label of requested properties.\n * @return {Object} collection of key-value pairs taken from the\n *   properties service. Will return null if the prefix is unrecognized.\n */\nfunction getObjectFromProperties(prefix) {\n  const properties = PropertiesService.getDocumentProperties();\n  const obj = {};\n  _.each(properties.getProperties(), (val, key) => {\n    if (key.indexOf(prefix) > -1) {\n      obj[key.substr(prefix.length + 1)] = JSON.parse(val);\n    }\n  });\n  if (_.keys(obj).length === 0) {\n    return null;\n  }\n  return obj;\n}\n\n/**\n * Given a string prefix, remove from the Document properties service all\n * properties whose keys start with that prefix.\n * @param {String} prefix label of properties to remove.\n */\nfunction deleteObjectFromProperties(prefix) {\n  const properties = PropertiesService.getDocumentProperties();\n  _.each(properties.getProperties(), (val, key) => {\n    if (key.indexOf(prefix) > -1) {\n      properties.deleteProperty(key);\n    }\n  });\n}\n\n/**\n * Generate a random alphanumeric string.\n * @return {String} report ID string.\n */\nfunction newReportId() {\n  return Math.random().toString(36).substring(2);\n}\n\n/**\n * Sheets-specific utility. Find a sheet within a spreadsheet with\n * the given id. If not present, return null.\n * @param {Object} ss a Spreadsheet object.\n * @param {Number} sheetId a Sheet id.\n * @return {Object} a Sheet object, or null if not found.\n */\nfunction getSheetById(ss, sheetId) {\n  if (sheetId === null) {\n    return null;\n  }\n  const sheets = ss.getSheets();\n  for (let i = 0; i < sheets.length; i++) {\n    if (sheets[i].getSheetId() === sheetId) {\n      return sheets[i];\n    }\n  }\n  return null;\n}\n\n/**\n * Sheets-specific utility. Given a base title for a sheet, check\n * for that it is unique in the spreadsheet. If not, find an integer\n * suffix to append to it to make it unique and return. This function\n * is used to avoid name collisions while adding or renaming sheets\n * automatically.\n * @param {Object} spreadsheet a Spreadsheet.\n * @param {String} baseName the initial suggested title for a sheet.\n * @return {String} a unique title for the sheet, based on the\n *     given base title.\n */\nfunction getUniqueSheetName(spreadsheet, baseName) {\n  let sheetName = baseName;\n  let i = 2;\n  while (spreadsheet.getSheetByName(sheetName) != null) {\n    sheetName = `${baseName} ${i++}`;\n  }\n  return sheetName;\n}\n\n/**\n * Sheets-specific utility. Given a spreadsheet and a triggerId string,\n * return the user trigger that corresponds to that ID. Returns null\n * if no such trigger exists.\n * @param {Spreadsheet} spreadsheet container of the user triggers.\n * @param {String} triggerId trigger ID string.\n * @return {Trigger} corresponding user trigger, or null if not found.\n */\nfunction getUserTriggerById(spreadsheet, triggerId) {\n  const triggers = ScriptApp.getUserTriggers(spreadsheet);\n  for (let i = 0; i < triggers.length; i++) {\n    if (triggers[i].getUniqueId() === triggerId) {\n      return triggers[i];\n    }\n  }\n  return null;\n}\n\n/**\n * Sheets-specific utility. Given a String sheet id, activate that\n * sheet if it exists.\n * @param {String} sheetId the sheet ID.\n */\nfunction activateById(sheetId) {\n  const ss = SpreadsheetApp.getActiveSpreadsheet();\n  const sheet = getSheetById(ss, Number.parseInt(sheetId));\n  if (sheet != null) {\n    sheet.activate();\n  }\n}\n"
  },
  {
    "path": "templates/sheets-import/intercom.js.html",
    "content": "<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<script>\n/*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */\nvar Intercom=function(){var g=function(){};g.createInterface=function(b){return{on:function(a,c){\"undefined\"===typeof this[b]&&(this[b]={});this[b].hasOwnProperty(a)||(this[b][a]=[]);this[b][a].push(c)},off:function(a,c){\"undefined\"!==typeof this[b]&&this[b].hasOwnProperty(a)&&i.removeItem(c,this[b][a])},trigger:function(a){if(\"undefined\"!==typeof this[b]&&this[b].hasOwnProperty(a))for(var c=Array.prototype.slice.call(arguments,1),e=0;e<this[b][a].length;e++)this[b][a][e].apply(this[b][a][e],c)}}};\nvar m=g.createInterface(\"_handlers\");g.prototype._on=m.on;g.prototype._off=m.off;g.prototype._trigger=m.trigger;var n=g.createInterface(\"handlers\");g.prototype.on=function(){n.on.apply(this,arguments);Array.prototype.unshift.call(arguments,\"on\");this._trigger.apply(this,arguments)};g.prototype.off=n.off;g.prototype.trigger=n.trigger;var f=window.localStorage;\"undefined\"===typeof f&&(f={getItem:function(){},setItem:function(){},removeItem:function(){}});var i={},h=function(){return(65536*(1+Math.random())|\n0).toString(16).substring(1)};i.guid=function(){return h()+h()+\"-\"+h()+\"-\"+h()+\"-\"+h()+\"-\"+h()+h()+h()};i.throttle=function(b,a){var c=0;return function(){var e=(new Date).getTime();e-c>b&&(c=e,a.apply(this,arguments))}};i.extend=function(b,a){if(\"undefined\"===typeof b||!b)b={};if(\"object\"===typeof a)for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b};i.removeItem=function(b,a){for(var c=a.length-1;0<=c;c--)a[c]===b&&a.splice(c,1);return a};var d=function(){var b=this,a=(new Date).getTime();\nthis.origin=i.guid();this.lastMessage=a;this.bindings=[];this.receivedIDs={};this.previousValues={};a=function(){b._onStorageEvent.apply(b,arguments)};window.attachEvent?document.attachEvent(\"onstorage\",a):window.addEventListener(\"storage\",a,!1)};d.prototype._transaction=function(b){var a=this,c=!1,e=!1,p=null,d=function(){if(!c){var g=(new Date).getTime(),s=parseInt(f.getItem(l)||0);s&&1E3>g-s?(e||(a._on(\"storage\",d),e=!0),p=window.setTimeout(d,20)):(c=!0,f.setItem(l,g),b(),e&&a._off(\"storage\",d),\np&&window.clearTimeout(p),f.removeItem(l))}};d()};d.prototype._cleanup_emit=i.throttle(100,function(){this._transaction(function(){for(var b=(new Date).getTime()-t,a=0,c=JSON.parse(f.getItem(j)||\"[]\"),e=c.length-1;0<=e;e--)c[e].timestamp<b&&(c.splice(e,1),a++);0<a&&f.setItem(j,JSON.stringify(c))})});d.prototype._cleanup_once=i.throttle(100,function(){var b=this;this._transaction(function(){var a,c=JSON.parse(f.getItem(k)||\"{}\");(new Date).getTime();var e=0;for(a in c)b._once_expired(a,c)&&(delete c[a],\ne++);0<e&&f.setItem(k,JSON.stringify(c))})});d.prototype._once_expired=function(b,a){if(!a||!a.hasOwnProperty(b)||\"object\"!==typeof a[b])return!0;var c=a[b].ttl||u,e=(new Date).getTime();return a[b].timestamp<e-c};d.prototype._localStorageChanged=function(b,a){if(b&&b.key)return b.key===a;var c=f.getItem(a);if(c===this.previousValues[a])return!1;this.previousValues[a]=c;return!0};d.prototype._onStorageEvent=function(b){var b=b||window.event,a=this;this._localStorageChanged(b,j)&&this._transaction(function(){for(var b=\n(new Date).getTime(),e=f.getItem(j),e=JSON.parse(e||\"[]\"),d=0;d<e.length;d++)if(e[d].origin!==a.origin&&!(e[d].timestamp<a.lastMessage)){if(e[d].id){if(a.receivedIDs.hasOwnProperty(e[d].id))continue;a.receivedIDs[e[d].id]=!0}a.trigger(e[d].name,e[d].payload)}a.lastMessage=b});this._trigger(\"storage\",b)};d.prototype._emit=function(b,a,c){if((c=\"string\"===typeof c||\"number\"===typeof c?String(c):null)&&c.length){if(this.receivedIDs.hasOwnProperty(c))return;this.receivedIDs[c]=!0}var e={id:c,name:b,origin:this.origin,\ntimestamp:(new Date).getTime(),payload:a},d=this;this._transaction(function(){var c=f.getItem(j)||\"[]\",c=[c.substring(0,c.length-1),\"[]\"===c?\"\":\",\",JSON.stringify(e),\"]\"].join(\"\");f.setItem(j,c);d.trigger(b,a);window.setTimeout(function(){d._cleanup_emit()},50)})};d.prototype.bind=function(b,a){for(var c=0;c<d.bindings.length;c++){var e=d.bindings[c].factory(b,a||null,this);e&&this.bindings.push(e)}};d.prototype.emit=function(b,a){this._emit.apply(this,arguments);this._trigger(\"emit\",b,a)};d.prototype.once=\nfunction(b,a,c){if(d.supported){var e=this;this._transaction(function(){var d=JSON.parse(f.getItem(k)||\"{}\");e._once_expired(b,d)&&(d[b]={},d[b].timestamp=(new Date).getTime(),\"number\"===typeof c&&(d[b].ttl=1E3*c),f.setItem(k,JSON.stringify(d)),a(),window.setTimeout(function(){e._cleanup_once()},50))})}};i.extend(d.prototype,g.prototype);d.bindings=[];d.supported=\"undefined\"!==typeof f;var j=\"intercom\",k=\"intercom_once\",l=\"intercom_lock\",t=5E4,u=36E5;d.destroy=function(){f.removeItem(l);f.removeItem(j);\nf.removeItem(k)};var q=null;d.getInstance=function(){q||(q=new d);return q};var r=function(b,a,c){a=i.extend({id:null,send:!0,receive:!0},a);if(a.receive){var d=[],f=function(f){-1===d.indexOf(f)&&(d.push(f),b.on(f,function(b){var d=\"function\"===typeof a.id?a.id(f,b):null,e=\"function\"===typeof a.receive?a.receive(f,b):!0;(e||\"boolean\"!==typeof e)&&c._emit(f,b,d)}))},g;for(g in c.handlers)for(var h=0;h<c.handlers[g].length;h++)f(g,c.handlers[g][h]);c._on(\"on\",f)}a.send&&c._on(\"emit\",function(c,d){var e=\n\"function\"===typeof a.send?a.send(c,d):!0;(e||\"boolean\"!==typeof e)&&b.emit(c,d)})};r.factory=function(b,a,c){return\"undefined\"===typeof b.socket?!1:new r(b,a,c)};d.bindings.push(r);return d}();\n</script>\n"
  },
  {
    "path": "templates/standalone/helloWorld.gs",
    "content": "/**\n * Copyright 2018 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_hello_world]\n/**\n * Creates a Google Doc and sends an email to the current user with a link to the doc.\n */\nfunction createAndSendDocument() {\n  try {\n    // Create a new Google Doc named 'Hello, world!'\n    const doc = DocumentApp.create(\"Hello, world!\");\n\n    // Access the body of the document, then add a paragraph.\n    doc\n      .getBody()\n      .appendParagraph(\"This document was created by Google Apps Script.\");\n\n    // Get the URL of the document.\n    const url = doc.getUrl();\n\n    // Get the email address of the active user - that's you.\n    const email = Session.getActiveUser().getEmail();\n\n    // Get the name of the document to use as an email subject line.\n    const subject = doc.getName();\n\n    // Append a new string to the \"url\" variable to use as an email body.\n    const body = `Link to your doc: ${url}`;\n\n    // Send yourself an email with a link to the document.\n    GmailApp.sendEmail(email, subject, body);\n  } catch (err) {\n    // TODO (developer) - Handle exception\n    console.log(\"Failed with error %s\", err.message);\n  }\n}\n// [END apps_script_hello_world]\n"
  },
  {
    "path": "templates/web-app/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Serves HTML of the application for HTTP GET requests.\n * If folderId is provided as a URL parameter, the web app will list\n * the contents of that folder (if permissions allow). Otherwise\n * the web app will list the contents of the root folder.\n *\n * @param {Object} e event parameter that can contain information\n *     about any URL parameters provided.\n * @return {HTML} The web app's HTML.\n */\nfunction doGet(e) {\n  const template = HtmlService.createTemplateFromFile(\"Index\");\n\n  // Retrieve and process any URL parameters, as necessary.\n  if (e.parameter.folderId) {\n    template.folderId = e.parameter.folderId;\n  } else {\n    template.folderId = \"root\";\n  }\n\n  // Build and return HTML in IFRAME sandbox mode.\n  return template.evaluate().setTitle(\"Web App Window Title\");\n}\n\n/**\n * Return an array of up to 20 filenames contained in the\n * folder previously specified (or the root folder by default).\n *\n * @param {String} folderId String ID of folder whose contents\n *     are to be retrieved; if this is 'root', the\n *     root folder is used.\n * @return {Object} list of content filenames, along with\n *     the root folder name.\n */\nfunction getFolderContents(folderId) {\n  let topFolder;\n  const contents = {\n    children: [],\n  };\n\n  if (folderId === \"root\") {\n    topFolder = DriveApp.getRootFolder();\n  } else {\n    // May throw exception if the folderId is invalid or app\n    // doesn't have permission to access.\n    topFolder = DriveApp.getFolderById(folderId);\n  }\n  contents.rootName = `${topFolder.getName()}/`;\n\n  const files = topFolder.getFiles();\n  let numFiles = 0;\n  while (files.hasNext() && numFiles < 20) {\n    const file = files.next();\n    contents.children.push(file.getName());\n    numFiles++;\n  }\n\n  return contents;\n}\n"
  },
  {
    "path": "templates/web-app/Index.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <base target=\"_top\">\n    <!-- Use a templated HTML printing scriptlet to import common stylesheet -->\n    <?!= HtmlService.createHtmlOutputFromFile('Stylesheet').getContent(); ?>\n  </head>\n  <body>\n    <cursor></cursor><h1>\n    To read a specific folder's contents, include that folder's ID as\n    a URL parameter: ?folderId=&lt;folder id&gt;. Otherwise the root folder\n    will be read. Up to 20 files may be displayed here, in no particular order.\n    </h1>\n    <h1 id=\"main-heading\">Loading...</h1>\n    <div class=\"block result-display\" id=\"results\">\n      <div class=\"hidden\" id=\"error-message\">\n        Verify that you have permission to access this folder and that the specified folder ID (if any) is correct.\n      </div>\n    </div>\n    <!-- Store data passed to template here, so it is available to the\n     imported JavaScript. -->\n    <script>\n      var folderId = <?= folderId ?>;\n    </script>\n\n    <!-- Use a templated HTML printing scriptlet to import JavaScript. -->\n    <?!= HtmlService.createHtmlOutputFromFile('JavaScript').getContent(); ?>\n  </body>\n</html>\n"
  },
  {
    "path": "templates/web-app/JavaScript.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\"></script>\n<script>\n  /**\n   * Run initializations on web app load.\n   */\n  $(function() {\n    // Call the server here to retrieve any information needed to build the page.\n    google.script.run\n       .withSuccessHandler(function(contents) {\n            // Respond to success conditions here.\n            updateDisplay(contents);\n          })\n       .withFailureHandler(function(msg) {\n            // Respond to failure conditions here.\n            $('#main-heading').text(msg);\n            $('#main-heading').addClass(\"error\");\n            $('#error-message').show();\n          })\n       .getFolderContents(folderId);\n  });\n\n  /**\n   * Updates display of folder contents.\n   *\n   * @param {Object} contents list of content filenames, along with\n   *     the root folder name.\n   */\n  function updateDisplay(contents) {\n    var headingText = \"Displaying contents for \" + contents.rootName + \" folder:\";\n    $('#main-heading').text(headingText);\n    for (var i = 0; i < contents.children.length; i++) {\n      var name = contents.children[i];\n      $('#results').append('<div>' + name + '</div>');\n    }\n  }\n</script>\n"
  },
  {
    "path": "templates/web-app/README.md",
    "content": "Template: Script as Web App\n===========================\n\nThis template provides a framework for creating a [web app](https://developers.google.com/apps-script/guides/web).\n\nIt shows the basic structure needed to define a UI and how to coordinate\ncommunication between the client and server. This template also includes some\nuseful aspects of Apps Script, including:\n\n* Using [Templated HTML](https://developers.google.com/apps-script/guides/html/templates)\n* Responding to HTTP GET requests with doGet(e)\n* Using IFRAME sandbox mode\n"
  },
  {
    "path": "templates/web-app/Stylesheet.html",
    "content": "<!--\n * Copyright 2014 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n -->\n\n<!-- This CSS package applies Google styling; it should always be included. -->\n<link rel=\"stylesheet\" href=\"https://ssl.gstatic.com/docs/script/css/add-ons.css\">\n\n<style>\n.result-display {\n  margin-left: 15px;\n  font-size: 125%;\n}\n\n.error {\n  color: #FF0000;\n}\n\n.hidden {\n  display: none;\n}\n\n</style>\n"
  },
  {
    "path": "triggers/form/AuthorizationEmail.html",
    "content": "<!--\nCopyright Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START apps_script_triggers_form] -->\n<p>The Google Sheets add-on <i><?= addonTitle ?></i> is set to run automatically\n    whenever a form is submitted. The add-on was recently updated and it needs you\n    to re-authorize it to run on your behalf.</p>\n\n<p>The add-on's automatic functions are temporarily disabled until you\n    re-authorize it. To do so, open Google Sheets and run the add-on from the\n    Add-ons menu. Alternatively, you can click this link to authorize it:</p>\n\n<p><a href=\"<?= url ?>\">Re-authorize the add-on.</a></p>\n\n<p>This notification email will be sent to you at most once per day until the\n    add-on is re-authorized.</p>\n<!-- [END apps_script_triggers_form] -->\n"
  },
  {
    "path": "triggers/form/Code.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// [START apps_script_triggers_form]\n/**\n * Responds to a form when submitted.\n * @param {event} e The Form submit event.\n */\nfunction respondToFormSubmit(e) {\n  const addonTitle = \"My Add-on Title\";\n  const props = PropertiesService.getDocumentProperties();\n  const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);\n\n  // Check if the actions of the trigger requires authorization that has not\n  // been granted yet; if so, warn the user via email. This check is required\n  // when using triggers with add-ons to maintain functional triggers.\n  if (\n    authInfo.getAuthorizationStatus() === ScriptApp.AuthorizationStatus.REQUIRED\n  ) {\n    // Re-authorization is required. In this case, the user needs to be alerted\n    // that they need to re-authorize; the normal trigger action is not\n    // conducted, since it requires authorization first. Send at most one\n    // \"Authorization Required\" email per day to avoid spamming users.\n    const lastAuthEmailDate = props.getProperty(\"lastAuthEmailDate\");\n    const today = new Date().toDateString();\n    if (lastAuthEmailDate !== today) {\n      if (MailApp.getRemainingDailyQuota() > 0) {\n        const html = HtmlService.createTemplateFromFile(\"AuthorizationEmail\");\n        html.url = authInfo.getAuthorizationUrl();\n        html.addonTitle = addonTitle;\n        const message = html.evaluate();\n        MailApp.sendEmail(\n          Session.getEffectiveUser().getEmail(),\n          \"Authorization Required\",\n          message.getContent(),\n          {\n            name: addonTitle,\n            htmlBody: message.getContent(),\n          },\n        );\n      }\n      props.setProperty(\"lastAuthEmailDate\", today);\n    }\n  } else {\n    // Authorization has been granted, so continue to respond to the trigger.\n    // Main trigger logic here.\n  }\n}\n// [END apps_script_triggers_form]\n"
  },
  {
    "path": "triggers/test_triggers.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Tests createTimeDrivenTrigger function of trigger.gs\n */\nfunction itShouldCreateTimeDrivenTriggers() {\n  console.log(\"> itShouldCreateTimeDrivenTriggers\");\n  createTimeDrivenTriggers();\n}\n\n/**\n * Tests createSpreadsheetOpenTrigger function of triggers.gs\n */\nfunction itShouldCreateSpreadsheetOpenTrigger() {\n  console.log(\"> itShouldCreateSpreadsheetOpenTrigger\");\n  createSpreadsheetOpenTrigger();\n}\n\n/**\n * Tests deleteTrigger function of triggers.gs\n */\nfunction itShouldDeleteTrigger() {\n  console.log(\"> itShouldDeleteTrigger\");\n  deleteTrigger();\n}\n\n/**\n * Run all the tests for triggers.gs\n */\nfunction RUN_ALL_TESTS() {\n  itShouldCreateSpreadsheetOpenTrigger();\n  itShouldCreateTimeDrivenTriggers();\n  itShouldDeleteTrigger();\n}\n"
  },
  {
    "path": "triggers/triggers.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_triggers_onopen]\n/**\n * The event handler triggered when opening the spreadsheet.\n * @param {Event} e The onOpen event.\n * @see https://developers.google.com/apps-script/guides/triggers#onopene\n */\nfunction onOpen(e) {\n  // Add a custom menu to the spreadsheet.\n  SpreadsheetApp.getUi() // Or DocumentApp, SlidesApp, or FormApp.\n    .createMenu(\"Custom Menu\")\n    .addItem(\"First item\", \"menuItem1\")\n    .addToUi();\n}\n// [END apps_script_triggers_onopen]\n// [START apps_script_triggers_onedit]\n/**\n * The event handler triggered when editing the spreadsheet.\n * @param {Event} e The onEdit event.\n * @see https://developers.google.com/apps-script/guides/triggers#onedite\n */\nfunction onEdit(e) {\n  // Set a comment on the edited cell to indicate when it was changed.\n  const range = e.range;\n  range.setNote(`Last modified: ${new Date()}`);\n}\n// [END apps_script_triggers_onedit]\n// [START apps_script_triggers_onselectionchange]\n/**\n * The event handler triggered when the selection changes in the spreadsheet.\n * @param {Event} e The onSelectionChange event.\n * @see https://developers.google.com/apps-script/guides/triggers#onselectionchangee\n */\nfunction onSelectionChange(e) {\n  // Set background to red if a single empty cell is selected.\n  const range = e.range;\n  if (\n    range.getNumRows() === 1 &&\n    range.getNumColumns() === 1 &&\n    range.getCell(1, 1).getValue() === \"\"\n  ) {\n    range.setBackground(\"red\");\n  }\n}\n// [END apps_script_triggers_onselectionchange]\n// [START apps_script_triggers_oninstall]\n/**\n * The event handler triggered when installing the add-on.\n * @param {Event} e The onInstall event.\n * @see https://developers.google.com/apps-script/guides/triggers#oninstalle\n */\nfunction onInstall(e) {\n  onOpen(e);\n}\n// [END apps_script_triggers_oninstall]\n// [START apps_script_triggers_time]\n/**\n * Creates two time-driven triggers.\n * @see https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers\n */\nfunction createTimeDrivenTriggers() {\n  // Trigger every 6 hours.\n  ScriptApp.newTrigger(\"myFunction\").timeBased().everyHours(6).create();\n  // Trigger every Monday at 09:00.\n  ScriptApp.newTrigger(\"myFunction\")\n    .timeBased()\n    .onWeekDay(ScriptApp.WeekDay.MONDAY)\n    .atHour(9)\n    .create();\n}\n// [END apps_script_triggers_time]\n// [START apps_script_triggers_open]\n/**\n * Creates a trigger for when a spreadsheet opens.\n * @see https://developers.google.com/apps-script/guides/triggers/installable\n */\nfunction createSpreadsheetOpenTrigger() {\n  const ss = SpreadsheetApp.getActive();\n  ScriptApp.newTrigger(\"myFunction\").forSpreadsheet(ss).onOpen().create();\n}\n// [END apps_script_triggers_open]\n// [START apps_script_triggers_delete]\n/**\n * Deletes a trigger.\n * @param {string} triggerId The Trigger ID.\n * @see https://developers.google.com/apps-script/guides/triggers/installable\n */\nfunction deleteTrigger(triggerId) {\n  // Loop over all triggers.\n  const allTriggers = ScriptApp.getProjectTriggers();\n  for (let index = 0; index < allTriggers.length; index++) {\n    // If the current trigger is the correct one, delete it.\n    if (allTriggers[index].getUniqueId() === triggerId) {\n      ScriptApp.deleteTrigger(allTriggers[index]);\n      break;\n    }\n  }\n}\n// [END apps_script_triggers_delete]\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"noEmit\": true,\n    \"target\": \"es2019\",\n    \"module\": \"commonjs\",\n    \"alwaysStrict\": true,\n    \"lib\": [\"es2019\"],\n    \"types\": [\"google-apps-script\"],\n    \"strict\": true,\n    \"noImplicitAny\": true\n  },\n  \"include\": [\"**/*.gs\", \".github/scripts/check-gs.ts\"]\n}\n"
  },
  {
    "path": "ui/communication/basic/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction doSomething() {\n  console.log(\"I was called!\");\n}\n"
  },
  {
    "path": "ui/communication/basic/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      google.script.run.doSomething();\n    </script>\n  </head>\n</html>"
  },
  {
    "path": "ui/communication/failure/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction getUnreadEmails() {\n  // 'got' instead of 'get' will throw an error.\n  return GmailApp.gotInboxUnreadCount();\n}\n"
  },
  {
    "path": "ui/communication/failure/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      function onFailure(error) {\n        var div = document.getElementById('output');\n        div.innerHTML = \"ERROR: \" + error.message;\n      }\n\n      google.script.run.withFailureHandler(onFailure)\n          .getUnreadEmails();\n    </script>\n  </head>\n  <body>\n    <div id=\"output\"></div>\n  </body>\n</html>"
  },
  {
    "path": "ui/communication/private/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction getBankBalance() {\n  const email = Session.getActiveUser().getEmail();\n  return deepSecret_(email);\n}\n\nfunction deepSecret_(email) {\n  // Do some secret calculations\n  return `${email} has $1,000,000 in the bank.`;\n}\n"
  },
  {
    "path": "ui/communication/private/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      function onSuccess(balance) {\n        var div = document.getElementById('output');\n        div.innerHTML = balance;\n      }\n\n      google.script.run.withSuccessHandler(onSuccess)\n          .getBankBalance();\n    </script>\n  </head>\n  <body>\n    <div id=\"output\">No result yet...</div>\n  </body>\n</html>"
  },
  {
    "path": "ui/communication/runner.gs",
    "content": "const myRunner = google.script.run.withFailureHandler(onFailure);\nconst myRunner1 = myRunner.withSuccessHandler(onSuccess);\nconst myRunner2 = myRunner.withSuccessHandler(onDifferentSuccess);\n\nmyRunner1.doSomething();\nmyRunner1.doSomethingElse();\nmyRunner2.doSomething();\n"
  },
  {
    "path": "ui/communication/success/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction getUnreadEmails() {\n  return GmailApp.getInboxUnreadCount();\n}\n"
  },
  {
    "path": "ui/communication/success/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      function onSuccess(numUnread) {\n        var div = document.getElementById('output');\n        div.innerHTML = 'You have ' + numUnread\n            + ' unread messages in your Gmail inbox.';\n      }\n\n      google.script.run.withSuccessHandler(onSuccess)\n          .getUnreadEmails();\n    </script>\n  </head>\n  <body>\n    <div id=\"output\"></div>\n  </body>\n</html>"
  },
  {
    "path": "ui/dialogs/alert/alert.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_alert_dialogs]\n/**\n * Creates a custom menu when a user opens a Spreadsheet.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .createMenu(\"Custom Menu\")\n    .addItem(\"Show alert\", \"showAlert\")\n    .addToUi();\n}\n\n/**\n * Shows an alert dialog.\n */\nfunction showAlert() {\n  const ui = SpreadsheetApp.getUi(); // Same variations.\n\n  const result = ui.alert(\n    \"Please confirm\",\n    \"Are you sure you want to continue?\",\n    ui.ButtonSet.YES_NO,\n  );\n\n  // Process the user's response.\n  if (result === ui.Button.YES) {\n    // User clicked \"Yes\".\n    ui.alert(\"Confirmation received.\");\n  } else {\n    // User clicked \"No\" or X in the title bar.\n    ui.alert(\"Permission denied.\");\n  }\n}\n// [END apps_script_alert_dialogs]\n"
  },
  {
    "path": "ui/dialogs/custom_dialog/Page.html",
    "content": "<!--\nCopyright 2018 Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START apps_script_custom_dialog] -->\nHello, world! <input type=\"button\" value=\"Close\" onclick=\"google.script.host.close()\" />\n<!-- [END apps_script_custom_dialog] -->"
  },
  {
    "path": "ui/dialogs/custom_dialog/custom_dialog.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_custom_dialog]\n/**\n * Creates a custom menu when a user opens a Spreadsheet.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .createMenu(\"Custom Menu\")\n    .addItem(\"Show dialog\", \"showDialog\")\n    .addToUi();\n}\n\n/**\n * Shows a custom dialog.\n */\nfunction showDialog() {\n  const html = HtmlService.createHtmlOutputFromFile(\"Page\")\n    .setWidth(400)\n    .setHeight(300);\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .showModalDialog(html, \"My custom dialog\");\n}\n// [END apps_script_custom_dialog]\n"
  },
  {
    "path": "ui/dialogs/custom_sidebar/Page.html",
    "content": "<!--\nCopyright 2018 Google LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n<!-- [START apps_script_custom_dialog] -->\nHello, world! <input type=\"button\" value=\"Close\" onclick=\"google.script.host.close()\" />\n<!-- [END apps_script_custom_dialog] -->"
  },
  {
    "path": "ui/dialogs/custom_sidebar/custom_sidebar.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_custom_sidebar]\n/**\n * Creates a custom sidebar when a user opens a Spreadsheet.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .createMenu(\"Custom Menu\")\n    .addItem(\"Show sidebar\", \"showSidebar\")\n    .addToUi();\n}\n\n/**\n * Shows a custom sidebar.\n */\nfunction showSidebar() {\n  const html = HtmlService.createHtmlOutputFromFile(\"Page\")\n    .setTitle(\"My custom sidebar\")\n    .setWidth(300);\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .showSidebar(html);\n}\n// [END apps_script_custom_sidebar]\n"
  },
  {
    "path": "ui/dialogs/menus.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_menu]\n/**\n * Handler for when a user opens the spreadsheet.\n * Creates a custom menu.\n */\nfunction onOpen() {\n  const ui = SpreadsheetApp.getUi();\n  // Or DocumentApp or FormApp.\n  ui.createMenu(\"Custom Menu\")\n    .addItem(\"First item\", \"menuItem1\")\n    .addSeparator()\n    .addSubMenu(ui.createMenu(\"Sub-menu\").addItem(\"Second item\", \"menuItem2\"))\n    .addToUi();\n}\n\n/**\n * Handler for when menu item 1 is clicked.\n */\nfunction menuItem1() {\n  SpreadsheetApp.getUi() // Or DocumentApp or FormApp.\n    .alert(\"You clicked the first menu item!\");\n}\n\n/**\n * Handler for when menu item 2 is clicked.\n */\nfunction menuItem2() {\n  SpreadsheetApp.getUi() // Or DocumentApp or FormApp.\n    .alert(\"You clicked the second menu item!\");\n}\n// [END apps_script_menu]\n\n// [START apps_script_show_message_box]\n/**\n * Shows a message box to the user.\n */\nfunction showMessageBox() {\n  Browser.msgBox(\"You clicked it!\");\n}\n// [END apps_script_show_message_box]\n\n// [START apps_script_sites_link]\n/**\n * A function that can be invoked from a Google Sites link.\n */\nfunction sitesLink() {\n  const recipient = Session.getActiveUser().getEmail();\n  GmailApp.sendEmail(recipient, \"Email from your site\", \"You clicked a link!\");\n}\n// [END apps_script_sites_link]\n"
  },
  {
    "path": "ui/dialogs/prompt/prompt.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_prompt_dialog]\n/**\n * Creates a custom menu when a user opens a Spreadsheet.\n */\nfunction onOpen() {\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .createMenu(\"Custom Menu\")\n    .addItem(\"Show prompt\", \"showPrompt\")\n    .addToUi();\n}\n\n/**\n * Shows a prompt dialog.\n */\nfunction showPrompt() {\n  const ui = SpreadsheetApp.getUi(); // Same variations.\n\n  const result = ui.prompt(\n    \"Let's get to know each other!\",\n    \"Please enter your name:\",\n    ui.ButtonSet.OK_CANCEL,\n  );\n\n  // Process the user's response.\n  const button = result.getSelectedButton();\n  const text = result.getResponseText();\n  if (button === ui.Button.OK) {\n    // User clicked \"OK\".\n    ui.alert(`Your name is ${text}.`);\n  } else if (button === ui.Button.CANCEL) {\n    // User clicked \"Cancel\".\n    ui.alert(\"I didn't get your name.\");\n  } else if (button === ui.Button.CLOSE) {\n    // User clicked X in the title bar.\n    ui.alert(\"You closed the dialog.\");\n  }\n}\n// [END apps_script_prompt_dialog]\n"
  },
  {
    "path": "ui/forms/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction processForm(formObject) {\n  const formBlob = formObject.myFile;\n  const driveFile = DriveApp.createFile(formBlob);\n  return driveFile.getUrl();\n}\n"
  },
  {
    "path": "ui/forms/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      // Prevent forms from submitting.\n      function preventFormSubmit() {\n        var forms = document.querySelectorAll('form');\n        for (var i = 0; i < forms.length; i++) {\n          forms[i].addEventListener('submit', function(event) {\n            event.preventDefault();\n          });\n        }\n      }\n      window.addEventListener('load', preventFormSubmit);\n\n      function handleFormSubmit(formObject) {\n        google.script.run.withSuccessHandler(updateUrl).processForm(formObject);\n      }\n      function updateUrl(url) {\n        var div = document.getElementById('output');\n        div.innerHTML = '<a href=\"' + url + '\">Got it!</a>';\n      }\n    </script>\n  </head>\n  <body>\n    <form id=\"my-form\" onsubmit=\"handleFormSubmit(this)\">\n      <input name=\"myFile\" type=\"file\" />\n      <input type=\"submit\" value=\"Submit\" />\n    </form>\n    <div id=\"output\"></div>\n </body>\n</html>"
  },
  {
    "path": "ui/html/printing_scriptlet.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    <?= 'My favorite Google products:' ?>\n    <? var data = ['Gmail', 'Docs', 'Android'];\n      for (var i = 0; i < data.length; i++) { ?>\n        <b><?= data[i] ?></b>\n    <? } ?>\n  </body>\n</html>"
  },
  {
    "path": "ui/html/scriptlet.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    Hello, World! The time is <?= new Date() ?>.\n  </body>\n</html>"
  },
  {
    "path": "ui/html/standard_scriptlet.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    <? if (true) { ?>\n      <p>This will always be served!</p>\n    <? } else  { ?>\n      <p>This will never be served.</p>\n    <? } ?>\n  </body>\n</html>"
  },
  {
    "path": "ui/sidebar/code.gs",
    "content": "// Use this code for Google Docs, Slides, Forms, or Sheets.\nfunction onOpen() {\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .createMenu(\"Dialog\")\n    .addItem(\"Open\", \"openDialog\")\n    .addToUi();\n}\n\nfunction openDialog() {\n  const html = HtmlService.createHtmlOutputFromFile(\"Index\");\n  SpreadsheetApp.getUi() // Or DocumentApp or SlidesApp or FormApp.\n    .showModalDialog(html, \"Dialog title\");\n}\n"
  },
  {
    "path": "ui/sidebar/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    Hello, World!\n    <input type=\"button\" value=\"Close\"\n        onclick=\"google.script.host.close()\" />\n  </body>\n</html>"
  },
  {
    "path": "ui/user/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n\nfunction getEmail() {\n  return Session.getActiveUser().getEmail();\n}\n"
  },
  {
    "path": "ui/user/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n    <script>\n      function updateButton(email, button) {\n        button.value = 'Clicked by ' + email;\n      }\n    </script>\n  </head>\n  <body>\n    <input type=\"button\" value=\"Not Clicked\"\n      onclick=\"google.script.run\n          .withSuccessHandler(updateButton)\n          .withUserObject(this)\n          .getEmail()\" />\n    <input type=\"button\" value=\"Not Clicked\"\n      onclick=\"google.script.run\n          .withSuccessHandler(updateButton)\n          .withUserObject(this)\n          .getEmail()\" />\n  </body>\n</html>"
  },
  {
    "path": "ui/webapp/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile(\"Index\");\n}\n"
  },
  {
    "path": "ui/webapp/index.html",
    "content": "<!DOCTYPE html>\n<!--\n Copyright 2022 Google LLC\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n-->\n\n<html>\n  <head>\n    <base target=\"_top\">\n  </head>\n  <body>\n    Hello, World!\n  </body>\n</html>"
  },
  {
    "path": "utils/logging.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// [START apps_script_logging_execution_time]\n/**\n * A placeholder function to be timed.\n * @param {Object} parameters\n */\nfunction myFunction(parameters) {\n  // Placeholder for the function being timed.\n}\n\n/**\n * Logs the time taken to execute 'myFunction'.\n */\nfunction measuringExecutionTime() {\n  // A simple INFO log message, using sprintf() formatting.\n  console.info(\"Timing the %s function (%d arguments)\", \"myFunction\", 1);\n\n  // Log a JSON object at a DEBUG level. The log is labeled\n  // with the message string in the log viewer, and the JSON content\n  // is displayed in the expanded log structure under \"jsonPayload\".\n  const parameters = {\n    isValid: true,\n    content: \"some string\",\n    timestamp: new Date(),\n  };\n  console.log({ message: \"Function Input\", initialData: parameters });\n  const label = \"myFunction() time\"; // Labels the timing log entry.\n  console.time(label); // Starts the timer.\n  try {\n    myFunction(parameters); // Function to time.\n  } catch (e) {\n    // Logs an ERROR message.\n    console.error(`myFunction() yielded an error: ${e}`);\n  }\n  console.timeEnd(label); // Stops the timer, logs execution duration.\n}\n// [END apps_script_logging_execution_time]\n\n// [START apps_script_logging_sheet_information]\n/**\n * Logs Google Sheet information.\n * @param {number} rowNumber The spreadsheet row number.\n * @param {string} email The email to send with the row data.\n */\nfunction emailDataRow(rowNumber, email) {\n  console.log(`Emailing data row ${rowNumber} to ${email}`);\n  const sheet = SpreadsheetApp.getActiveSheet();\n  const data = sheet.getDataRange().getValues();\n  const rowData = data[rowNumber - 1].join(\" \");\n  console.log(`Row ${rowNumber} data: ${rowData}`);\n  MailApp.sendEmail(email, `Data in row ${rowNumber}`, rowData);\n}\n// [END apps_script_logging_sheet_information]\n"
  },
  {
    "path": "utils/test_logging.gs",
    "content": "/**\n * Copyright Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * create test spreadsheets\n * @return {string} spreadsheet\n */\nfunction createTestSpreadsheet() {\n  const spreadsheet = SpreadsheetApp.create(\"Test Spreadsheet\");\n  for (let i = 0; i < 3; ++i) {\n    spreadsheet.appendRow([1, 2, 3]);\n  }\n  return spreadsheet.getId();\n}\n\n/**\n * populate the created spreadsheet with values\n * @param {string} spreadsheetId\n */\nfunction populateValues(spreadsheetId) {\n  const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest();\n  const repeatCellRequest = Sheets.newRepeatCellRequest();\n\n  /** @type {string[][]} */\n  const values = [];\n  for (let i = 0; i < 10; ++i) {\n    values[i] = [];\n    for (let j = 0; j < 10; ++j) {\n      values[i].push(\"Hello\");\n    }\n  }\n  const range = \"A1:J10\";\n  SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values);\n  SpreadsheetApp.flush();\n}\n\n/**\n * Test emailDataRow of logging.gs\n */\nfunction itShouldEmailDataRow() {\n  console.log(\"> itShouldEmailDataRow\");\n  const email = Session.getActiveUser().getEmail();\n  const spreadsheetId = createTestSpreadsheet();\n  populateValues(spreadsheetId);\n  const data = SpreadsheetApp.openById(spreadsheetId);\n  emailDataRow(1, email);\n}\n\n/**\n * runs all the functions of logging.gs\n */\nfunction RUN_ALL_TESTS() {\n  console.log(\"> itShouldMeasureExecutionTime\");\n  measuringExecutionTime();\n  itShouldEmailDataRow();\n}\n"
  },
  {
    "path": "wasm/README.md",
    "content": "# Unleashing the power of Rust, Python, and WebAssembly in Apps Script\n\nThis folder is the companion to the talk \"Unleashing the power of Rust, Python, and WebAssembly in Apps Script\" at Google Cloud Next '24.\n\n## Development\n\nThe development of this proof of concept requires a deep understanding of the Apps Script runtime, JavaScript bundlers, the Rust programming language, and the WebAssembly ecosystem. Please note that this is a quickly evolving space, and the tools and techniques used in this project may become outdated quickly.\n\n### Prerequisites\n\n- Node.js - https://nodejs.org/en/download\n- Rust - https://www.rust-lang.org/tools/install\n- Binaryen (for wasm-opt) - https://github.com/WebAssembly/binaryen\n\n"
  },
  {
    "path": "wasm/hello-world/.clasp.json",
    "content": "{\n  \"scriptId\": \"1xt1CvoUyFAzfoCdkwCHXBXzu3oaNz2a6iNsPW2GA6rOAsyBv66r4TarA\",\n  \"rootDir\": \"./dist\"\n}\n"
  },
  {
    "path": "wasm/hello-world/.gitattributes",
    "content": "package-lock.json merge=binary -diff\nCargo.lock merge=binary -diff\n"
  },
  {
    "path": "wasm/hello-world/.gitignore",
    "content": "/target\n/node_modules\n/dist\n/src/pkg\n.wireit"
  },
  {
    "path": "wasm/hello-world/Cargo.toml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"1.57\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nwasm-bindgen = { version = \"0.2.91\", features = [] }\n\n[dev-dependencies]\nwasm-bindgen-cli = \"0.2.91\"\n\n[profile.release]\nopt-level = 's'\n"
  },
  {
    "path": "wasm/hello-world/README.md",
    "content": "# Unleashing the power of Rust, Python, and WebAssembly in Apps Script\n\nThis folder is the companion to the talk \"Unleashing the power of Rust, Python, and WebAssembly in Apps Script\" at Google Cloud Next '24.\n\n## Development\n\nThe development of this proof of concept requires a deep understanding of the Apps Script runtime, JavaScript bundlers, the Rust programming language, and the WebAssembly ecosystem. Please note that this is a quickly evolving space, and the tools and techniques used in this project may become outdated quickly.\n\n### Prerequisites\n\n- Node.js - https://nodejs.org/en/download\n- Rust - https://www.rust-lang.org/tools/install\n- Binaryen (for wasm-opt) - https://github.com/WebAssembly/binaryen\n\n### Build\n\n1. `npm i`\n1. `npm run build`\n"
  },
  {
    "path": "wasm/hello-world/build.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport esbuild from \"esbuild\";\nimport { wasmLoader } from \"esbuild-plugin-wasm\";\n\nconst outdir = \"dist\";\nconst sourceRoot = \"src\";\n\nawait esbuild.build({\n  entryPoints: [\"./src/wasm.js\"],\n  bundle: true,\n  outdir,\n  sourceRoot,\n  platform: \"neutral\",\n  format: \"esm\",\n  plugins: [wasmLoader({ mode: \"embedded\" })],\n  inject: [\"polyfill.js\"],\n  minify: true,\n  banner: { js: \"// Generated code DO NOT EDIT\\n\" },\n});\n\nconst passThroughFiles = [\"main.js\", \"test.js\", \"appsscript.json\"];\n\nawait Promise.all(\n  passThroughFiles.map(async (file) =>\n    fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)),\n  ),\n);\n"
  },
  {
    "path": "wasm/hello-world/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"0.1.0\",\n  \"description\": \"An example integration of WASM with Rust into Apps Script\",\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"build:rust\": \"wireit\",\n    \"build:wasm\": \"wireit\",\n    \"clean\": \"rm -rf dist pkg target\",\n    \"deploy\": \"wireit\",\n    \"format\": \"cargo fmt\",\n    \"start\": \"wireit\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"node build.js\",\n      \"dependencies\": [\"build:wasm\"],\n      \"files\": [\"src/*.js\", \"*.js\", \"package.json\"],\n      \"output\": [\"dist\"]\n    },\n    \"build:rust\": {\n      \"command\": \"cargo build --release --target wasm32-unknown-unknown\",\n      \"output\": [\"./target/wasm32-unknown-unknown/release/example.wasm\"],\n      \"files\": [\"Cargo.lock\", \"Cargo.toml\", \"src/**/*.rs\", \"package.json\"]\n    },\n    \"build:wasm\": {\n      \"command\": \"wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm\",\n      \"dependencies\": [\"build:rust\"],\n      \"files\": [\n        \"./target/wasm32-unknown-unknown/release/example_bg.wasm\",\n        \"package.json\"\n      ],\n      \"output\": [\"src/pkg\"]\n    },\n    \"start\": {\n      \"command\": \"node dist/index.js\",\n      \"dependencies\": [\"build\"]\n    },\n    \"deploy\": {\n      \"command\": \"clasp push -f\",\n      \"dependencies\": [\"build\"],\n      \"files\": [\".clasp.json\", \".claspignore\"]\n    }\n  },\n  \"author\": \"Justin Poehnelt <justin.poehnelt@gmail.com>\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@google/clasp\": \"^2.4.2\",\n    \"esbuild\": \"^0.20.1\",\n    \"esbuild-plugin-wasm\": \"github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd\",\n    \"vitest\": \"^1.3.1\",\n    \"wireit\": \"^0.14.4\"\n  },\n  \"dependencies\": {\n    \"fastestsmallesttextencoderdecoder\": \"^1.0.22\"\n  },\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "wasm/hello-world/polyfill.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport {\n  TextEncoder,\n  TextDecoder,\n} from \"fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js\";\n"
  },
  {
    "path": "wasm/hello-world/src/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Denver\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}\n"
  },
  {
    "path": "wasm/hello-world/src/lib.rs",
    "content": "// Copyright 2024 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen]\npub fn hello(name: &str) -> JsValue {\n    format!(\"Hello, {} from Rust!\", name).into()\n}\n"
  },
  {
    "path": "wasm/hello-world/src/main.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nasync function main() {\n  const name = \"world\";\n  console.log(await hello_(name));\n}\n"
  },
  {
    "path": "wasm/hello-world/src/test.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nasync function test() {\n  await assert(hello_(\"world\"), \"Hello, world from Rust!\");\n}\n\nasync function assert(a, b, message) {\n  const aVal = await a;\n  const bVal = await b;\n\n  if (aVal !== bVal) {\n    throw message ?? `'${aVal}' !== '${bVal}'`;\n  }\n}\n\nasync function latency(func, iterations, argsFunc = () => []) {\n  const executionTimes = [];\n\n  for (let i = 0; i < iterations; i++) {\n    const args = argsFunc();\n\n    const startTime = Date.now();\n    let endTime;\n\n    try {\n      await func(...args);\n      endTime = Date.now();\n    } catch (e) {\n      endTime = Number.POSITIVE_INFINITY;\n      console.error(e);\n      continue;\n    }\n\n    executionTimes.push(endTime - startTime);\n  }\n\n  // Calculate statistics\n  const min = Math.min(...executionTimes);\n  const max = Math.max(...executionTimes);\n  const totalTime = executionTimes.reduce((sum, time) => sum + time, 0);\n  const average = totalTime / iterations;\n\n  return {\n    min: min,\n    max: max,\n    average: average,\n    totalTime,\n    // times: executionTimes // Array of all execution times\n  };\n}\n\nasync function benchmark() {\n  await hello_(\"world\"); // Warmup\n\n  console.log(await latency(hello_, 100, () => [generateRandomString(10)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(100)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(1000)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(10000)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(100000)]));\n  console.log(await latency(hello_, 30, () => [generateRandomString(1000000)]));\n}\n\nfunction generateRandomString(length = 1024) {\n  // Choose your desired character set\n  const characters =\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n  const charactersLength = characters.length;\n\n  let result = \"\";\n  for (let i = 0; i < length; i++) {\n    result += characters.charAt(Math.floor(Math.random() * charactersLength));\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "wasm/hello-world/src/wasm.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Wrapper function for hello\n * @param {string} name\n * @returns\n */\nasync function hello_(name) {\n  const wasm = await import(\"./pkg/example_bg.wasm\");\n  const { __wbg_set_wasm, hello } = await import(\"./pkg/example_bg.js\");\n\n  __wbg_set_wasm(wasm);\n\n  return hello(name);\n}\n\nglobalThis.hello_ = hello_;\n"
  },
  {
    "path": "wasm/image-add-on/.clasp.json",
    "content": "{\n  \"scriptId\": \"1gP1tiV1KkhVbMADIA_M-d4IJP1GNXwU7-7MundfqlESmSAdo0sC_Nml4\",\n  \"rootDir\": \"./dist\"\n}\n"
  },
  {
    "path": "wasm/image-add-on/.gitattributes",
    "content": "package-lock.json merge=binary -diff\nCargo.lock merge=binary -diff\n"
  },
  {
    "path": "wasm/image-add-on/.gitignore",
    "content": "/target\n/node_modules\n/dist\n/src/pkg\n.wireit"
  },
  {
    "path": "wasm/image-add-on/Cargo.toml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"1.57\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nbase64 = \"0.22.0\"\nconsole_error_panic_hook = \"0.1.7\"\nimage = {version = \"0.24.9\", features = [\"jpeg\"]}\njs-sys = \"0.3.69\"\nwasm-bindgen = { version = \"0.2.91\", features = [] }\n\n[dev-dependencies]\nwasm-bindgen-cli = \"0.2.91\"\n\n[profile.release]\nopt-level = 's'\n"
  },
  {
    "path": "wasm/image-add-on/README.md",
    "content": "# Unleashing the power of Rust, Python, and WebAssembly in Apps Script\n\nThis folder is the companion to the talk \"Unleashing the power of Rust, Python, and WebAssembly in Apps Script\" at Google Cloud Next '24.\n\n## Development\n\nThe development of this proof of concept requires a deep understanding of the Apps Script runtime, JavaScript bundlers, the Rust programming language, and the WebAssembly ecosystem. Please note that this is a quickly evolving space, and the tools and techniques used in this project may become outdated quickly.\n\n### Prerequisites\n\n- Node.js - https://nodejs.org/en/download\n- Rust - https://www.rust-lang.org/tools/install\n- Binaryen (for wasm-opt) - https://github.com/WebAssembly/binaryen\n\n### Build\n\n1. `npm i`\n1. `npm run build`\n"
  },
  {
    "path": "wasm/image-add-on/build.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport esbuild from \"esbuild\";\nimport { wasmLoader } from \"esbuild-plugin-wasm\";\n\nconst outdir = \"dist\";\nconst sourceRoot = \"src\";\n\nawait esbuild.build({\n  entryPoints: [\"./src/wasm.js\"],\n  bundle: true,\n  outdir,\n  sourceRoot,\n  platform: \"neutral\",\n  format: \"esm\",\n  plugins: [wasmLoader({ mode: \"embedded\" })],\n  inject: [\"polyfill.js\"],\n  minify: true,\n  banner: { js: \"// Generated code DO NOT EDIT\\n\" },\n});\n\nconst passThroughFiles = [\"main.js\", \"test.js\", \"appsscript.json\", \"add-on.js\"];\n\nawait Promise.all(\n  passThroughFiles.map(async (file) =>\n    fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)),\n  ),\n);\n"
  },
  {
    "path": "wasm/image-add-on/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"0.1.0\",\n  \"description\": \"An example integration of WASM with Rust into Apps Script\",\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"build:rust\": \"wireit\",\n    \"build:wasm\": \"wireit\",\n    \"clean\": \"rm -rf dist pkg target\",\n    \"deploy\": \"wireit\",\n    \"format\": \"cargo fmt\",\n    \"start\": \"wireit\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"node build.js\",\n      \"dependencies\": [\"build:wasm\"],\n      \"files\": [\"src/*.js\", \"src/*.json\", \"*.js\", \"package.json\"],\n      \"output\": [\"dist\"]\n    },\n    \"build:rust\": {\n      \"command\": \"cargo build --release --target wasm32-unknown-unknown\",\n      \"output\": [\"./target/wasm32-unknown-unknown/release/example.wasm\"],\n      \"files\": [\"Cargo.lock\", \"Cargo.toml\", \"src/**/*.rs\", \"package.json\"]\n    },\n    \"build:wasm\": {\n      \"command\": \"wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm\",\n      \"dependencies\": [\"build:rust\"],\n      \"files\": [\n        \"./target/wasm32-unknown-unknown/release/example_bg.wasm\",\n        \"package.json\"\n      ],\n      \"output\": [\"src/pkg\"]\n    },\n    \"start\": {\n      \"command\": \"node dist/index.js\",\n      \"dependencies\": [\"build\"]\n    },\n    \"deploy\": {\n      \"command\": \"clasp push -f\",\n      \"dependencies\": [\"build\"],\n      \"files\": [\".clasp.json\", \".claspignore\"]\n    }\n  },\n  \"author\": \"Justin Poehnelt <justin.poehnelt@gmail.com>\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@google/clasp\": \"^2.4.2\",\n    \"esbuild\": \"^0.20.1\",\n    \"esbuild-plugin-wasm\": \"github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd\",\n    \"vitest\": \"^1.3.1\",\n    \"wireit\": \"^0.14.4\"\n  },\n  \"dependencies\": {\n    \"@types/google-apps-script\": \"^1.0.82\",\n    \"fastestsmallesttextencoderdecoder\": \"^1.0.22\"\n  },\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "wasm/image-add-on/polyfill.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport {\n  TextEncoder,\n  TextDecoder,\n} from \"fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js\";\n"
  },
  {
    "path": "wasm/image-add-on/src/add-on.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst COLORS = {\n  RED: \"#EA4335\",\n};\n\nconst properties = PropertiesService.getUserProperties();\n\nasync function card(items) {\n  const builder = CardService.newCardBuilder();\n\n  const { quality, format, width, height } = loadSettings();\n\n  const controls = CardService.newCardSection()\n    .addWidget(\n      CardService.newSelectionInput()\n        .setFieldName(\"quality\")\n        .setTitle(\"Quality\")\n        .setType(CardService.SelectionInputType.RADIO_BUTTON)\n        .addItem(\"Low\", \"low\", quality === \"low\")\n        .addItem(\"Medium\", \"medium\", quality === \"medium\")\n        .addItem(\"High\", \"high\", quality === \"high\"),\n    )\n    .addWidget(\n      CardService.newTextInput()\n        .setFieldName(\"height\")\n        .setTitle(\"Height\")\n        .setMultiline(false)\n        .setValue(height ?? \"\"),\n    )\n    .addWidget(\n      CardService.newTextInput()\n        .setFieldName(\"width\")\n        .setTitle(\"Width\")\n        .setMultiline(false)\n        .setValue(width ?? \"\"),\n    )\n    .addWidget(\n      CardService.newTextButton()\n        .setBackgroundColor(COLORS.RED)\n        .setText(\"Apply Settings\")\n        .setOnClickAction(\n          CardService.newAction()\n            .setFunctionName(\"updateSettings\")\n            .setParameters({})\n            .setLoadIndicator(CardService.LoadIndicator.SPINNER),\n        ),\n    )\n    .setCollapsible(true)\n    .setNumUncollapsibleWidgets(0);\n\n  builder.addSection(controls);\n\n  const sections = await Promise.all(\n    (\n      items ??\n      JSON.parse(\n        PropertiesService.getUserProperties().getProperty(\"selectedItems\"),\n      )\n    )\n      .filter((item) => item.mimeType.startsWith(\"image\"))\n      .map(async (item) => {\n        const section = CardService.newCardSection();\n\n        const bytes = DriveApp.getFileById(item.id).getBlob().getBytes();\n\n        const newBytes = await compress_(bytes, {\n          quality: qualityToInt(quality),\n          format: item.mimeType.split(\"/\").pop(),\n          width: Number.parseInt(width ?? \"0\"),\n          height: Number.parseInt(height ?? \"0\"),\n        });\n\n        const dataUrl = `data:${item.mimeType};base64,${Utilities.base64Encode(\n          newBytes,\n        )}`;\n\n        section.addWidget(CardService.newImage().setImageUrl(dataUrl));\n\n        section.addWidget(\n          CardService.newDecoratedText().setText(bytesToText(newBytes.length)),\n        );\n\n        section.addWidget(\n          CardService.newButtonSet()\n            .addButton(\n              CardService.newTextButton()\n                .setBackgroundColor(COLORS.RED)\n                .setText(\"Save\")\n                .setOnClickAction(\n                  CardService.newAction()\n                    .setFunctionName(\"save\")\n                    .setParameters({\n                      bytes: Utilities.base64Encode(newBytes),\n                      action: \"save\",\n                      item: JSON.stringify(item),\n                    }),\n                ),\n            )\n            .addButton(\n              CardService.newTextButton()\n                .setBackgroundColor(COLORS.RED)\n                .setText(\"Save Copy\")\n                .setOnClickAction(\n                  CardService.newAction()\n                    .setFunctionName(\"save\")\n                    .setParameters({\n                      bytes: Utilities.base64Encode(newBytes),\n                      action: \"save-as\",\n                      item: JSON.stringify(item),\n                    }),\n                ),\n            ),\n        );\n        return section;\n      }),\n  );\n\n  for (const section of sections) {\n    builder.addSection(section);\n  }\n\n  return builder;\n}\n\n/**\n * Build a simple card that checks selected items' quota usage. Checking\n * quota usage requires user-permissions, so this add-on provides a button\n * to request `drive.file` scope for items the add-on doesn't yet have\n * permission to access.\n *\n * @param e The event object passed containing contextual information about\n *    the Drive items selected.\n * @return {Card}\n */\nasync function onItemsSelectedTrigger(e) {\n  PropertiesService.getUserProperties().setProperty(\n    \"selectedItems\",\n    JSON.stringify(e.drive.selectedItems),\n  );\n  return (await card(e.drive.selectedItems)).build();\n}\n\n/**\n * Callback function for a button action. Instructs Drive to display a\n * permissions dialog to the user, requesting `drive.file` scope for a\n * specific item on behalf of this add-on.\n *\n * @param {Object} e The parameters object that contains the item's\n *   Drive ID.\n * @return {DriveItemsSelectedActionResponse}\n */\nfunction onRequestFileScopeButtonClicked(e) {\n  const idToRequest = e.parameters.id;\n  return CardService.newDriveItemsSelectedActionResponseBuilder()\n    .requestFileScope(idToRequest)\n    .build();\n}\n\nfunction onFileScopeGrantedTrigger(e) {\n  console.info(\"after granting item\");\n  console.info(e);\n  const builder = CardService.newCardBuilder();\n  return builder.build();\n}\n\nfunction onHomePageTrigger() {\n  return CardService.newCardBuilder()\n    .setHeader(CardService.newCardHeader().setTitle(\"Drive Image Compress\"))\n    .addSection(\n      CardService.newCardSection().addWidget(\n        CardService.newTextParagraph().setText(\n          \"Select one or more files in Drive to compress the image.\",\n        ),\n      ),\n    )\n    .build();\n}\n\nfunction bytesToText(bytes) {\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  if (bytes === 0) return \"0 Byte\";\n  const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));\n  return `${Math.round(bytes / 1024 ** i)} ${sizes[i]}`;\n}\n\nasync function save(...args) {\n  console.log(args);\n  return CardService.newActionResponseBuilder()\n    .setNavigation(CardService.newNavigation().popToRoot())\n    .build();\n}\n\nasync function updateSettings(e) {\n  console.log({ e });\n  const { formInput } = e;\n\n  persistSettings(formInput);\n\n  return CardService.newActionResponseBuilder()\n    .setNavigation(\n      CardService.newNavigation()\n        .popToRoot()\n        .updateCard((await card()).build()),\n    )\n    .build();\n}\n\nfunction persistSettings(settings) {\n  properties.setProperty(\n    \"settings\",\n    JSON.stringify({\n      ...loadSettings,\n      ...settings,\n    }),\n  );\n}\n\nfunction loadSettings() {\n  const defaults = {\n    quality: \"medium\",\n  };\n\n  return {\n    ...defaults,\n    ...JSON.parse(properties.getProperty(\"settings\") ?? \"{}\"),\n  };\n}\n\nfunction qualityToInt(quality) {\n  switch (quality) {\n    case \"low\":\n      return 50;\n    case \"medium\":\n      return 80;\n    case \"high\":\n      return 90;\n  }\n}\n"
  },
  {
    "path": "wasm/image-add-on/src/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Denver\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"version\": \"v3\",\n        \"serviceId\": \"drive\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"addOns\": {\n    \"common\": {\n      \"logoUrl\": \"https://ssl.gstatic.com/docs/script/images/logo/script-64.png\",\n      \"name\": \"Drive Image Compress Add-on\",\n      \"universalActions\": []\n    },\n    \"drive\": {\n      \"homepageTrigger\": {\n        \"runFunction\": \"onHomePageTrigger\",\n        \"enabled\": true\n      },\n      \"onItemsSelectedTrigger\": {\n        \"runFunction\": \"onItemsSelectedTrigger\"\n      }\n    }\n  },\n  \"oauthScopes\": [\n    \"https://www.googleapis.com/auth/script.locale\",\n    \"https://www.googleapis.com/auth/drive.addons.metadata.readonly\",\n    \"https://www.googleapis.com/auth/drive\"\n  ]\n}\n"
  },
  {
    "path": "wasm/image-add-on/src/lib.rs",
    "content": "// Copyright 2024 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen]\npub fn compress(data: &[u8], quality: u8, width: u32, height: u32) -> JsValue {\n    console_error_panic_hook::set_once();\n\n    let img = match image::load_from_memory_with_format(data, image::ImageFormat::Jpeg) {\n        Ok(img) => img,\n        Err(_) => return JsValue::from_str(\"something went wrong\"),\n    };\n\n    let img = match width == 0 || height == 0 {\n        true => img,\n        false => img.resize(width, height, image::imageops::FilterType::Lanczos3),\n    };\n\n    let mut data = Vec::new();\n\n    let _ = img.write_with_encoder(image::codecs::jpeg::JpegEncoder::new_with_quality(\n        &mut data, quality,\n    ));\n\n    js_sys::Uint8Array::from(&data[..]).into()\n}\n"
  },
  {
    "path": "wasm/image-add-on/src/main.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst QUALITY = 80;\n\nasync function main() {\n  const iterator = DriveApp.getFilesByType(\"image/jpeg\");\n\n  while (iterator.hasNext()) {\n    const file = iterator.next();\n    const bytes = file.getBlob().getBytes();\n\n    const dataUrl = await compress_(bytes, QUALITY);\n\n    if (dataUrl) {\n      console.log(dataUrl);\n    } else {\n      console.warn(\"failed to decode image\");\n    }\n  }\n}\n"
  },
  {
    "path": "wasm/image-add-on/src/test.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nasync function test() {}\n\nasync function assert(a, b, message) {\n  const aVal = await a;\n  const bVal = await b;\n\n  if (aVal !== bVal) {\n    throw message ?? `'${aVal}' !== '${bVal}'`;\n  }\n}\n\nasync function latency(func, iterations, argsFunc = () => []) {\n  const executionTimes = [];\n\n  for (let i = 0; i < iterations; i++) {\n    const args = argsFunc();\n\n    const startTime = Date.now();\n    let endTime;\n\n    try {\n      await func(...args);\n      endTime = Date.now();\n    } catch (e) {\n      endTime = Number.POSITIVE_INFINITY;\n      console.error(e);\n      continue;\n    }\n\n    executionTimes.push(endTime - startTime);\n  }\n\n  // Calculate statistics\n  const min = Math.min(...executionTimes);\n  const max = Math.max(...executionTimes);\n  const totalTime = executionTimes.reduce((sum, time) => sum + time, 0);\n  const average = totalTime / iterations;\n\n  return {\n    min: min,\n    max: max,\n    average: average,\n    totalTime,\n    // times: executionTimes // Array of all execution times\n  };\n}\n\nasync function benchmark() {\n  await hello_(\"world\"); // Warmup\n\n  console.log(await latency(hello_, 100, () => [generateRandomString(10)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(100)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(1000)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(10000)]));\n  console.log(await latency(hello_, 100, () => [generateRandomString(100000)]));\n  console.log(await latency(hello_, 30, () => [generateRandomString(1000000)]));\n}\n\nfunction generateRandomString(length = 1024) {\n  // Choose your desired character set\n  const characters =\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n  const charactersLength = characters.length;\n\n  let result = \"\";\n  for (let i = 0; i < length; i++) {\n    result += characters.charAt(Math.floor(Math.random() * charactersLength));\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "wasm/image-add-on/src/wasm.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nasync function compress_(bytes, { quality, format, width, height }) {\n  const wasm = await import(\"./pkg/example_bg.wasm\");\n  const { __wbg_set_wasm, compress } = await import(\"./pkg/example_bg.js\");\n\n  __wbg_set_wasm(wasm);\n\n  width = width || 0;\n  height = height || 0;\n\n  console.log({ quality, format, width, height });\n\n  const result = compress(bytes, quality, format, width, height);\n\n  if (typeof result === \"string\") {\n    throw new Error(result);\n  }\n\n  return result;\n}\n\nglobalThis.compress_ = compress_;\n"
  },
  {
    "path": "wasm/python/.clasp.json",
    "content": "{\n  \"scriptId\": \"1_tU8IFkT1ZZ-b08YFeC8umntrH92WVQ27jvUmsCo1W4ZqKKqcytBLdcn\",\n  \"rootDir\": \"./dist\"\n}\n"
  },
  {
    "path": "wasm/python/.gitattributes",
    "content": "package-lock.json merge=binary -diff\nCargo.lock merge=binary -diff\n"
  },
  {
    "path": "wasm/python/.gitignore",
    "content": "/target\n/node_modules\n/dist\n/src/pkg\n.wireit"
  },
  {
    "path": "wasm/python/Cargo.toml",
    "content": "# Copyright 2025 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\nrust-version = \"1.57\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nconsole_error_panic_hook = \"0.1.7\"\ngetrandom = { version = \"0.2\", features = [\"js\"] }\nrustpython-vm = { version = \"0.3.0\", features = [\"compiler\", \"serde\"] }\nserde-wasm-bindgen = \"0.6.5\"\nwasm-bindgen = { version = \"0.2.91\", features = [] }\nweb-sys = { version = \"0.3.69\", features = [\"console\"] }\n\n[dev-dependencies]\nwasm-bindgen-cli = \"0.2.91\"\n\n[profile.release]\nopt-level = 's'\n"
  },
  {
    "path": "wasm/python/README.md",
    "content": "# Unleashing the power of Rust, Python, and WebAssembly in Apps Script\n\nThis folder is the companion to the talk \"Unleashing the power of Rust, Python, and WebAssembly in Apps Script\" at Google Cloud Next '24.\n\n## Development\n\nThe development of this proof of concept requires a deep understanding of the Apps Script runtime, JavaScript bundlers, the Rust programming language, and the WebAssembly ecosystem. Please note that this is a quickly evolving space, and the tools and techniques used in this project may become outdated quickly.\n\n### Prerequisites\n\n- Node.js - https://nodejs.org/en/download\n- Rust - https://www.rust-lang.org/tools/install\n- Binaryen (for wasm-opt) - https://github.com/WebAssembly/binaryen\n\n### Build\n\n1. `npm i`\n1. `npm run build`\n"
  },
  {
    "path": "wasm/python/build.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport esbuild from \"esbuild\";\nimport { wasmLoader } from \"esbuild-plugin-wasm\";\n\nconst outdir = \"dist\";\nconst sourceRoot = \"src\";\n\nawait esbuild.build({\n  entryPoints: [\"./src/wasm.js\"],\n  bundle: true,\n  outdir,\n  sourceRoot,\n  platform: \"neutral\",\n  format: \"esm\",\n  plugins: [wasmLoader({ mode: \"embedded\" })],\n  inject: [\"polyfill.js\"],\n  minify: true,\n  banner: { js: \"// Generated code DO NOT EDIT\\n\" },\n});\n\nconst passThroughFiles = [\"main.js\", \"test.js\", \"appsscript.json\"];\n\nawait Promise.all(\n  passThroughFiles.map(async (file) =>\n    fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)),\n  ),\n);\n"
  },
  {
    "path": "wasm/python/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"0.1.0\",\n  \"description\": \"An example integration of WASM with Rust into Apps Script\",\n  \"scripts\": {\n    \"build\": \"wireit\",\n    \"build:rust\": \"wireit\",\n    \"build:wasm\": \"wireit\",\n    \"clean\": \"rm -rf dist pkg target\",\n    \"deploy\": \"wireit\",\n    \"format\": \"cargo fmt\",\n    \"start\": \"wireit\"\n  },\n  \"wireit\": {\n    \"build\": {\n      \"command\": \"node build.js\",\n      \"dependencies\": [\"build:wasm\"],\n      \"files\": [\"src/*.js\", \"src/*.json\", \"*.js\", \"package.json\"],\n      \"output\": [\"dist\"]\n    },\n    \"build:rust\": {\n      \"command\": \"cargo build --release --target wasm32-unknown-unknown\",\n      \"output\": [\"./target/wasm32-unknown-unknown/release/example.wasm\"],\n      \"files\": [\"Cargo.lock\", \"Cargo.toml\", \"src/**/*.rs\", \"package.json\"]\n    },\n    \"build:wasm\": {\n      \"command\": \"wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm\",\n      \"dependencies\": [\"build:rust\"],\n      \"files\": [\n        \"./target/wasm32-unknown-unknown/release/example_bg.wasm\",\n        \"package.json\"\n      ],\n      \"output\": [\"src/pkg\"]\n    },\n    \"start\": {\n      \"command\": \"node dist/index.js\",\n      \"dependencies\": [\"build\"]\n    },\n    \"deploy\": {\n      \"command\": \"clasp push -f\",\n      \"dependencies\": [\"build\"],\n      \"files\": [\".clasp.json\", \".claspignore\"]\n    }\n  },\n  \"author\": \"Justin Poehnelt <justin.poehnelt@gmail.com>\",\n  \"license\": \"Apache-2.0\",\n  \"devDependencies\": {\n    \"@google/clasp\": \"^2.4.2\",\n    \"esbuild\": \"^0.20.1\",\n    \"esbuild-plugin-wasm\": \"github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd\",\n    \"vitest\": \"^1.3.1\",\n    \"wireit\": \"^0.14.4\"\n  },\n  \"dependencies\": {\n    \"fastestsmallesttextencoderdecoder\": \"^1.0.22\"\n  },\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "wasm/python/polyfill.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport {\n  TextEncoder,\n  TextDecoder,\n} from \"fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js\";\n"
  },
  {
    "path": "wasm/python/src/appsscript.json",
    "content": "{\n  \"timeZone\": \"America/Denver\",\n  \"dependencies\": {\n    \"enabledAdvancedServices\": [\n      {\n        \"userSymbol\": \"Drive\",\n        \"version\": \"v3\",\n        \"serviceId\": \"drive\"\n      }\n    ]\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"oauthScopes\": []\n}\n"
  },
  {
    "path": "wasm/python/src/lib.rs",
    "content": "// Copyright 2024 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nuse rustpython_vm::{\n    builtins::PyBaseException, compiler::Mode, convert::ToPyObject, py_serde, Interpreter,\n    PyObjectRef, PyRef, VirtualMachine,\n};\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen]\npub fn python(source: &str, data: &JsValue) -> JsValue {\n    console_error_panic_hook::set_once();\n\n    Interpreter::with_init(Default::default(), |_vm| {})\n    .enter(|vm| {\n        let data = match deserialize(data.clone(), vm) {\n            Ok(data) => data,\n            Err(err) => return PyError::new(err.to_string().into()).into(),\n        };\n\n        let scope = vm.new_scope_with_builtins();\n        match scope.globals.set_item(\"args\", data, vm) {\n            Ok(()) => {}\n            Err(err) => return serialize_exception(err, vm).into(),\n        }\n\n        let py_obj = match vm\n            .compile(source, Mode::BlockExpr, \"<embedded>\".to_owned())\n            .map_err(|err| vm.new_syntax_error(&err, Some(source)))\n            .and_then(|code_obj| vm.run_code_obj(code_obj, scope).clone())\n        {\n            Ok(py_obj) => py_obj,\n            Err(err) => return serialize_exception(err, vm).into(),\n        };\n\n        serialize(&py_obj, vm)\n    })\n}\n\n#[wasm_bindgen(inline_js = r\"\nexport class PyError extends Error {\n    constructor(message) {\n        super(message);\n    }\n}\n\")]\nextern \"C\" {\n    pub type PyError;\n    #[wasm_bindgen(constructor)]\n    fn new(message: JsValue) -> PyError;\n}\n\nfn serialize_exception(err: PyRef<PyBaseException>, vm: &VirtualMachine) -> PyError {\n    PyError::new(serialize(&err.args().to_pyobject(vm), vm))\n}\n\nfn serialize(py_obj: &PyObjectRef, vm: &VirtualMachine) -> JsValue {\n    py_serde::serialize(vm, py_obj, &serde_wasm_bindgen::Serializer::new())\n        .unwrap_or(format!(\"Failed to serialize: {:?}\", py_obj).into())\n}\n\nfn deserialize(\n    value: JsValue,\n    vm: &VirtualMachine,\n) -> Result<PyObjectRef, serde_wasm_bindgen::Error> {\n    let deserializer: serde_wasm_bindgen::Deserializer = value.into();\n\n    py_serde::deserialize(vm, deserializer)\n}\n"
  },
  {
    "path": "wasm/python/src/main.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Execute Python code and return the result.\n * @param {string} code\n * @param {...*} args - Arguments to pass to the Python code. Accessible as\n *   `args` in the Python code.\n *\n * @customfunction\n */\nasync function PYTHON(code = \"args\", ...args) {\n  const result = await python_(`${code}`, ...args);\n\n  if (result instanceof Error) {\n    throw result;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "wasm/python/src/test.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nasync function test() {}\n\nasync function assert(a, b, message) {\n  const aVal = await a;\n  const bVal = await b;\n\n  if (aVal !== bVal) {\n    throw message ?? `'${aVal}' !== '${bVal}'`;\n  }\n}\n"
  },
  {
    "path": "wasm/python/src/wasm.js",
    "content": "/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nglobalThis.crypto = {\n  getRandomValues: (array) => array.map(() => Math.floor(Math.random() * 256)),\n};\n\n/**\n * Wrapper function for hello\n * @param {string} name\n * @returns\n */\nasync function python_(source, ...args) {\n  const wasm = await import(\"./pkg/example_bg.wasm\");\n  const { __wbg_set_wasm, python } = await import(\"./pkg/example_bg.js\");\n\n  __wbg_set_wasm(wasm);\n\n  return await python(source, args);\n}\n\nglobalThis.python_ = python_;\n"
  }
]