Перейти к содержимому

Discord Webhook из PowerShell: Invoke-RestMethod, embed и файлы

Discord webhook из PowerShell: Invoke-RestMethod с hashtable и ConvertTo-Json, embed, отправка файлов в PowerShell 7 и 5.1, планировщик задач, обработка 429.

6 мин чтения Доступно также на English
На этой странице

PowerShell уже стоит на каждой Windows-машине, а именно на Windows крутится масса скучной, но важной рутины: бэкапы, скрипты для AD, журналы событий, планировщик задач. Чтобы всё это докладывало в канал Discord, хватает одного командлета — Invoke-RestMethod — и знания нескольких граблей: ConvertTo-Json -Depth, кодировка в Windows PowerShell 5.1, загрузка файлов с -Form и без него, и что делать, когда Discord отвечает 429.

Что понадобится

Проверьте версию:

$PSVersionTable.PSVersion
  • Windows PowerShell 5.1 идёт в комплекте с Windows и работает на .NET Framework. Всё из статьи на нём работает, но с файлами и кодировкой придётся повозиться.
  • PowerShell 7 (winget install Microsoft.PowerShell) — кроссплатформенный, умеет -Form для загрузки файлов и -SkipHttpErrorCheck для аккуратной обработки ошибок. Если есть выбор — берите его.

Ещё нужен URL вебхука из «Настройки сервера → Интеграции → Вебхуки» (подробно — в статье как получить URL вебхука). Относитесь к нему как к паролю: любой, у кого он есть, может писать в ваш канал. В скрипт его не вшивайте — читайте из переменной окружения:

$WebhookUrl = $env:DISCORD_WEBHOOK_URL
if (-not $WebhookUrl) { throw 'Переменная DISCORD_WEBHOOK_URL не задана' }

Invoke-RestMethod: минимальный запрос

Собираем payload в hashtable, конвертируем в JSON, отправляем:

$payload = @{
    content  = 'Бэкап на **SRV-01** завершён в ' + (Get-Date -Format 'HH:mm')
    username = 'Backup Bot'
}

Invoke-RestMethod -Uri $WebhookUrl -Method Post `
    -ContentType 'application/json' `
    -Body ($payload | ConvertTo-Json -Depth 10)

Три детали, на которых спотыкаются:

  • -Depth 10. По умолчанию ConvertTo-Json сериализует только два уровня вложенности. fields у embed лежат на третьем, и всё, что глубже лимита, превращается в строку System.Collections.Hashtable (PowerShell 7 хотя бы предупреждает, 5.1 делает это молча), а Discord отвечает 400. Всегда передавайте -Depth 10.
  • Успешная отправка возвращает 204 No Content, поэтому Invoke-RestMethod ничего не печатает. Это не ошибка. Добавьте к URL ?wait=true, если нужен объект созданного сообщения — его id пригодится для редактирования и удаления.
  • Кодировка в 5.1. Windows PowerShell кодирует строковое тело в ISO-8859-1, если в content type не сказано иное, и кириллица с эмодзи приезжают как ????. Либо укажите -ContentType 'application/json; charset=utf-8', либо передавайте байты — этот способ одинаково работает в обеих версиях:
$json  = $payload | ConvertTo-Json -Depth 10
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
Invoke-RestMethod -Uri $WebhookUrl -Method Post -ContentType 'application/json' -Body $bytes

Дальше в статье используется вариант с байтами, чтобы каждый пример без правок запускался и в 5.1, и в 7.

Embed из hashtable

Embed — это просто вложенная hashtable. embeds обязан быть массивом даже для одного embed, color — десятичное число, timestamp — ISO 8601:

$disk    = Get-PSDrive C
$freeGb  = [math]::Round($disk.Free / 1GB, 1)
$usedPct = [math]::Round($disk.Used / ($disk.Used + $disk.Free) * 100)
$color   = if ($usedPct -gt 90) { 15548997 } else { 5763719 }   # красный / зелёный

