<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Boris Baublys</title>
    <description>The latest articles on DEV Community by Boris Baublys (@bbaublys).</description>
    <link>https://dev.to/bbaublys</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F747798%2F3a737365-c01f-4ba9-943c-730f1441e5d3.jpg</url>
      <title>DEV Community: Boris Baublys</title>
      <link>https://dev.to/bbaublys</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbaublys"/>
    <language>en</language>
    <item>
      <title>Extracting Images from Google Sheets with Sheets Image Exporter</title>
      <dc:creator>Boris Baublys</dc:creator>
      <pubDate>Fri, 30 May 2025 21:22:53 +0000</pubDate>
      <link>https://dev.to/bbaublys/extracting-images-from-google-sheets-with-sheets-image-exporter-aga</link>
      <guid>https://dev.to/bbaublys/extracting-images-from-google-sheets-with-sheets-image-exporter-aga</guid>
      <description>&lt;p&gt;Original here: &lt;a href="https://baublys.ru/how-to-export-images-from-google-sheets-with-sheets-image-exporter/" rel="noopener noreferrer"&gt;How to Export Images from Google Sheets with Sheets Image Exporter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google Sheets is a powerful tool for managing data, but unlike Microsoft Excel, it lacks a native feature for exporting images embedded in spreadsheets. Whether you’re working with product photos, visual assets, or other images, there’s no built-in way to extract them without manual effort, which can be time-consuming. The Sheets Image Exporter Chrome extension fills this gap, enabling users to export images from Google Sheets efficiently. This article explores how Sheets Image Exporter works, its practical applications, and key considerations for its use.&lt;/p&gt;

&lt;h4&gt;
  
  
  What Sheets Image Exporter Does
&lt;/h4&gt;

&lt;p&gt;Sheets Image Exporter is a Chrome extension designed to extract images embedded in Google Sheets, a functionality not available natively in the platform. It enables the export of all images from a spreadsheet in a single operation, saving them as PNG or JPEG files. Users can choose to store the images on Google Drive or download them directly to their device. Additionally, file names can be customized based on cell values or sequential numbering for better organization.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Core Innovation: Image Detection Mechanism
&lt;/h4&gt;

&lt;p&gt;One of the biggest challenges in developing the Sheets Image Exporter was reliably identifying images embedded in Google Sheets, particularly distinguishing between selected and unselected images. Initially, I explored using the Google Sheets API, hoping to leverage methods like &lt;code&gt;getUrl()&lt;/code&gt; from the &lt;code&gt;OverGridImage&lt;/code&gt; class to retrieve image data. However, this approach quickly proved problematic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem with Google Sheets API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Google Sheets API, while powerful for many tasks, has significant limitations when it comes to handling images. The OverGridImage class, which represents images floating over the grid in a spreadsheet, does not provide a robust way to differentiate one image from another or to determine whether an image is selected by the user. The &lt;code&gt;getUrl()&lt;/code&gt; method, which was intended to retrieve the URL of an image, was inconsistent even in its early days. More recently, Google deprecated this method entirely, rendering it unusable.&lt;/p&gt;

&lt;p&gt;As documented in the &lt;a href="https://developers.google.com/apps-script/reference/spreadsheet/over-grid-image#geturl" rel="noopener noreferrer"&gt;Google Apps Script Reference for OverGridImage&lt;/a&gt;, the &lt;code&gt;getUrl()&lt;/code&gt; method is marked as deprecated with the following note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Deprecated.&lt;/strong&gt; For most newly inserted images, the source URL is unavailable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This deprecation left a gap in the ability to programmatically access image data directly through the API, forcing a rethink of the approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A DOM-Based Solution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Faced with these limitations, I turned to the browser’s Document Object Model (DOM) for a solution. The idea was to bypass the Google Sheets API entirely and instead identify images directly in the webpage’s DOM structure. By inspecting the Google Sheets interface, I conducted experiments that revealed images embedded in the spreadsheet are represented as &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements with src attributes starting with &lt;code&gt;blob:https://docs.google.com/&lt;/code&gt; or &lt;code&gt;filesystem:https://docs.google.com/&lt;/code&gt;. This discovery was pivotal, as it allowed the extension to detect images by querying the DOM rather than relying on the unreliable API.&lt;/p&gt;

