Files
tnsu-approval-route/backend/Tnsu.ApprovalRoute.Infrastructure/TnsuPortal/HttpSharePointWebhookSubscriber.cs
T
gorbenko.pavel a1e6c5c8b4
Tests / frontend-test (pull_request) Successful in 47s
Tests / backend-test (pull_request) Successful in 2m36s
подписка
2026-08-19 13:31:38 +05:00

95 lines
5.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
/// <summary>
/// POST {BaseUrl}/Webhook/Subscribe?listName=…&amp;url=…&amp;notificationUrl=… — просит
/// портал завести подписку SharePoint на только что созданный список данных.
///
/// Параметры уходят строкой запроса, тела у запроса нет — так описана ручка.
///
/// Отдельный клиент, а не метод в ITnsuPortalClient: тот про справочник сотрудников
/// и обёрнут кешем (CachingTnsuPortalClient), а здесь команда, которую кешировать
/// нельзя ни в каком виде. Авторизация общая с ним — см. TnsuPortalAuthorizer.
///
/// Адрес приёмника уведомлений живёт в SharePoint:NotificationUrl — в одном блоке с
/// остальными настройками SharePoint, а не в своём: подписка относится к спискам,
/// а не к порталу как таковому.
///
/// Ошибки НЕ пробрасываются наружу. Список к этому моменту уже создан, и уронить
/// провижининг из-за недоступности портала значило бы оставить партиционирование
/// нерабочим целиком (задача Hangfire ушла бы в повторы, а порог по элементам
/// продолжал бы срабатывать). Поэтому сбой — Error в лог с полным контекстом
/// (имя списка и сайт видны, подписку можно завести руками), а провижининг идёт
/// дальше. Отдельно предупреждаем про 401/403: в фоновой задаче нет входящего
/// токена, а client_credentials требует заполненных TnsuPortal:ClientSecret/Scope
/// (в конфигурации они пустые — см. TnsuPortalAuthorizer).
/// </summary>
public sealed class HttpSharePointWebhookSubscriber(
HttpClient httpClient,
TnsuPortalAuthorizer authorizer,
IOptions<SharePointOptions> options,
ILogger<HttpSharePointWebhookSubscriber> logger) : ISharePointWebhookSubscriber
{
public async Task<bool> TrySubscribeAsync(string siteUrl, string listTitle, CancellationToken cancellationToken)
{
var notificationUrl = options.Value.NotificationUrl;
if (string.IsNullOrWhiteSpace(notificationUrl))
{
// Без адреса приёмника подписка бессмысленна — он же служит выключателем
// (см. SharePointOptions.NotificationUrl).
logger.LogInformation(
"SharePoint:NotificationUrl не задан — список «{ListTitle}» на уведомления не подписан.",
listTitle);
return false;
}
if (string.IsNullOrWhiteSpace(listTitle))
{
// Без имени списка ручке нечего подписывать: у неё нет другого способа
// его найти (адрес списка она не принимает).
logger.LogError("Заголовок нового списка пуст — подписка на уведомления невозможна.");
return false;
}
// Полный адрес сайта целиком («https://tnsukz.sharepoint.com/sites/requests»),
// а не короткое имя — именно это ждёт ручка.
var site = siteUrl.TrimEnd('/');
var url =
$"/Webhook/Subscribe?listName={Uri.EscapeDataString(listTitle)}" +
$"&url={Uri.EscapeDataString(site)}" +
$"&notificationUrl={Uri.EscapeDataString(notificationUrl)}";
try
{
using var request = new HttpRequestMessage(HttpMethod.Post, url);
await authorizer.AuthorizeAsync(request, cancellationToken);
using var response = await httpClient.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
// Тело читаем именно на неуспехе — в нём причина отказа портала.
var body = await response.Content.ReadAsStringAsync(cancellationToken);
logger.LogError(
"Портал отказал в подписке списка «{ListTitle}» (сайт {Site}): {StatusCode}. Ответ: {Body}",
listTitle, site, (int)response.StatusCode, body);
return false;
}
logger.LogInformation(
"Список «{ListTitle}» (сайт {Site}) подписан на уведомления SharePoint.", listTitle, site);
return true;
}
catch (Exception ex)
{
logger.LogError(
ex,
"Не удалось подписать список «{ListTitle}» (сайт {Site}) на уведомления — подпишите вручную.",
listTitle, site);
return false;
}
}
}