$embed = [ordered]@{
    title       = "Отчёт по диску: $env:COMPUTERNAME"
    description = "Диск C: заполнен на **$usedPct%**."
    color       = $color
    fields      = @(
        @{ name = 'Свободно'; value = "$freeGb ГБ"; inline = $true }
        @{ name = 'Занято';   value = "$usedPct%";  inline = $true }
    )
    footer      = @{ text = 'PowerShell monitor' }
    timestamp   = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
}

$payload = @{ username = 'Disk Monitor'; embeds = @($embed) }
$bytes   = [Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Depth 10))
Invoke-RestMethod -Uri $WebhookUrl -Method Post -ContentType 'application/json' -Body $bytes

[ordered] сохраняет порядок ключей — JSON проще читать при отладке; самому Discord порядок безразличен. Лимиты: заголовок 256 символов, описание 4096, до 25 полей, 6000 символов суммарно на все embed одного сообщения (полный список — в статье про лимиты embed).

Если embed удобнее собрать глазами, соберите его в конструкторе Discord Webhook, экспортируйте JSON и вставьте в here-string в одинарных кавычках (@' ... '@). PowerShell ничего внутри неё не подставляет, и JSON остаётся ровно таким, каким его экспортировали.

Отправка файлов

Файлы уходят как multipart/form-data: части files[0], files[1] и так далее плюс часть payload_json с самим сообщением. До 10 файлов, до 10 МБ каждый на сервере без буста.

PowerShell 7: -Form

$payload = @{ content = 'Последние 200 строк лога приложения 📎' } | ConvertTo-Json -Compress

Invoke-RestMethod -Uri $WebhookUrl -Method Post -Form @{
    payload_json = $payload
    'files[0]'   = Get-Item 'C:\logs\app.log'
}

-Form сам выставляет multipart content type; любое значение типа FileInfo (его возвращает Get-Item) уходит как файловая часть. Не сочетайте -Form с -ContentType и -Body.

Windows PowerShell 5.1: HttpClient

В 5.1 параметра -Form нет, а собирать multipart-тело руками с boundary — хрупко. System.Net.Http.HttpClient есть в .NET Framework и формирует тело правильно:

Add-Type -AssemblyName System.Net.Http

$payload = @{ content = 'Последние 200 строк лога приложения' } | ConvertTo-Json -Compress
$path    = 'C:\logs\app.log'

$form = [System.Net.Http.MultipartFormDataContent]::new()
$form.Add([System.Net.Http.StringContent]::new($payload, [Text.Encoding]::UTF8, 'application/json'), 'payload_json')

$file = [System.Net.Http.ByteArrayContent]::new([IO.File]::ReadAllBytes($path))
$form.Add($file, 'files[0]', [IO.Path]::GetFileName($path))

$client   = [System.Net.Http.HttpClient]::new()
$response = $client.PostAsync($WebhookUrl, $form).Result
"$([int]$response.StatusCode) $($response.Content.ReadAsStringAsync().Result)"

Чтобы показать загруженную картинку внутри embed, укажите её в image.url как attachment://filename.png. Больше вариантов, включая несколько файлов разом, — в статье отправка файлов через вебхук.

Обработка ошибок: try/catch на 4xx и 429

Invoke-RestMethod бросает исключение на любой статус вне 2xx. Код статуса лежит в $_.Exception.Response.StatusCode в обеих версиях, а JSON-тело ответа Discord — в $_.ErrorDetails.Message. Этого хватает на переиспользуемую функцию, которая повторяет запрос при 429 и сдаётся на всём остальном:

