{
    "version": "https:\/\/jsonfeed.org\/version\/1.1",
    "title": "Блоги: заметки с тегом програмування",
    "_rss_description": "Автоматически собираемая лента заметок, написанных в блогах на Эгее",
    "_rss_language": "ru",
    "_itunes_email": "",
    "_itunes_categories_xml": "",
    "_itunes_image": false,
    "_itunes_explicit": "no",
    "home_page_url": "https:\/\/blogengine.ru\/blogs\/tags\/programuvannya\/",
    "feed_url": "https:\/\/blogengine.ru\/blogs\/tags\/programuvannya\/json\/",
    "icon": false,
    "authors": [
        {
            "name": "Илья Бирман",
            "url": "https:\/\/blogengine.ru\/blogs\/",
            "avatar": false
        }
    ],
    "items": [
        {
            "id": "120298",
            "url": "https:\/\/stefaniuk.website\/all\/push-notification-via-asp-net-core\/",
            "title": "Push Notification via ASP.NET Core",
            "content_html": "<p>Недавно запустили на роботі новий функціонал інбоксів. Якщо коротко, то це універсальний месенджер з підтримкою різних платформ та каналів що дозволяє вести комунікацію з кінцевими клієнтами в одному місці.<\/p>\n<p>Для кращого UX треба було добавити пуш нотифікації. На цей момент у нас уже були підключені веб сокети по яким ганяли різні дані, в тому числі і про нові повідомлення. Але такі сповіщення можна відобразити тільки коли відкрита сторінка в браузері.<\/p>\n<p>Тому вирішили добавити підтримку браузерних пушів. Саме про те як їх підключити та використовувати в звʼязці з ASP.NET Core я хочу розказати.<\/p>\n<h2><b>Як це працює?<\/b><\/h2>\n<p>Процес роботи технології пуш нотифікацій у браузері досить простий. Спочатку ми запрошуємо права на отримання сповіщень у користувача. Браузер покаже йому модалку із запитанням «Do you want to receive push notifications?».<\/p>\n<p>Після того як він підтвердить вибір нам потрібно підписати користувача на отримання нотифікацій використовуючи Push API браузера. Під капотом браузер робить запит на спеціальний push service та повертає нам обʼєкт PushSubscription в якому є вся необхідна інформація для відправки пушів:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">{\r\n  &quot;endpoint&quot;: &quot;https:\/\/fcm.googleapis.com\/fcm\/send\/c1KrmpTuRm…&quot;,\r\n  &quot;expirationTime&quot;: null,\r\n  &quot;keys&quot;: {\r\n    &quot;p256dh&quot;: &quot;BGyyVt9FFV…&quot;,\r\n    &quot;auth&quot;: &quot;R9sidzkcdf…&quot;\r\n  }\r\n}<\/code><\/pre><p>Далі ми берем цей обʼєкт і відправляємо на наш сервер, де кладемо його у базу даних або зберігаємо іншим способом щоб використовувати його в подальшому.<\/p>\n<p>Якщо придивитися до структури PushSubscription, можна помітити адрес ендпоінта. Суть в тому, що наш сервер не відправляє сповіщення напряму в браузер. Для цього використовується спеціальний push service, який у кожного розробника браузера свій.<\/p>\n<div style=\"max-width: 740px;\"><div class=\"e2-text-picture\">\n<img src=\"https:\/\/stefaniuk.website\/pictures\/push-notifications-structure.png\" width=\"1761\" height=\"646\" alt=\"\" \/>\n<\/div>\n<\/div><p>Нам повезло, що браузери змогли домовитись і стандартизувати протокол push сервіса, тому нам можна не паритись за формат даних для кожного вендора. Усі вони приймають один формат даних, який містить:<\/p>\n<ul>\n<li>Контент повідомлення<\/li>\n<li>Кому його відправити<\/li>\n<li>Як саме його відправити, з яким пріорітетом, в який топік та з яким таймаутом.<\/li>\n<\/ul>\n<p>Кожне повідомлення шифрується за допомогою ключів, що були створені при генерації підписки. Крім цього, потрібно також підписувати повідомлення за допомогою приватного ключа, щоб підтвердити, що це саме наш сервер відправляє повідомлення. Виглядає це ось так:<\/p>\n<ol start=\"1\">\n<li>Генеруємо пару публічного та приватного ключа, які ще називають VAPID ключами. Цей крок треба зробити всього один раз. В інтернеті купа онлайн сервісів де можна згенерувати ключі.<\/li>\n<li>Публічний ключ використовуємо при створенні підписки на стороні браузера. Дальше цей ключ асоціюється з ендпоінтом для конкретної підписки.<\/li>\n<li>Приватний ключ використовуємо для підписки повідомлення на стороні нашого сервера.<\/li>\n<li>Дальше за допомогою публічного ключа ми можемо перевірити що повідомлення було відправлено саме нашим сервером а не кимось іншим.<\/li>\n<\/ol>\n<p>Структура VAPID ключа:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">{\r\n    &quot;sub&quot;: &quot;mailto:bohdan@stefaniuk.io&quot;,\r\n    &quot;public_key&quot;: &quot;sdgdklsnvi2f0m-12dgsg...&quot;,\r\n    &quot;private_key&quot;: &quot;1fn0ASFfnaksf...&quot;\r\n }<\/code><\/pre><p>Але, як в тому жарті, є нюанс. Subject в VAPID ключі є опціональним і його не обовʼязково відправляти разом з запитом на push сервіс. Проте Сафарі так не вважає, ще й вимагає специфічний формат. Коли я генерував ключі, то мав ось такий sub <i>mailto: &lt;bohdan@stefaniuk.io&gt;<\/i>.<\/p>\n<p>В хромі все працювало чудово, а сафарі кидав помилку, що запит невалідний. Виявилось, що вони не підтримують пробіли та кутові лапки, замінив sub на <i>mailto:bohdan@stefaniuk.io<\/i> і все запрацювало.<\/p>\n<h2><b>Реалізація бекенду<\/b><\/h2>\n<p class=\"note-md\">Якщо у вас macOS, то ви не зможете локально протестувати сповіщення. Проблема в тому що .NET не підтримує шифрування AesGcm на маках, тому при відправці вилетить ексепшен. Поки я не знайшов як це обійти.<\/p>\n<p>На стороні сервера будемо використовувати бібліотеку <i>Lib.Net.Http.WebPush<\/i> яка під капотом буде шифрувати, підписувати та відправляти наші сповіщення. Для цього вона дає декілька базових модельок та один клас для відправки пушів.<\/p>\n<p>Почнемо з реалізації створення та видалення підписок та зберігання їх в БД. Для цього добавимо клас ApplicationPushSubscription, який описує структуру таблиці та підключимо його в Entity Framework контекст:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">public class ApplicationPushSubscription\r\n{\r\n    [Key]\r\n    public string P256Dh { get; set; }\r\n    public string Endpoint { get; set; }\r\n    public string Auth { get; set; }\r\n    public Guid UserId { get; set } \/\/ Id користувача в нашій системі\r\n}\r\n\r\npublic class DatabaseContext : DbContext\r\n{\r\n    public DbSet&lt;ApplicationPushSubscription&gt; PushSubscriptions { get; set; }\r\n}<\/code><\/pre><p>Тепер створимо клас PushNotificationsService в який добавимо методи для створення та видалення підписок. Метод створення буде приймати екземпляр класу PushSubscription з бібліотеки Lib.Net.Http.WebPush.<\/p>\n<p>Його можна було б змінити на свій клас, щоб логіка бібліотеки не просочувалась в інші модулі але задля простоти залишимо так як є.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">public class PushNotificationsService\r\n{\r\n    private readonly DatabaseContext _context;\r\n    private readonly PushServiceClient _pushClient;\r\n    \r\n    public PushNotificationsService (DatabaseContext context, PushServiceClient pushClient)\r\n    {\r\n        _context = context;\r\n        _pushClient = pushClient;\r\n    }\r\n    \r\n    public async Task CreateSubscription(Guid userId, PushSubscription pushSubscription)\r\n    {\r\n        var subscription = new ApplicationPushSubscription()\r\n        {\r\n            UserId = userId,\r\n            AccountId = currentUser.AccountId,\r\n            Endpoint = subscription.Endpoint,\r\n            P256Dh = subscription.GetKey(PushEncryptionKeyName.P256DH),\r\n            Auth = subscription.GetKey(PushEncryptionKeyName.Auth)\r\n        };\r\n        \r\n        if (await _context.PushSubscriptions.AnyAsync(x =&gt; x.P256Dh == subscription.P256Dh))\r\n        {\r\n            return;\r\n        }\r\n        \r\n        _context.PushSubscriptions.Add(subscription);\r\n    \r\n        try\r\n        {\r\n            await _context.SaveChangesAsync();\r\n        }\r\n        catch (DbUpdateException)\r\n        {\r\n            var alreadyExists = await _context.PushSubscriptions.AnyAsync(x =&gt; x.P256Dh == subscription.P256Dh);\r\n            if (!alreadyExists)\r\n            {\r\n                throw;\r\n            }\r\n        }\r\n    }\r\n    \r\n    public async Task DeleteSubscription(Guid userId, string endpoint)\r\n    {\r\n        var subscription = await _context.PushSubscriptions\r\n            .FirstOrDefaultAsync(x =&gt; x.UserId == userId &amp;&amp; x.Endpoint == endpoint);\r\n        \r\n        if (subscription == null)\r\n        {\r\n            return;\r\n        }\r\n        \r\n        _context.PushSubscriptions.Remove(subscription);\r\n        await _context.SaveChangesAsync();\r\n    }\r\n}<\/code><\/pre><p>Реалізовуємо метод для відправки сповіщень нашим користувачам. Він буде приймати Id користувача якому ми відправляємо повідомлення і модель з даними.<\/p>\n<p>Нам залишається отримати підписку з БД, серіалізувати повідомлення в JSON і скормити це все бібліотеці.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">public async Task SendNotificationAsync(Guid userId, PushNotificationModel model)\r\n{\r\n    try\r\n    {\r\n        var applicationPushSubscriptions = await _context.PushNotifications\r\n            .Where(x =&gt; x.UserId == userId)\r\n            .ToList();\r\n            \r\n        foreach(var applicationPushSubscription in applicationPushSubscriptions)\r\n        {\r\n            var pushSubscription = new PushSubscription();\r\n            pushSubscription.Endpoint = applicationPushSubscription.Endpoint;\r\n            pushSubscription.SetKey(PushEncryptionKeyName.P256DH, applicationPushSubscription.P256Dh);\r\n            pushSubscription.SetKey(PushEncryptionKeyName.Auth, applicationPushSubscription.Auth);\r\n            \r\n            var settings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };\r\n            var payload = JsonConvert.SerializeObject(model, settings);\r\n    \r\n            var pushMessage = new PushMessage(payload);\r\n            await _pushClient.RequestPushMessageDeliveryAsync(\r\n                pushSubscription,\r\n                pushMessage,\r\n                _pushClient.DefaultAuthentication,\r\n                VapidAuthenticationScheme.WebPush);\r\n        }\r\n    }\r\n    catch (PushServiceClientException exception)\r\n    {\r\n        if (exception.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Gone)\r\n        {\r\n            _context.PushSubscriptions.Remove(subscription);\r\n            await _context.SaveChangesAsync();\r\n        }\r\n        else\r\n        {\r\n            \/\/ Re-Throw exception or log\r\n        }\r\n    }\r\n    catch (Exception exception)\r\n    {\r\n        \/\/ Re-Throw exception or log\r\n    }\r\n}<\/code><\/pre><p>Структура класу <i>PushNotificationModel<\/i> досить проста та універсальна. Я передаю заголовок сповіщення, текст та додаткові дані:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">public class PushNotificationModel\r\n{\r\n    public string Title { get; set; }\r\n    public string Body { get; set; }\r\n    public object AdditionalData { get; set; }\r\n}<\/code><\/pre><p>Підключаємо наш сервіс в ендопінт:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">[Route(&quot;api\/push-notifications&quot;)]\r\n[Authorize]\r\n[ApiController]\r\npublic class PushNotificationsController : ControllerBase\r\n{\r\n    private readonly PushNotificationService _pushNotificationService;\r\n\r\n    public PushNotificationsController(PushNotificationService pushNotificationService)\r\n    {\r\n        _pushNotificationService = pushNotificationService;\r\n    }\r\n    \r\n    [HttpPost(&quot;subscriptions&quot;)]\r\n    public async Task&lt;IActionResult&gt; CreateSubscription([FromBody] PushSubscription subscription)\r\n    {\r\n        var user = HttpContext.GetCurrentUser(); \/\/ Our custom method to get current logged-in user\r\n        await _pushNotificationService.CreateSubscription(user.Id, subscription);\r\n        return Ok();\r\n    }\r\n\r\n    [HttpDelete(&quot;subscriptions&quot;)]\r\n    public async Task&lt;IActionResult&gt; DeleteSubscription([FromQuery] string endpoint)\r\n    {\r\n        var user = HttpContext.GetCurrentUser();\r\n        await _pushNotificationService.DeleteSubscription(user.Id, endpoint);\r\n        return Ok();\r\n    }\r\n}<\/code><\/pre><p>Все що залишається — підключити наші класи в DI і все готово. Для простоти підключення можемо добавити ще один пакет <i>Lib.AspNetCore.WebPush<\/i>.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">services.AddTransient&lt;PushNotificationService&gt;();\r\nservices.AddMemoryVapidTokenCache();\r\nservices.AddPushServiceClient(options =&gt;\r\n{\r\n    var clientOptions = configuration.GetSection(nameof(PushServiceClient));\r\n    options.Subject = clientOptions.GetValue&lt;string&gt;(nameof(options.Subject));\r\n    options.PublicKey = clientOptions.GetValue&lt;string&gt;(nameof(options.PublicKey));\r\n    options.PrivateKey = clientOptions.GetValue&lt;string&gt;(nameof(options.PrivateKey));\r\n});<\/code><\/pre><h2><b>Реалізація фронтенда<\/b><\/h2>\n<p class=\"note-md\">Підтримка технології Web Push була додана в Сафарі тільки в версії MacOS 13 та вище.<\/p>\n<p>Перше, що потрібно реалізувати на стороні клієнта — це запит прав на отримання сповіщень. Для цього використовуємо обʼєкт <i>Notification<\/i>:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">\/\/ Запит у браузера прав на підключення пушей\r\nasync function askPermission() {\r\n    const permissionPromiseResult = await new Promise(function (resolve, reject) {\r\n        const permissionResult = Notification.requestPermission(function (result) {\r\n            resolve(result);\r\n        });\r\n\r\n        if (permissionResult) {\r\n            permissionResult.then(resolve, reject);\r\n        }\r\n    });\r\n    \r\n    if (permissionPromiseResult !== 'granted') {\r\n        alert(&quot;We weren't granted permission.&quot;);\r\n    } else {\r\n        await subscribeUserToPush();\r\n    }\r\n}<\/code><\/pre><p>Після того, як отримали права, нам потрібно зареєструвати service worker в якому буде логіка відображення сповіщень, створити підписку та відправити її на наш сервер.<\/p>\n<p>При створенні підписки необхідно передати наш публічний ключ. Але він повинен бути в форматі масиву байтів, тому треба спочатку його декодувати і вже тоді передавати.<\/p>\n<p>В результаті отримуємо обʼєкт <i>pushSubscription<\/i>, який ми відправляємо на створений раніше ендпоінт. Крім цього, ще відправляємо <i>Bearer<\/i> токен, щоб розуміти до якого користувача привʼязати підписку.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">const publicKey = &quot;vapid-public-key&quot;;\r\n\r\nasync function subscribeUserToPush() {\r\n    const registration = await navigator.serviceWorker.register('\/service-worker.js');\r\n    const subscribeOptions = {\r\n        userVisibleOnly: true,\r\n        applicationServerKey: urlBase64ToUint8Array(publicKey),\r\n    };\r\n\r\n    var pushSubscription = await registration.pushManager.subscribe(subscribeOptions);\r\n   \r\n    fetch('http:\/\/localhost:5000\/api\/push-notifications\/subscriptions', {\r\n        method: 'POST',\r\n        headers: {\r\n            'Content-Type': 'application\/json',\r\n            'Authorization': 'Bearer 1245'\r\n        },\r\n        body: JSON.stringify(pushSubscription)\r\n    }).then(function (response) {\r\n        if (response.ok) {\r\n            alert('Successfully subscribed for Push Notifications');\r\n        } else {\r\n            alert('Failed to store the Push Notifications subscription on server');\r\n        }\r\n    }).catch(function (error) {\r\n        console.log('Failed to store the Push Notifications subscription on server: ' + error);\r\n    });\r\n    \r\n    return pushSubscription;\r\n}\r\n\r\nfunction urlBase64ToUint8Array(base64String) {\r\n    var padding = '='.repeat((4 - (base64String.length % 4)) % 4);\r\n    var base64 = (base64String + padding).replace(\/\\-\/g, '+').replace(\/_\/g, '\/');\r\n\r\n    var rawData = window.atob(base64);\r\n    var outputArray = new Uint8Array(rawData.length);\r\n\r\n    for (var i = 0; i &lt; rawData.length; ++i) {\r\n        outputArray[i] = rawData.charCodeAt(i);\r\n    }\r\n    return outputArray;\r\n};<\/code><\/pre><p>Наш сервіс воркер буде максимально простим. Для початку додамо логіку відображення пушей та передачі в них додаткових даних з сервера.<\/p>\n<p>Також необхідно додати логіку обробки кліку по сповіщенню. Ми будемо перевіряти чи відкритий сайт в браузері. Якщо ні, то відкривати його в новій вкладці. А якщо у нас вже є вкладка з сайтом, то переключаємось на неї і робимо редірект на потрібну сторінку.<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">self.addEventListener('push', function (event) {\r\n    var data = event.data.json();\r\n    \/\/ {\r\n    \/\/     &quot;title&quot;: &quot;New Notification&quot;,\r\n    \/\/     &quot;body&quot;: &quot;This is the body of the notification&quot;\r\n    \/\/     &quot;additionalData&quot;: {\r\n    \/\/         &quot;pageId&quot;: &quot;12512515&quot;\r\n    \/\/     }\r\n    \/\/ }\r\n\r\n    event.waitUntil(self.registration.showNotification(data.title, {\r\n        body: data.body,\r\n        icon: '\/logo.png',\r\n        data: {\r\n            pageId: data.additionalData?.pageId,\r\n        },\r\n    }));\r\n});\r\n\r\nself.addEventListener('notificationclick', function (event) {\r\n    var notification = event.notification;\r\n\r\n    event.waitUntil(\r\n        self.clients.matchAll().then(function (allClients) {\r\n            if (allClients.length === 0) {\r\n                self.clients.openWindow(\r\n                    `${self.location.origin}\/page\/${notification?.data?.pageId}`,\r\n                );\r\n                notification.close();\r\n                return;\r\n            }\r\n\r\n            allClients[0]?.navigate(`\/page\/${notification?.data?.pageId}`);\r\n            allClients[0]?.focus();\r\n            notification.close();\r\n        }),\r\n    );\r\n});<\/code><\/pre><p>В результаті всіх цих маніпуляцій у нас є сервер, який вміє відправляти сповіщення та клієнт, який вміє створювати підписки, відображати сповіщення та обробляти кліки.<\/p>\n<p>Окремо ще можна поговорити про топіки та пріоритети сповіщень, але це виходить за рамки цієї статті. Можливо, розкажу про це пізніше :)<\/p>\n<h2><b>Ресурси<\/b><\/h2>\n<ul>\n<li><a href=\"https:\/\/web.dev\/push-notifications-overview\/\">Оглад того як працюють пуші<\/a><\/li>\n<li><a href=\"https:\/\/www.tpeczek.com\/2017\/12\/push-notifications-and-aspnet-core-part.html\">Стаття з прикладом реалізації пушів за допомогою ASP.NET<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/tpeczek\/Demo.AspNetCore.PushNotifications\">Репозиторій з прикладами використання бібліотеки<\/a><\/li>\n<\/ul>\n",
            "date_published": "2023-06-09T05:43:17+05:00",
            "date_modified": "2023-06-09T05:42:47+05:00",
            "tags": [
                "asp.net core",
                "код",
                "програмування"
            ],
            "author": {
                "name": "Bohdan Stefaniuk",
                "url": "https:\/\/stefaniuk.website\/",
                "avatar": "https:\/\/stefaniuk.website\/pictures\/userpic\/userpic@2x.jpg?1565716580"
            },
            "_date_published_rfc2822": "Fri, 09 Jun 2023 05:43:17 +0500",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "120298",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": null,
                "og_images": []
            }
        },
        {
            "id": "119850",
            "url": "https:\/\/stefaniuk.website\/all\/kod-tayny-yazyk-informatiki\/",
            "title": "Код: тайный язык информатики",
            "content_html": "<div class=\"e2-text-picture\">\n<img src=\"https:\/\/stefaniuk.website\/pictures\/code-hidden-language-of-computer-cover.jpg\" width=\"300\" height=\"410\" alt=\"\" \/>\n<\/div>\n<p>Составили вместе с другом план, по которому хотим изучить Computer Science. Первым пунктом плана стала книга — Код: тайный язык информатики. Прочитав её я был в шоке от подачи и крутости материала. Автор шаг за шагом рассказывает про развитие компьютерных технологий.<\/p>\n<p>Книга начинается с того, что вы изучаете как работает фонарик и как с его помощью можно общаться. Потом вы изобретаете телеграф, а вместе с ним и реле. Итак шаг за шагом вы плавно подходите к тому, как устроены компьютеры и большую часть книги вы будете изобретать свой.<\/p>\n<p>Также автор рассказывает, почему появилась и для чего нужна шестнадцатеричная и двоичная системы исчисления, как устроена оперативная память. Ближе к концу книги вы придумаете «свой» язык программирование и окажется, что вы придумали ассемблер. Ну а в самом конце автор расскажет, как мы пришли от командной строки к графическим интерфейсам.<\/p>\n<p>Каждая глава содержит множество иллюстраций, которые объясняют тот или иной аспект.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/stefaniuk.website\/pictures\/code-hidden-language-of-computer.png\" width=\"2337\" height=\"653\" alt=\"\" \/>\n<\/div>\n<p>Книга сильно зацепила меня, прочитал всего за неделю. Советую всем кто хочет разобраться как устроен компьютер и его самые мелкие части.<\/p>\n",
            "date_published": "2020-08-09T17:18:12+05:00",
            "date_modified": "2023-06-03T05:26:05+05:00",
            "tags": [
                "книги",
                "програмування"
            ],
            "author": {
                "name": "Bohdan Stefaniuk",
                "url": "https:\/\/stefaniuk.website\/",
                "avatar": "https:\/\/stefaniuk.website\/pictures\/userpic\/userpic@2x.jpg?1565716580"
            },
            "_date_published_rfc2822": "Sun, 09 Aug 2020 17:18:12 +0500",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "119850",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": null,
                "og_images": []
            }
        },
        {
            "id": "119851",
            "url": "https:\/\/stefaniuk.website\/all\/create-workplace-messenger-bot\/",
            "title": "Как я делал бота для Facebook workplace",
            "content_html": "<p>Дали мне задачу: написать бота для мессенджера workplace, с помощью которого можно получать уведомления из нашей CRM и управлять разными вещами. Расскажу о разных интересных вещах с которыми я столкнулся. Бота писал с помощью ASP.NET Core Web API.<\/p>\n<h2>Вебхуки<\/h2>\n<p>Для того чтобы бот мог обрабатывать запросы и сообщения от Facebook нам надо настроить вебхуки. Webhook — механизм оповещения системы о событиях. Для того чтобы Facebook принял наш хук, он должен обрабатывать как GET так и POST запросы.<\/p>\n<p class=\"note\"><a href=\"https:\/\/developers.facebook.com\/docs\/messenger-platform\/getting-started\/webhook-setup\">Подробнее о Webhook в официальной документации<\/a><\/p>\n<p>GET запрос служит для валидации работы нашего эндпоинта. POST принимает данные связанные с активностью пользователя, будь то нажатие на кнопки или другая активность.<\/p>\n<p class=\"cut-button\">Показать код метода-обработчика GET запроса<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">public IActionResult Receive(\u2028[FromQuery(Name = &quot;hub.mode&quot;)] string mode,\u2028\r\n    [FromQuery(Name = &quot;hub.challenge&quot;)] string challenge,\u2028\r\n    [FromQuery(Name = &quot;hub.verify_token&quot;)] string verifyToken)\r\n{\u2028\r\n   if (string.IsNullOrEmpty(verifyToken)) {\u2028\r\n       return Unauthorized();\u2028\r\n    }\u2028\u2028\r\n    if (verifyToken.Equals(FacebookEnvironment.FacebookVToken)) {\u2028\r\n        return Ok(challenge);\u2028\r\n    }\u2028\r\n    return Unauthorized();\u2028\r\n}<\/code><\/pre><p>В качестве verify_token используется токен, который мы указали при регистрации нашего хука.<\/p>\n<\/div><p class=\"cut-button\">Показать код метода-обработчика POST запроса<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">public async Task&lt;IActionResult&gt; Receive([FromBody]FbResponse response = null)\r\n{\r\n    if (response is null) {\r\n        return BadRequest();\r\n    }\r\n\r\n    if (response.Object != &quot;page&quot;) {\r\n        return Ok();\r\n    }\r\n\r\n    foreach (var entry in response.Entries) {\r\n        foreach (var message in entry.Messaging) {\r\n            await PrepareMessageAsync(message);\r\n        }\r\n    }\r\n    return Ok(&quot;EVENT_RECEIVED&quot;);\r\n}<\/code><\/pre><\/div><h2>Авторизация запросов Facebook<\/h2>\n<p>Для авторизации Facebook использует специальный http заголовок (X-Hub-Signature), в нем он передает некую сигнатуру с помощью которой мы можем авторизовать запрос. Для того чтобы добавить такую функциональность в наш контроллер, добавим фильтр.<\/p>\n<p class=\"cut-button\">Пример кода, для проверки подписи<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">private const string Sha1Prefix = &quot;sha1=&quot;;\r\n\r\npublic static bool Validate(string signature, string contentString) {\r\n    if (!signature.StartsWith(Sha1Prefix, StringComparison.OrdinalIgnoreCase)) {\r\n        return false;\r\n    }\r\n    var secret = Encoding.ASCII.GetBytes(FacebookEnvironment.AppSecret);\r\n    var signatureWithoutPrefix = signature.Substring(Sha1Prefix.Length);\r\n    var content = Encoding.ASCII.GetBytes(contentString);\r\n    return GetIsHashValid(secret, signatureWithoutPrefix, content);\r\n}\r\n\r\nprivate static bool GetIsHashValid(byte[] secret, string signature, byte[] content) {\r\n    using var hmac = new HMACSHA1(secret);\r\n    var hash = hmac.ComputeHash(content);\r\n    var hashString = ToHexString(hash);\r\n    return hashString.Equals(signature);\r\n}\r\n\r\nprivate static string ToHexString(IReadOnlyCollection&lt;byte&gt; bytes)\r\n{\r\n    var builder = new StringBuilder(bytes.Count * 2);\r\n    foreach (var b in bytes)\r\n    {\r\n        builder.AppendFormat(&quot;{0:x2}&quot;, b);\r\n    }\r\n\r\n    return builder.ToString();\r\n}<\/code><\/pre><\/div><h2>Тестирование бота<\/h2>\n<p class=\"note-md\"><a href=\"http:\/\/winitpro.ru\/index.php\/2017\/11\/03\/ustanovka-besplatnogo-ssl-sertifikata-lets-encrypt-na-iis-v-windows-server-2012-r2\/\">Как установить letsencrypt сертификат для IIS<\/a>. Если же вы используете связку в виде ubuntu и nginx вам подойдет <a href=\"https:\/\/www.digitalocean.com\/community\/tutorials\/nginx-let-s-encrypt-ubuntu-18-04-ru\">эта инструкция<\/a>.<\/p>\n<p>Для тестирования нужно развернуть бот на сервере, который смотрит в мир. Также необходимо чтобы у сервера было доменное имя и валидный SSL сертификат. В моем случае, в качестве сервера выступала машина на винде, так как другой внутри нашей сети не было. Как мне показалось захостить приложение написанное на .NET Core намного проще под Ubuntu + nginx нежели под Windows + IIS. В качестве поставщика сертификатов выбрал letsencrypt, так как они предоставляют бесплатный сертификат на 3 месяца, с возможностью дальнейшего обновления.<\/p>\n",
            "date_published": "2019-11-26T19:22:26+05:00",
            "date_modified": "2023-06-03T05:26:31+05:00",
            "tags": [
                "asp.net core",
                "dotnet",
                "програмування",
                "проекти",
                "робота"
            ],
            "author": {
                "name": "Bohdan Stefaniuk",
                "url": "https:\/\/stefaniuk.website\/",
                "avatar": "https:\/\/stefaniuk.website\/pictures\/userpic\/userpic@2x.jpg?1565716580"
            },
            "_date_published_rfc2822": "Tue, 26 Nov 2019 19:22:26 +0500",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "119851",
            "_rss_enclosures": [],
            "_e2_data": {
                "is_favourite": false,
                "links_required": null,
                "og_images": []
            }
        }
    ],
    "_e2_version": 4079,
    "_e2_ua_string": "Aegea 11.0 (v4079e)"
}