&lt;p&gt;To make this approach user-friendly and flexible, I implemented a CSS selector-based mechanism to locate images. The extension uses a default selector to identify images that the user has selected (highlighted) with the mouse, leveraging a specific CSS property in the Google Sheets interface. Specifically, selected images are contained within elements that have a style attribute including &lt;code&gt;cursor: -webkit-grab&lt;/code&gt;. This led to the default selector defined in popupImages.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div.waffle-borderless-embedded-object-overlay[style*="cursor: -webkit-grab"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This selector targets &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; elements with the class &lt;code&gt;waffle-borderless-embedded-object-overlay&lt;/code&gt; that have the &lt;code&gt;cursor: -webkit-grab&lt;/code&gt; style, which corresponds to images actively selected by the user in the Google Sheets interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flexibility with Custom Selectors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recognizing that users might want to customize how images are detected, I added the ability to input a custom CSS selector through the extension’s popup interface. This allows advanced users to target specific elements in the DOM if needed. For example, to retrieve all images (including unselected ones), a user could use a selector like &lt;code&gt;div.waffle-borderless-embedded-object-overlay[style*="cursor: default"]&lt;/code&gt;, which targets images that are not actively selected.&lt;/p&gt;

&lt;p&gt;The logic for handling selectors, including falling back to the default if a custom selector is invalid, is implemented in content.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;selectedImageContainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selectedImageContainers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; containers with selector: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid selector: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, falling back to default: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;defaultSelector&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defaultSelector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;selectedImageContainers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaultSelector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;selectedImageContainers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; containers with default selector`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code ensures that if a user-provided selector is invalid, the extension gracefully reverts to the default selector, maintaining functionality while providing feedback via the console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processing Images in the DOM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the images are identified, the extension processes them by fetching their blob URLs and converting them to data URLs for further use (e.g., saving to Google Drive or downloading locally). This is handled in content.js with the following logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img[src^="blob:https://docs.google.com/"], img[src^="filesystem:https://docs.google.com/"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;processImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;selectedImageContainers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredDataUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Sending &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;filteredDataUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unique images.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filteredDataUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;selectorUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet demonstrates how the extension identifies &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements with blob or filesystem URLs, processes them into data URLs, and sends the results back to the popup script for display or export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The DOM-based image detection mechanism is the heart of the Sheets Image Exporter. By moving away from the Google Sheets API and leveraging the browser’s DOM, the extension overcomes the limitations of the deprecated &lt;code&gt;getUrl()&lt;/code&gt; method and provides a reliable way to identify and extract images. The default selector ensures ease of use for most users, while the custom selector option adds flexibility for advanced scenarios. This approach not only solves the technical challenge but also enhances the user experience by making image extraction intuitive and robust.&lt;/p&gt;

&lt;h4&gt;
  
  
  Practical Applications
&lt;/h4&gt;

&lt;p&gt;The extension is useful across various scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce&lt;/strong&gt;: Extract product images from inventory spreadsheets for use on websites or marketplaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Education&lt;/strong&gt;: Retrieve visual assets from teaching materials or student projects for reports or digital portfolios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Management&lt;/strong&gt;: Export images, such as logos or screenshots, from project tracking sheets for inclusion in reports or presentations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing&lt;/strong&gt;: Access campaign visuals, like social media graphics or banners, directly from a spreadsheet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These use cases illustrate how the extension addresses a critical need for users managing visual data in Google Sheets.&lt;/p&gt;

&lt;h4&gt;
  
  
  Key Features
&lt;/h4&gt;

&lt;p&gt;Sheets Image Exporter provides several practical features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch Export&lt;/strong&gt;: Extracts all images from a spreadsheet in one operation, addressing the absence of native export functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Formats&lt;/strong&gt;: Saves images as PNG or JPEG, based on user preference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Naming Options&lt;/strong&gt;: Allows naming files using cell values or sequential numbers for organized output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Drive Integration&lt;/strong&gt;: Enables saving images directly to Google Drive, keeping them accessible within the Google ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local downloading&lt;/strong&gt;: Download of all selected images to your device in a single ZIP archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extension operates through the Chrome browser and has a straightforward interface, requiring minimal setup.&lt;/p&gt;

&lt;h4&gt;
  
  
  Considerations for Use
&lt;/h4&gt;

&lt;p&gt;When using Sheets Image Exporter, a few factors should be kept in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image Quality&lt;/strong&gt;: The extension preserves the original resolution of images, so the quality of exported files depends on the quality of images uploaded to the spreadsheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google API Limits&lt;/strong&gt;: For spreadsheets with many images, exporting may need to be done in batches to stay within Google’s API quotas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Types&lt;/strong&gt;: The extension extracts images placed over cells but does not support exporting charts or drawings created within Google Sheets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;Sheets Image Exporter provides a critical capability that Google Sheets lacks: the ability to export images embedded in spreadsheets, a feature readily available in Microsoft Excel. For users managing visual data—whether for business, education, or personal projects—this Chrome extension offers a practical solution to a significant limitation. It is available through the &lt;a href="https://chromewebstore.google.com/detail/sheets-image-exporter/infelhpemoennpbejdfpfaiiffackhnf" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt; and can be integrated into any Google Sheets workflow accessed via Chrome.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Telegram bot + GoogleSheets: solving some problems</title>
      <dc:creator>Boris Baublys</dc:creator>
      <pubDate>Sat, 07 May 2022 23:39:21 +0000</pubDate>
      <link>https://dev.to/bbaublys/telegram-bot-googlesheets-solving-some-problems-pba</link>
      <guid>https://dev.to/bbaublys/telegram-bot-googlesheets-solving-some-problems-pba</guid>
      <description>&lt;h2&gt;
  
  
  What problems you may encounter and how to get around them
&lt;/h2&gt;

&lt;p&gt;The topic of Telegram bots is only growing. If you're developing Google Sheets add-ons, you're writing Google App Script (GAS) code. Therefore, probably, the brains for these bots are written in GAS.&lt;br&gt;
There was an idea to write a bot that would look for keywords in chat messages and do something with these messages. For example, send me a notification that someone has written something on a topic that interests me. Or, on the contrary, delete obscene language and spam, and ban the authors of such messages in the chat.&lt;br&gt;
As always, in the process of practical implementation of the plan, pitfalls emerge. I will talk about some of them and ways to bypass them below. But first, some theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  getUpdates VS setWebhook?
&lt;/h2&gt;

&lt;p&gt;It doesn't matter what language you write the brains for your bot in - GAS, PHP, Pyton or something else. In any case, you are using the &lt;a href="https://core.telegram.org/bots/api" rel="noopener noreferrer"&gt;Telegram API&lt;/a&gt;.&lt;br&gt;
The API currently supports two ways to handle bot updates: &lt;em&gt;getUpdates&lt;/em&gt; and &lt;em&gt;setWebhook&lt;/em&gt;.&lt;br&gt;
&lt;em&gt;getUpdates&lt;/em&gt; is a pull mechanism, &lt;em&gt;setWebhook&lt;/em&gt; is a push mechanism.&lt;br&gt;
For example, you run &lt;em&gt;getUpdates&lt;/em&gt; and get all the messages written in the chat during that hour. In GAS, you can set up a time trigger with this interval. But this means that for our example with an anti-spam bot, spam will hang in the chat with impunity for an hour. This is not good, it is advisable to shoot spam immediately on approach.&lt;br&gt;
Therefore it is necessary to use &lt;em&gt;setWebhook&lt;/em&gt;. As stated in &lt;a href="https://core.telegram.org/bots/webhooks" rel="noopener noreferrer"&gt;Marvin's Wonderful Guide to All Things Webhook&lt;/a&gt;:&lt;br&gt;
&lt;em&gt;setWebhook&lt;/em&gt;, compared to &lt;em&gt;getUpdates&lt;/em&gt;,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;saves your bot from having to frequently request updates.&lt;/li&gt;
&lt;li&gt;avoids the need for any polling mechanism in your code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  setWebhook() + fetch(url)
&lt;/h2&gt;

&lt;p&gt;Now a few words about the mechanism for receiving updates or, more simply, messages from a Telegram chat.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We write the simplest script in the GAS editor:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;function doPost(e) {&lt;br&gt;
    var contents = JSON.parse(e.postData.contents);&lt;br&gt;
    var chat_id = contents.message.from.id; &lt;br&gt;
    // code for extracting data and writing to the sheet&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
That's it, we have an update in the &lt;em&gt;contents&lt;/em&gt; variable. With the help of further processing, everything that is needed can be extracted from it. For example, &lt;em&gt;chat_id&lt;/em&gt; is the identifier of the message sender, &lt;em&gt;text&lt;/em&gt; is the text of the message.&lt;br&gt;
Then you can write this data on a sheet, send it to someone, etc.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Let's deploy the script as a web application.&lt;br&gt;
At the same time, we specify the parameters “Run as: On my behalf, Who has access: Everyone” and copy the link to the web application (webAppUrl).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We form a link.&lt;br&gt;
To do this, you need to connect the bot token and the web application URL as follows:&lt;br&gt;
&lt;code&gt;var token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"; &lt;br&gt;
var webAppUrl = "https://script.google.com/macros/s/XXXXXXXX/exec";&lt;br&gt;
var telegramUrl = "https://api.telegram.org/bot" + token;&lt;br&gt;
var telegramUrl = "https://api.telegram.org/bot" + token;&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fetch the url and set the webhook:&lt;br&gt;
&lt;code&gt;function setWebhook() {&lt;br&gt;
var url = telegramUrl + "/setWebhook?url=" + webAppUrl;&lt;br&gt;
var response = UrlFetchApp.fetch(url);&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can go to this url in the browser without any script and see&lt;br&gt;
&lt;code&gt;{"ok":true,"result":true,"description":"Webhook is already set"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is a JSON object, parsing which, you can extract all 3 fields separately - &lt;em&gt;ok&lt;/em&gt;, &lt;em&gt;result&lt;/em&gt; and &lt;em&gt;description&lt;/em&gt;. Then they can be used in conditional logic, shown to the user and all that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;p&gt;So we have a script that instantly writes all messages from the Telegram chat to a sheet. Then you can search for keywords in them and do something with these messages.&lt;br&gt;
But imagine that you, delighted, have posted a link to this bot of yours in the public domain and expect that satisfied users will thank you for a useful tool.&lt;br&gt;
It wasn't there! &lt;/p&gt;

&lt;p&gt;Everyone is comfortable with Google services. And they provide you with sheets, and a script development environment, and powerful servers around the world on which these scripts work, and many other delicious goodies. However there are pitfalls and one of them is &lt;a href="https://developers.google.com/apps-script/guides/services/quotas" rel="noopener noreferrer"&gt;Google's quotas and limits&lt;/a&gt;. “Apps Script Services has daily quotas and limits on some features. If you exceed the quota or limit, your script will throw an exception and execution will stop.” For example, URL Fetch calls are limited to 20,000 per day, and the limit for simultaneously script executions is 30 users. This means that if the number of users of the bot becomes large enough, it will stop working.&lt;br&gt;
And what to do?&lt;br&gt;
For example, you can buy your own server or rent a server and process all the logic on it. It costs extra money and time, so it's logical to think about something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Way out
&lt;/h2&gt;

&lt;p&gt;The developer may come up with the following.&lt;br&gt;
Since the web application is running on behalf of me, my quotas are consumed. But what if you deploy it not on my behalf, but on behalf of the user?&lt;br&gt;
An interested user can do this in 3 clicks. You just need to give him a sheet template with a script embedded in it. He will make a copy of the sheet for himself, deploy the script as a web application and insert the webAppUrl into the desired cell.&lt;br&gt;
Now quotas and restrictions will be consumed not by the developer, but by the user.&lt;br&gt;
And it is very unlikely that this user will have more than 20,000 messages per day in his Telegram group.&lt;br&gt;
Everything seems to work. &lt;br&gt;
It wasn't there! &lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls again
&lt;/h2&gt;

&lt;p&gt;Every developer wants the fruits of his creativity to be useful. Both for him and for the users.&lt;br&gt;
How to distribute our script so that interested users know about it? You can write articles on thematic sites, advertise on Google, etc.&lt;br&gt;
It's all great, but it's a sin not to use free tools.&lt;br&gt;
Speaking of Google goodies, one cannot fail to mention the &lt;a href="https://workspace.google.com/marketplace" rel="noopener noreferrer"&gt;Google Workspace Marketplace&lt;/a&gt;. There, Google developers publish add-ons for Google products - for sheets, documents, mail, etc.&lt;br&gt;
Thus, the idea of ​​​​implementing our Telegram bot appears as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We make a Google sheet template.&lt;br&gt;
It draws an interface with the necessary headings, a web application script is attached to the table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We transfer all the message processing logic from the webapp to the add-on and publish it on the Google Workspace Marketplace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user installs the add-on, makes a copy of the template for himself, makes the initial settings (enters the bot token, web application URL, his keywords into the required cells) and uses it to his delight.&lt;br&gt;
Yes, here we shifted part of the work to the user, but that's why I am writing this article. Perhaps someone who reads will suggest workarounds.&lt;br&gt;
Moreover, it was not there again, there are still pitfalls here.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Again
&lt;/h2&gt;

&lt;p&gt;The problem is that you can NOT pass data directly into an add-on in ANY WAY. That is, you received a message via the webhook, but you cannot send this message to the handler, which, let me remind you, is in add-on.&lt;br&gt;
You cannot run a function that is inside an add-on.&lt;br&gt;
You can't even pass some tag or signal like “Hey add-on! There's a message! Run fetch(url) and read!”.&lt;br&gt;
Never. I do not know why, whether for security reasons, or for some other reason. You can read about it here: &lt;a href="https://stackoverflow.com/questions/69789961/how-to-transfer-data-from-webapp-to-addon" rel="noopener noreferrer"&gt;How to transfer data from webapp to addon&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The thought may come to mind: “We write messages on a sheet. Put an onEdit() or onChange() trigger and as soon as the message is written to the sheet, the trigger will fire, read the data and process everything as it should.”&lt;br&gt;
There are 2 catches here.&lt;br&gt;
&lt;strong&gt;One&lt;/strong&gt; of them is that all I/O type spreadsheet operations are relatively slow - it's a fraction of a second. With intensive communication in the Telegram chat, multiple calls to the sheet and from the sheet can cause significant delays.&lt;br&gt;
In contrast to the situation, if the data is passed bypassing the sheets, directly from fetch() to the add-on code. Google's servers are fast and such operations take milliseconds.&lt;br&gt;
&lt;strong&gt;The second catch&lt;/strong&gt; is that onEdit() or onChange() triggers do NOT fire when the data on the sheet is changed programmatically.&lt;br&gt;
These triggers are fired if the data on the worksheet has been changed by the user. For example, entered from the keyboard or selected from the menu.&lt;br&gt;
More about this here: &lt;a href="https://developers.google.com/apps-script/guides/triggers/events" rel="noopener noreferrer"&gt;Event Objects&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is an way!
&lt;/h2&gt;

&lt;p&gt;The answer to the question in the post on the Stack was still found. The author even assigned a bounty for the correct answer, but the best thing that they suggested to him then was to use the &lt;a href="https://cloud.google.com/free" rel="noopener noreferrer"&gt;Google cloud service&lt;/a&gt;. Yes, among the delicious buns there is one. And it's certainly powerful stuff.&lt;br&gt;
That's just it is paid when certain indicators are exceeded.&lt;br&gt;
And I have not seen good manuals for it.&lt;br&gt;
According to GAS there are plenty of such resources. This is &lt;a href="https://developers.google.com/apps-script/overview" rel="noopener noreferrer"&gt;Google Apps Script&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web" rel="noopener noreferrer"&gt;mdn web docs&lt;/a&gt; and a bunch more.&lt;br&gt;
On Google Cloud there are much fewer such resources.&lt;/p&gt;

&lt;p&gt;Therefore, when Alan Wells came to the post six months later and wrote about the Sheets API and USER_ENTERED, everything immediately became clear.&lt;/p&gt;

&lt;p&gt;The scheme is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Enable the Sheets API in web application services. You can verify that everything is correct if you look at the code in the appsscript.json file. If anyone forgot, then for this, in the script settings, check the box “Show the appsscript.json manifest file in the editor”.&lt;br&gt;
It should contain the following code:&lt;br&gt;
&lt;code&gt;"dependencies": {&lt;br&gt;
"enabledAdvancedServices": [{&lt;br&gt;
  "userSymbol": "Sheets",&lt;br&gt;
  "serviceId": "sheets",&lt;br&gt;
  "version": "v4"&lt;br&gt;
}]&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
Later, when the user does a copy of your template, this service will be included in its copy of the script.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How do we usually write data to a sheet?&lt;br&gt;
We use &lt;a href="https://developers.google.com/apps-script/reference/spreadsheet/range#setvaluevalue" rel="noopener noreferrer"&gt;setValue(value)&lt;/a&gt;.&lt;br&gt;
Like this:&lt;br&gt;
&lt;code&gt;var ss = SpreadsheetApp.getActiveSpreadsheet();&lt;br&gt;
varsheet = ss.getSheets()[0];&lt;br&gt;
varcell = sheet.getRange("B2");&lt;br&gt;
cell.setValue(100);&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=QcH_HCRQczM" rel="noopener noreferrer"&gt;Into the fire!&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Now we use &lt;a href="https://developers.google.com/sheets/api/reference/rest" rel="noopener noreferrer"&gt;Google Sheets API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The code should look something like this:&lt;br&gt;
&lt;code&gt;var SHEETID = 'XXXXXXX';&lt;br&gt;
function doPost(e) {&lt;br&gt;
    var data = e.postData.contents;&lt;br&gt;
    var rowValues ​​= [&lt;br&gt;
      [data, ""],&lt;br&gt;
    ];&lt;br&gt;
    var request = {&lt;br&gt;
      'valueInputOption': 'USER_ENTERED',&lt;br&gt;
      'insertDataOption': 'INSERT_ROWS',&lt;br&gt;
      'data': [&lt;br&gt;
        {&lt;br&gt;
          "range": "TEMP!A2:B2",&lt;br&gt;
          "majorDimension": "ROWS",&lt;br&gt;
          "values": rowValues,&lt;br&gt;
        },&lt;br&gt;
      ],&lt;br&gt;
    };&lt;br&gt;
    var response = Sheets.Spreadsheets.Values.batchUpdate(request, SHEETID);&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There are two key points here:&lt;br&gt;
A. We write to the sheet using &lt;em&gt;batchUpdate()&lt;/em&gt;.&lt;br&gt;
B. The &lt;em&gt;'USER_ENTERED'&lt;/em&gt; as it were, tells the trigger that the data on the sheet has been changed not by the program, but by the user.&lt;br&gt;
Everything, now the trigger will work. The data, albeit indirectly - through the sheet, is transferred to the add-in, and the whole circuit functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You can read more about all this here: &lt;a href="https://baublys.ru/telegram-assistant-description-and-manual/" rel="noopener noreferrer"&gt;Telegram Assistant: Description and manual&lt;/a&gt;. There is also a link to the add-on, and to the spreadsheet with the script. You can use all of this freely.&lt;br&gt;
And I will be grateful for constructive criticism and advice!&lt;/p&gt;

</description>
      <category>googleappsscript</category>
      <category>telegram</category>
      <category>googlesheets</category>
      <category>bot</category>
    </item>
  </channel>
</rss>