function Send-DiscordMessage {
    param(
        [Parameter(Mandatory)] [string] $Url,
        [Parameter(Mandatory)] [hashtable] $Payload,
        [int] $MaxAttempts = 3
    )
    $body = [Text.Encoding]::UTF8.GetBytes(($Payload | ConvertTo-Json -Depth 10))

    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            Invoke-RestMethod -Uri $Url -Method Post -ContentType 'application/json' -Body $body
            return $true
        }
        catch {
            $status = [int]$_.Exception.Response.StatusCode
            $detail = $_.ErrorDetails.Message

            if ($status -eq 429) {
                $retry = 1.0
                try { $retry = [double]($detail | ConvertFrom-Json).retry_after } catch {}
                Write-Warning "Rate limit, повтор через $retry с (попытка $attempt/$MaxAttempts)"
                Start-Sleep -Milliseconds ([int]($retry * 1000) + 100)
                continue
            }
            if ($status -in 401, 404) { throw "Вебхук не найден или токен сменился: $detail" }
            if ($status -eq 400)      { throw "Discord отклонил payload: $detail" }
            throw
        }
    }
    return $false
}

Send-DiscordMessage -Url $WebhookUrl -Payload @{ content = 'Привет из PowerShell' }

Тело 400 логируйте: в нём названо проблемное поле, например embeds.0.description, если описание перевалило за 4096 символов. 404 с {"message":"Unknown Webhook","code":10015} означают, что URL неверный или вебхук удалили. При 429 в теле приходит retry_after в секундах — спите как минимум столько. Discord не публикует единого фиксированного лимита для вебхуков, поэтому цифры из статьи про rate limits вебхуков воспринимайте как ориентир, а retry_after уважайте всегда.

В PowerShell 7 можно обойтись без исключений: добавьте к вызову -SkipHttpErrorCheck -StatusCodeVariable status — код ответа окажется в $status, а тело вернётся как результат командлета.

Автоматизация через планировщик задач

Сохраните скрипт как C:\Scripts\Send-DiskReport.ps1 и зарегистрируйте ежедневную задачу. URL вебхука положите в переменную окружения уровня машины — тогда задача увидит её под любой учётной записью (пользовательские переменные задаче, запущенной от SYSTEM, не видны):

# Выполнить один раз в консоли с правами администратора
[Environment]::SetEnvironmentVariable('DISCORD_WEBHOOK_URL', 'https://discord.com/api/webhooks/ID/TOKEN', 'Machine')

$action  = New-ScheduledTaskAction -Execute 'pwsh.exe' `
    -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Send-DiskReport.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At '09:00'
Register-ScheduledTask -TaskName 'Discord disk report' -Action $action -Trigger $trigger `
    -User 'NT AUTHORITY\SYSTEM' -RunLevel Highest

В 5.1 замените pwsh.exe на powershell.exe. Для опроса каждые 15 минут используйте New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15). Переменную уровня машины может прочитать любой локальный пользователь; на общих хостах держите URL в файле с жёстким ACL и читайте его через Get-Content. Перед регистрацией прогоните скрипт из обычной консоли командой pwsh -NoProfile -File ... — так всплывут зависимости от вашего профиля и сетевых дисков, которых у задачи не будет.

Пример: журнал событий Windows в Discord

Готовый скрипт: собирает критические события и ошибки за последние 15 минут, кладёт каждое в поле embed и отправляет, только если есть что сообщать.

$WebhookUrl = $env:DISCORD_WEBHOOK_URL
. "$PSScriptRoot\Send-DiscordMessage.ps1"   # функция с повторами из раздела выше

$events = Get-WinEvent -FilterHashtable @{
    LogName   = 'System', 'Application'
    Level     = 1, 2                          # 1 = Critical, 2 = Error
    StartTime = (Get-Date).AddMinutes(-15)
} -ErrorAction SilentlyContinue | Select-Object -First 10

if (-not $events) { return }

$fields = foreach ($e in $events) {
    $text = ("$($e.Message)" -split "`n")[0].Trim()
    if (-not $text) { $text = '(текст сообщения отсутствует)' }
    if ($text.Length -gt 900) { $text = $text.Substring(0, 900) + '…' }
    $unix = ([DateTimeOffset]$e.TimeCreated).ToUnixTimeSeconds()
    @{
        name  = "$($e.LevelDisplayName) · $($e.ProviderName) · ID $($e.Id)"
        value = "<t:${unix}:R>`n$text"
    }
}

