Discord Webhook из PowerShell: Invoke-RestMethod, embed и файлы
Discord webhook из PowerShell: Invoke-RestMethod с hashtable и ConvertTo-Json, embed, отправка файлов в PowerShell 7 и 5.1, планировщик задач, обработка 429.
На этой странице
- Что понадобится
- Invoke-RestMethod: минимальный запрос
- Embed из hashtable
- Отправка файлов
- PowerShell 7: -Form
- Windows PowerShell 5.1: HttpClient
- Обработка ошибок: try/catch на 4xx и 429
- Автоматизация через планировщик задач
- Пример: журнал событий Windows в Discord
- Частые ошибки
- FAQ
- Invoke-RestMethod или Invoke-WebRequest?
- Зачем ConvertTo-Json нужен -Depth 10?
- Как отредактировать или удалить сообщение, отправленное из PowerShell?
- Можно ли отправить кнопки из PowerShell?
- Итого
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.
Смежные статьи:
- Discord Webhook через curl — те же запросы из bash, пригодится на WSL и Linux-хостах
- Discord Webhook на C# — когда скрипт вырастает в сервис: тот же HttpClient в полноценном .NET
- Отправка файлов через Discord Webhook — детали multipart, attachment-ссылки и лимиты размера