<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" version="2.0">

<channel>
	<title>Anton Shevchuk</title>
	<atom:link href="https://anton.shevchuk.name/feed/?announce=1" rel="self" type="application/rss+xml"/>
	<link>https://anton.shevchuk.name</link>
	<description>Staff Solution Architect at NIX</description>
	<lastBuildDate>Sun, 12 Apr 2026 11:34:58 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://anton.shevchuk.name/wp-content/uploads/2020/11/cropped-logo-32x32.png</url>
	<title>Anton Shevchuk</title>
	<link>https://anton.shevchuk.name</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>PHP — живіший за всіх мертвих</title>
		<link>https://anton.shevchuk.name/php/php-still-alive/</link>
					<comments>https://anton.shevchuk.name/php/php-still-alive/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 07:40:29 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4482</guid>

					<description><![CDATA[PHP «ховають» стабільно раз на квартал. Найцікавіше, що роблять це всі, крім самих розробників — ми зазвичай занадто зайняті обговоренням будь-чого іншого, тільки не мови. Та я вирішив додати свої п&#8217;ять копійок у цей нескінченний некролог і пропоную подивитися у майбутнє. Код як нова грамотність Давайте відразу: винесемо за дужки прогнози про те, що скоро &#8230; <a href="https://anton.shevchuk.name/php/php-still-alive/" class="more-link">Continue reading <span class="screen-reader-text">PHP — живіший за всіх мертвих</span></a>]]></description>
										<content:encoded><![CDATA[<p>PHP «ховають» стабільно раз на квартал. Найцікавіше, що роблять це всі, крім самих розробників — ми зазвичай занадто зайняті обговоренням будь-чого іншого, тільки не мови.</p>
<p>Та я вирішив додати свої п&#8217;ять копійок <del datetime="2026-04-11T18:20:23+00:00">у цей нескінченний некролог</del> і пропоную подивитися у майбутнє.</p>
<p><span id="more-4482"></span></p>
<h2>Код як нова грамотність</h2>
<p>Давайте відразу: винесемо за дужки прогнози про те, що скоро код писатиме лише нейромережа, а ми будемо просто вирощувати органічну моркву. Будемо вважати, що програмування — це все ще актуальна навичка, принаймні для того, щоб виступати диригентом для зграї ШІ-агентів. Тож завдання на зараз — навчити цієї грамотності. Яка це буде мова програмування? Один з багатьох варіантів — звісно, PHP.</p>
<blockquote><p>Які ще мови програмування можна розглянути для базової навички програмування? Python, так, але будь ласка, ніякого JavaScript.</p></blockquote>
<h2>PHP no.1</h2>
<p>Це новий маніфест. Я навіть пропоную зробити це офіційним логотипом майбутніх змін.</p>
<p><img fetchpriority="high" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/04/php.n.1-e1775981269325.png" alt="php no.1" width="420" height="165" class="aligncenter size-full wp-image-4483" /></p>
<blockquote><p>
— Ей, ми ж програмісти, відлік починається з нуля!<br />
— Згоден. Тому титул «no.0» ми з повагою залишимо для «C».
</p></blockquote>
<p>А зміни будуть потрібні, саме тому що я не хотів би навчати дітей та студентів чомусь, що виглядає як пошук голки в стозі сіна. Але про це трохи згодом.</p>
<h2>Програмування для всіх</h2>
<p>Головна моя теза — для розвитку будь-якої мови програмування потрібні нові та молоді розробники. Тож далі я буду намагатися розвити думку з приводу того, як можна привабити нових людей до програмування, та до програмування на PHP у нашому випадку.</p>
<p>Перше — треба рухатись у напрямку продавати програмування як базовий скіл будь-якого інженера, а можливо, не лише інженера. Це прям повинно стати метою всього цього руху з навчанням.</p>
<p>Друге — треба звернути увагу на програмування для шкіл, причому комплексно, щоб це не було якесь програмування на рівні циклів та роботи з консоллю. Потрібно навчати з прикладної точки зору, тож розвивати прикладне програмування — це повинно зацікавити дітей. Треба йти не від «ось PHP, на ньому будемо робити ось це», а «хочете зробити сайт — давайте візьмемо найпопулярнішу мову програмування для сайтів». Ось там і HTML, і CSS, і база JavaScript зайде, і робота з базами даних — і так, вже в школі.</p>
<blockquote><p>Чому я так вважаю, що треба змінити підхід і йти від зацікавленості? В мене був негативний досвід співпраці з ВНЗ Харкова, коли ми намагалися розробити програму навчання з PHP, яка включала б і лекції, і практики. Але ми отримали конфлікт з викладачами ВНЗ та витратили свій час даремно, бо «неможна розповідати про HTTP — це програма курсу мережевих протоколів», «ми не розповідаємо про бази даних, то буде окремий курс», «звісно, ми HTML та CSS вже пройшли, але ви не можете…», і так далі.</p></blockquote>
<p>Що треба, щоб стати мовою програмування «no.1» у школах?</p>
<p>Контент повинен бути доступним для школярів:</p>
<ul>
<li>Готові програми навчання, коли викладач може сам легко все зрозуміти та відтворити, бо впровадження — це основна з перепон нових методів навчання.</li>
<li>Готові рішення, які можна скачати чи придбати. Це може бути, наприклад, міні-сервер на Raspberry Pi з розгорнутим LAMP та доступом, щоб можна було програмувати прямо в браузері чи легко деплоїти на сервер (SFTP-конект прямо з IDE).</li>
<li>Цікаво також надати можливість використовувати PHP на Arduino.</li>
<li>Якісь забавки для розумного будинку.</li>
<li>Якісь готові агенти, які можуть зацікавити дітей.</li>
<li>Може, навіть буде потрібен TikTok-канал (знов-таки, в першу чергу це про програмування, в другу — що приклади будуть на PHP).</li>
<li>Уроки на GitHub, щоб школярі могли свій профайл прокачувати теж (але тут питання, чи можуть вони мати свій профіль за правилами GitHub).</li>
<li>IDE для школярів на основі PHPStorm, але прибрати все зайве (можливо за прихованими пунктами меню), спростити максимально, може відразу вбудувати доступ до GitHub-уроків. Вбудувати AI з препромптами, що пояснювати треба школярам.</li>
<li>Переробити нарешті той мануал, щоб він був написаний для людей, щоб батьки дитини, коли сіли розбиратися, могли прочитати та все пояснити дитині.</li>
<li>Мануал повинен мати інтерактивні приклади.</li>
<li>Додати в мануал шлях вивчення мови.</li>
</ul>
<p>І вже після шкіл можна переходити до ВНЗів.</p>
<blockquote><p>Згадав кейс з LeetCode, коли одне з завдань я зробив на PHP та опинився в топ-1 зі своїм рішенням. Та це не про мене і моє рішення, а про те, що ніхто не розв&#8217;язує задачі на PHP на LeetCode :(</p></blockquote>
<h2>PHP X</h2>
<p>Я вже казав, щоб заходити до шкіл та ВНЗів — PHP потрібні зміни.<br />
Скоріш за все, вже є затверджені плани на зміни у PHP 9, але я дивлюся трохи далі.<br />
Цей пункт складний, але він обов&#8217;язково потрібен для успішної реалізації попереднього розділу, тож, можливо, його треба робити навіть раніше.<br />
PHP X — це повинна бути мова, в якій не буде місця $needle та $haystack, asort, strstr, str_replace і так далі. Я маю на увазі — мова повинна бути консистентна, оці всі хвороби неймінгу функцій, класів та методів — їх треба буде залишити в минулому. Не треба дітей навчати цим «особливостям» мови. Цей смітник треба буде розгрібти, і якщо комусь для сумісності потрібна буде нова версія PHP — то хай ставить собі екстеншен для цього, за замовчуванням цього не треба.</p>
<p>Насправді буде багато холіварів. Наприклад, є речі, які я б сам змінив:</p>
<ul>
<li>Конкатенація строк — використовувати «+» було б логічно.</li>
<li>Синтаксис масиву як <code>[ "a" : 42 ]</code>.</li>
<li>Є й інші питання синтаксису.</li>
</ul>
<p>Тож тут головне — створити PHP X саме мовою no.1 для навчання, яка реально буде допомагати, навіть якщо потім будеш переходити на іншу мову програмування.</p>
<p>Можливо, PHP 9 та PHP X будуть співіснувати одночасно та розвиватися разом як дві паралельні гілки.</p>
<h2>Чесно про конкурентів</h2>
<p>Перш ніж говорити про стратегію, варто визнати очевидне: Python уже виграв гонку за місце в освіті. І виграв не через технічну перевагу, а через екосистему навчання — Jupyter Notebooks, Google Colab, інтерактивність «з коробки», тисячі безкоштовних курсів. Коли школяр відкриває Colab, він пише код у браузері через 10 секунд. Коли школяр хоче спробувати PHP — йому треба піднімати сервер.</p>
<p>Це не привід здаватися, це привід розуміти, з чим ми конкуруємо. PHP має свою нішу — веб, і ця ніша величезна. Але щоб зайти в освіту, треба дати порівнянний досвід «від нуля до результату за хвилину». Все, що нижче — це просто програма для мотивованих, а не для всіх.</p>
<h2>Стратегія і тактика</h2>
<p>На що треба звернути увагу:</p>
<h3>Cloud presence</h3>
<p>Було б вагомою перемогою, щоб PHP став однією з мов програмування, доступних для Lambda в AWS. Це прям топ-топ — зв&#8217;язатися з Amazon та спитати, що їм для цього потрібно від PHP. Можливо, не лише з Amazon.</p>
<p>Lambda — це лише частина картини. Cloudflare Workers, Vercel, Fly.io — всі ці платформи зараз формують уявлення про те, які мови «сучасні». PHP там або відсутній, або на задвірках. Cloud presence загалом — це системна задача, не разовий запит до одного вендора.</p>
<h3>Інфраструктура для навчання</h3>
<p>Колаборація з якимось Linux-дістрибутивом для швидкого сетапу лаптопу, готового для навчання стеку PHP (може, до DHH з цим запитом зайти — в нього є <a href="https://omarchy.org/">Omarchy Linux</a>, який нормально працює на слабкому залізі, що ок для навчання).</p>
<h3>Спільнота та амбасадори</h3>
<p>Контент і інструменти — це добре, але хтось має це все нести в школи та ВНЗ. Python зріс в освіті багато в чому завдяки PSF Education Initiative — програмі, де ентузіасти приходили у школи, проводили воркшопи, організовували хакатони. PHP Foundation міг би запустити щось подібне: програму амбасадорів, менторів, які готові витратити кілька годин на місяць на живі заходи. Один воркшоп у школі дає більше, ніж сто банерів на сайтах.</p>
<h3>Сертифікація та портфоліо</h3>
<p>Для школярів і студентів важливо мати щось «відчутне» після навчання. Бейджі, сертифікати, рівні — геймифікація шляху вивчення мови. Це добре лягає до пункту про GitHub-уроки, але заслуговує окремої уваги: програма рівнів на кшталт freeCodeCamp, де кожен етап завершується мікропроєктом, який можна показати. «Я зробив свій перший сайт на PHP» з бейджем у профілі — це мотивація, яка працює.</p>
<h3>Showcase: «Built with PHP»</h3>
<p>Треба банери, що щось розроблено на PHP, намагатися просувати це де можливо, просити авторів відповідних сайтів додати на їхні сайти, навіть без посилань.</p>
<p>Але можна піти далі — зробити окремий ресурс «Built with PHP» з реальними кейсами: від WordPress і Laravel до конкретних стартапів та сервісів. Діти та студенти мотивуються не абстрактною мовою, а тим, що на ній зроблено щось, чим вони користуються щодня. «Цей сайт, яким ти користуєшся — він на PHP» — це працює краще за будь-який маніфест.</p>
<h3>Інтеграція з AI-екосистемою</h3>
<p>Я на початку казав про «диригента для зграї ШІ-агентів» — і ось тут PHP поки програє. Немає офіційного <a href="https://developers.openai.com/api/docs/libraries">SDK OpenAI</a> для PHP, а <a href="https://platform.claude.com/docs/en/api/sdks/php">SDK Anthropic для PHP</a> ніяк не вийде з beta, та має лише <a href="https://github.com/anthropics/anthropic-sdk-php">130+ зірочок на GitHub</a>. Якщо PHP хоче бути мовою «no.1», треба лобіювати появу першокласних AI SDK, або хоча б офіційних прикладів інтеграції. Для навчання це теж критично — дітям цікавіше зробити свого чат-бота чи AI-агента, ніж черговий CRUD. А якщо для цього треба перейти на Python — вони перейдуть і не повернуться.</p>
<h3>Ринки, що зростають</h3>
<p>Арабський та африканський ринок — вони починають зростати, відстають років на 15, та зараз швидко намагаються надолужити все. Тож туди можна принести вже готову програму навчання та підсадити на PHP.</p>
<p>Але «принести програму» — це не просто перекласти README. Це локалізація документації, приклади з місцевим контекстом, партнерство з місцевими освітніми ініціативами. І до речі, якщо вже говоримо про школи в Україні — мануал українською мовою був би логічним першим кроком. Важко продавати PHP як мову «для всіх», якщо мануал існує лише англійською.</p>
<h2>Замість висновку</h2>
<p>Звісно, я тут прописав кроки для розвитку PHP, але хто буде першим — питання досі відкрите. Це не план дій для одного розробника і не wish-list у порожнечу. Це текст для тих, хто приймає рішення — і в першу чергу для PHP Foundation. Саме у них зараз є ресурс, авторитет і можливість перетворити задум у реальність, щоб PHP став no.1.</p>
<p>Та конкуренція велика, а вікно можливостей не безкінечне. Python не чекає, Rust заходить у нові ніші, а нове покоління розробників обирає мову не за потужність ядра, а за перший досвід — і цей досвід формується прямо зараз. Тож не можна зволікати.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/php/php-still-alive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>jQuery для початківців — тепер українською</title>
		<link>https://anton.shevchuk.name/javascript/jquery-for-beginners-ukrainian-version/</link>
					<comments>https://anton.shevchuk.name/javascript/jquery-for-beginners-ukrainian-version/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Fri, 03 Apr 2026 16:09:19 +0000</pubDate>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[jQuery]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4472</guid>

					<description><![CDATA[Так, ви не помилились. 2026 рік, на дворі — React, Vue, Svelte, HTMX, та ще з десяток фреймворків, які з&#8217;явились поки ви читали цей абзац. А я тут з підручником по jQuery. Серйозно. Але давайте подивимось правді в очі: за статистикою W3Techs, jQuery досі використовується на 70% всіх сайтів в інтернеті. Сімдесят. Відсотків. Тобто десь &#8230; <a href="https://anton.shevchuk.name/javascript/jquery-for-beginners-ukrainian-version/" class="more-link">Continue reading <span class="screen-reader-text">jQuery для початківців — тепер українською</span></a>]]></description>
										<content:encoded><![CDATA[<p>Так, ви не помилились. 2026 рік, на дворі — React, Vue, Svelte, HTMX, та ще з десяток фреймворків, які з&#8217;явились поки ви читали цей абзац. А я тут з підручником по jQuery. Серйозно.</p>
<p><span id="more-4472"></span></p>
<p>Але давайте подивимось правді в очі: за статистикою <a href="https://w3techs.com/technologies/details/js-jquery">W3Techs</a>, jQuery досі використовується на <strong>70% всіх сайтів</strong> в інтернеті. Сімдесят. Відсотків. Тобто десь три з чотирьох сайтів, які ви відвідуєте щодня, тягнуть за собою цю «застарілу» бібліотеку. WordPress, Shopify, Bootstrap хоч і не останній, та всі вони досі на jQuery. Ваш улюблений інтернет-банкінг — теж, скоріш за все.</p>
<p>Тож поки розробники сперечаються, яка з наявних LLM зараз рулить у генерації коду, решта світу продовжує тихенько писати <code>$('.button').click()</code> і воно просто працює. Та мені здалося, Claude теж не без гріха.</p>
<h3>Що нового</h3>
<p>Підручник «jQuery для початківців» отримав оновлення до версії 2.0.0:</p>
<ul>
<li><strong>Переклад українською</strong> — повний переклад всіх 77 файлів підручника. Тепер можна вивчати jQuery рідною мовою, а не продиратись крізь російський текст.</li>
<li><strong>Переклад англійською</strong> — для тих, хто хоче поділитись підручником з колегами з-за кордону, або просто звик до англомовної документації.</li>
<li><strong>Виправлено 50+ помилок</strong> — друкарські помилки, баги в прикладах коду, некоректні технічні твердження. Так, навіть у підручнику про jQuery бувають баги. Ніхто не ідеальний.</li>
<li><strong>Оновлено інформацію про jQuery 4.0</strong> — додано детальний список видалених shorthand-методів та застарілих утиліт. Бо jQuery теж розвивається. Хто б міг подумати.</li>
<li><strong>Виправлено приклади коду</strong> — у супутньому репозиторії з прикладами знайдено та виправлено критичні помилки, які не давали коду працювати.</li>
</ul>
<h3>Посилання</h3>
<ul>
<li>Підручник українською: <a href="https://antonshevchuk.gitbook.io/jquery-for-beginners/uk">gitbook — uk</a></li>
<li>Підручник англійською: <a href="https://antonshevchuk.gitbook.io/jquery-for-beginners/en">gitbook — en</a></li>
<li>Підручник російською: <a href="https://antonshevchuk.gitbook.io/jquery-for-beginners/ru">gitbook — ru</a></li>
<li>Приклади коду: <a href="https://anton.shevchuk.name/book/code/">anton.shevchuk.name/book/code</a></li>
<li>Вихідний код прикладів: <a href="https://github.com/AntonShevchuk/jquery-for-beginners-code">GitHub</a></li>
<li>Вихідний код підручника: <a href="https://github.com/AntonShevchuk/jquery-book">GitHub</a></li>
</ul>
<h3>Для кого цей підручник</h3>
<p>Для всіх, хто стикається з jQuery у реальних проєктах. А це, як ми з&#8217;ясували, майже всі. Ви можете скільки завгодно писати у резюме «React, TypeScript, Next.js», але на першому ж проєкті підтримки вас зустріне <code>$(document).ready()</code> та legacy-код, якому років більше, ніж деяким фронтенд-фреймворкам, та часом навіть більше ніж років деяким фронтенд-розробникам.</p>
<hr />
<p><em>jQuery is dead. Long live jQuery.</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/javascript/jquery-for-beginners-ukrainian-version/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Міграція з Talend до Boomi</title>
		<link>https://anton.shevchuk.name/architecture/migration-from-talend-to-boomi/</link>
					<comments>https://anton.shevchuk.name/architecture/migration-from-talend-to-boomi/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Mon, 12 Jan 2026 07:00:21 +0000</pubDate>
				<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Boomi]]></category>
		<category><![CDATA[Talend]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4435</guid>

					<description><![CDATA[У попередній статті я вже згадував, що зараз працюю над міграцією ETL процесів з Talend до Boomi. Тож тепер я можу порівняти ці дві системи: що зручно, що не дуже, та де які особливості. Перед тим як я продовжу, хочу зазначити, що всі мої судження суб’єктивні та можуть відрізнятися від вашого досвіду. Я працював з &#8230; <a href="https://anton.shevchuk.name/architecture/migration-from-talend-to-boomi/" class="more-link">Continue reading <span class="screen-reader-text">Міграція з Talend до Boomi</span></a>]]></description>
										<content:encoded><![CDATA[<p>У <a href="https://anton.shevchuk.name/architecture/enterprise-architecture-introduction-to-boomi/">попередній статті</a> я вже згадував, що зараз працюю над міграцією ETL процесів з Talend до Boomi. Тож тепер я можу порівняти ці дві системи: що зручно, що не дуже, та де які особливості.</p>
<p><span id="more-4435"></span></p>
<blockquote><p>Перед тим як я продовжу, хочу зазначити, що всі мої судження суб’єктивні та можуть відрізнятися від вашого досвіду. Я працював з процесами, які починали розробляти на Talend 10 років тому, та зараз вони можуть здаватися спадщиною «кривавого Enterprise», тож не дивуйтесь, що мій досвід скоріш негативний.</p></blockquote>
<h2>Середовище розробника</h2>
<p>Перше, з чим стикається розробник, — це робоче середовище.</p>
<p>Talend пропонує Talend Studio, яка зібрана на базі Eclipse. Рішення універсальне, та не скажу, що зручне. Можливо, це в мене посттравматичне з минулого досвіду роботи з Eclipse, але мені не зайшло, бо все працювало, м&#8217;яко кажучи, нешвидко:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-studio.png"><img decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-studio.png" alt="Talend Studio Interface" width="1024" height="552" class="aligncenter size-full wp-image-4437" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-studio.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-studio-460x248.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-studio-768x414.png 768w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></p>
<p>Boomi Platform — це сучасне рішення для iPaaS. Для розробки вам достатньо браузера, нічого додатково встановлювати не треба, не треба інструкцій, ліцензій тощо. Все зроблено добре, у мене майже немає нарікань на роботу їхньої платформи.</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-scaled.png"><img decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1024x538.png" alt="Boomi Platform Interface" width="660" height="347" class="aligncenter size-large wp-image-4423" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1024x538.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-460x242.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-768x404.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1536x807.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-2048x1076.png 2048w" sizes="(max-width: 660px) 100vw, 660px" /></a></p>
<h2>Версійність «коду»</h2>
<p>Talend використовує Git «під капотом», і усі процеси ви зберігаєте у репозиторії. Ви маєте можливість віддати процес штучному інтелекту, він розбере його та зможе в подальшому відповідати на питання стосовно того, що та як робить цей процес. Це дуже допомагає, особливо коли відсутні вимоги, та процес міграції перетворюється на такий собі реверс-інженіринг.</p>
<p>Boomi теж має версійність, вона вбудована в платформу. Ви можете швидко перейти до попередньої версії процесу або навіть компонента, можете завантажити XML вашого процесу. Це, звісно, зручно, та ось доступу до репозиторію, як у Talend, ви не маєте. Додайте сюди ще відсутність блокування процесу під час редагування, і потенційно ви матимете клопіт під час одночасного редагування процесів.</p>
<blockquote><p>
⚠️ Обидві системи мають можливість створювати гілки, та моя вам порада: не користуйтесь цією можливістю без нагальної потреби та тримайтесь однієї гілки (або створіть гілку на процес). Інакше розгрібати конфлікти в цих системах — це ще той виклик. Дотримуйтесь простого правила — одночасно над процесом повинен працювати лише один розробник.
</p></blockquote>
<h2>Робота з базами даних</h2>
<p>Обидві системи в мене працювали з MSSQL, обидві системи для цього використовували один і той самий Microsoft JDBC Driver. Для Talend ця можливість їде «з коробки», для Boomi слід було завантажувати драйвер як JAR файл на Runtime.</p>
<p>Та далі я розкажу про особливості, з якими стикнувся вже у Boomi, бо у Talend я бачив реалізацію, та не знаю, які були виклики перед розробниками і чому вони реалізовували те або інше рішення.</p>
<p>Boomi на даний момент має два конектори до баз даних.</p>
<p>Перший — це «Database (Legacy)». Він досить гнучкий та універсальний, та він має одну ваду: він оперує документами, які використовують профіль особливого формату. Вони фактично описують не документ, а саме запит до бази даних. Це незручно, коли ви будуєте процес: вам треба створювати потім окремий профіль для документів та мапінг для перетворення з одного в інший.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile-1024x441.png" alt="Boomi Database Legacy Profile" width="660" height="284" class="aligncenter size-large wp-image-4439" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile-1024x441.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile-460x198.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile-768x331.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile-1536x661.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-profile.png 1766w" sizes="auto, (max-width: 660px) 100vw, 660px" /><br />
<img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-1024x336.png" alt="Boomi Database Legacy Operation" width="660" height="217" class="aligncenter size-large wp-image-4441" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-1024x336.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-460x151.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy-768x252.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-database-legacy.png 1220w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Цю проблему вирішує конектор «Database V2». Він вже працює з універсальним профілем документів у форматі JSON, та і запит до БД перенесли до «операцій», що виглядає більш логічно:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile-1024x535.png" alt="Boomi JSON Profile" width="660" height="345" class="aligncenter size-large wp-image-4442" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile-1024x535.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile-460x240.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile-768x402.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile-1536x803.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-json-profile.png 1886w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Але цей конектор, на мій погляд, вже не такий гнучкий, як попередня версія, та можу навіть сказати, що він трохи «сирий», бо має дитячі вади з валідацією синтаксису запитів.</p>
<p>Також у Boomi є обмеження щодо роботи з temporary tables. Оскільки такі таблиці існують в межах одного з&#8217;єднання (connection), то працювати з ними можна лише в межах одного шейпу процесу. Boomi не гарантує, що два окремих шейпи з викликом до БД будуть відбуватися в межах одного і того ж з&#8217;єднання, а не іншого з пулу конектів.</p>
<p>Та що для мене було важливо — це те, що зрозумілим та знайомим виявився механізм роботи з транзакціями та блоком try-catch, і це майже в кожному процесі доводилось робити:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch-1024x431.png" alt="Boomi Transaction with try-catch" width="660" height="278" class="aligncenter size-large wp-image-4443" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch-1024x431.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch-460x194.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch-768x323.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch-1536x647.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-transaction-with-try-catch.png 1900w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<p>Виглядає це дуже примітивно, та тим не менш працює ефективно та зрозуміло будь-якому розробнику.</p>
<h2>Mapping</h2>
<p>Talend має можливість мульти-мапінгу — це коли фактично ми зливаємо декілька документів в один або перетворюємо в декілька.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapper.png" alt="Talend Mapper" width="512" height="320" class="aligncenter size-full wp-image-4444" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapper.png 512w, https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapper-460x288.png 460w" sizes="auto, (max-width: 512px) 100vw, 512px" /></p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapping.png" alt="Talend Mapping" width="778" height="434" class="aligncenter size-full wp-image-4445" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapping.png 778w, https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapping-460x257.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/talend-mapping-768x428.png 768w" sizes="auto, (max-width: 778px) 100vw, 778px" /></p>
<p><em>Скріни взяті з <a href="https://help.qlik.com/talend/en-US/studio-user-guide/8.0-R2024-05/tmap-operation">офіційного мануалу</a> компанії QlikTech International AB.</em></p>
<p>В Boomi важче злити два документи до одного. Для цього треба додавати мапінг-функцію на всі поля, або робити кастомний скрипт, який буде це робити, та ще, можливо, слід буде городити додатковий кеш до цього всього:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-1024x457.png" alt="Boomi Multi-mapping" width="660" height="295" class="aligncenter size-large wp-image-4446" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-1024x457.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-460x205.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-768x343.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-1536x686.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-multimapping-2048x914.png 2048w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup-1024x412.png" alt="Boomi Mapping with Lookup" width="660" height="266" class="aligncenter size-large wp-image-4447" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup-1024x412.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup-460x185.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup-768x309.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup-1536x617.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-mapping-with-lookup.png 1632w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup-1024x506.png" alt="Boomi Cache Lookup" width="660" height="326" class="aligncenter size-large wp-image-4448" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup-1024x506.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup-460x227.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup-768x379.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup-1536x758.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-cache-lookup.png 1576w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<p>Наче Boomi-варіант менш гнучкий, та реалізація в Talend має великий недолік: подібний мапінг погіршує читабельність (readability) процесу. Додайте до цього відсутність пошуку полів, і перевага Talend дуже швидко перетворюється на суцільний головний біль.</p>
<p>На мою думку, мапінг в Boomi більш зрозумілий, використання функцій для мапінгу дисциплінує та дозволяє зручно перевикористовувати код (я про це ще розповім).</p>
<p>Мій досвід з мапінгом в Talend я б назвав травматичним, бо кожного разу він виглядав для мене як зустріч з Демогоргоном:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/demogorgon-stranger-things-lego-minifigure-460x460.jpg" alt="Demogorgon Lego" width="460" height="460" class="aligncenter size-medium wp-image-4449" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/demogorgon-stranger-things-lego-minifigure-460x460.jpg 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/demogorgon-stranger-things-lego-minifigure-300x300.jpg 300w, https://anton.shevchuk.name/wp-content/uploads/2026/01/demogorgon-stranger-things-lego-minifigure.jpg 600w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<h2>Динамічна конфігурація</h2>
<p>Те, чого концептуально неможливо зробити в Boomi, — це конфігурувати з&#8217;єднання під час виконання процесу. Не можна змінити connection string до бази даних під час виконання процесу; не можна підставити іншу конфігурацію SFTP після запуску. Ці та інші налаштування ви повинні встановлювати для процесу перед початком виконання.</p>
<p>З одного боку, це звісно «хард» обмеження, та це зроблено навмисно, бо фактично це є фундамент для ваших розрахунків з Boomi (ліцензування конекторів). Але з іншого боку, гарно спроєктований процес буде знати заздалегідь всі можливі конекти ще на початку своєї роботи.</p>
<p>Зрештою, процеси, які потребували подібного підходу, були реалізовані, та це потребувало додаткового часу на конфігурацію.</p>
<h2>Перевикористання «коду»</h2>
<p>І Talend, і Boomi дають можливість створювати саб-процеси та використовувати їх у основному процесі.</p>
<p>Можу зазначити, що виклик саб-процесів у Talend зроблено зручніше, бо налаштування запуску конфігуруються безпосередньо у властивостях елемента, який викликає саб-процес. В Boomi для цього доводиться використовувати окремий додатковий елемент «Set Properties»:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-call-sub-process-1024x336.png" alt="Boomi call sub-process" width="660" height="217" class="aligncenter size-large wp-image-4450" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-call-sub-process-1024x336.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-call-sub-process-460x151.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-call-sub-process-768x252.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-call-sub-process.png 1220w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Наче й не складно, та все ж таки варіант, який пропонує Talend, мені здається більш логічним. А варіант від Boomi у випадку виклику саб-процесу декілька разів потребує додаткової уваги, щоб не наробити помилок.</p>
<h2>Кастомний код</h2>
<p>Talend дозволяє вставити Java код майже в кожне поле. Це виглядає гнучко як концепція, але моя робота дуже часто виглядала як оцей мем з розслідуванням:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/investigation-red-string-board-meme.jpg" alt="Red String Board" width="600" height="500" class="aligncenter size-full wp-image-4453" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/investigation-red-string-board-meme.jpg 600w, https://anton.shevchuk.name/wp-content/uploads/2026/01/investigation-red-string-board-meme-460x383.jpg 460w" sizes="auto, (max-width: 600px) 100vw, 600px" /></p>
<p>Також в Talend є можливість створювати та додавати кастомні Java бібліотеки, щоб потім використовувати їх. Угу, викликати Java-бібліотеку можна майже з будь-якого поля, тут вже інший мем…</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/hide-the-pain-harold.jpg" alt="Hide the pain Harold" width="750" height="500" class="aligncenter size-full wp-image-4454" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/hide-the-pain-harold.jpg 750w, https://anton.shevchuk.name/wp-content/uploads/2026/01/hide-the-pain-harold-460x307.jpg 460w" sizes="auto, (max-width: 750px) 100vw, 750px" /></p>
<p>При роботі з Boomi ви теж можете використовувати кастомні Java-бібліотеки, такі як iText чи jsoup, або написати свою власну. Але розробляти та компілювати ви будете поза Boomi, а на платформу завантажуватимете вже готові JAR-файли.</p>
<p>Використання кастомного коду в Boomi обмежено Process Script та Map Script, їх завжди легко знайти серед компонентів:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-search-377x460.png" alt="Boomi Component Search" width="377" height="460" class="aligncenter size-medium wp-image-4451" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-search-377x460.png 377w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-search.png 738w" sizes="auto, (max-width: 377px) 100vw, 377px" /></p>
<p>Для використання кастомних бібліотек ви все одно будете використовувати JavaScript або Groovy. Як я зазначав у попередній статті, в мене виникають питання лише з приводу версій, які підтримує Boomi: код слід писати як 10 років тому, наче й не проблема, та все одно трохи дратує.</p>
<h2>Переваги Boomi</h2>
<p>Є у Boomi один великий плюс — це можливість перевикористовувати майже усі компоненти: екшени, профілі, мапінги та скрипти без того, щоб оформляти їх як Java-бібліотеки. Це прям сильно спрощує життя розробникам. Головне — оці загальні компоненти оформлювати, документувати та шерити знання про них серед команди.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-common.png" alt="Boomi common components" width="456" height="572" class="aligncenter size-full wp-image-4452" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-common.png 456w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-common-367x460.png 367w" sizes="auto, (max-width: 456px) 100vw, 456px" /></p>
<p>Як я вже зазначив вище, реалізація транзакцій та робота з try-catch реалізована зручно та передбачувано.</p>
<p>Були й інші моменти, коли я радів функціоналу Boomi, якого не було в Talend (чи, можливо, розробники просто не використовували той функціонал, бо він з’явився пізніше за створення процесу).</p>
<p>Як приклад — PGP Encrypt/Decrypt. В Boomi ви завантажуєте сертифікат через інтерфейс та використовуєте його в Data Process шейпі:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-data-process.png" alt="Boomi Data Process" width="508" height="720" class="aligncenter size-full wp-image-4456" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-data-process.png 508w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-data-process-325x460.png 325w" sizes="auto, (max-width: 508px) 100vw, 508px" /></p>
<p>Як бачите, є робота з Base64 через UI, а не з коду; те саме з Zip архівацією.</p>
<p>Дуже просунуте логування, яке дозволяє легко дебажити процеси, аналізувати перформанс та знаходити проблемні місця:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/process-state-details.png" alt="Boomi Process State Details" width="582" height="392" class="aligncenter size-full wp-image-4457" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/process-state-details.png 582w, https://anton.shevchuk.name/wp-content/uploads/2026/01/process-state-details-460x310.png 460w" sizes="auto, (max-width: 582px) 100vw, 582px" /></p>
<p>Додам, що Boomi має ще одну велику перевагу — він має вбудований функціонал з AI, який вміє документувати процеси (і це не єдине, що він вміє). Робить він це, не сказати, що прям добре — я б оцінив на 3 з 5 зірочок. Мені особисто не вистачає можливості, щоб AI розумів саб-процеси та переходив по ним.</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-ai-documentation.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-ai-documentation-1024x867.png" alt="Boomi AI Documentation" width="660" height="559" class="aligncenter size-large wp-image-4459" /></a></p>
<p>Можу відзначити також значний прогрес їхнього AI для побудови процесів: він стає розумнішим, та, на мою думку, йому ще зарано будувати процеси самостійно. Можливо, за пів року або рік це стане дійсно корисним інструментом.</p>
<h2>Недоліки Boomi</h2>
<p>Головний недолік, на який я вже скаржився в попередній статті, — це те, що логування, яке я нахвалював раніше, дуже сильно впливає на швидкодію. Вимкнути його для процесів, які виконуються за розкладом, неможливо, і як результат — різниця в часі виконання між Talend та Boomi одного і того ж процесу у 3-4 рази.</p>
<p>Окремо існує проблема: коли у вас велика кількість документів (мільйони), то не варто намагатися їх порахувати в Boomi, це сильно впливає на швидкодію. Ось так не робіть:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count-1024x380.png" alt="Boomi document count" width="660" height="245" class="aligncenter size-large wp-image-4458" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count-1024x380.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count-460x171.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count-768x285.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-document-count.png 1202w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<h2>Що можна покращити в Boomi?</h2>
<blockquote><p>Це нотатка скоріш для розробників Boomi, і сподіваюсь вона дістанеться свого адресата.</p></blockquote>
<p>Робота з Dynamic Process Properties (DPP) та Dynamic Document Properties (DDP) вимагає постійно «пам’ятати» назви цих Properties. Використовуєш їх постійно, доводиться копіювати назви, а це незручно і викликає помилки. Було б добре, щоб система відслідковувала DPP та DDP під час розробки та давала можливість обирати з попередньо створених. Також під час виконання процесу у Test Mode хотілось би мати можливість бачити, що у тих Properties відбувається, щоб не робити це за допомогою окремих елементів.</p>
<p>Роботу з профілями документів теж можна покращити — коли перший елемент процесу повертає якийсь профіль, та ми додаємо наступний елемент, такий як Map, то було б зручно, якби він автоматично розумів контекст та брав відповідний профіль. Наразі система не підказує, який профіль обрати, що призводить до помилок, які доволі часто трапляються під час розробки, хоча і діагностуються теж легко.</p>
<h2>Що треба знати? Або чому Low-Code не означає No-Knowledge</h2>
<p>Якщо ви думаєте, що робота з Boomi — це лише перетягування іконок, мушу вас розчарувати. Щоб будувати серйозні рішення, вам знадобиться комплексний технічний бекграунд, і ось чому.</p>
<p>Почнемо з програмування. Так, Boomi дозволяє багато зробити візуально, але як тільки логіка стає нестандартною, вам доведеться писати скрипти. Тут панує JavaScript, але є нюанс: це старий добрий ECMAScript 5, тому забудьте про модні фішки сучасного JS. А оскільки платформа працює на Java-машині, то знання Java або Groovy стає вашим секретним козирем для вирішення задач, які не під силу звичайному JS. Ну і, звісно, розуміння структур даних та вміння писати тести ніхто не скасовував.</p>
<p>Далі — робота з даними, адже інтеграція — це, по суті, перекладання даних з однієї кишені в іншу. Ви маєте вільно почуватися з базами даних, і мова не про прості SELECT-запити. Реальні кейси вимагають роботи зі збереженими процедурами (Stored Procedures) та транзакціями, особливо в MSSQL. Крім того, вашими постійними супутниками стануть JSON та XML. І якщо JSON — це стандарт сучасного світу, то XML та вміння писати для нього XSLT-трансформації — це сувора реальність Enterprise-систем, яку треба прийняти.</p>
<p>Щодо зв&#8217;язку систем, то тут ви маєте бути поліглотом. REST API — це ваша база: ви повинні розуміти, як працюють HTTP-методи та авторизація. Але Enterprise не був би Enterprise&#8217;ом без SOAP-сервісів, тож доведеться навчитися «дружити» і з ними. А для побудови сучасних асинхронних архітектур вам знадобиться досвід роботи з чергами повідомлень, зокрема з Apache Kafka, щоб ваші системи могли обмінюватися даними миттєво і надійно.</p>
<p>І наостанок — інфраструктура та безпека. Ваш процес не висить у вакуумі. Ви повинні розуміти хмарне середовище: що таке балансувальники навантаження, як працює автоскейлінг та як моніторити здоров&#8217;я системи. І, звісно, безпека: розібратися в OAuth, SSL сертифікатах та інших методах автентифікації — це не опція, а необхідність, щоб ваші інтеграції не стали діркою в безпеці компанії.</p>
<h2>Boomi. Такий шлях</h2>
<p>Ця стаття не претендує на хайповий заголовок Boomi VS Talend, бо в першу чергу я хотів поділитися своїм досвідом та мав за ціль підкреслити один факт: міграція з Talend до Boomi можлива. Та чи потрібна вона вам і вашій компанії? Ось тут я не зможу відповісти, я лише намагався трошки надати вам аргументів, якщо перед вами постане таке питання. Для однозначної відповіді вам треба зважити дуже багато факторів, серед яких буде і ціна рішення, і ціна міграції, і ціна навчання нової системи.</p>
<p>З останнім я вам підкажу трохи. На мою думку, щоб навчитися працювати з Boomi, слід орієнтуватися на 1 місяць інтенсивного навчання. Перші два тижні потрібні, щоб пройти <a href="https://train.boomi.com/path/integration-developer">Integration Developer Path</a> та декілька <a href="https://community.boomi.com/s/developer-techniques">супутніх топіків</a>. Після цього можна буде починати брати перші прості інтеграційні процеси до роботи. Наступні 2 тижні — це буде занурення до прикладів реалізації, до мануалів, напрацювання best practice, та в залежності від ваших вимог та потреб можливо слід буде пройти курс <a href="https://train.boomi.com/associate-event-streams">Associate Event Streams</a> та <a href="https://train.boomi.com/professional-api-design">Professional API Design</a>. Загалом, за місяць перекваліфікуватися до System Integration Engineer можливо, але за умови, що вже є базові знання.</p>
<h2>P.S.</h2>
<p>Якщо ви теж проходили цей шлях міграції або маєте свої «милиці» та лайфхаки для Boomi — діліться в коментарях, цікаво почути про ваш досвід. І окремо буду щиро вдячний, якщо хтось підкаже елегантний спосіб обійти проблему з гальмуванням логів. Бо поки що це виглядає як вибір без вибору, і мене це гризе.</p>
<h2>P.P.S.</h2>
<p>Під час міграції жоден Talend-розробник не постраждав…</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/architecture/migration-from-talend-to-boomi/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Enterprise-архітектура та Boomi: Гід для початківців</title>
		<link>https://anton.shevchuk.name/architecture/enterprise-architecture-introduction-to-boomi/</link>
					<comments>https://anton.shevchuk.name/architecture/enterprise-architecture-introduction-to-boomi/#comments</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Mon, 05 Jan 2026 07:00:44 +0000</pubDate>
				<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Boomi]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4419</guid>

					<description><![CDATA[З лютого 2025-го року почалось моє знайомство з Boomi платформою, та що воно таке, навіщо створено, та чи дійсно цей інструмент вам може стати у нагоді, на ці питання я спробую відповісти у цій «коротенькій» оглядовій статті. Та перед тим як перейти безпосередньо до історії Boomi, я б хотів почати свою розповідь з передумов появи &#8230; <a href="https://anton.shevchuk.name/architecture/enterprise-architecture-introduction-to-boomi/" class="more-link">Continue reading <span class="screen-reader-text">Enterprise-архітектура та Boomi: Гід для початківців</span></a>]]></description>
										<content:encoded><![CDATA[<p>З лютого 2025-го року почалось моє знайомство з Boomi платформою, та що воно таке, навіщо створено, та чи дійсно цей інструмент вам може стати у нагоді, на ці питання я спробую відповісти у цій «коротенькій» оглядовій статті.</p>
<p><span id="more-4419"></span></p>
<p>Та перед тим як перейти безпосередньо до історії Boomi, я б хотів почати свою розповідь з передумов появи такої системи, та можливе це розставить все відразу на свої місця, і все стане зрозуміло без додаткових пояснень.</p>
<blockquote><p>Ця стаття написана для техлідів, архітекторів та менеджерів Enterprise-проєктів. Вона містить багато спрощень, та я гадаю, це допоможе у розумінні матеріалу. У будь-якому разі, я буду радий почути фідбек від кожного.</p></blockquote>
<h2>Prequel «Enterprise»</h2>
<p>Рано чи пізно, але більшість IT спеціалістів стикається з розробкою різноманітних систем для Enterprise, це може бути як внутрішня розробка компанії, так і розробка для замовника. Потрапивши сюди, ви не залишитесь таким як були раніше, можливо вам буде подобатися робота в такому середовище, можливо ні, але з часом ви точно не пожалкуєте про свій досвід. </p>
<p><em>— Що то таке Enterprise, та як він з&#8217;являється?</em></p>
<p>Enterprise — це комплексна система яка забезпечує роботу критичних для бізнесу процесів, які проходять крізь різні підрозділи компанії, та можуть поєднувати в собі багато окремих підсистем.</p>
<p><em>— Тож виглядає так, що якщо в вас маленька outsource компанія на 10 людей, яка користується одночасно CRM/HRM/Jira/GitHub/etc то вже є Enterprise?</em></p>
<p>І так, і ні.</p>
<p>Тут важлива різниця в підході до процесів:</p>
<p>— Якщо ваші процеси не автоматизовані та здебільшого виглядають як чек-листи для людей. Схоже, ваші процеси ще не зрілі та мають «поспіти».</p>
<p>— Якщо подія у компанії (наприклад, вихід нового співробітника) автоматично запускає декілька паралельних процесів та організовує взаємодію людей і систем — оце вже ознака Enterprise.</p>
<p>Enterprise-системи значною мірою складаються саме з таких процесів — з багатьох таких процесів.</p>
<p>Скільки процесів має бути, щоб впевнено сказати: «А ось це Enterprise»? Ніхто не дасть точної відповіді — можливо 100, можливо 1000. Але рано чи пізно зростаюча компанія подолає цей бар&#8217;єр і постане перед новими викликами.</p>
<p>Як це буде відбуватися? Тут може бути декілька шляхів:</p>
<ul class="dashed">
<li>Еволюційний</li>
<li>Революційний</li>
<li>Або мікс обох у різній пропорції.</li>
</ul>
<h3>Enterprise Еволюція</h3>
<p>Це класична історія успішного стартапу або маленької компанії. Зміни відбуваються поступово, «під тиском» нових вимог, коли старі рішення перестають працювати.</p>
<p>Давайте розглянемо декілька прикладів.</p>
<h4>Етап I. MVP що затягнувся</h4>
<p>Компанія має один або декілька ключових сервісів, навколо яких зав&#8217;язаний увесь бізнес. Можливо, є й інші сервіси (наприклад, за підпискою), та вони майже не пов&#8217;язані один з одним.</p>
<p>Взаємодія здебільшого відбувається через комунікацію людей, які користуються різними системами (&#8220;Human middleware&#8221;).</p>
<p>З часом бізнес зростає, координація забирає забагато часу. Компанія змушена наймати людей, які фактично займаються лише супроводом цих процесів.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP.png" alt="" width="600" height="460" class="aligncenter size-full wp-image-4421" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP.png 600w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP-460x353.png 460w" sizes="auto, (max-width: 600px) 100vw, 600px" /></p>
<h4>Етап II. Зоопарк сервісів та Point-to-Point</h4>
<p>Компанія зростає, з&#8217;являється відповідальний за інфраструктуру. Він намагається зібрати команду з різних відділів, але наштовхується на вічну проблему — у всіх &#8220;немає часу&#8221;. </p>
<p>Сценаріїв розвитку безліч: від розпилу монолітів на мікросервіси та купівлі SaaS (Salesforce, Zendesk) до повного хаосу, коли все це впроваджується одночасно й неконтрольовано.</p>
<p>Результатом такого хаосу стає Point-to-Point взаємодія. Система перетворюється на клубок зв&#8217;язків: сервіси тісно зчіплені (tight coupling) та знають забагато один про одного. Як наслідок — підтримка ускладнюється, а вартість будь-яких змін зростає в геометричній прогресії.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Point-to-Point.png" alt="" width="600" height="480" class="aligncenter size-full wp-image-4422" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Point-to-Point.png 600w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Point-to-Point-460x368.png 460w" sizes="auto, (max-width: 600px) 100vw, 600px" /></p>
<h4>Етап III. Middleware зрілість</h4>
<p>З часом стає зрозумілим, що одна людина вже фізично не спроможна встигати за розвитком системи, тому компанія формує окремий підрозділ, який займатиметься інфраструктурою.</p>
<p>Хтось із нового відділу бере на себе роль Enterprise Architect&#8217;а та доходить висновку, що подальше зростання та зміни потребують впровадження middleware. Що зміниться:</p>
<ul class="dashed">
<li>Розробників познайомлять з Enterprise Service Bus</li>
<li>З&#8217;явиться поняття Canonical Data Model (єдиний формат даних для всієї компанії)</li>
<li>Заборонять прямі виклики сервісів один до одного, усі будуть спілкуватися через брокер/шину</li>
<li>Застосують патерн Strangler Fig (фікус-душитель): старий легасі-код поступово замінють новими сервісами, не вимикаючи систему.</li>
</ul>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB.png" alt="" width="800" height="480" class="aligncenter size-full wp-image-4429" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB.png 800w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-460x276.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-768x461.png 768w" sizes="auto, (max-width: 800px) 100vw, 800px" /></p>
<p>Як альтернативу або доповнення починають розглядати Integration Platform (однією з яких є Boomi, але про неї згодом).</p>
<p><strong>Еволюція — це про рефакторинг та стандартизацію.</strong> Це про поступове введення правил гри (API-контракти, події), які зменшують ентропію (хаос).</p>
<h3>Enterprise Революція</h3>
<p>Це стрес-тест для архітектури. Це відбувається, коли велика компанія купує іншу або коли два гіганти зливаються (M&amp;A). Тут немає часу на повільний рефакторинг — бізнес вимагає результату «на вчора».</p>
<p>Тут немає етапів — тут є виклики, які треба вирішувати негайно.</p>
<p>Уявіть ситуацію: Банк А купує Фінтех Б. Перед Enterprise Architect постає ряд завдань.</p>
<h4>Виклик 1. Дублювання функцій</h4>
<p>Ситуація:</p>
<ul class="dashed">
<li>У Банку А є своя CRM («Самописна, стара, але надійна»).</li>
<li>У Фінтеха Б є своя CRM («Модна, у хмарі»).</li>
</ul>
<p>CTO приймає вольове рішення — «Rip and Replace». Протягом 6 місяців Фінтех Б зобов&#8217;язаний викинути свою систему і мігрувати в CRM Банку (чи були сумніви, хто залишиться?).</p>
<p>Що буде відбуватися далі? Зануритись у пекло міграційних скриптів, мапінгу полів і втрати даних? Ні. Тут критично потрібні ETL-інструменти (Extract, Transform, Load), щоб коректно «перекачати» дані з однієї структури в іншу.</p>
<h4>Виклик другий. Ізоляція даних (Data Silos)</h4>
<p>Ситуація:</p>
<ul class="dashed">
<li>Ви маєте інформацію про клієнтів в двох різних системах, вони між собою не зв&#8217;язані, та не сінхронізуються.</li>
</ul>
<p>Наслідки: </p>
<ul class="dashed">
<li>Суперечливість даних (Inconsistency), дублювання зусиль та некоректна аналітика.</li>
</ul>
<p>Що робити? Тут слід впровадити MDM (Master Data Management). Це технологія, процеси та політики, що дозволяють створити «Золотий запис» (Golden Record) та отримати Єдине Джерело Істини (Single Source of Truth).</p>
<h4>Виклик третій. «Федерація»</h4>
<p>Що відбувається:</p>
<ul class="dashed">
<li>Іноді об&#8217;єднати системи неможливо швидко, але треба забезпечити їх одночасну роботу вже зараз.</li>
</ul>
<p>Що буде відбуватися далі? Ми будуємо «Фасад» — спільний API, який приймає запит, а всередині «під капотом» вирішує, куди його направити: в стару систему Банку чи нову систему Фінтеху. Для користувача це виглядає як одна система, хоча всередині це «Франкенштейн».</p>
<p><strong>Революція — це про інтеграцію та міграцію.</strong> Це про поєднання чужорідних систем, про адаптери, шини даних та жорсткі адміністративні рішення («ми вимикаємо цей сервер 1-го числа»).</p>
<h2>Boomi. Пролог</h2>
<p>Настав час розповісти безпосередньо про Boomi.</p>
<p>Boomi — це iPaaS (Integration Platform as a Service). Інструмент для реалізації бекенд-логіки без хардкорного кодингу (тут сумний смайлик).</p>
<p>Архітектурно Boomi складається з двох частин:</p>
<p><strong>1. Boomi Platform (The Build)</strong></p>
<p>Це безпосередньо &#8220;Visual IDE&#8221; у вашому браузері, де ми розробляємо та налаштовуємо процеси. Замість написання функцій ми перетягуємо іконки:</p>
<ul class="dashed">
<li>HTTP Client Connector</li>
<li>Map Shape</li>
<li>Database Connector</li>
<li>та інші…</li>
</ul>
<p>Ми поєднуємо їх стрілочками у єдиний flow. </p>
<p>Тут також відбуваються адміністративні дії: перегляд логів, керування налаштуваннями, формування розкладів запусків.</p>
<p>Все це працює у хмарі — швидко та передбачувано. Хіба що автоматичне вирівнювання на складних процесах може викликати нарікання, а так все достатньо «гладенько»:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1024x538.png" alt="" width="660" height="347" class="aligncenter size-large wp-image-4423" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1024x538.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-460x242.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-768x404.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-1536x807.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/boomi-platfotm-2048x1076.png 2048w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p><strong>2. The Runtime (The Execution)</strong><br />
Друга складова — це Runtime. Фактично, це Java-додаток (JVM), який виконує той «код», що згенерувала платформа.</p>
<p>Як це працює: Коли ви натискаєте &#8220;Deploy&#8221; на платформі, Boomi компілює ваш візуальний процес у пакет і відправляє його на Runtime для виконання.</p>
<p>Важливий момент: Існує декілька типів Runtime:</p>
<ul class="dashed">
<li>Basic Runtime</li>
<li>Runtime Cluster</li>
<li>Runtime Cloud (Private або Public)</li>
</ul>
<p>Чим вони відрізняються, коли та що обирати я про це ще розповім.</p>
<h2>Boomi. Епізод I</h2>
<p>Надалі я буду намагатися йти «шляхом» розвитку Enterprise та розповім де та як можна застосувати Boomi, та який Runtime краще обрати.</p>
<p>Почнемо з першого сценарію, коли нам треба зв&#8217;язати між собою сервіси які живуть своє життя:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP.png" alt="" width="600" height="460" class="aligncenter size-full wp-image-4421" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP.png 600w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-MVP-460x353.png 460w" sizes="auto, (max-width: 600px) 100vw, 600px" /></p>
<p>Уявимо, що це маленька компанія, яка використовує сервіси за підпискою, та ці сервіси не вміють в інтеграцію один з одним. Процеси раніше підтримували люди: там статус на борді змінити, десь файли завантажити, комусь рахунок виписати.</p>
<p>В такому випадку нам підійде Boomi Runtime Cloud &#8211; це хмарний runtime, який хоститься і керується самою компанією Boomi, це мінімум додаткових зусиль, лише розробити процес, залити та користуватися:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cloud.png" alt="" width="1024" height="640" class="aligncenter size-full wp-image-4425" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cloud.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cloud-460x288.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cloud-768x480.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></p>
<p>Реалтайм нам не потрібен, тож для кожного процеса можемо створити свій розклад для запуску. Процеси будуть бігати Point-to-Point між сервісами. Подібну схему можно достатньо швидко реалізувати, бо Boomi підтримує більше 1000 конекторів з коробки — <a href="https://boomi.com/connectors/">https://boomi.com/connectors/</a> (та треба пам&#8217;ятати, що платити треба буде саме за підключені конектори):</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.Connectors.png" alt="" width="1024" height="800" class="aligncenter size-full wp-image-4426" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.Connectors.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.Connectors-460x359.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.Connectors-768x600.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></p>
<p>Фактично для цього вигаданого сценарію, це виглядає як сервіс IFTTT на максималках.</p>
<p>Давайте розглянемо альтернативну компанію, яка має сервіси у себе в локальній мережі, чи VPC, у такому випадку Boomi Runtime Cloud не буде мати можливість співпрацювати з сервісами компанії, варіант налаштувати firewall, щоб Boomi міг стукатись до всіх внутрішніх сервісів виглядає не секьюрно. Тож ми підемо іншим шляхом, і будемо піднімати свій Runtime вже за firewall&#8217;ом:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Basic-Runtime.png" alt="" width="1024" height="640" class="aligncenter size-full wp-image-4424" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Basic-Runtime.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Basic-Runtime-460x288.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Basic-Runtime-768x480.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></p>
<p>З точки зору розробки процесів, тут не буде ніяких змін, та будуть зміни в деплойменті, та відповідальність за інфраструктуру буде лягати вже на плечі DevOps&#8217;ів, тепер це їх клопіт як інстанц підняти та як його моніторити. Про те який інстанц потрібно обрати можна дізнатися з навчальних матеріалів, приведу тут лише табличку щоб було уявлення про вимоги:</p>
<table style="font-size:14px">
<tr>
<th width="70">Type</th>
<th>Transactions Per Day</th>
<th>Size (cpu × mem × disk)</th>
<th width="100">AWS</th>
<th width="135">Azure</th>
</tr>
<tr>
<td>Small</td>
<td>&lt;500k</td>
<td>4×16×100</td>
<td>m5.xlarge</td>
<td>Standard_D4s_v4</td>
</tr>
<tr>
<td>Medium</td>
<td>500k &#8211; 1m</td>
<td>8×32×200</td>
<td>m5.2xlarge</td>
<td>Standard_D8s_v4</td>
</tr>
<tr>
<td>Large</td>
<td>1m+</td>
<td>16×64×200</td>
<td>m5.4xlarge</td>
<td>Standard_D16s_v4</td>
</tr>
</table>
<blockquote><p>Якщо чесно, що саме вклали в цю «кількість транзакцій на день», мені важко сказати, будемо вважати, що одна «транзакція», це один простий процесс, який обробляє один документ &lt;100Кб.</p></blockquote>
<h2>Boomi. Епізод II</h2>
<p>Поїхали далі, наш наступний приклад, це Enterprise з зоопарком Point-to-Point сервісів.<br />
Ми не будемо прибирати цей безлад, ми опановуємо його. В цьому вигаданому сценарії ми будемо переносити процеси до Boomi, та оскільки процесів багато і вони життєво важливі для роботи компанії то нам слід буде забезпечити безвідмовну роботу нашої системи під постійним навантаженням. У Boomi для цього передбачено використання Runtime Cluster, який забезпечить як high availability так і scalability системи за рахунок часу вашої DevOps команди та можливостей Cloud провайдерів:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cluster.png" alt="" width="1024" height="640" class="aligncenter size-full wp-image-4427" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cluster.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cluster-460x288.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-Boomi-Runtime-Cluster-768x480.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></p>
<p>У кластері Boomi є ряд особливістей.<br />
Перша особливість, це використання Shared File System, усі ноди монтують папку, яка відповідає за зберігання конфігів, логів та черг повідомлень, все лежить в одному місці. </p>
<blockquote><p>Будь ласка обережно використовуйте Shared File System для зберігання тимчасових файлів якщо вам такі будуть потрібні під час виконання процесу, це може мати негативний вплив на швидкість роботи таких процесів.</p></blockquote>
<p>Друга особливість, про яку слід пам&#8217;ятати, це те, що кількість нод в нас обмежена через Network Chatter, рекомендуєме обмеження, це 10 нод. Якщо цього вам не вистачить, то слід будувати новий кластер, і тоді треба буде розподіляти процеси по цим кластерам, що додасть клопоту усім.</p>
<p>Якщо що, то я трохи погуглив, та ось вам декілька статей, про сетап Cluster&#8217;а:</p>
<ul class="dashed">
<li><a href="https://community.boomi.com/s/article/High-Availability-AWS-Integration-Runtime">High Availability AWS Integration Runtime</a></li>
<li><a href="https://community.boomi.com/s/article/integration-runtime-installation-molecule-on-aws-ecs">Integration Runtime Installation &#8211; runtime cluster on AWS ECS</a></li>
<li><a href="https://community.boomi.com/s/article/Molecule-Kubernetes-Azure-Quick-Start">Runtime cluster Kubernetes Azure Quick Start</a></li>
</ul>
<blockquote><p>Ще пів року назад, Boomi використовували неймінг Atom (Runtime) та Molecula (Cluster), тож час від часу ви будете зустрічати цю термінологію у документації.</p></blockquote>
<p>Інформація по рекомендованому розміру кластера також є в навчальних матеріалах:</p>
<table style="font-size:14px">
<tr>
<th width="70">Type</th>
<th>Transactions Per Day</th>
<th width="70">Nodes</th>
<th>Size (cpu × mem × disk)</th>
<th width="100">File Share</th>
</tr>
<tr>
<td>Small</td>
<td>&lt;500k &amp;<br />&lt;1 GB/batch</td>
<td>3</td>
<td>4×16×100</td>
<td>200 GB<br />50 MiBs</td>
</tr>
<tr>
<td>Medium</td>
<td>500k &#8211; 1m &amp;<br />1-2 GB/batch</td>
<td>3-5</td>
<td>8×32×200</td>
<td>500 GB<br />50-150 MiBs</td>
</tr>
<tr>
<td>Large</td>
<td>1m+ &amp;<br />2 GB/batch</td>
<td>5-10</td>
<td>16×64×200</td>
<td>1 TB<br />150+ MiBs</td>
</tr>
</table>
<table style="font-size:14px">
<tr>
<th width="135">Type</th>
<th width="135">AWS</th>
<th>Real-Time Optimized AWS</th>
<th width="135">Azure</th>
</tr>
<tr>
<td>Small</td>
<td>m5.xlarge</td>
<td>c5.2xlarge</td>
<td>Standard_D4s_v4</td>
</tr>
<tr>
<td>Medium</td>
<td>m5.2xlarge</td>
<td>c5.4xlarge</td>
<td>Standard_D8s_v4</td>
</tr>
<tr>
<td>Large</td>
<td>m5.4xlarge</td>
<td>c5.4xlarge</td>
<td>Standard_D16s_v4</td>
</tr>
</table>
<h2>Boomi. Епізод III</h2>
<p>Настав час розповісти про організацію EDA (Event Driven Architecture) на базі Boomi.<br />
Коли ваша Enterprise система починає використовувати шину для обміну повідомленнями ваша робота концентрується на розробці адаптерів для сервісів:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-Adapter.png" alt="" width="640" height="400" class="aligncenter size-full wp-image-4428" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-Adapter.png 640w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-Adapter-460x288.png 460w" sizes="auto, (max-width: 640px) 100vw, 640px" /></p>
<p>Саме такі адаптери будуть відповідати за бізнес-логіку, яка зв&#8217;язує шину подій та API сервісів. І звісно реалізацію таких сервісів ви можете покласти на Boomi:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-with-iPaaS.png" alt="" width="1024" height="640" class="aligncenter size-full wp-image-4430" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-with-iPaaS.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-with-iPaaS-460x288.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Enterprise.-ESB-with-iPaaS-768x480.png 768w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></p>
<p>А ось з приводу яку шину вибрати якщо в вас її ще немає, то тут є декілька опцій.</p>
<p>Перша — використовувати вбудований до Runtime механізм черг &#8211; Boomi Atom Queue. Він живе на вашому Runtime або Runtime Cluster. Ця шина має дуже вагоме обмеження — взаємодіяти з нею можна лише з Boomi, тож ніякі зовнішні сервіси не будуть мати доступ до черги без участі Boomi. Ну і використання файлової системи (мережової для кластеру) для зберігання теж накладає обмеження на швидкодію, та і надійнійсність, це буде ваш клопіт. Моніторинг такої черги вкрай обмежений.</p>
<blockquote><p>Boomi Atom Queue вже кличуть Legacy, та все рівно будуть підтримувати</p></blockquote>
<p>Друга опція — Boomi Event Streams, це вже окремий SaaS сервіс від Boomi. За інфраструктуру відповідає Boomi. За допомоги API до такої шини вже можна звертатися ззовні та відправляти повідомлення до черги, а ось слухати зась. Для моніторінгу є дашборд в інтерфейсі Boomi Platform. Тут вже сплачуємо гроші за об&#8217;єм даних.</p>
<p>Третя опція — використовувати Kafka чи RabbitMQ, чи будь який інший брокер повідомлень. Ніяких обмежень на використання, та вся відповідальність за роботу лягає вже на вас, надійність та моніторинг &#8211; все це ваша зона відповідальності. З мого досвіду, Boomi чудово працює з Kafka, та звісно у такому випадку конектор до зовнішньої системи буде враховуватися до вашого чеку за Boomi.</p>
<h2>Boomi. Епізод IV</h2>
<p>Перейдемо до революційних змін Enterprise. Як ми можемо використати Boomi, коли часу в нас обмаль, а від нас вимагають результату «на вчора»?</p>
<p>Boomi спроєктовано таким чином, щоб ви як найшвидше пройшли шлях від вимог, розробки, та тестування та отримали результат на production.</p>
<p>І якщо буде потрібно швидко розробити ETL (Extract, Transform, Load) процес, то Boomi вам підійде, бо має багато вбудованих інструментів для його організації, а якщо їх вам не вистачить, то тоді вже можна і код написати для обробки документів. Boomi підтримує як JavaScript так і Groovy.</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-scaled.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-1024x238.png" alt="" width="660" height="153" class="aligncenter size-large wp-image-4431" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-1024x238.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-460x107.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-768x179.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-1536x357.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.ETL_-2048x476.png 2048w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<p>Попередній параграф був як з рекламного буклету? Ну добре, перейдемо до специфіки системи. </p>
<p>Почнемо з коду який ми можемо додавати до процесів.</p>
<p>Перша доступна опція, це може бути JavaScript, та підтримується лише ECMAScript 5, чому? Бо там не NodeJs виконує JavaScript, це робить Java машина, вона має свій власний рушій для виконання JavaScript. З плюсів &#8211; ви можете викликати вбудовані Java-класи, та навіть більше &#8211; ви можете завантажити JAR файл до вашого Runtime, та викликати його.</p>
<p>Друга опція, це Groovy, ми можемо використовувати версію 1.5 або 2.4. Я звісно не Groovy-розробник, але щось мені підказує, ще це доволі старі версії 🙁</p>
<blockquote><p>Окремо варто зазначити, що прикладів коду не так багато, і це має побічну дію, що ШІ не може коректно генерувати більшість скриптів, отакої.</p></blockquote>
<p>Що є ще? Перформанс. Тут я можу порівнювати як працює Boomi та Talend, бо ми мігрували багато процесів з одною системи до іншої, і я побачив що реалізація процесу у Boomi працює повільніше, та в мене є думка чому так відбувається &#8211; Boomi для процесів які виконуються за розкладом використовує так званий General Process Mode, у цьому режимі Boomi логує все, кожен крок, кожен документ, кожну трансформацію, все це потребує багато часу, та багато місця для логів. Чи можна обрати інший режим запуску? Так, є режим Low Latency але він не буде працювати для процесів за розкладом.</p>
<p>Тож який висновок? Якщо ви обрали Boomi у якості iPaaS для вашої компанії, то його можливостей скоріш за все вистачить і для побудови ETL процесів які будуть обробляти гігабайти даних. Можливо це не буде так швидко, як з рішеннями від конкурентів, та я сподіваюсь це питання розробники вирішать.</p>
<h2>Boomi. Епізод V</h2>
<p>Для реалізації Master Data Management в Boomi є інструмент який називається Boomi DataHub, за його допомогою ви можете вирішити проблему ізоляції даних. Ну принаймні це слідує з опису цього сервісу, бо в мене не було досвіду роботи з ним.</p>
<p>Отакої, коротенький епізод вийшов…</p>
<h2>Boomi. Епізод VI</h2>
<p>Так, а що у Boomi з можливостями по реалізації API для нашої вигаданої історії?</p>
<p>Boomi має 2 інструмента для цього, перший, це API Management — тут ми можемо налаштовувати доступ, генерувати ключі, встановлювати квоти. Та тут також доступний Swagger «з коробки» та Developer Portal за потреби.</p>
<p>Другий інструмент, це безпосередньо Integration, ви будуєте процеси, які будуть спрацьовувати за запитом зовні на відповідні endpoint&#8217;и, і далі вже можете за якимось правилами викликати той чи інший внутрішній сервіс:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-1024x274.png" alt="" width="660" height="177" class="aligncenter size-large wp-image-4432" srcset="https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-1024x274.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-460x123.png 460w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-768x206.png 768w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-1536x411.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2026/01/Boomi.API_-2048x549.png 2048w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<h2>Boomi. Епілог</h2>
<p>Щось моя розповідь затягнулась, та я сподіваюсь я допоміг вам трохи розібратися що то таке Boomi, та у яких випадках цей інструмент підійде саме вам.</p>
<p>Додам ще трохи матеріалів.</p>
<p>По перше, Boomi має досить непогані <a href="https://train.boomi.com/">навчальні матеріали</a>, вони доступні безкоштовно, єдина умова &#8211; для реєстрації на порталі вам слід використовувати робочу пошту, gmail не підійде. З усіх матеріалів, якщо ми кажемо про розробку інтеграцій, то я рекомендую йти <a href="https://train.boomi.com/path/integration-developer">шляхом розробника</a>, та через пару тижнів вже зможете показувати перші результати своєї роботи.</p>
<p>Багато практичних статей ви зможете знайти на сторінці <a href="https://community.boomi.com/s/boomi-blueprint">Boomi Blueprint</a>.</p>
<p>Статті від ком&#8217;юніті, траплялось що відповіді я знаходив лише там &#8211; <a href="https://community.boomi.com/s/">https://community.boomi.com/s/</a></p>
<p>Ну і звісно <a href="https://help.boomi.com/">офіційний мануал</a>, хоч і не самий інформативний, та все ж таки досить корисний.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/architecture/enterprise-architecture-introduction-to-boomi/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Google Chat Bot. Публікація на Google Workspace Marketplace</title>
		<link>https://anton.shevchuk.name/google/google-chat-bot-marketplace/</link>
					<comments>https://anton.shevchuk.name/google/google-chat-bot-marketplace/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Wed, 03 Jul 2024 11:00:14 +0000</pubDate>
				<category><![CDATA[Google]]></category>
		<category><![CDATA[Apps Script]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4294</guid>

					<description><![CDATA[Це нарешті сталося, бот Bender 2.0 тепер можна встановити з Google Workspace Marketplace. В цьому посту я розповім про кроки які треба буде зробити на шляху до маркету. Я не буду детально зупинятися на розробці бота у цій статті, і я вважаю що він у вас вже є, та працює як слід, тож залишається зробити &#8230; <a href="https://anton.shevchuk.name/google/google-chat-bot-marketplace/" class="more-link">Continue reading <span class="screen-reader-text">Google Chat Bot. Публікація на Google Workspace Marketplace</span></a>]]></description>
										<content:encoded><![CDATA[<p>Це нарешті сталося, бот <a href="https://github.com/AntonShevchuk/bender-2.0">Bender 2.0</a> тепер можна встановити з <a href="https://workspace.google.com/marketplace/app/bender_20/667012984956">Google Workspace Marketplace</a>. В цьому посту я розповім про кроки які треба буде зробити на шляху до маркету.</p>
<p><span id="more-4294"></span></p>
<blockquote><p>Я не буду детально зупинятися на розробці бота у цій статті, і я вважаю що він у вас вже є, та працює як слід, тож залишається зробити лише останній крок до маркету.</p></blockquote>
<p>Почнемо нашу роботу, для початку нам треба підключити <a href="https://console.cloud.google.com/apis/library/appsmarket-component.googleapis.com?project=">Google Workspace Marketplace SDK</a> до нашого проєкту:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/gwm-enable-1024x402.png" alt="Google Workspace Marketplace SDK" width="660" height="259" class="aligncenter size-large wp-image-4343" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/gwm-enable-1024x402.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/gwm-enable-460x181.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/gwm-enable-768x302.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/gwm-enable.png 1044w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Далі нам слід перейти до <a href="https://console.cloud.google.com/apis/api/appsmarket-component.googleapis.com/googleapps_sdk?project=">налаштувань Google Workspace Marketplace SDK</a> та обрати потрібні пункти. </p>
<h3>App Configuration</h3>
<p>Почнемо з «App Visibility», бот буде публічним, та його можна буде знайти у пошуку:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/app-visibility-1024x496.png" alt="Application visibility" width="660" height="320" class="aligncenter size-large wp-image-4344" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/app-visibility-1024x496.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/app-visibility-460x223.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/app-visibility-768x372.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/app-visibility.png 1098w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Встановлювати його зможуть усі бажаючі:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/installation-settings-1024x383.png" alt="Installation settings" width="660" height="247" class="aligncenter size-large wp-image-4346" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/installation-settings-1024x383.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/installation-settings-460x172.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/installation-settings-768x287.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/installation-settings.png 1042w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>І так, це лише додаток до чату:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/integration-chat-1024x993.png" alt="Chat applicaiton" width="660" height="640" class="aligncenter size-large wp-image-4347" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/integration-chat-1024x993.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/integration-chat-460x446.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/integration-chat-768x745.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/integration-chat.png 1062w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Для роботи будуть потрібні лише базові scopes, які не потребують додаткових дозволів:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/oauth-scopes.png" alt="OAuth scopes" width="1018" height="414" class="aligncenter size-full wp-image-4348" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/oauth-scopes.png 1018w, https://anton.shevchuk.name/wp-content/uploads/2024/05/oauth-scopes-460x187.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/oauth-scopes-768x312.png 768w" sizes="auto, (max-width: 1018px) 100vw, 1018px" /></p>
<p>Трохи про себе:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/developer-information-1024x839.png" alt="Developer information" width="660" height="541" class="aligncenter size-large wp-image-4345" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/developer-information-1024x839.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/developer-information-460x377.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/developer-information-768x630.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/developer-information.png 1076w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<h3>Store Listing</h3>
<p>Розділ «Store Listing» також треба уважно заповнити, почнемо з детального опису, я розписав бота на англійський та українській мовах, але здається мені, що опис англійською то буде обов&#8217;язковим пунктом:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/details-1024x573.png" alt="Store Listing Details" width="660" height="369" class="aligncenter size-large wp-image-4349" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/details-1024x573.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/details-460x257.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/details-768x430.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/details.png 1076w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Підготуйте іконки та банери:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/icons-1024x636.png" alt="Icons and banners" width="660" height="410" class="aligncenter size-large wp-image-4350" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/icons-1024x636.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/icons-460x285.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/icons-768x477.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/icons.png 1086w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Красиві скріншоти, треба лише впевнитись, що на них немає нічого сенсетивного:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/screenshots.png" alt="Set of the Screenshots" width="974" height="694" class="aligncenter size-full wp-image-4352" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/screenshots.png 974w, https://anton.shevchuk.name/wp-content/uploads/2024/05/screenshots-460x328.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/screenshots-768x547.png 768w" sizes="auto, (max-width: 974px) 100vw, 974px" /></p>
<p>І тепер дуже важлива частина, ви повинні підготувати Terms та Privacy сторінки та додати посилання на ці сторінки:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/supports-links-1024x852.png" alt="Supports Links" width="660" height="549" class="aligncenter size-large wp-image-4353" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/supports-links-1024x852.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/supports-links-460x383.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/supports-links-768x639.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/supports-links.png 1072w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Як бачите, в мене усі посилання ведуть на <a href="https://github.com/AntonShevchuk/bender-2.0">github</a>:</p>
<ul>
<li><a href="https://github.com/AntonShevchuk/bender-2.0/blob/main/PRIVACY.md">Privacy Policy</a></li>
<li><a href="https://github.com/AntonShevchuk/bender-2.0/blob/main/TERMS.md">Terms of Service</a></li>
<li><a href="https://github.com/AntonShevchuk/bender-2.0/blob/main/SUPPORT.md">Support Page</a></li>
<li><a href="https://github.com/AntonShevchuk/bender-2.0/wiki">User Manual</a></li>
</ul>
<p>Після цього, ви зможете відправити вашого бота на модерацію до Google Workspace Marketplace, але в мене не вийшло з першого разу пройти модерацію, та і з другого разу не вийшло:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/messages-1024x407.png" alt="Google Workspace Marketplace Reviews Team" width="660" height="262" class="aligncenter size-large wp-image-4359" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/messages-1024x407.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/messages-460x183.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/messages-768x305.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/messages-1536x611.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2024/05/messages.png 1594w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Як бачите, я отримав 3 рази відповідь — <code>Rejected</code>, і кожного разу з тим самим текстом:</p>
<blockquote><p>
<strong>1. Links for Privacy Policy, Terms of Service and Support are provided and point to the correct information.</strong><br />
You can check additional info about this requirement here (https://developers.google.com/workspace/marketplace/create-listing#support_links).<br />
All provided links work and point to the correct information. For example, a link provided for a privacy policy points to a page describing the privacy policy.</p>
<p><strong>2. Functionality &#8211; There are no obvious bugs and all visible actions are fully functional.</strong><br />
You must ensure the Add-on is operational and functions in accordance with the description of the Add-on. You should thoroughly test all the add-ons you create before publishing using the available test options.
</p></blockquote>
<p>Я вже не знав, що робити, бо посилання були, але здається модератору не подобалось, що текст розміщений на GitHub, та він просто ігнорив посилання на нього. Тому коли я в 4-ий раз отримав листа від Google Workspace Marketplace Reviews Team що вони почали працювати над перевіркою, то я у відповідь розписав їм, що це за додаток, навіщо він створений, та чому усі посилання ведуть на GitHub.</p>
<blockquote><p>Не пропустіть того листа, я отримував його у проміжок часу від 12:00 до 15:00 за Київом, а вже за 2-3 години отпримував вже відмову.</p></blockquote>
<p>І далі була тиша, rejected-відповіді я отримував впродовж дня, а тут відповідь затягнулась, схоже мій запит пройшов першу лінію модерації, та перейшов на інший рівень, де вже було прийнято рішення про <code>Approved</code>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/approved.png" alt="Approved" width="1012" height="782" class="aligncenter size-full wp-image-4360" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/approved.png 1012w, https://anton.shevchuk.name/wp-content/uploads/2024/05/approved-460x355.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/approved-768x593.png 768w" sizes="auto, (max-width: 1012px) 100vw, 1012px" /></p>
<p>Все, тепер мій бот Bender 2.0 доступний у <a href="https://workspace.google.com/marketplace/app/bender_20/667012984956">Google Workspace Market</a>, встановлюйте, тестуйте, залишайте відгуки.</p>
<p>Якщо у вас також вийшло опублікувати вашого бота, залишайте посилання у коментарях :)</p>
<blockquote><p>І так, ось є <a href="https://developers.google.com/workspace/marketplace/how-to-publish">офіційна документація</a>, беріть та користуйтесь, а статтю залиште у обраному, щоб якщо що було в кого перепитати ;)</p></blockquote>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/google/google-chat-bot-marketplace/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Українська типографська розкладка для MacOS</title>
		<link>https://anton.shevchuk.name/my-life/ukraine-typography-layout/</link>
					<comments>https://anton.shevchuk.name/my-life/ukraine-typography-layout/#comments</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Tue, 11 Jun 2024 13:00:17 +0000</pubDate>
				<category><![CDATA[My Life]]></category>
		<category><![CDATA[MacOS]]></category>
		<category><![CDATA[Ukraine]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4325</guid>

					<description><![CDATA[Настав час великого оновлення Української типографської розкладки клавіатури для MacOS. Спочатку я розповім вам про розкладку, а потім вже про зміни які відбулися. Для початку, а що то таке типографська розкладка, звідкіля вона з&#8217;явилась, та як нею користуватися? Перша типографська розкладка з якою я познайомився була типографська розкладки Іллі Бірмана, вона була і є лише &#8230; <a href="https://anton.shevchuk.name/my-life/ukraine-typography-layout/" class="more-link">Continue reading <span class="screen-reader-text">Українська типографська розкладка для MacOS</span></a>]]></description>
										<content:encoded><![CDATA[<p>Настав час великого оновлення <a href="https://github.com/AntonShevchuk/ukrainian-typography-keyboard-layout">Української типографської розкладки клавіатури</a> для MacOS. Спочатку я розповім вам про розкладку, а потім вже про зміни які відбулися.</p>
<p><span id="more-4325"></span></p>
<p>Для початку, а що то таке типографська розкладка, звідкіля вона з&#8217;явилась, та як нею користуватися?</p>
<p>Перша типографська розкладка з якою я познайомився була <a href="https://ilyabirman.ru/typography-layout/">типографська розкладки Іллі Бірмана</a>, вона була і є лише для російської та англійської мов. Але мені була потрібна розкладка для українскої мови, і серед можливих варіантів я обрав шлях створення своєї на основі розкладки від Іллі.</p>
<p>— Навіщо потрібні всі ці «типографічні» розкладки?<br />
— Як раз для того, щоб написати цей діалог не використовуючи нічого окрім української розкладки…</p>
<p>Так, типографічні розкладки створюють як раз для того, щоб швидко друкувати такі елементи як лапки <code>«</code>, <code>»</code>, та оте довге тире <code>—</code>, а не якісь дефіс. І це не все, давайте ще приведу приклади використання:</p>
<ul class="dashed">
<li>спеціальні сімволи: <code>©</code>, <code>®</code>, та <code>™</code></li>
<li>символ градусів — <code>°</code>, дивіться як гарно виглядає <code>+24°C</code></li>
<li>математичні символи: <code>1¹+2²+3³≠30</code>, <code>¹⁄₂+¹⁄₃+¹⁄₄≈1</code>, та <code>2×2=4</code>, зі справжнім знаком множення, а не літерою <code>x</code></li>
<li>звісно валюти: жміть <code>⌥</code> + <code>р</code> отримаєте <code>₴</code> (тому що латинкою то h — hrivnya), <code>£</code>, <code>¥</code>, <code>€</code>, <code>$</code>, та <code>¢</code></li>
<li>стрілочки <code>←</code>, <code>→</code>, <code>↑</code>, та <code>↓</code></li>
<li>скажи <s>Ющенко</s> <code>✓</code>так</li>
</ul>
<p>Насправді ще багато чого, подивіться самі на це різноманіття символів, і так, багато з чого я дійсно використовую:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/option-1024x479.png" alt="Option" width="660" height="309" class="aligncenter size-large wp-image-4326" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/06/option-1024x479.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option-460x215.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option-768x360.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option.png 1196w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/option-shift-1024x479.png" alt="Option Shift" width="660" height="309" class="aligncenter size-large wp-image-4329" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/06/option-shift-1024x479.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option-shift-460x215.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option-shift-768x360.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/06/option-shift.png 1196w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>— А що то за незрозуміле <code>Ы</code> у розкладці, навіщо воно треба?<br />
— То треба, щоб видалити російську розкладку з обігу, тож навіть якщо є потреба надрукувати оті <code>Ы</code> та <code>Э</code> вам не треба більше ставити ще одну розкладку, або шукати ці символи у інтернеті.</p>
<p>Заголом, я намагався додати можливість друкувати будь які літери <a href="https://uk.wikipedia.org/wiki/%D0%9A%D0%B8%D1%80%D0%B8%D0%BB%D0%B8%D1%86%D1%8F">кирилиці</a>:</p>
<pre class="brush: bash; title: ; notranslate">
А	Б	В	Г	Ґ	Д	Ѓ
Ђ	Е	Ѐ	Є	Ё	Ж	З
З́	 Ѕ	 И	 Ѝ	 І	 Ї	 Й
Ј	К	Л	Љ	М	Н	Њ
О	П	Р	С	С́	 Т	 Ћ
Ќ	У	Ў	Ф	Х	Ц	Ч
Џ	Ш	Щ	Ъ	Ы	Ь	Э
Ю	Я
</pre>
<p>Але все не так просто, бо для більшості символів з модифікаторами накшталт акцентів (<code>ў</code>), наголосів (<code>а́</code>), умлаутів (<code>ä</code>) слід використовувати так звані «мертві клавіші». Ось декілька прикладів:</p>
<ul class="dashed">
<li>нажміть одночасно <code>⌥</code> + <code>⌃</code> + <code>˘</code>, а потім вже <code>у</code> отримаєте <code>ў</code>;</li>
<li>нажміть одночасно <code>⌥</code> + <code>⌃</code> + <code>¨</code>, а потім вже <code>ж</code> отримаєте <code>ӝ</code>.</li>
</ul>
<p>Хоча я ще не все реалізував з кирилиці, мені ще <a href="https://github.com/AntonShevchuk/ukrainian-typography-keyboard-layout/issues/8">є над чим працювати</a>:</p>
<pre class="brush: bash; title: ; notranslate">
Ђ	Ѕ	Ј	Љ	Њ	Ћ	Џ
</pre>
<p>Але давайте повернемось до того, що вже зроблено:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/ukrainian-1024x479.png" alt="Ukrainian Typography Layout" width="660" height="309" class="aligncenter size-large wp-image-4327" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/06/ukrainian-1024x479.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/06/ukrainian-460x215.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/06/ukrainian-768x360.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/06/ukrainian.png 1196w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Тут для нас все досить звично, для тих хто користувався Windows то розташування максимально знайоме, з особливостей відмічу:</p>
<ul class="dashed">
<li>для вводу літери <code>ґ</code> слід використовувати комбінацію <code>⌥</code> + <code>г</code></li>
<li>для вводу символа <code>₴</code> слід використовувати комбінацію <code>⌥</code> + <code>р</code></li>
</ul>
<p>А ось у останньому оновленні в мене змінилась концепція щодо символів, які ховаються за <code>shift ⇧</code>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/shift-1024x479.png" alt="Shift" width="660" height="309" class="aligncenter size-large wp-image-4328" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/06/shift-1024x479.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/06/shift-460x215.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/06/shift-768x360.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/06/shift.png 1196w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Така зміна пов&#8217;язана з тим, що я вже багато років використовую MacBook лише з англійською розкладкою, тож в мене на клавіатурі немає інших символів ¯\_(ツ)_/¯. Так, я друкую українською «наосліп», і виходячи з цього я намагався побудувати розкладку яка використовувала би максимально сумісний вигляд з нативною англійською розкладкою:</p>
<ul class="dashed">
<li>верхній рядок символів над цифрами повністю сумісний з англійською версією: <code>! @ # $ % ^ &amp; * ( ) _ +</code></li>
<li>символи які ховаються за <code>option ⌥</code>: <code>? &lt; &gt; : [ ]</code></li>
<li>символи які ховаються за сполученням клавіш <code>option ⌥</code> + <code>shift ⇧</code>: <code>… { } ; { }</code></li>
</ul>
<h2>Як встановити?</h2>
<p>Перейдіть на <a href="https://github.com/AntonShevchuk/ukrainian-typography-keyboard-layout/releases">сторінку проєкту на GitHub</a> та завантажте з останнього релізу файл <strong>ukrainian-typography.dmg</strong>. Далі дабл-клік по файлу:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/dmg.png" alt="DMG with Keyaboard Layout" width="608" height="216" class="aligncenter size-full wp-image-4330" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/06/dmg.png 608w, https://anton.shevchuk.name/wp-content/uploads/2024/06/dmg-460x163.png 460w" sizes="auto, (max-width: 608px) 100vw, 608px" /></p>
<p>Тепер запустіть <strong>Keyboard Installer</strong> (MacOS буде попереджати про те, що цей файл завантажено з інтернету), а потім перетягніть до вікна інсталера файл <strong>ukrainian-typography.bundle</strong>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/06/drag-and-drop.gif" alt="Keyboard Installer" width="800" height="338" class="aligncenter size-full wp-image-4331" /></p>
<p>Далі оберіть встановлення до поточного користувача. Все, тепер вам потрібно лише перейти до налаштувань клавіатури:</p>
<p><code>System Settings</code> > <code>Keyboard</code> > <code>Text Input: Input Sources</code> > <code>Edit</code> та обрати <code>🇺🇦 Ukrainian - Typography</code></p>
<p>Насолоджуйтесь :)</p>
<h2>Варіанти?</h2>
<p>На поточний час, у вас є можливість обрати серед чисельних інших розкладок:</p>
<ul class="dashed">
<li>Моя <a href="https://github.com/AntonShevchuk/ukrainian-typography-keyboard-layout/releases/tag/1.5.0" target="_blank" rel="noopener">попередня версія 1.5.0</a>, більш звична для користувачів;</li>
<li><a href="https://github.com/denysdovhan/ukrainian-typographic-keyboard" target="_blank" rel="noopener">Ukrainian Typographic Keyboard</a> від Дениса Довганя;</li>
<li><a href="https://alexkolodko.com/projects/ua-ru-typography-layout/" target="_blank" rel="noopener">Українсько-російська типографська розкладка</a> від Олександра Колодько;</li>
<li><a href="https://github.com/neochief/ukrainian-typographic-layouts" target="_blank" rel="noopener">Сумісні українські типографічні розкладки для Windows/Mac/Ubuntu</a> від Олександра Швеца;</li>
<li>Версія від <a href="https://gagadget.com/another/4076-ukrainskaya-tipografskaya-raskladka-klaviaturyi/" target="_blank" rel="noopener">GaGadget</a>.</li>
</ul>
<p>Або навіть обрати свій шлях, та створити свою розкладку за допомоги утіліти <a href="https://software.sil.org/ukelele/">ukelele</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/my-life/ukraine-typography-layout/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>Google Chat Bot. Структура бота та CLI утіліта clasp</title>
		<link>https://anton.shevchuk.name/google/google-chat-bot-structure-and-clasp-cli-tool/</link>
					<comments>https://anton.shevchuk.name/google/google-chat-bot-structure-and-clasp-cli-tool/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Mon, 03 Jun 2024 08:09:33 +0000</pubDate>
				<category><![CDATA[Google]]></category>
		<category><![CDATA[Apps Script]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4013</guid>

					<description><![CDATA[Останнім часом мені доводиться все більше приймати участь у розробці ботів для Google Chat, тому в мене вже з&#8217;явились дякі практики з оформлення проєкта, якими я і хочу поділитися з вами. Почнемо з початку, з редактору коду. Але знаєте що? Мене редагування коду у браузері трохи бісить, тому будемо налаштовувати робочий енвайронмент здорової людини локально &#8230; <a href="https://anton.shevchuk.name/google/google-chat-bot-structure-and-clasp-cli-tool/" class="more-link">Continue reading <span class="screen-reader-text">Google Chat Bot. Структура бота та CLI утіліта clasp</span></a>]]></description>
										<content:encoded><![CDATA[<p>Останнім часом мені доводиться все більше приймати участь у розробці ботів для Google Chat, тому в мене вже з&#8217;явились дякі практики з оформлення проєкта, якими я і хочу поділитися з вами.</p>
<p><span id="more-4013"></span></p>
<p>Почнемо з початку, з редактору коду. Але знаєте що? Мене редагування коду у браузері трохи бісить, тому будемо налаштовувати робочий енвайронмент здорової людини локально на робочому комп&#8217;ютері. Для цього нам знадобиться git, ваша улюблена IDE та eslint для перевірки що у вас все добре. Все це звучить досить знайомо, тож так ми будемо розробляти код локально, перевіряти кодестайл за допомоги eslint, а за версіонування коду в нас буде відповідати git з усіма його можливостями.</p>
<p>Подивіться як виглядає <code>package.json</code> мого проєкту:</p>
<pre class="brush: jscript; title: ; notranslate">
{
  &quot;name&quot;: &quot;bender-2.0&quot;,
  &quot;version&quot;: &quot;7.0.0&quot;,
  &quot;description&quot;: &quot;The Bender bot for Google Workspace&quot;,
  &quot;scripts&quot;: {
    &quot;test&quot;: &quot;echo \&quot;Error: no test specified\&quot; &amp;&amp; exit 1&quot;
  },
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;git+https://github.com/AntonShevchuk/bender-2.0.git&quot;
  },
  &quot;keywords&quot;: &#x5B;
    &quot;Google Workspace&quot;,
    &quot;Google Chat API&quot;,
    &quot;Apps Script&quot;
  ],
  &quot;author&quot;: &quot;Anton Shevchuk&quot;,
  &quot;license&quot;: &quot;MIT&quot;,
  &quot;bugs&quot;: {
    &quot;url&quot;: &quot;https://github.com/AntonShevchuk/bender-2.0/issues&quot;
  },
  &quot;homepage&quot;: &quot;https://github.com/AntonShevchuk/bender-2.0#readme&quot;,
  &quot;devDependencies&quot;: {
    &quot;@eslint/js&quot;: &quot;^9.2.0&quot;,
    &quot;eslint&quot;: &quot;^9.2.0&quot;,
    &quot;eslint-plugin-googleappsscript&quot;: &quot;^1.0.5&quot;,
    &quot;eslint-plugin-jsdoc&quot;: &quot;^48.2.3&quot;,
    &quot;globals&quot;: &quot;^15.1.0&quot;
  }
}
</pre>
<p>Нічого зайвого, запускай <code>npm install</code> та працюй з кодом&#8230;</p>
<p>— Але ж код у нас на <a href="https://script.google.com/home/start">Apps Script</a>, його ж не треба ручками буде постійно копіювати?<br />
— Ні, не треба, для цього є консольна утиліта <a href="https://developers.google.com/apps-script/guides/clasp">clasp</a>, яка нам як раз допоможе тягати код туди-сюди.</p>
<h2>The Apps Script CLI</h2>
<blockquote><p>
Далі вас чекає невеличкий переказ <a href="https://developers.google.com/apps-script/guides/clasp">офіційної документації</a>, можете відразу по мануалу робити, або навіть пройти <a href="https://codelabs.developers.google.com/codelabs/clasp/#0">відповідну лабу</a>. Тож вибір за вами :)
</p></blockquote>
<p>Почнемо з того, що нам буде потрібно буде <a href="https://nodejs.org/en/download/package-manager">встановити nodejs</a>, і вже потім встановлювати clasp глобально:</p>
<pre class="brush: bash; title: ; notranslate">
npm i @google/clasp -g
</pre>
<p>Якщо все добре, то наступна команда повинна вам вивести перелік доступних команд:</p>
<pre class="brush: bash; title: ; notranslate">
clasp -h
</pre>
<blockquote><p>
Я особисто ніяк не можу звикнути до назви цієї тулзи, постійно набираю claps  ¯\_(ツ)_/¯
</p></blockquote>
<p>Тепер нам треба авторизуватися, виконайте команду <code>login</code>:</p>
<pre class="brush: bash; title: ; notranslate">
clasp login
</pre>
<p>Далі вам слід буде авторизуватися використовуючи ваш gmail акаунт, та надати доступи до нього. Вам також слід буде ввімкнути Google Apps Script API на <a href="https://script.google.com/home/usersettings">сторінці налаштувань</a>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/google-appscript-api-460x335.png" alt="Enable Google AppScript API" width="460" height="335" class="aligncenter size-medium wp-image-4309" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/google-appscript-api-460x335.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/google-appscript-api-768x559.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/google-appscript-api.png 992w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Після цього можна буде подивитися на перелік ваших проєктів:</p>
<pre class="brush: bash; title: ; notranslate">
clasp list
</pre>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-list-1024x108.png" alt="clasp list" width="660" height="70" class="aligncenter size-large wp-image-4321" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-list-1024x108.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-list-460x49.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-list-768x81.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-list.png 1238w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Тепер щоб стягнути код до себе використовуйте команду <code>clasp clone</code> та ID вашого проєкту:</p>
<pre class="brush: bash; title: ; notranslate">
clasp clone &lt;scriptID&gt;
</pre>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-clone.gif" alt="clasp clone" width="800" height="400" class="aligncenter size-full wp-image-4311" /></p>
<p>Все, тепер ваш код у вас є локально, але з ним відбулись деякі зміни:</p>
<pre class="brush: bash; title: ; notranslate">
# було
actions/actionEditCard.gs
actions/actionEditNotes.gs
commands/slashBender.gs
commands/slashWhisky.gs

# стало
actions/
  |-- actionEditCard.js
  `-- actionEditNotes.js
commands/
  |-- slashBender.js
  `-- slashWhisky.js
</pre>
<p>Насправді, з теками стало набагато зручніше!</p>
<p>Тепер ви можете вносити зміни до коду, комітити їх до git, а потім заливати в одну команду на script.google.com:</p>
<pre class="brush: bash; title: ; notranslate">
clasp push
</pre>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/clasp-push.gif" alt="clasp push" width="800" height="216" class="aligncenter size-full wp-image-4312" /></p>
<p>А тепер прочитайте дуже уважно, та запам&#8217;ятайте:</p>
<ul>
<li>коли ви використовуєте команду <code>clasp pull</code>, то всі локальні файли будут замінені на файли з script.google.com</li>
<li>коли ви використовуєте команду <code>clasp push</code>, то всі локальні файли будут відправлені до script.google.com, та перетруть всі зміни там</li>
</ul>
<p>Тож, робимо висновок, що щоб потім не жалітися, то слід редагувати або локально, або безпосередньо на сайті <a href="http://script.google.com">script.google.com</a>, але не треба одночасно і тут, і там вносити зміни, закінчиться тим, що ви щось та загубите.</p>
<blockquote><p>
І ще один момент, до сайту script.google.com будуть залиті лише js-файли та файл appsscript.json, усі інші файли залишаються на вашій відповідальності, тож не забувайте змінити заливати до git.
</p></blockquote>
<h2>Підключаємо eslint</h2>
<p>Для того щоб перевірити ваш код слід до теки з проєктом додати <code>package.json</code> який я приводив на самому початку та запустити команду <code>npm install</code>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/npm-install.png" alt="npm install" width="874" height="286" class="aligncenter size-full wp-image-4319" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/npm-install.png 874w, https://anton.shevchuk.name/wp-content/uploads/2024/05/npm-install-460x151.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/npm-install-768x251.png 768w" sizes="auto, (max-width: 874px) 100vw, 874px" /></p>
<p>Тепер додайте конфігураційний файл самого eslint:</p>
<pre class="brush: jscript; title: eslint.config.mjs; notranslate">
import globals from 'globals';
import pluginJs from '@eslint/js';
import jsdoc from 'eslint-plugin-jsdoc';
import appsScript from 'eslint-plugin-googleappsscript';

export default &#x5B;
  pluginJs.configs.recommended,
  {
    files: &#x5B;'**/*.js'],
    plugins: {
      jsdoc,
      googleappsscript: appsScript,
    },
    languageOptions: {
      globals: {
        ...globals.browser,
      },
    },
    rules: {
      'comma-dangle': &#x5B;'error', 'never'],
      'max-len': &#x5B;'error', { code: 120, 'ignoreTrailingComments': true  }],
      'camelcase': &#x5B;
        'error',
        {
          ignoreDestructuring: true,
          ignoreImports: true,
          allow: &#x5B;'access_type', 'redirect_uris'],
        },
      ],
      'guard-for-in': 'off',
      'no-var': 'off',
      'no-unused-vars': 'off', // Functions aren't used.
      'no-undef': 'off', // Ignore undefined errors for global variables
    },
  }
];
</pre>
<p>Тепер можна запустити перевірку коде-стайлу вашого коду:</p>
<pre class="brush: bash; title: ; notranslate">
npx eslint
</pre>
<p>Сподіваюсь, ви не побачити ніяких помилок:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/eslint-ok.png" alt="eslint ok" width="896" height="92" class="aligncenter size-full wp-image-4320" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/eslint-ok.png 896w, https://anton.shevchuk.name/wp-content/uploads/2024/05/eslint-ok-460x47.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/eslint-ok-768x79.png 768w" sizes="auto, (max-width: 896px) 100vw, 896px" /></p>
<p>З іншого боку, у цій конфігурації досить лайтові перевірки, тож якщо маєте хист, то відправлйте pull request до мого <a href="https://github.com/AntonShevchuk/bender-2.0/">репозіторію</a> ;)</p>
<h2>Структура проєкта</h2>
<p>Як я вже писав раніше, в мене в розробці декілька проєктів, тож я намагаюсь розробити зручну структуру для розробки функціоналу. В мене поки виходить наступна структура проєкта:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-1024x633.png" alt="Google Bot Structure" width="660" height="408" class="aligncenter size-large wp-image-4313" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-1024x633.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-460x285.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-768x475.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-1536x950.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2024/05/structure-825x510.png 825w, https://anton.shevchuk.name/wp-content/uploads/2024/05/structure.png 1652w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Така структура дає можливість швидко найти потрібну частину коду. Далі я буду приводити приклади за алфавітом з поясненнями.</p>
<p>Усі екшени потрапили до відповідної теки <code>actions</code>, це те що відбувається коли ви клікаєте по кнопках на картці, там де прописано <code>onClick => action</code>:</p>
<pre class="brush: bash; title: ; notranslate">
actions/
  |-- actionEditCard.js
  |-- ...
  `-- actionEditNotes.js
</pre>
<p>Усі картки, які ми відправляємо до чату оформлюємо у окремі функції, всі ці функції до окремої теки:</p>
<pre class="brush: bash; title: ; notranslate">
cards/
  |-- cardHome.js
  `-- cardPoll.js
</pre>
<blockquote><p>Насправді, бот Bender 2.0 в нас має 5 окремих карток, але деякі з них я не виносив до окремих функцій, хоча можливо це слід було зробити.</p></blockquote>
<p>Для кожної slash-команди окрема функція, в окремому файлі у теку з усіма іншими slash-командами:</p>
<pre class="brush: bash; title: ; notranslate">
commands/
  |-- slashBender.js
  |-- ...
  `-- slashWhisky.js
</pre>
<p>Всякі різні допоміжні класи та функції, загалом то тут теж ще слід наводити лад:</p>
<pre class="brush: bash; title: ; notranslate">
components/
  |-- FormInputHandler.js
  |-- ...
  `-- Response.js
</pre>
<p>Діалоги для взаємодії з користувачами, один файл — один діалог:</p>
<pre class="brush: bash; title: ; notranslate">
dialogs/
  |-- dialogCard.js
  `-- dialogPoll.js
</pre>
<p>Події, саме тут в нас будуть жити такі важливі функції як <code>onCardClick()</code>, <code>onMessage()</code> та інші:</p>
<pre class="brush: bash; title: ; notranslate">
events/
  |-- onCardClick.js
  |-- ...
  `-- onMessage.js
</pre>
<p>Допоміжні функції:</p>
<pre class="brush: bash; title: ; notranslate">
helpers/
  `-- htmlEntities.js
</pre>
<p>Функції, для створення віджетів:</p>
<pre class="brush: bash; title: ; notranslate">
widgets/
  |-- widgetDecoratedText.js
  |-- ...
  `-- widgetTextParagraph.js
</pre>
<p>Звісно, після того як ви використовуєте команду <code>clasp push</code>, то вся ця структура на script.google.com перетвориться на «плоску» <code>тека/файл.gs</code>, але то вже не так важливо, бо ви вже майже не будете використовувати редагування у браузері.</p>
<h3>Source Code</h3>
<p>Код бота доступний на <a href="https://github.com/AntonShevchuk/bender-2.0/">GitHub</a>, реліз 7.0.0 відповідає коду з цієї статті.</p>
<figure id="attachment_4016" aria-describedby="caption-attachment-4016" style="width: 240px" class="wp-caption aligncenter"><a href="https://github.com/AntonShevchuk/bender-2.0/releases/tag/7.0.0"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png" alt="Bender, реліз 7.0.0" width="240" height="240" class="size-full wp-image-4016" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png 240w, https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark-200x200.png 200w" sizes="auto, (max-width: 240px) 100vw, 240px" /></a><figcaption id="caption-attachment-4016" class="wp-caption-text">Bender, білд 7.0.0</figcaption></figure>
<p>P.S. Функціонал бота навмисно спрощено відносно релізу 6.0.0, це зроблено задля підготовки до публікації у Google Workspace Market.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/google/google-chat-bot-structure-and-clasp-cli-tool/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Google Chat Bot. Інтеграція з Jira. Частина II</title>
		<link>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-two/</link>
					<comments>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-two/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Thu, 02 May 2024 08:00:23 +0000</pubDate>
				<category><![CDATA[Google]]></category>
		<category><![CDATA[Apps Script]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4289</guid>

					<description><![CDATA[У попередній частині ми вже додали до бота інтеграцію з Jira. У цій частині ми внесемо зміни до того функціонала, та попрацюємо над зворотньої інтеграцією — від Jira до Google Apps Script. За моїм задумом, у випадку оновлення issue (редагування або зміни статусу) ми будемо відправляти до треду оновленні дані та оновлювати картку. Але почнемо &#8230; <a href="https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-two/" class="more-link">Continue reading <span class="screen-reader-text">Google Chat Bot. Інтеграція з Jira. Частина II</span></a>]]></description>
										<content:encoded><![CDATA[<p>У <a href="https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-one/">попередній частині</a> ми вже додали до бота інтеграцію з Jira. У цій частині ми внесемо зміни до того функціонала, та попрацюємо над зворотньої інтеграцією — від Jira до Google Apps Script. </p>
<p><span id="more-4289"></span></p>
<p>За моїм задумом, у випадку оновлення issue (редагування або зміни статусу) ми будемо відправляти до треду оновленні дані та оновлювати картку.</p>
<p>Але почнемо з далеку, з далекого далеку: нам треба зробити так, щоб повідомлення до чату було відправлено не від імені користувача, а від самого бота, бо інакше ми потім не зможемо оновити картку без участі користувача. </p>
<p>Для цього нам треба буде зробити сервісний акаунт.</p>
<h3>Підготовка сервісного акаунту</h3>
<p>Для створення сервісного акаунту вам слід перейти до <a href="https://console.cloud.google.com/iam-admin/serviceaccounts?project=">сторінки налаштувань</a> вашого проєкту, а потім тицнути «Create Service Account»:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/service-account-460x457.png" alt="Create Service Account" width="460" height="457" class="aligncenter size-medium wp-image-4278" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/service-account-460x457.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/service-account-1024x1018.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/service-account-768x764.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/service-account.png 1088w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Далі, перейдіть до налаштувань цього акаунту, та створіть новий JSON ключ:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/private-key-460x296.png" alt="Private JSON Key" width="460" height="296" class="aligncenter size-medium wp-image-4279" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/private-key-460x296.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/private-key-1024x658.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/private-key-768x494.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/private-key.png 1108w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Після цього, ви отримаєте відповідний JSON файл, який буде виглядати наступним чином:</p>
<pre class="brush: jscript; title: ; notranslate">
{
  &quot;type&quot;: &quot;service_account&quot;,
  &quot;project_id&quot;: &quot;bender-20&quot;,
  &quot;private_key_id&quot;: &quot;-----&quot;,
  &quot;private_key&quot;: &quot;-----BEGIN PRIVATE KEY-----\nAAAAAA\n-----END PRIVATE KEY-----\n&quot;,
  &quot;client_email&quot;: &quot;-----@-----.iam.gserviceaccount.com&quot;,
  &quot;client_id&quot;: &quot;-----&quot;,
  &quot;auth_uri&quot;: &quot;https://accounts.google.com/o/oauth2/auth&quot;,
  &quot;token_uri&quot;: &quot;https://oauth2.googleapis.com/token&quot;,
  &quot;auth_provider_x509_cert_url&quot;: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,
  &quot;client_x509_cert_url&quot;: &quot;https://www.googleapis.com/robot/v1/metadata/x509/-----%40-----.iam.gserviceaccount.com&quot;,
  &quot;universe_domain&quot;: &quot;googleapis.com&quot;
}
</pre>
<p>Вам потрібно буде перенести до Properties вашого бота декілька полів:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-key-460x162.png" alt="Apps Script Properties" width="460" height="162" class="aligncenter size-medium wp-image-4280" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-key-460x162.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-key-1024x361.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-key-768x271.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-key.png 1440w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Все, сервісний акаунт створено та ключі додано. Поїхали далі.</p>
<blockquote><p>Доречі, ви можете використовувати створений акаунт якщо вам потрібно буде надати дозвіл на будь який ресурс у межах Google документів. Це так, на майбутнє.</p></blockquote>
<h3>OAuth</h3>
<p>Наступний крок — створення <a href="https://console.cloud.google.com/apis/credentials/consent">OAuth Consent Screen</a>:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-oauth-consent-screen-460x420.png" alt="Create OAuth Consent Screen" width="460" height="420" class="aligncenter size-medium wp-image-4285" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-oauth-consent-screen-460x420.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-oauth-consent-screen-1024x935.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-oauth-consent-screen-768x701.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-oauth-consent-screen.png 1126w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<blockquote><p>Якщо ви розробляєте для Google Workspace, то у вас буде можливість обрати опцію <strong>Internal</strong>, таким чином ви обмежити використання вашого додатку та він буде «лише для своїх»</p></blockquote>
<p>Додайте всю необхідну інформацію:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/oauth-consent-screen-460x389.png" alt="OAuth Consent Screen" width="460" height="389" class="aligncenter size-medium wp-image-4286" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/oauth-consent-screen-460x389.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/oauth-consent-screen-1024x866.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/oauth-consent-screen-768x650.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/oauth-consent-screen.png 1026w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>На кроці додавання скоупів, я додав лише наступні:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/scopes-1024x286.png" alt="OAuth Scopes" width="660" height="184" class="aligncenter size-large wp-image-4287" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/scopes-1024x286.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/scopes-460x128.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/scopes-768x214.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/scopes.png 1470w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Після цього нас чекають ще трошки «напрягів» — для того, щоб відправити повідомлення до чату слід використовувати наступну нотацію для виклику метода <code>Chat.Spaces.Messages.create()</code>:</p>
<pre class="brush: jscript; highlight: [5]; title: ; notranslate">
Chat.Spaces.Messages.create(
  {'text': 'Hello world!'},
  event.space.name,
  {},
  {'Authorization': `Bearer ${serviceToken}`}
);
</pre>
<p>Але щоб отримати отой <code>serviceToken</code> нам слід підключити <a href="https://github.com/googleworkspace/apps-script-oauth2">бібліотеку OAuth</a> до нашого чату використовуючи ідентифікатор:</p>
<pre class="brush: bash; title: ; notranslate">1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF</pre>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/add-library-460x444.png" alt="" width="460" height="444" class="aligncenter size-medium wp-image-4284" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/add-library-460x444.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/add-library-768x742.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/add-library.png 1006w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Створимо функцію <code>getServiceAccessToken()</code>:</p>
<pre class="brush: jscript; title: getServiceAccessToken.gs; notranslate">
/**
 * Get Access Token by service credentials
 *
 * Examples of usage:
 * 
 *  getServiceAccessToken('messages', &#x5B;'https://www.googleapis.com/auth/chat.messages.create']);
 *  getServiceAccessToken('spaces', &#x5B;'https://www.googleapis.com/auth/chat.spaces.readonly']);
 */
function getServiceAccessToken(serviceName = 'messages', scopes = &#x5B;]) {
  const scriptProperties = PropertiesService.getScriptProperties()

  const service = OAuth2.createService(serviceName)
    .setTokenUrl('https://oauth2.googleapis.com/token')
    .setPrivateKey(scriptProperties.getProperty('PRIVATE_KEY').replace(/\\n/g, '\n')) // lifehack ^_^
    .setIssuer(scriptProperties.getProperty('CLIENT_EMAIL'))
    .setPropertyStore(scriptProperties)
    .setScope(scopes.join(' '));

  if (!service.hasAccess()) {
    Logger.log('Authentication error: %s', service.getLastError());
    return null;
  }

  return service.getAccessToken();
}
</pre>
<p>Все, збираємо до кучі:</p>
<pre class="brush: jscript; title: ; notranslate">
const serviceToken = getServiceAccessToken('messages', &#x5B;'https://www.googleapis.com/auth/chat.messages.create']);

Chat.Spaces.Messages.create(
  {'text': 'Hello world!'},
  event.space.name,
  {},
  {'Authorization': `Bearer ${serviceToken}`}
);
</pre>
<p>Результат:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/bender-test.png" alt="Message in chat" width="426" height="148" class="aligncenter size-full wp-image-4288" /></p>
<p>Повертаємось до редагування <code>actionNewIssue.gs</code>, та замінимо створення повідомлення вже використовуючи сервісний токен:</p>
<pre class="brush: jscript; highlight: [11,16]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewIssue(event) {
  //
  // ... the code was cropped
  // 

  try {
    const card = cardIssue(event, issue)
    const serviceToken = getServiceAccessToken('messages', &#x5B;'https://www.googleapis.com/auth/chat.messages']);
    const message = Chat.Spaces.Messages.create(
      card, 
      event.space.name,
      {},
      {'Authorization': `Bearer ${serviceToken}`}
    );

    // Extracting the thread name from the response
    // Assuming ID is spaces/AAAAAAAA/threads/BBBBBBBB
    const &#x5B; , spaceId, , threadAndMessageId] = message.thread.name.split('/');

    const threadUrl = `https://chat.google.com/room/${spaceId}/${threadAndMessageId}/${threadAndMessageId}`;

    Logger.log(`New thread ${threadUrl}`)

    let updateData = {
      &quot;fields&quot;: {}
    }

    updateData.fields&#x5B;jiraApi.customField] = threadUrl

    jiraApi.updateIssue(response.key, updateData)
  } catch (err) {
    Logger.log('Failed to create message with error %s', err.message);
  }

  return OK()
}
</pre>
<p>На цьому ми завершили роботу по створенню issue </p>
<h3>Web Application</h3>
<p>Далі, нам треба додати можливість викликати нашого бота з зовні, для цього слід створити новий Deployment:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/new-deployment-web-application-1024x808.png" alt="" width="660" height="521" class="aligncenter size-large wp-image-4295" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/new-deployment-web-application-1024x808.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-deployment-web-application-460x363.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-deployment-web-application-768x606.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-deployment-web-application.png 1498w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Після цього у вас з&#8217;явиться Web app URL, ви можете навіть спробувати відкрити посилання, але вас буде чекати помилка:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/doGet-error.png" alt="Error" width="982" height="284" class="aligncenter size-full wp-image-4296" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/doGet-error.png 982w, https://anton.shevchuk.name/wp-content/uploads/2024/04/doGet-error-460x133.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/doGet-error-768x222.png 768w" sizes="auto, (max-width: 982px) 100vw, 982px" /></p>
<p>Так, нас чекають нові методи для обробки запитів ззовні, зустрічайте <code>doGet()</code> та <code>doPost()</code>:</p>
<pre class="brush: jscript; title: doGet.gs; notranslate">
/**
 * GET request to Web app URL
 */
function doGet(event) {
  Logger.log(`WebHook doGet()`)

  return HtmlService.createHtmlOutput(&quot;GET request processed&quot;);
}
</pre>
<pre class="brush: jscript; title: doPost.gs; notranslate">
/**
 * POST request to Web app URL
 */
function doPost(event) {
  Logger.log(`WebHook doPost()`)

  return HtmlService.createHtmlOutput(&quot;POST request processed&quot;);
}
</pre>
<p>Спробуйте тепер постукати до того Web URL, ви отримаєте відповідь від вашого бота. Але нас цікавить саме URL, скопіюйте його, він нам знадобиться.</p>
<h3>Автоматизація в Jira</h3>
<p>Настав час повернутися до Jira та налаштувати автоматизацію, для цього у вас повинні бути адмінські права до проєкту. Перейдіть до Automation та створіть правила які будуть виконуватися під час роботи над issue у Jira. Я додав 3 простих правила:</p>
<ul>
<li>коли хтось ассайнить issue</li>
<li>коли змінюється статус</li>
<li>коли редагують Summary або Description</li>
</ul>
<p>Для цього нам якраз стане у нагоді URL до Web Application, але ми трошки змінемо його відповідно до призначення кожного правила:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-1024x423.png" alt="" width="660" height="273" class="aligncenter size-large wp-image-4298" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-1024x423.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-460x190.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-768x318.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-1536x635.png 1536w, https://anton.shevchuk.name/wp-content/uploads/2024/04/new-rule-2048x847.png 2048w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<p>Так, так, ми додали параметр <code>action</code> до URL, і тепер можемо побудувати логіку виходячи з нього:</p>
<pre class="brush: jscript; title: doPost.gs; notranslate">
/**
 * POST request to Web app URL
 */
function doPost(event) {
  const action = event.parameter.action ? event.parameter.action : null

  Logger.log(`WebHook doPost(&quot;${action}&quot;)`)

  if (!event.postData || !event.postData.contents) {
    Logger.log(`POST data is empty`)
  }

  const data = JSON.parse(event.postData.contents);

  if (!data) {
    Logger.log(`POST data is invalid`)
  }

  try {
    // You can now call your bot method based on event type
    switch (action) {
      case 'assigned':
        // ...
        break;
      case 'updated':
        // ...
        break;
      case 'transitioned':
        // ...
        break;
      default:
        Logger.log(&quot;Invalid action&quot;)
        break;
    }
  } catch (e) {
    return HtmlService.createHtmlOutput(`POST request processed with error: ${e}`);
  }

  return HtmlService.createHtmlOutput(&quot;POST request processed&quot;);
}
</pre>
<blockquote><p>Звірніть увагу, що я також вказав значення «Webhook body» як «Issue Data»!</p></blockquote>
<h3>Відправка відповіді у тред</h3>
<p>Тепер починається саме цікаве — нам треба відправити повідомлення як відповідь до початкового повідомлення, посилання на яке ми зберегли у Custom Field. Почнемо звісно з того, що дістанеме те посилання, та отримаємо з нього ідентифікатор потрібного треда (цей код я додам до <code>doPost.gs</code>):</p>
<pre class="brush: jscript; title: ; notranslate">
const scriptProperties = PropertiesService.getScriptProperties();

// JIRA threadUrl
const threadUrl = data.fields&#x5B;scriptProperties.getProperty('JIRA_CUSTOM_FIELD')]

const parts = threadUrl.split('/'); // Split the URL into parts by '/'
const spaceId = parts&#x5B;4]; // The space ID is expected to be the fifth part
const threadId = parts&#x5B;5]; // The thread ID is expected to be the sixth part
</pre>
<p>Давайте зробимо окрему функцію, яка буде відправляти повідомлення до треду:</p>
<pre class="brush: jscript; highlight: [16,25]; title: hookAssigned.gs; notranslate">
/**
 * @param {String} spaceId the ID of the space
 * @param {String} threadId the ID of the thread
 * @param {Object} issue the issue data from JIRA
 */
function hookAssigned(spaceId, threadId, issue) {

  Logger.log(`Send update message to space &quot;${spaceId}&quot; to thread &quot;${threadId}&quot;`)

  const space = `spaces/${spaceId}`
  const thread = `spaces/${spaceId}/threads/${threadId}`

  let message = {
    &quot;text&quot;: &quot;Assigned&quot;,
    &quot;thread&quot;: {
      &quot;name&quot;: thread
    }
  }

  const serviceToken = getServiceAccessToken('messages', &#x5B;'https://www.googleapis.com/auth/chat.messages']);

  return Chat.Spaces.Messages.create(
    message,
    space,
    { &quot;messageReplyOption&quot;: &quot;REPLY_MESSAGE_OR_FAIL&quot; },
    { &quot;Authorization&quot;: `Bearer ${serviceToken}` }
  );
}
</pre>
<p>Зверніть увагу на рядок 16, таким чином слід вказати до якого треду треба відправити відповідь, а також на рядок 25, бо без цього параметру до треду теж не дістатися. Додамо виклик цієї функції до <code>doPost.gs</code> та спробуємо заасайнити таск на себе:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/assigned-457x460.png" alt="" width="457" height="460" class="aligncenter size-medium wp-image-4299" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/assigned-457x460.png 457w, https://anton.shevchuk.name/wp-content/uploads/2024/04/assigned-768x773.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/assigned.png 978w" sizes="auto, (max-width: 457px) 100vw, 457px" /></p>
<blockquote><p>У коді бота буде цікавіше приклад відповіді у тред, але то вже самі подивитесь</p></blockquote>
<h3>Оновлення картки</h3>
<p>Тепер нам треба ще оновити безпосередньо саму картку яку ми відправляли до спейсу, для цього не так багато треба — взяти дані з Jira, знову згенерувати оновлену картку, та відправити її назад до чату:</p>
<pre class="brush: jscript; highlight: [14,16,18]; title: hookUpdated.gs; notranslate">
/**
 * @param {String} spaceId the ID of the space
 * @param {String} threadId the ID of the thread
 * @param {Object} issue the issue data from JIRA
 */
function hookUpdated(spaceId, threadId, issue) {

  Logger.log(`Update the card relative to issue &quot;${issue.key}&quot;`)

  const serviceToken = getServiceAccessToken('messages', &#x5B;'https://www.googleapis.com/auth/chat.messages']);

  const name = `spaces/${spaceId}/messages/${threadId}.${threadId}`

  let message = Chat.Spaces.Messages.get(name, {}, { &quot;Authorization&quot;: `Bearer ${serviceToken}` })

  message = Object.assign({}, message, cardIssue(false, issue))

  return Chat.Spaces.Messages.update(message, name, { &quot;updateMask&quot;: &quot;cardsV2&quot; }, { &quot;Authorization&quot;: `Bearer ${serviceToken}` })
}
</pre>
<p>Невеличке пояснення, у рядку 14 ми отримуємо всю-всю картку зі спейсу, у 16 рядку — генеруємо оновлену картку по новим даним з Jira, у рядку 18 — оновлюємо картку, обов&#8217;язково вказавши потрібну нам <code>updateMask = cardsV2</code>.</p>
<p>Аналогічно треба буде зробити для редагування та зміни статусу issue, в мене в результаті вийшов ось такий <code>doPost.gs</code>:</p>
<pre class="brush: jscript; title: doPost.gs; notranslate">
/**
 * POST request to Web app URL
 */
function doPost(event) {
  const action = event.parameter.action ? event.parameter.action : null

  Logger.log(`WebHook doPost(&quot;${action}&quot;)`)

  if (!event.postData || !event.postData.contents) {
    Logger.log(`POST data is empty`)
  }

  const data = JSON.parse(event.postData.contents);

  if (!data) {
    Logger.log(`POST data is invalid`)
  }

  const scriptProperties = PropertiesService.getScriptProperties();

  // JIRA threadUrl
  const threadUrl = data.fields&#x5B;scriptProperties.getProperty('JIRA_CUSTOM_FIELD')]

  if (!threadUrl || threadUrl.length === 0) {
    Logger.log(`Invalid thread URL for issue &quot;${data.key}&quot;`)
    return HtmlService.createHtmlOutput(`POST request processed: Invalid thread URL`);
  }

  try {
    const parts = threadUrl.split('/'); // Split the URL into parts by '/'
    const spaceId = parts&#x5B;4]; // The space ID is expected to be the fifth part
    const threadId = parts&#x5B;5]; // The thread ID is expected to be the sixth part

    // You can now call your bot method based on event type
    switch (action) {
      case 'assigned':
        hookUpdated(spaceId, threadId, data)
        hookAssigned(spaceId, threadId, data)
        break;
      case 'updated':
        hookUpdated(spaceId, threadId, data)
        break;
      case 'transitioned':
        hookUpdated(spaceId, threadId, data)
        hookTransitioned(spaceId, threadId, data)
        break;
      default:
        Logger.log(&quot;Invalid action&quot;)
        break;
    }
  } catch (e) {
    return HtmlService.createHtmlOutput(`POST request processed with error: ${e}`);
  }

  return HtmlService.createHtmlOutput(&quot;POST request processed&quot;);
}
</pre>
<h3>Діаграма послідовності</h3>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/issue-698x1024.png" alt="Google Chat and Jira integration" width="660" height="968" class="aligncenter size-large wp-image-4300" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/issue-698x1024.png 698w, https://anton.shevchuk.name/wp-content/uploads/2024/04/issue-313x460.png 313w, https://anton.shevchuk.name/wp-content/uploads/2024/04/issue.png 765w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
#
# https://sequencediagram.org/
#
title Google Chat + Jira 
 
actor &quot;User&quot; as U
materialdesignicons F0822 &quot;Chat&quot; as C
materialdesignicons F167A &quot;Apps Script&quot; as S
materialdesignicons F0303 &quot;JIRA&quot; as J
actor &quot;Developer&quot; as D

U-&gt;C:run /issue
activate C
C-&gt;S:call slashIssue()
activate S
S-&gt;C:return dialogIssue() 
deactivate S
note over U,C: &#x5B; Summary ]\n\n&#x5B; Description ]\n\n    &#x5B;  Send  ]
deactivate C

space -3
C-&gt;S:call actionNewIssue()
activate S
S-&gt;J: create issue
activate J
J-&gt;S: issue key
deactivate J
S-&gt;J: get issue
activate J
J-&gt;S: issue data
deactivate J
S-&gt;C: call create()
deactivate S
 
note over U,C: Summary\n\nDescription...\n\n&#x5B; JIRA ]

space -3
C-&gt;S: link to thread
space -3
S-&gt;J: update issue

space -10
create D

D-&gt;J: — assign\n— update\n— comment\n— change status

J-&gt;S: issue data to webhook

space -3
S-&gt;C: message to thread
space -2
S-&gt;C: update card
</pre>
<h3>Домашнє завдання</h3>
<p>Розібратися як створити web-hook для того, щоб додавати коментарі з Jira до треду у Google Chat.</p>
<p>Я лише дам підказку, що для автоматизації краще використовувати «custom data» з можливостями «<a href="https://support.atlassian.com/cloud-automation/docs/smart-values-in-jira-automation/">smart values</a>»:</p>
<pre class="brush: jscript; title: ; notranslate">
{
  &quot;key&quot;: &quot;{{issue.key}}&quot;,
  &quot;fields&quot;: {
    &quot;summary&quot;: &quot;{{issue.fields.summary.jsonEncode}}&quot;,
    &quot;description&quot;: &quot;{{issue.fields.description.jsonEncode}}&quot;,
    &quot;customfield_14033&quot;: &quot;{{issue.fields.customfield_14033}}&quot;
  },
  &quot;comment&quot;: {
    &quot;body&quot;: &quot;{{issue.comments.last.body.jsonEncode}}&quot;,
    &quot;author&quot;: {
      &quot;displayName&quot;: &quot;{{issue.comments.last.author.displayName}}&quot;,
      &quot;name&quot;: &quot;{{issue.comments.last.author.name}}&quot;
    }
  }
}
</pre>
<h3>Source Code</h3>
<p>Код бота доступний на <a href="https://github.com/AntonShevchuk/bender-2.0/">GitHub</a>, реліз 6.0.0 відповідає коду з цієї статті.</p>
<figure id="attachment_4016" aria-describedby="caption-attachment-4016" style="width: 240px" class="wp-caption aligncenter"><a href="https://github.com/AntonShevchuk/bender-2.0/releases/tag/6.0.0"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png" alt="Bender, реліз 6.0.0" width="240" height="240" class="size-full wp-image-4016" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png 240w, https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark-200x200.png 200w" sizes="auto, (max-width: 240px) 100vw, 240px" /></a><figcaption id="caption-attachment-4016" class="wp-caption-text">Bender, білд 6.0.0</figcaption></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-two/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Google Chat Bot. Інтеграція з Jira. Частина I</title>
		<link>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-one/</link>
					<comments>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-one/#comments</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Tue, 30 Apr 2024 08:00:57 +0000</pubDate>
				<category><![CDATA[Google]]></category>
		<category><![CDATA[Apps Script]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4263</guid>

					<description><![CDATA[Статті для початківців закінчились, настав час розробки «дорослого» функціоналу. В цій статті ми будемо додавати до нашого бота інтеграцію з Jira. З початку розповім свій задум: я хочу додати до бота можливість створення тасків та репорту помилок до обраного проєкту у Jira, усі апдейти з Jira повинні прилітати назад до нашого чату, бо користувачі повинні &#8230; <a href="https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-one/" class="more-link">Continue reading <span class="screen-reader-text">Google Chat Bot. Інтеграція з Jira. Частина I</span></a>]]></description>
										<content:encoded><![CDATA[<p>Статті для початківців закінчились, настав час розробки «дорослого» функціоналу. В цій статті ми будемо додавати до нашого бота інтеграцію з Jira.</p>
<p><span id="more-4263"></span></p>
<p>З початку розповім свій задум: я хочу додати до бота можливість створення тасків та репорту помилок до обраного проєкту у Jira, усі апдейти з Jira повинні прилітати назад до нашого чату, бо користувачі повинні бачити що відбувається з issue у Jira.</p>
<p>Як це буде працювати:</p>
<ol>
<li>користувач нашого бота викликає slash-команду <code>/task</code></li>
<li>заповнює форму для таска у Jira</li>
<li>данні з форми летять до Jira, де ми створюємо відповідний issue</li>
<li>до чату відправляємо картку з посиланням на Jira issue</li>
<li>отримавши посилання на тред у чаті, додаємо його до Jira issue</li>
</ol>
<h3>Діалог</h3>
<p>Все знов починається з додавання slash-команди до нашого боту, нагадую, що для цього нам треба перейти до налаштувань <a href="https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat">Google Chat API</a>.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-slash-task-460x402.png" alt="" width="460" height="402" class="aligncenter size-medium wp-image-4268" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-slash-task-460x402.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-slash-task-768x672.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-slash-task.png 990w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Ми вже створювали діалоги, тож не забудьте поставити позначку <code>&#x2705; Opens a dialog</code>.</p>
<p>Зміни до <code>onMessage()</code> теж не складні, нам треба лише піймати нову команду з ID 12:</p>
<pre class="brush: jscript; highlight: [17,18]; title: ; notranslate">
/**
 * Responds to a MESSAGE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onMessage(event) {
  if (event.message.slashCommand) {
    // Checks for the presence of event.message.slashCommand
    // The ID for your slash command
    switch (event.message.slashCommand.commandId) {
      case 1:
        return slashHelp(event)
      case 10:
        return slashBender(event)
      case 11:
        return slashWhisky(event)
      case 12:
        return slashTask(event)
      case 20:
        return slashCard(event)
      case 21:
        return slashNotes(event)
      case 22:
        return slashPoll(event)
    }
  } else {
    // If the Chat app doesn't detect a slash command
    // ...
  }
}
</pre>
<p>Створимо функцію <code>slashTask()</code> у відповідному файлі:</p>
<pre class="brush: jscript; highlight: [10]; title: slashTask.gs; notranslate">
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function slashTask(event) {
  // nothing for now
  return dialogIssue(event)
}
</pre>
<p>Тут лише виклик <code>dialogIssue()</code>, тож перейдемо до відповідного <code>dialogIssue.gs</code>. Я відразу приведу код з обробкою помилок, бо у попередній частині ми вже детально розбирали як те робиться, а повторюватися мені ліньки:</p>
<pre class="brush: jscript; collapse: true; highlight: [21,31]; light: false; title: dialogIssue.gs; toolbar: true; notranslate">
/**
 * Create a dialog for Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 * @param {Object} request the data from the form or from parameters.
 * @param {String} type the type of issue
 * @return {object} open a Dialog in Google Chat.
 */
function dialogIssue(event, request = null, type = &quot;Task&quot;) {
  let card = {
    &quot;action_response&quot;: {
      &quot;type&quot;: RESPONSE_TYPE_DIALOG,
      &quot;dialog_action&quot;: {
        &quot;dialog&quot;: {
          &quot;body&quot;: {
            &quot;sections&quot;: &#x5B;
              {
                &quot;widgets&quot;: &#x5B;]
              }
            ],
            &quot;name&quot;: `New ${type}`,
            &quot;fixedFooter&quot;: {
              &quot;primaryButton&quot;: {
                &quot;text&quot;: &quot;Create&quot;,
                &quot;onClick&quot;: {
                  &quot;action&quot;: {
                    &quot;function&quot;: &quot;actionNewIssue&quot;,
                    &quot;parameters&quot;: &#x5B;
                      {
                        &quot;key&quot;: &quot;type&quot;,
                        &quot;value&quot;: type
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  if (request &amp;&amp; !request.summary) {
    card.action_response.dialog_action.dialog.body.sections&#x5B;0].widgets.push({
        &quot;textParagraph&quot;: {
          &quot;text&quot;: &quot;&lt;font color='#c1121f'&gt;The summary is required&lt;/font&gt;&quot;
        }
      }
    )
  }

  card.action_response.dialog_action.dialog.body.sections&#x5B;0].widgets.push(
    {
      &quot;textInput&quot;: {
        &quot;label&quot;: &quot;Summary*&quot;,
        &quot;type&quot;: &quot;SINGLE_LINE&quot;,
        &quot;name&quot;: &quot;summary&quot;,
        &quot;hintText&quot;: &quot;&quot;,
        &quot;value&quot;: request ? request.summary : &quot;&quot;
      }
    }
  )

  if (request &amp;&amp; !request.description) {
    card.action_response.dialog_action.dialog.body.sections&#x5B;0].widgets.push({
        &quot;textParagraph&quot;: {
          &quot;text&quot;: &quot;&lt;font color='#c1121f'&gt;The description is required&lt;/font&gt;&quot;
        }
      }
    )
  }

  card.action_response.dialog_action.dialog.body.sections&#x5B;0].widgets.push(
    {
      &quot;textInput&quot;: {
        &quot;label&quot;: &quot;Description*&quot;,
        &quot;type&quot;: &quot;MULTIPLE_LINE&quot;,
        &quot;name&quot;: &quot;description&quot;,
        &quot;hintText&quot;: &quot;&quot;,
        &quot;value&quot;: request ? request.description : &quot;&quot;
      }
    }
  )

  card.action_response.dialog_action.dialog.body.sections&#x5B;0].widgets.push({
      &quot;textParagraph&quot;: {
        &quot;text&quot;: &quot;&lt;font color='#c1121f'&gt;*&lt;/font&gt; - all fields are required.&quot;
      }
    }
  )

  return card
}
</pre>
<blockquote><p>Тут я додав ще аргумент <code>type</code>, бо ми не тільки таски будемо створювати, а й репортити про помилки.</p></blockquote>
<p>Після цього ви вже можете користуватися командою <code>/task</code>, та отримаєте наступний діалог:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-460x221.png" alt="Task dialog" width="460" height="221" class="aligncenter size-medium wp-image-4269" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-460x221.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-1024x491.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-768x368.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue.png 1268w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Відправка ще не буде працювати, треба додати до <code>onCardClick()</code> підтримку нового action <code>actionNewIssue</code>:</p>
<pre class="brush: jscript; highlight: [12,13]; title: ; notranslate">
/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  switch (event.common.invokedFunction) {
    // 
    // ... the code was cropped
    // 
    // - /task
    case 'actionNewIssue':
      return actionNewIssue(event)
  }
}
</pre>
<p>Відповідний функціонал <code>actionNewIssue.gs</code> ми будемо роздивлятися по частинам. </p>
<p>Частина перша — валідація даних:</p>
<pre class="brush: jscript; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewIssue(event) {

  const parameters = event.common.parameters
  const formHandler = new FormInputHandler(event)

  const summary = formHandler.getTextValue('summary')
  const description = formHandler.getTextValue('description')
  const type = parameters.type

  // validation
  if (!summary.length || !description.length) {
    return dialogIssue(event, { summary, description })
  }

  //
  // ... the code was cropped
  // 
}
</pre>
<p>Тут в нас лише перевірка на те що поля не пусті, та у випадку помилки, ми повертаємо картку з <code>dialogIssue()</code> з текстом помилок:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-errors-460x267.png" alt="Task dialog with errors" width="460" height="267" class="aligncenter size-medium wp-image-4270" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-errors-460x267.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-errors-1024x595.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-errors-768x446.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-issue-errors.png 1260w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<blockquote><p>Ви можете додати більш складні перевірки до коду, дійте на свій розсуд</p></blockquote>
<p>Далі нам вже треба працювати з Jira, але вона ще не готова :)</p>
<h3>Підготовка Jira проєкту</h3>
<p>Для початку, нам потрібен JIRA проєкт, та краще окремий користувач для нашого бота, у проєкті йому треба буде надати адмінські права:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-bot-460x64.png" alt="Jira Bot account" width="460" height="64" class="aligncenter size-medium wp-image-4271" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-bot-460x64.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-bot-768x107.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-bot.png 990w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>А ще треба створити персональний Access Token для вашого користувача:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/access-token-417x460.png" alt="Access Token" width="417" height="460" class="aligncenter size-medium wp-image-4272" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/access-token-417x460.png 417w, https://anton.shevchuk.name/wp-content/uploads/2024/04/access-token-929x1024.png 929w, https://anton.shevchuk.name/wp-content/uploads/2024/04/access-token-768x847.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/access-token.png 1034w" sizes="auto, (max-width: 417px) 100vw, 417px" /></p>
<p>Access Token який ви отримаєте слід зберегти у Properties вашого бота, туди також слід додати посилання на Jira та ключ вашого проєкту:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-1024x348.png" alt="Properties" width="660" height="224" class="aligncenter size-large wp-image-4273" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-1024x348.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-460x156.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties-768x261.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/properties.png 1536w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>А тепер увага, для того, щоб у вашому issue зберігалось посилання на відповідне повідомлення у Google Chat слід додати Custom Field, типу URL до потрібних Screens у вашому проєкті. Якщо ви не розумієте про що мова — попросіть вашого адміністратора Jira зробити це. Screen мого проєкту у Jira виглядає наступним чином:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-screen-1024x708.png" alt="Jira Screen Settings" width="660" height="456" class="aligncenter size-large wp-image-4274" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-screen-1024x708.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-screen-460x318.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-screen-768x531.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-screen.png 1400w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Ідентифікатор новоствореного Custom Field я також додаю до Properties:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/custom-field-1024x113.png" alt="Jira Custom Field" width="660" height="73" class="aligncenter size-large wp-image-4275" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/custom-field-1024x113.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/custom-field-460x51.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/custom-field-768x85.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/custom-field.png 1452w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<h3>Створення Issue</h3>
<p>Так, Jira готова, додамо класс для взаємодією з нею, та створимо той issue:</p>
<pre class="brush: jscript; title: JiraApi.gs; notranslate">
/**
 * Class for working with JIRA API v2
 *
 * @link https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/
 */
class JiraAPI {
  constructor() {
    const scriptProperties = PropertiesService.getScriptProperties();
    this.baseUrl = scriptProperties.getProperty('JIRA_URL');
    this.token = scriptProperties.getProperty('JIRA_TOKEN');
    this.project = scriptProperties.getProperty('JIRA_PROJECT');
    this.customField = scriptProperties.getProperty('JIRA_CUSTOM_FIELD');
    this.options = {
      headers: {
        &quot;Authorization&quot;: &quot;Bearer &quot; + this.token,
        &quot;Content-Type&quot;: &quot;application/json&quot;
      },
      muteHttpExceptions: true // Set to false to throw exceptions for HTTP errors
    }
  }

  /**
   * Makes a HTTP request to the JIRA API with error handling.
   *
   * @param {String} endpoint - The API endpoint.
   * @param {Object} options - The options for the fetch call, including method, headers, and payload.
   * @return {Object} The JSON response from the JIRA API.
   */
  fetch(endpoint, options) {
    const url = `${this.baseUrl}${endpoint}`;

    Logger.log(url)
    Logger.log(options)

    try {
      const response = UrlFetchApp.fetch(url, Object.assign({}, options, this.options));
      const responseCode = response.getResponseCode()
      const contentText = response.getContentText()
      const contentType = response.getHeaders()&#x5B;'Content-Type']

      Logger.log(`Response code: ${responseCode}`)
      Logger.log(`Content type: ${contentType}`)

      if (responseCode &gt;= 200 &amp;&amp; responseCode &lt; 300) {
        // it strange, FIXME
        if (contentText.length &amp;&amp; contentType.search('application/json') !== -1) {
          return JSON.parse(contentText)
        } else {
          return response.getResponseCode()
        }
      } else {
        console.error(`JIRA API request to ${endpoint} failed with code: ${responseCode}, response: ${contentText}`);
        throw new Error(`Request failed with code: ${responseCode}`);
      }
    } catch (e) {
      console.error(`Error making request to JIRA API: ${e}`);
      throw e; // Rethrow the error after logging
    }
  }

  /**
   * Retrieves details of an issue from JIRA.
   *
   * @param {String} issueIdOrKey - The ID or key of the issue
   */
  getIssue(issueIdOrKey) {
    return this.fetch(`/rest/api/2/issue/${issueIdOrKey}`, {
      &quot;method&quot;: &quot;get&quot;
    });
  }

  /**
   * Creates a new issue in JIRA.
   *
   * @param {Object} issueData
   */
  createIssue(issueData) {
    return this.fetch(&quot;/rest/api/2/issue&quot;, {
      &quot;method&quot;: &quot;post&quot;,
      &quot;payload&quot;: JSON.stringify(issueData)
    });
  }

  /**
   * Updates an issue in JIRA.
   *
   * @param {String} issueIdOrKey - The ID or key of the issue
   * @param {Object} issueData
   */
  updateIssue(issueIdOrKey, issueData) {
    return this.fetch(`/rest/api/2/issue/${issueIdOrKey}`, {
      &quot;method&quot;: &quot;put&quot;,
      &quot;payload&quot;: JSON.stringify(issueData)
    });
  }
}
</pre>
<p>Цей клас вміє не так багато, але нам цього вистачить:</p>
<ul>
<li><code>getIssue()</code> — отримати issue по ключу або ідентифікатору</li>
<li><code>createIssue()</code> — створити issue</li>
<li><code>updateIssue()</code> — оновити issue по ключу або ідентифікатору</li>
</ul>
<p>⚠️ Після додавання цього коду, ваш бот буде питати користувачів дозвіл на відправку запитів на сторонні ресурси:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/configure-460x369.png" alt="Google Configure Alert" width="460" height="369" class="aligncenter size-medium wp-image-4276" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/configure-460x369.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/configure-1024x821.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/configure-768x616.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/configure.png 1300w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/access-460x249.png" alt="Google Configure Access" width="460" height="249" class="aligncenter size-medium wp-image-4277" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/access-460x249.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/access-768x415.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/access.png 992w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Давайте продовжимо роботу над <code>actionNewIssue.gs</code>. Додамо другу частину до нашого пазлу — створення issue:</p>
<pre class="brush: jscript; highlight: [24,28]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewIssue(event) {
  //
  // ... the code was cropped
  // 

  Logger.log(`New Issue: ${summary}`)

  const jiraApi = new JiraAPI()

  let issueData = {
    &quot;fields&quot;: {
      &quot;project&quot;: { &quot;key&quot;: jiraApi.project },
      &quot;summary&quot;: summary,
      &quot;description&quot;: googleChatToJira(description),
      &quot;issuetype&quot;: { &quot;name&quot;: type },
      &quot;reporter&quot;: { &quot;name&quot;: event.user.email },
      &quot;assignee&quot;: null,
    }
  };

  const response = jiraApi.createIssue(issueData);

  Logger.log(`The Issue Key is &quot;${response.key}&quot;`)

  const issue = jiraApi.getIssue(response.key)

  //
  // ... the code was cropped
  // 
}
</pre>
<p>Коли ми створюємо іssue (рядок 24), то Jira повертає не весь issue у відповідь, тому щоб отримати все що нам потрібно ми робимо ще один запит до Jira API (рядок 28).</p>
<p>Наш наступний крок — то сформувати картку та відправити її до чату. За формування картки буде відповідати функція <code>cardIssue()</code>:</p>
<pre class="brush: jscript; collapse: true; light: false; title: cardIssue.gs; toolbar: true; notranslate">
/**
 * Create a card for Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 * @param {Object} issue the JIRA issue
 *
 * @return {Object} the card object
 */
function cardIssue(event, issue) {

  const scriptProperties = PropertiesService.getScriptProperties();
  const jiraUrl = scriptProperties.getProperty('JIRA_URL')
  const jiraIssueUrl = `${jiraUrl}/browse/${issue.key}`

  Logger.log(`The URL to Issue is ${jiraIssueUrl}`)

  let card = {
    &quot;cardsV2&quot;: &#x5B;
      {
        &quot;cardId&quot;: &quot;presale-request&quot;,
        &quot;card&quot;: {
          &quot;sections&quot;: &#x5B;
            {
              &quot;widgets&quot;: &#x5B;
                {
                  &quot;decoratedText&quot;: {
                    &quot;icon&quot;: {
                      // &quot;iconUrl&quot;: issue.fields.issuetype.iconUrl
                      &quot;materialIcon&quot;: {
                        &quot;name&quot;: issue.fields.issuetype.name === &quot;Task&quot; ? &quot;task&quot; : &quot;bug_report&quot;
                      }
                    },
                    &quot;text&quot;: issue.fields.summary,
                  }
                },
                {
                  &quot;textParagraph&quot;: {
                    &quot;text&quot;: jiraToGoogleChat(issue.fields.description)
                  }
                }
              ]
            },
            {
              &quot;widgets&quot;: &#x5B;
                {
                  &quot;buttonList&quot;: {
                    &quot;buttons&quot;: &#x5B;
                      {
                        &quot;text&quot;: &quot;JIRA&quot;,
                        &quot;color&quot;: {
                          &quot;red&quot;: 0.75,
                          &quot;green&quot;: 0.85,
                          &quot;blue&quot;: 0.95,
                          &quot;alpha&quot;: 1
                        },
                        &quot;icon&quot;: {
                          &quot;materialIcon&quot;: {
                            &quot;name&quot;: &quot;task&quot;
                          }
                        },
                        &quot;onClick&quot;: {
                          &quot;openLink&quot;: {
                            &quot;url&quot;: jiraIssueUrl
                          }
                        }
                      }
                    ]
                  }
                }
              ]
            },
            {
              &quot;widgets&quot;: &#x5B;
                {
                  &quot;columns&quot;: {
                    &quot;columnItems&quot;: &#x5B;
                      {
                        &quot;widgets&quot;: &#x5B;
                          {
                            &quot;decoratedText&quot;: {
                              &quot;icon&quot;: {
                                &quot;iconUrl&quot;: issue.fields.status.iconUrl
                              },
                              &quot;topLabel&quot;: &quot;Status&quot;,
                              &quot;text&quot;: issue.fields.status.name,
                              &quot;bottomLabel&quot;: &quot;&quot;,
                            }
                          }

                        ]
                      },
                      {
                        &quot;widgets&quot;: &#x5B;
                          {
                            &quot;decoratedText&quot;: {
                              &quot;icon&quot;: {
                                &quot;knownIcon&quot;: &quot;PERSON&quot;
                                // &quot;iconUrl&quot;: issue.fields.assignee.avatarUrls&#x5B;&quot;48x48&quot;]
                              },
                              &quot;topLabel&quot;: &quot;Assigned&quot;,
                              &quot;text&quot;: issue.fields.assignee ? issue.fields.assignee.displayName : 'Unassigned',
                              &quot;bottomLabel&quot;: issue.fields.assignee ? issue.fields.assignee.emailAddress : '',
                            }
                          }
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }

  // we have the event if a user works with chat
  if (event) {
    card.cardsV2&#x5B;0].card&#x5B;&quot;header&quot;] = {
      &quot;title&quot;: event.user.displayName,
      &quot;subtitle&quot;: event.user.email,
      &quot;imageUrl&quot;: event.user.avatarUrl,
      &quot;imageType&quot;: &quot;CIRCLE&quot;
    }

    card.cardsV2&#x5B;0].card&#x5B;&quot;sections&quot;].push(
      {
        &quot;widgets&quot;: &#x5B;
          {
            &quot;decoratedText&quot;: {
              &quot;icon&quot;: {
                &quot;knownIcon&quot;: &quot;CLOCK&quot;
              },
              &quot;text&quot;: new Date().toLocaleString(),
            }
          }
        ]
      }
    )
  }

  return card
}
</pre>
<p>Далі буде ще складніше, нам треба відправити підготовлену картку до чату за допомоги API, бо таким чином, ми можемо отримати ідентифікатор повідомлення та зберегти його до issue у Jira (так, до того custom field). Але це не так просто зробити. </p>
<h3>Chat API</h3>
<p>Нашим наступним кроком буде додавання до проєкту сервіса Chats API, тож поверніться до нашого редактора коду, та жміть «+» напроти Services:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/add-service-358x460.png" alt="Add service to Apps Script" width="358" height="460" class="aligncenter size-medium wp-image-4281" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/add-service-358x460.png 358w, https://anton.shevchuk.name/wp-content/uploads/2024/04/add-service-798x1024.png 798w, https://anton.shevchuk.name/wp-content/uploads/2024/04/add-service-768x986.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/add-service.png 1000w" sizes="auto, (max-width: 358px) 100vw, 358px" /></p>
<p>Оберіть та додайте Google Chat API (документація до API доступна на сторінці <a href="https://developers.google.com/apps-script/advanced/chat?authuser=0">https://developers.google.com/apps-script/advanced/chat</a>).</p>
<p>Тепер в нас з&#8217;явиться можливість відправляти до чату повідомлення:</p>
<pre class="brush: jscript; title: ; notranslate">
Chat.Spaces.Messages.create(
  {'text': 'Hello world!'},
  event.space.name
);
</pre>
<p>Хоча, ні, ви отримаєте помилку:</p>
<pre class="brush: bash; title: ; notranslate">
GoogleJsonResponseException: API call to chat.spaces.messages.create failed with error: Request had insufficient authentication scopes.
</pre>
<p>Треба ще додати відповідний скоуп до <code>appssript.json</code>:</p>
<pre class="brush: jscript; title: ; notranslate">
{
  &quot;oauthScopes&quot;: &#x5B;&quot;https://www.googleapis.com/auth/chat.messages.create&quot;], 
}
</pre>
<p>Тепер все буде працювати, звісно у користувача бот запитає потрібні дозволи, щоб писати щось у чат, та все ж таки результат буде:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/hello-world.png" alt="" width="386" height="142" class="aligncenter size-full wp-image-4283" /></p>
<p>У коді, ви отримаєте відповідь від API:</p>
<pre class="brush: jscript; title: ; notranslate">
{ 
  name: 'spaces/AAAABBBB/messages/QWERTY.QWERTY',
  argumentText: 'Hello world!',
  text: 'Hello world!',
  sender: { type: 'HUMAN', name: 'users/111111234567890000000' },
  createTime: '2024-04-27T21:58:02.427887Z',
  space: { name: 'spaces/AAAABBBB' },
  formattedText: 'Hello world!',
  thread: { name: 'spaces/AAAABBBB/threads/QWERTY' }
}
</pre>
<p>Тож вже можна посилання на тред закинути до Jira. Повертаємось до редагування <code>actionNewIssue.gs</code>:</p>
<pre class="brush: jscript; highlight: [12,28]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewIssue(event) {
  //
  // ... the code was cropped
  // 

  try {
    const card = cardIssue(event, issue)

    const message = Chat.Spaces.Messages.create(card, event.space.name);

    // Extracting the thread name from the response
    // Assuming ID is spaces/AAAAAAAA/threads/BBBBBBBB
    const &#x5B; , spaceId, , threadAndMessageId] = message.thread.name.split('/');

    const threadUrl = `https://chat.google.com/room/${spaceId}/${threadAndMessageId}/${threadAndMessageId}`;

    Logger.log(`New thread ${threadUrl}`)

    let updateData = {
      &quot;fields&quot;: {}
    }

    updateData.fields&#x5B;jiraApi.customField] = threadUrl

    jiraApi.updateIssue(response.key, updateData)
  } catch (err) {
    Logger.log('Failed to create message with error %s', err.message);
  }

  return OK()
}
</pre>
<p>Спробуємо створити таск використовуючи команду <code>/task</code> та отримаємо у чаті наступну картку:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/task.png" alt="Task card in Google Chat" width="574" height="860" class="aligncenter size-full wp-image-4291" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/task.png 574w, https://anton.shevchuk.name/wp-content/uploads/2024/04/task-307x460.png 307w" sizes="auto, (max-width: 574px) 100vw, 574px" /></p>
<p>В Jira буде створено відповідний issue:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-issue-1024x707.png" alt="" width="660" height="456" class="aligncenter size-large wp-image-4292" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-issue-1024x707.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-issue-460x318.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-issue-768x530.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-issue.png 1298w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>І це вже непоганий результат. Мені подобається що в нас виходить.</p>
<h3>Діаграма послідовності</h3>
<p>Приведу ще діаграму послідовності, вона наче не складна вийшла:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-integration.png" alt="Jira integration sequence diagram" width="533" height="946" class="aligncenter size-full wp-image-4290" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-integration.png 533w, https://anton.shevchuk.name/wp-content/uploads/2024/04/jira-integration-259x460.png 259w" sizes="auto, (max-width: 533px) 100vw, 533px" /></p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
#
# https://sequencediagram.org/
#
title Dialogs
 
actor &quot;User&quot; as U
materialdesignicons F0822 &quot;Chat&quot; as C
materialdesignicons F167A &quot;Apps Script&quot; as S
materialdesignicons F0303 &quot;JIRA&quot; as J

U-&gt;C:run /issue
activate C
C-&gt;S:call slashIssue()
activate S
S-&gt;C:return dialogIssue() 
deactivate S
note over U,C: &#x5B; Summary ]\n\n&#x5B; Description ]\n\n    &#x5B;  Send  ]
deactivate C

space -3
C-&gt;S:call actionNewIssue()
activate S
S-&gt;J: create issue
activate J
J-&gt;S: issue key
deactivate J
S-&gt;J: get issue
activate J
J-&gt;S: issue data
deactivate J
S-&gt;C: call create()
deactivate S
 
note over U,C: Summary\n\nDescription...\n\n&#x5B; JIRA ]

space -3
C-&gt;S: link to thread
space -3
S-&gt;J: update issue
</pre>
<p>Тож на цьому першу частину я вважаю можна завершувати, але це ще не весь функціонал який я хотів реалізувати, але можливо вам його вистачить.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/google/google-chat-bot-jira-integration-part-one/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Google Chat Bot. Взаємодія та оновлення карток</title>
		<link>https://anton.shevchuk.name/google/google-chat-bot-interactions-and-card-updates/</link>
					<comments>https://anton.shevchuk.name/google/google-chat-bot-interactions-and-card-updates/#respond</comments>
		
		<dc:creator><![CDATA[Anton Shevchuk]]></dc:creator>
		<pubDate>Tue, 16 Apr 2024 06:40:15 +0000</pubDate>
				<category><![CDATA[Google]]></category>
		<category><![CDATA[Apps Script]]></category>
		<guid isPermaLink="false">https://anton.shevchuk.name/?p=4240</guid>

					<description><![CDATA[Настав час продовжити розробку бота для Google Chat. Цього разу, ми навчимо його створювати голосування, це той функціонал якого дуже не вистачає при постійній комунікації у робочому спейсі. Для реалізації нам треба буде розібратися як взаємодіяти та оновлювати картку у чаті. Діалог Почнемо знов з додавання slash-команди до нашого боту, нагадую, що для цього нам &#8230; <a href="https://anton.shevchuk.name/google/google-chat-bot-interactions-and-card-updates/" class="more-link">Continue reading <span class="screen-reader-text">Google Chat Bot. Взаємодія та оновлення карток</span></a>]]></description>
										<content:encoded><![CDATA[<p>Настав час продовжити розробку бота для Google Chat. Цього разу, ми навчимо його створювати голосування, це той функціонал якого дуже не вистачає при постійній комунікації у робочому спейсі. Для реалізації нам треба буде розібратися як взаємодіяти та оновлювати картку у чаті.</p>
<p><span id="more-4240"></span></p>
<h3>Діалог</h3>
<p>Почнемо знов з додавання slash-команди до нашого боту, нагадую, що для цього нам треба перейти до налаштувань <a href="https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat">Google Chat API</a>.</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/slash-command-poll-460x403.png" alt="" width="460" height="403" class="aligncenter size-medium wp-image-4243" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/slash-command-poll-460x403.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/slash-command-poll-768x672.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/slash-command-poll.png 996w" sizes="auto, (max-width: 460px) 100vw, 460px" /></p>
<p>Ми вже створювали діалоги, тож не забудьте поставити позначку <code>&#x2705; Opens a dialog</code>.</p>
<p>Зміни до <code>Code.gs</code> теж не складні, нам треба лише піймати нову команду з ID 22:</p>
<pre class="brush: jscript; highlight: [21,22]; title: ; notranslate">
/**
 * Responds to a MESSAGE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onMessage(event) {
  if (event.message.slashCommand) {
    // Checks for the presence of event.message.slashCommand
    // The ID for your slash command
    switch (event.message.slashCommand.commandId) {
      case 1:
        return slashHelp(event)
      case 10:
        return slashBender(event)
      case 11:
        return slashWhisky(event)
      case 20:
        return slashCard(event)
      case 21:
        return slashNotes(event)
      case 22:
        return slashPoll(event)
    }
  } else {
    // If the Chat app doesn't detect a slash command
    // ...
  }
}
</pre>
<p>Створимо функцію <code>slashPoll()</code> у відповідному файлі:</p>
<pre class="brush: jscript; highlight: [10]; title: slashPoll.gs; notranslate">
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function slashPoll(event) {
  // nothing for now
  return dialogPoll(event)
}
</pre>
<p>Тут в нас буде лише виклик <code>dialogPoll()</code>, так зроблено для подальшого розширення функціоналу, наразі у <code>dialogPoll.gs</code> буде лише створення діалогу:</p>
<pre class="brush: jscript; collapse: true; highlight: [81]; light: false; title: dialogPoll.gs; toolbar: true; notranslate">
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function dialogPoll(event) {
  return {
    &quot;action_response&quot;: {
      &quot;type&quot;: &quot;DIALOG&quot;,
      &quot;dialog_action&quot;: {
        &quot;dialog&quot;: {
          &quot;body&quot;: {
            &quot;sections&quot;: &#x5B;
              {
                &quot;header&quot;: &quot;Create New Poll&quot;,
                &quot;collapsible&quot;: true,
                &quot;uncollapsibleWidgetsCount&quot;: 4,
                &quot;widgets&quot;: &#x5B;
                  {
                    &quot;textParagraph&quot;: {
                      &quot;text&quot;: &quot;Enter the poll topic and up to 10 choices in the poll. Blank options will be omitted.&quot;
                    }
                  },
                  {
                    &quot;textInput&quot;: {
                      &quot;name&quot;: &quot;question&quot;,
                      &quot;label&quot;: &quot;Ask a question*&quot;,
                    }
                  },
                  {
                    &quot;textInput&quot;: {
                      &quot;name&quot;: &quot;option1&quot;,
                      &quot;label&quot;: &quot;1️⃣ Option*&quot;,
                    }
                  },
                  {
                    &quot;textInput&quot;: {
                      &quot;name&quot;: &quot;option2&quot;,
                      &quot;label&quot;: &quot;2️⃣ Option*&quot;,
                    }
                  },
                  /* Options 3, 4, 5 .. 9 */
                  {
                    &quot;textInput&quot;: {
                      &quot;name&quot;: &quot;option10&quot;,
                      &quot;label&quot;: &quot; Option&quot;,
                    }
                  }
                ]
              },
              {
                &quot;header&quot;: &quot;Options&quot;,
                &quot;collapsible&quot;: false,
                &quot;widgets&quot;: &#x5B;
                  {
                    &quot;decoratedText&quot;: {
                      &quot;text&quot;: &quot;Multiple Answers&quot;,
                      &quot;bottomLabel&quot;: &quot;If this checked the voters can choose more than option&quot;,
                      &quot;switchControl&quot;: {
                        &quot;name&quot;: &quot;multi&quot;,
                        &quot;selected&quot;: true,
                        &quot;controlType&quot;: &quot;SWITCH&quot;
                      }
                    }
                  }
                ]
              }
            ],
            &quot;fixedFooter&quot;: {
              &quot;primaryButton&quot;: {
                &quot;icon&quot;: {
                  &quot;materialIcon&quot;: {
                    &quot;name&quot;: &quot;send&quot;
                  }
                },
                &quot;text&quot;: &quot;Send&quot;,
                &quot;onClick&quot;: {
                  &quot;action&quot;: {
                    &quot;function&quot;: &quot;actionNewPoll&quot;
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
</pre>
<blockquote><p>Нагадую, що для створення діалогів та карток зручно використовувати сервіс <a href="https://addons.gsuite.google.com/uikit/builder">Card Builder</a></p></blockquote>
<p>Після цього ви вже можете користуватися командою <code>/poll</code>, та навіть зможете отримати наступний діалог:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-new-poll-377x460.png" alt="" width="377" height="460" class="aligncenter size-medium wp-image-4244" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/create-new-poll-377x460.png 377w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-new-poll-838x1024.png 838w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-new-poll-768x938.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/create-new-poll.png 922w" sizes="auto, (max-width: 377px) 100vw, 377px" /></p>
<p>Тепер слід додати обробку onclick-функції з попереднього лістингу кода, для цього внесіть зміни до <code>onCardClick(event)</code>:</p>
<pre class="brush: jscript; highlight: [14,15]; title: ; notranslate">
/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  switch (event.common.invokedFunction) {
 
    //
    // ... the code was cropped
    //

    // - /poll
    case 'actionNewPoll':
      return actionNewPoll(event)
  }
}
</pre>
<p>Знов створимо нову функцію <code>actionNewPoll()</code> у новому файлі <code>actionNewPoll.gs</code>:</p>
<pre class="brush: jscript; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {

  const formHandler = new FormInputHandler(event)

  formHandler.getTextValue('question')
  formHandler.getBooleanValue('multi')
  formHandler.getTextValue('option1')
  // ...
  formHandler.getTextValue('option10')

  //
  // ... the code was cropped
  //

}
</pre>
<blockquote><p>Тут трохи треба призупинитися, та нагадати, що клас <code>FormInputHandler</code> ми створювали раніше, та він відповідає за отримання даних з форми, а зараз ми лише додали новий метод <code>getBooleanValue()</code>, та я гадаю з ним не виникне непорозумінь. </p></blockquote>
<p>Але я не просто хочу отримати дані, я хочу зробити валідацію, щоб можна було внести зміни у форму у випадку, якщо виникла помилка.<br />
Для цього я додам декілька перевірок, і у випадку невдачі буду повертати попередню форму з текстом помилки:</p>
<pre class="brush: jscript; highlight: [22,26]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {

  const formHandler = new FormInputHandler(event)

  let data = {
    question: formHandler.getTextValue('question'),
    multi: formHandler.getBooleanValue('multi'),
    options: &#x5B;]
  }

  for (let i = 1; i &lt;= 10; i++) {
    let option = formHandler.getTextValue(`option${i}`)
    if (option.length) {
      data.options.push(option)
    }
  }

  if (!data.question.length) {
    return dialogPoll(event, data)
  }

  if (data.options.length &lt; 2) {
    return dialogPoll(event, data)
  }

  //
  // ... the code was cropped
  //

}
</pre>
<p>У цьомі коді я формую об&#8217;єкт data, щоб було зручніше працювати з даними з форми, та роблю усього дві перевірки — що в нас є питання і що варіантів відповідей не менше двох. Коли виникає помилка я повертаю знов діалог, який ми додали до функції <code>dialogPoll()</code>. Нам лише треба внести зміни, щоб на формі зберігались попередньо внесені дані та відображався текст помилки:</p>
<pre class="brush: jscript; highlight: [9,32,39,55,81]; title: ; notranslate">
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 * @param {Object} data the data from a form.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function dialogPoll(event, data = null) {

  let card = {
    'action_response': {
      'type': 'DIALOG',
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': &#x5B;
              {
                'header': 'Create New Poll',
                'collapsible': true,
                'uncollapsibleWidgetsCount': 4,
                'widgets': &#x5B;
                  {
                    &quot;textParagraph&quot;: {
                      &quot;text&quot;: &quot;Enter the poll topic and up to 10 choices in the poll. Blank options will be omitted.&quot;
                    }
                  },
                  {
                    'textInput': {
                      'name': 'question',
                      'label': 'Ask a question*',
                      'value': data &amp;&amp; data.question ? data.question : ''
                    }
                  },
                  {
                    'textInput': {
                      'name': 'option1',
                      'label': '1️⃣ Option*',
                      'value': data &amp;&amp; data.options &amp;&amp; data.options&#x5B;0] ? data.options&#x5B;0] : ''
                    }
                  }
                  /* Options 2, 3, 4, 5 .. 10 */
                ]
              },
              {
                &quot;header&quot;: &quot;Options&quot;,
                &quot;collapsible&quot;: false,
                &quot;widgets&quot;: &#x5B;
                  {
                    &quot;decoratedText&quot;: {
                      &quot;text&quot;: &quot;Multiple Answers&quot;,
                      &quot;bottomLabel&quot;: &quot;If this checked the voters can choose more than option&quot;,
                      &quot;switchControl&quot;: {
                        &quot;name&quot;: &quot;multi&quot;,
                        &quot;selected&quot;: (data &amp;&amp; data.multi) ? data.multi : true,
                        &quot;controlType&quot;: &quot;SWITCH&quot;
                      }
                    }
                  }
                ]
              }
            ],
            'fixedFooter': { /* ... */ }
          }
        }
      }
    }
  }

  if (data) {
    let section = {
      'widgets': &#x5B;
        {
          &quot;textParagraph&quot;: {
            &quot;text&quot;: &quot;&lt;b&gt;&lt;font color='#ff0000'&gt;Please fill in all required data, including the question and two or more options.&lt;/font&gt;&lt;/b&gt;&quot;
          }
        }
      ]
    }

    card.action_response.dialog_action.dialog.body.sections.unshift(section)
  }

  return card;
}
</pre>
<p>У такий спосіб ми повернемо користувачу форму редагування голосування, та не загубимо його дані (зверніть увагу на рядки 9,32,39 та 55). Для більшої інформативності додаємо текст помилки, щоб користувач не розгубився, що наразі відбувається (рядок 81).</p>
<h3>Картка для голосування</h3>
<p>Перевірки всі зроблені, настав час створити картку для голосування та відправити її до чату. Відразу продемонструю дизайн картки, і потім приведу відповідний код до неї:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-1024x620.png" alt="" width="660" height="400" class="aligncenter size-large wp-image-4245" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-1024x620.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-460x278.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-768x465.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card.png 1028w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<p>Повернемось на попередній крок, та додамо до функції <code>actionNewPoll()</code> виклик іншої функції — <code>cardPoll()</code>:</p>
<pre class="brush: jscript; highlight: [17]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {
  const formHandler = new FormInputHandler(event)

  let data = {
    question: formHandler.getTextValue('question'),
    multi: formHandler.getBooleanValue('multi'),
    options: &#x5B;]
  }

  //
  // ... the code was cropped
  //

  return cardPoll(event, data)
}
</pre>
<p>Звісно реалізація <code>cardPoll()</code> буде у відповідному файлі:</p>
<pre class="brush: jscript; collapse: true; highlight: [70]; light: false; title: cardPoll.gs; toolbar: true; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 * @param {Object} data the data from the request
 * 
 * @return {Object} the card object
 */
function cardPoll(event, data) {
  let card = {
    'actionResponse': {
      'type': 'NEW_MESSAGE',
    },
    &quot;cardsV2&quot;: &#x5B;
      {
        &quot;cardId&quot;: &quot;poll&quot;,
        &quot;card&quot;: {
          &quot;header&quot;: {
            &quot;title&quot;: event.user.displayName,
            &quot;subtitle&quot;: event.user.email,
            &quot;imageUrl&quot;: event.user.avatarUrl,
            &quot;imageType&quot;: &quot;CIRCLE&quot;
          },
          &quot;sections&quot;: &#x5B;]
        }
      }
    ]
  }

  let sections = &#x5B;];

  sections.push({
    &quot;widgets&quot;: &#x5B;
      {
        &quot;decoratedText&quot;: {
          &quot;text&quot;: data.question,
          &quot;endIcon&quot;: {
            &quot;materialIcon&quot;: {
              &quot;name&quot;: data.multi ? &quot;checklist_rtl&quot; : &quot;rule&quot;
            }
          }
        }
      }
    ]
  })

  for (let i = 0; i &lt; data.options.length; i++) {
    sections.push({
      &quot;collapsible&quot;: true,
      &quot;uncollapsibleWidgetsCount&quot;: 1,
      &quot;widgets&quot;: &#x5B;
        {
          &quot;decoratedText&quot;: {
            &quot;startIcon&quot;: {
              &quot;materialIcon&quot;: {
                &quot;name&quot;: &quot;arrow_right&quot;
              }
            },
            &quot;topLabel&quot;: data.options&#x5B;i],
            &quot;text&quot;: &quot;⬜️⬜️⬜️⬜️⬜️⬜️⬜️⬜️⬜️⬜️&quot;,
            &quot;bottomLabel&quot;: &quot;0%&quot;,
            &quot;button&quot;: {
              &quot;text&quot;: &quot;vote&quot;,
              &quot;icon&quot;: {
                &quot;materialIcon&quot;: {
                  &quot;name&quot;: &quot;add&quot;
                }
              },
              &quot;altText&quot;: &quot;Vote&quot;,
              &quot;onClick&quot;: {
                &quot;action&quot;: {
                  &quot;function&quot;: &quot;actionVotePoll&quot;,
                  &quot;parameters&quot;: &#x5B;
                    {
                      &quot;key&quot;: &quot;option&quot;,
                      &quot;value&quot;: `${i+1}`
                    }
                  ]
                }
              }
            }
          }
        }
      ]
    })
  }

  card.cardsV2&#x5B;0].card.sections = sections

  return card
}
</pre>
<p>Тут з основного — додано виклик функції <code>actionVotePoll()</code> у action кнопки, і про неї розповім далі.</p>
<h3>Оновлення картки</h3>
<p>Для початку звісно знов повернемося до <code>onCardClick()</code>, та додамо підтримку та виклик функції <code>actionVotePoll()</code>:</p>
<pre class="brush: jscript; highlight: [16,17]; title: ; notranslate">
/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  switch (event.common.invokedFunction) {
 
    //
    // ... the code was cropped
    //

    // - /poll
    case 'actionNewPoll':
      return actionNewPoll(event)
    case 'actionVotePoll':
      return actionVotePoll(event)
  }
}
</pre>
<p>Далі будемо створювати вже саму функцію у файлі <code>actionVotePoll.gs</code>:</p>
<pre class="brush: jscript; highlight: [6,8]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionVotePoll(event) {

  let parameters = event.common.parameters

  let card = event.message.cardsV2&#x5B;0]

  //
  // ... the code was cropped
  //


}
</pre>
<p>Давайте поступово — окрім параметрів функції (рядок 6) нас загалом цікавить уся картка яка в нас є у чаті (рядок 8).</p>
<p>А тепер ключовий момент — ми можемо оновити картку у чаті після того як користувач буде взаємодіяти з нею:</p>
<pre class="brush: jscript; highlight: [16,18]; title: ; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionVotePoll(event) {

  let parameters = event.common.parameters

  let card = event.message.cardsV2&#x5B;0]

  //
  // ... the code was cropped
  //

  return {
    &quot;actionResponse&quot;: {
      &quot;type&quot;: &quot;UPDATE_MESSAGE&quot;,
    },
    &quot;cardsV2&quot;: &#x5B; card ]
  }
}
</pre>
<p>Використовуючи цю можливість можна зберігати голоси користувачів як частину картки:</p>
<p><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-voted-1024x743.png" alt="" width="660" height="479" class="aligncenter size-large wp-image-4246" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-voted-1024x743.png 1024w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-voted-460x334.png 460w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-voted-768x557.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/poll-card-voted.png 1036w" sizes="auto, (max-width: 660px) 100vw, 660px" /></p>
<blockquote><p>Cards are usually displayed below the text body of a Chat message, but can situationally appear other places, such as dialogs. Each card can have a <strong>maximum size of 32 KB</strong>.</p></blockquote>
<p>Тож залишилось лише реалізувати цю логіку:</p>
<pre class="brush: jscript; collapse: true; light: false; title: actionVotePoll.gs; toolbar: true; notranslate">
/**
 * @param {Object} event the event object from Google Chat
 */
function actionVotePoll(event) {

  let parameters = event.common.parameters

  let sections = event.message.cardsV2&#x5B;0].card.sections

  let multi = sections&#x5B;0].widgets&#x5B;0].decoratedText.endIcon.materialIcon.name === &quot;checklist_rtl&quot;

  let option = parseInt(parameters.option)

  let user = event.user.displayName
  let avatar = event.user.avatarUrl

  let widget = {
    &quot;decoratedText&quot;: {
      &quot;startIcon&quot;: {
        &quot;iconUrl&quot;: avatar
      },
      &quot;text&quot;: user,
    }
  }

  let votes = &#x5B;]

  for (let i = 1; i &lt; sections.length; i++) {
    let widgets = sections&#x5B;i].widgets
    let people = &#x5B;]

    if (widgets.length &gt; 1) {
      for (let j = 1; j &lt; widgets.length; j++) {
        people.push(
          widgets&#x5B;j].decoratedText.text
        )
      }
    }

    votes&#x5B;i] = people.filter(n =&gt; n)

    Logger.log(`Votes for &quot;${i}&quot;: ${votes&#x5B;i].length}`, votes&#x5B;i])
  }

  // update votes
  let index = votes&#x5B;option].indexOf(user)
  if (index === -1) {
    Logger.log(`+1 vote for ${option}`)
    votes&#x5B;option].push(user)
    sections&#x5B;option].widgets.push(widget)
  } else {
    Logger.log(`-1 vote for ${option}, index is ${index}`)
    votes&#x5B;option].splice(index, 1);
    sections&#x5B;option].widgets.splice(index + 1, 1)
  }

  // for non-multi voting poll
  // remove user votes for another options
  if (!multi) {
    for (let i = 1; i &lt; sections.length; i++) {
      if (i === option) {
        continue
      }
      let index = votes&#x5B;i].indexOf(user)
      if (index !== -1) {
        Logger.log(`-1 vote for ${i}, index is ${index}`)
        votes&#x5B;i].splice(index, 1)
        sections&#x5B;i].widgets.splice(index + 1, 1)
      }
    }
  }

  Logger.log(`Vote for &quot;${option}&quot;: ${votes&#x5B;option].length}`, votes&#x5B;option])

  // removed empty value from position 0
  votes.shift()

  let percentages = calculatePercentages(votes)

  Logger.log(`%:`, percentages)

  for (let i = 1; i &lt; sections.length; i++) {
    sections&#x5B;i].widgets&#x5B;0].decoratedText.text = fillProgressBar(percentages&#x5B;i - 1])
    sections&#x5B;i].widgets&#x5B;0].decoratedText.bottomLabel = `${percentages&#x5B;i - 1]}%`
  }

  return {
    &quot;actionResponse&quot;: {
      &quot;type&quot;: &quot;UPDATE_MESSAGE&quot;,
    },
    &quot;cardsV2&quot;: &#x5B;
      {
        &quot;cardId&quot;: &quot;poll&quot;,
        &quot;card&quot;: {
          &quot;header&quot;: event.message.cardsV2&#x5B;0].card.header,
          &quot;sections&quot;: sections
        }
      }
    ]
  }
}

/**
 * @param {Array} data
 */
function calculatePercentages(data) {
  // Flatten the array by concatenating all sub-arrays
  let flattenedArray = &#x5B;].concat.apply(&#x5B;], data);

  // Calculate total number of items across all sub-arrays
  let totalItems = flattenedArray.length

  // Filter out duplicates by using a temporary object where properties represent the unique items found so far
  let uniqueItems = flattenedArray.filter(function(item, index, self) {
    return self.indexOf(item) === index;
  });

  let totalUniqueItems = uniqueItems.length;

  // Calculate the percentage of each subarray based on the total items
  return data.map(function (sublist) {
    return totalUniqueItems ? Math.round(sublist.length / totalUniqueItems * 100) : 0;
  });
}

/**
 * @param {Number} percentage
 */
function fillProgressBar(percentage) {
  let totalBoxes = 10; // Total number of boxes in the text
  let filledBoxes = Math.round(percentage / 10); // Calculate number of filled boxes (each box represents 10%)
  let progressBar = &quot;&quot;; // Initialize an empty string for the progress bar

  // Build the progress bar string
  for (let i = 0; i &lt; totalBoxes; i++) {
    if (i &lt; filledBoxes) {
      progressBar += &quot;&quot;; // Add a filled box for each 10% completed
    } else {
      progressBar += &quot;⬜️&quot;; // Fill the rest with empty boxes
    }
  }

  return progressBar;
}

</pre>
<p>У цьому коді для зберігання голосів використовуються віджети, тож кожна опція — це окрема секція, кожен голос — окремий віджет. Коли користувач голосує, то ми отримуємо всю картку, розібравши її на частинки можна відновити результати голосування та внести зміни.</p>
<h3>Діаграма послідовності</h3>
<p>Хотів привести діаграму послідовності, можливо вона допоможе вам краще розібратися у функціоналі:</p>
<p><a href="https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll.png"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll-779x1024.png" alt="" width="660" height="868" class="aligncenter size-large wp-image-4250" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll-779x1024.png 779w, https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll-350x460.png 350w, https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll-768x1009.png 768w, https://anton.shevchuk.name/wp-content/uploads/2024/04/The-Poll.png 1075w" sizes="auto, (max-width: 660px) 100vw, 660px" /></a></p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
title The Poll

actor User

materialdesignicons F0822 &quot;Chat&quot; as C


participantgroup Apps Script
participant slashPoll()
participant dialogPoll()
participant actionNewPoll()
participant cardPoll()
participant actionVotePoll()
end

User-&gt;C: /poll


create slashPoll()

C-&gt;slashPoll(): onMessage()
activate slashPoll()
space -6
create dialogPoll()
slashPoll()-&gt;dialogPoll(): 
activate dialogPoll()
dialogPoll()-&gt;slashPoll(): The Form Card
deactivate dialogPoll()
slashPoll()-&gt;C: The Form Card
deactivate slashPoll()

space -2
box over C:\n\n&#x5B;  Question Input ]\n\n&#x5B;  Answers 1..10 ]\n\n&#x5B;  Options  ]\n\n&#x5B; Send ]

space -7
create actionNewPoll()
C-&gt;actionNewPoll(): The Form Data
activate actionNewPoll()
actionNewPoll()--&gt;dialogPoll(): The Form Data\nValidation Error
space -4
dialogPoll()--&gt;C: The Form Card\nWith Data and Error

space -10
create cardPoll()
actionNewPoll()-&gt;cardPoll(): Build the Poll Card
activate cardPoll()
cardPoll()-&gt;actionNewPoll(): The Poll Card
deactivate cardPoll()

actionNewPoll()-&gt;C: The Poll Card

deactivate actionNewPoll()

space -2
box over C:\n\n  Question  \n\n  Answers 1  &#x5B;+ vote ]\n\n  Answers 2  &#x5B;+ vote ]\n

space -8
create actionVotePoll()

C-&gt;actionVotePoll(): Vote for Options
activate actionVotePoll()
actionVotePoll()-&gt;C: The Poll Card + data
deactivate actionVotePoll()


space -2
box over C:\n\n  Question  \n\n  Answers 1  &#x5B;+ vote ]\n\n  Answers 2  &#x5B;+ vote ]\n\n  data as part of card
</pre>
<h3>Source Code</h3>
<p>Код бота доступний на <a href="https://github.com/AntonShevchuk/bender-2.0/">GitHub</a>, реліз 5.1.0 відповідає коду з цієї статті.</p>
<figure id="attachment_4016" aria-describedby="caption-attachment-4016" style="width: 240px" class="wp-caption aligncenter"><a href="https://github.com/AntonShevchuk/bender-2.0/releases/tag/5.1.0"><img loading="lazy" decoding="async" src="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png" alt="Bender, реліз 5.1.0" width="240" height="240" class="size-full wp-image-4016" srcset="https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark.png 240w, https://anton.shevchuk.name/wp-content/uploads/2024/02/github-mark-200x200.png 200w" sizes="auto, (max-width: 240px) 100vw, 240px" /></a><figcaption id="caption-attachment-4016" class="wp-caption-text">Bender, білд 5.1.0</figcaption></figure>
<h3>P.S.</h3>
<p>З початку був реліз <a href="https://github.com/AntonShevchuk/bender-2.0/releases/tag/5.0.0">5.0.0</a>, але я потім подивився на той код, та вирішив навести в ньому лад, саме тому краще дивитися версію <a href="https://github.com/AntonShevchuk/bender-2.0/releases/tag/5.1.0">5.1.0</a> :)</p>
]]></content:encoded>
					
					<wfw:commentRss>https://anton.shevchuk.name/google/google-chat-bot-interactions-and-card-updates/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>