$payload = @{
    username = "События $env:COMPUTERNAME"
    embeds   = @(@{
        title     = "Ошибок за последние 15 минут: $($events.Count)"
        color     = 15548997
        fields    = @($fields)
        timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
    })
}

Send-DiscordMessage -Url $WebhookUrl -Payload $payload

<t:UNIX:R> показывается как «5 минут назад» в локальном времени каждого читателя. Зарегистрируйте скрипт задачей с 15-минутным триггером из предыдущего раздела — и канал превратится в живую ленту сбоев сервера.

Частые ошибки

  • 400 Bad Request, в теле упоминается embeds: чаще всего -Depth остался по умолчанию и вложенные hashtable превратились в строки, либо embeds передан одной hashtable вместо @( ... ). Выведите $payload | ConvertTo-Json -Depth 10 и прочитайте глазами перед отправкой.
  • 400 на пустом сообщении: payload без content, без embeds и без файлов отклоняется. Защититесь от пустых строк, которые выдаёт ваша же логика.
  • Кириллица или эмодзи приходят как ????: Windows PowerShell 5.1 отправил тело в ISO-8859-1. Передавайте UTF-8-байты или добавьте ; charset=utf-8 в -ContentType.
  • «Основное соединение закрыто» или «Не удалось создать защищённый канал SSL/TLS» в 5.1: .NET Framework на этой машине договаривается о старой версии TLS. Поставьте в начало скрипта [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12.
  • 404 «Unknown Webhook» (code 10015): неверный URL, либо вебхук удалили и создали заново — у него теперь новый токен. Обновите переменную окружения.
  • username отклонён: не длиннее 80 символов и без подстрок «clyde» и «discord» в любом регистре.
  • Из консоли работает, из планировщика ничего не приходит: задача выполняется от другого пользователя, без ваших переменных и профиля. «Результат последнего запуска 0x1» означает, что скрипт упал с исключением; добавьте в начало Start-Transcript -Path C:\Scripts\report.log и читайте лог.

FAQ

Invoke-RestMethod или Invoke-WebRequest?

Оба отправляют один и тот же запрос. Invoke-RestMethod разбирает JSON-ответ в объекты — удобно с ?wait=true. Invoke-WebRequest возвращает сырой ответ со StatusCode и Headers — пригодится, когда нужны заголовки X-RateLimit-* без try/catch.

Зачем ConvertTo-Json нужен -Depth 10?

Глубина по умолчанию — 2, а массив fields у embed лежит на глубине 3 (embeds → embed → fields → field). Более глубокие уровни сериализуются как System.Collections.Hashtable, и Discord отвечает 400. Десяти хватает с запасом любому payload вебхука.

Как отредактировать или удалить сообщение, отправленное из PowerShell?

Отправьте с ?wait=true, сохраните id из ответа и вызовите Invoke-RestMethod -Method Patch (или Delete) на $WebhookUrl/messages/$id с тем же JSON. Подробности — в статье редактирование и удаление сообщений вебхука.

Можно ли отправить кнопки из PowerShell?

Кнопки-ссылки и другие неинтерактивные компоненты — да: добавьте к URL ?with_components=true и передайте массив components. Кнопки, которые что-то делают (выдают роль, запускают действие), требуют бота — именно его интеграция сайта Discord Webhook добавляет поверх обычного вебхука.

Итого

Invoke-RestMethod, hashtable, -Depth 10 и try/catch, который уважает retry_after, закрывают почти любую задачу «PowerShell → Discord». Оформление сообщений удобнее прототипировать в бесплатном конструкторе discord-webhook.com/app и вставлять экспортированный JSON в here-string.

Смежные статьи:

Теги: powershellinvoke-restmethoddiscord webhookвебхук discordwindowsпланировщик задачавтоматизация

Похожие статьи

Все статьи

Соберите это в визуальном редакторе

Embed, Components V2, кнопки и опросы с живым предпросмотром Discord. Бесплатно, без регистрации.