Skip to content

Instantly share code, notes, and snippets.

@alan-null
Created March 8, 2026 13:11
Show Gist options
  • Select an option

  • Save alan-null/fa64ea3db3545eb11527be2b09f7bb5c to your computer and use it in GitHub Desktop.

Select an option

Save alan-null/fa64ea3db3545eb11527be2b09f7bb5c to your computer and use it in GitHub Desktop.
wg-portal Webhook Receiver -> Discord
FROM mcr.microsoft.com/powershell:7.4-alpine-3.20
WORKDIR /app
COPY webhook-receiver.ps1 .
EXPOSE 8081
ENTRYPOINT ["pwsh", "-File", "/app/webhook-receiver.ps1"]
# wg-portal Webhook Receiver -> Discord
# Listens on HTTP, translates wg-portal events into Discord embeds
param(
[int]$Port = 8081,
[string]$DiscordWebhookUrl = $env:DISCORD_WEBHOOK_URL
)
if (-not $DiscordWebhookUrl) {
Write-Error "DISCORD_WEBHOOK_URL environment variable is not set."
exit 1
}
# --- Color map per event type ---
$ColorMap = @{
connect = 0x2ECC71 # green
disconnect = 0xE74C3C # red
create = 0x3498DB # blue
update = 0xF39C12 # orange
delete = 0x95A5A6 # grey
error = 0xFF0000 # bright red
}
# --- Emoji map ---
$EmojiMap = @{
connect = ":green_circle:"
disconnect = ":red_circle:"
create = ":new:"
update = ":pencil2:"
delete = ":wastebasket:"
error = ":warning:"
}
function Get-FriendlyEntity([string]$entity) {
switch ($entity) {
"peer" { return "Peer" }
"peer_metric" { return "Peer" }
"user" { return "User" }
"interface" { return "Interface" }
default { return $entity }
}
}
function Send-ToDiscord([string]$jsonBody) {
Invoke-RestMethod -Uri $DiscordWebhookUrl `
-Method Post `
-ContentType "application/json" `
-Body $jsonBody | Out-Null
}
function Send-ErrorEmbed([string]$message) {
try {
$embed = @{
title = ":warning: Webhook Receiver Error"
color = $ColorMap["error"]
fields = @(
@{ name = "Error"; value = $message; inline = $false }
)
footer = @{ text = "wg-portal • $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') UTC" }
timestamp = (Get-Date -Format "o")
}
$payload = @{ embeds = @($embed) } | ConvertTo-Json -Depth 10
Send-ToDiscord $payload
}
catch {
Write-Warning "Failed to send error embed to Discord: $_"
}
}
function Build-DiscordPayload($wgEvent) {
$event = $wgEvent.event
$entity = $wgEvent.entity
$identifier = $wgEvent.identifier
$payload = $wgEvent.payload
$color = if ($ColorMap[$event]) { $ColorMap[$event] } else { 0x7F8C8D }
$emoji = if ($EmojiMap[$event]) { $EmojiMap[$event] } else { ":bell:" }
$fEntity = Get-FriendlyEntity $entity
$title = "$emoji $fEntity $(([string]$event).ToUpper())"
$fields = [System.Collections.Generic.List[object]]::new()
$shortId = if ($identifier.Length -gt 24) { $identifier.Substring(0, 24) + "…" } else { $identifier }
$fields.Add(@{ name = "Identifier"; value = "``$shortId``"; inline = $true })
if ($entity -eq "peer_metric" -and $payload.Status) {
$s = $payload.Status
$fields.Add(@{ name = "Endpoint"; value = if ($s.Endpoint) { $s.Endpoint } else { "—" }; inline = $true })
$fields.Add(@{ name = "Connected"; value = "$($s.IsConnected)"; inline = $true })
$fields.Add(@{ name = "↓ Received"; value = "$([math]::Round($s.BytesReceived / 1KB, 1)) KB"; inline = $true })
$fields.Add(@{ name = "↑ Sent"; value = "$([math]::Round($s.BytesTransmitted / 1KB, 1)) KB"; inline = $true })
if ($s.LastHandshake) {
$fields.Add(@{ name = "Last Handshake"; value = $s.LastHandshake; inline = $false })
}
if ($payload.Peer -and $payload.Peer.DisplayName) {
$fields.Add(@{ name = "Display Name"; value = $payload.Peer.DisplayName; inline = $true })
}
}
elseif ($entity -eq "peer" -and $payload) {
if ($payload.DisplayName) { $fields.Add(@{ name = "Name"; value = $payload.DisplayName; inline = $true }) }
if ($payload.Endpoint) { $fields.Add(@{ name = "Endpoint"; value = $payload.Endpoint; inline = $true }) }
if ($payload.AllowedIPsStr) { $fields.Add(@{ name = "Allowed IPs"; value = $payload.AllowedIPsStr; inline = $false }) }
}
elseif ($entity -eq "user" -and $payload) {
if ($payload.Email) { $fields.Add(@{ name = "Email"; value = $payload.Email; inline = $true }) }
if ($payload.Firstname) { $fields.Add(@{ name = "Name"; value = "$($payload.Firstname) $($payload.Lastname)"; inline = $true }) }
}
elseif ($entity -eq "interface" -and $payload) {
if ($payload.DisplayName) { $fields.Add(@{ name = "Interface"; value = $payload.DisplayName; inline = $true }) }
if ($payload.Addresses) { $fields.Add(@{ name = "Addresses"; value = ($payload.Addresses -join ", "); inline = $true }) }
}
$embed = @{
title = $title
color = $color
fields = $fields.ToArray()
footer = @{ text = "wg-portal • $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') UTC" }
timestamp = (Get-Date -Format "o")
}
return @{ embeds = @($embed) } | ConvertTo-Json -Depth 10
}
# --- HTTP Listener ---
$listener = [System.Net.HttpListener]::new()
$prefix = if ($IsLinux) { "http://*:$Port/" } else { "http://+:$Port/" }
$listener.Prefixes.Add($prefix)
$listener.Start()
Write-Host "wg-portal webhook receiver listening on port $Port ..."
while ($listener.IsListening) {
$ctx = $listener.GetContext()
$req = $ctx.Request
$res = $ctx.Response
try {
if ($req.HttpMethod -eq "POST") {
$reader = [System.IO.StreamReader]::new($req.InputStream, $req.ContentEncoding)
$body = $reader.ReadToEnd()
$reader.Close()
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Received: $($body.Substring(0, [Math]::Min(120, $body.Length)))..."
$wgEvent = $body | ConvertFrom-Json
$discord = Build-DiscordPayload $wgEvent
Send-ToDiscord $discord
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Forwarded to Discord OK"
$res.StatusCode = 200
}
else {
$res.StatusCode = 405
}
}
catch {
$errMsg = $_.Exception.Message
Write-Warning "[$(Get-Date -Format 'HH:mm:ss')] Unhandled exception: $errMsg"
Send-ErrorEmbed $errMsg
$res.StatusCode = 500
}
finally {
$res.Close()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment