Created
March 8, 2026 13:11
-
-
Save alan-null/fa64ea3db3545eb11527be2b09f7bb5c to your computer and use it in GitHub Desktop.
wg-portal Webhook Receiver -> Discord
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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