This commit is contained in:
Lanta 2025-04-15 17:12:33 +02:00
parent 1be9732279
commit 01aa843d0b
18 changed files with 4122 additions and 2393 deletions

View file

@ -1,350 +1,542 @@
$logprefix = Get-Date -Format "ddMMyyyy_HHmmss"
if ($PSScriptRoot.length -eq 0) {
$scriptroot = Get-Location
}
else {
$scriptroot = $PSScriptRoot
}
$RelativeLogdir = Join-Path -Path $scriptroot -ChildPath "..\logs"
$logDir = (Resolve-Path -Path $RelativeLogdir).Path
Start-Transcript -Path "$logDir/report_new_matches_$logprefix.log" -Append
. $scriptroot\..\includes\ps1\lockfile.ps1
new-lock -by "report_new_matches"
write-output "Scriptroot: $scriptroot"
write-output "Scriptname: $($MyInvocation.MyCommand)"
write-output "Script: $($MyInvocation.MyCommand.Path)"
write-output "PSScriptroot: $PSScriptRoot"
write-output "Logdir: $logDir"
$fileContent = Get-Content -Path "$scriptroot/../config/config.php" -Raw
# Use regex to match the apiKey value
if ($fileContent -match "\`$apiKey\s*=\s*\'([^\']+)\'") {
$apiKey = $matches[1]
}
else {
Write-Output "API Key not found"
}
$headers = @{
'accept' = 'application/vnd.api+json'
'Authorization' = "$apiKey"
}
$fileContent = Get-Content -Path "$scriptroot/../discord/config.php" -Raw
# Use regex to match the apiKey value
if ($fileContent -match "\`$webhookurl\s*=\s*'([^']+)'") {
$webhookurl = $matches[1]
# --- Script Setup ---
$logPrefix = Get-Date -Format "yyyyMMdd_HHmmss" # Use standard sortable format
# Determine script root directory reliably
if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
Write-Output "No web url found"
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
}
Write-Output "Script root identified as: $scriptRoot"
# Use regex to match the losers webhook url
if ($fileContent -match "\`$webhookurl_losers\s*=\s*'([^']+)'") {
$webhookurl_losers = $matches[1]
} else {
Write-Output "No losers web url found"
}
# Define paths using Join-Path
$includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
$discordConfigPath = Join-Path -Path $scriptRoot -ChildPath "config.php" # Config is in the same dir
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
$logDir = Join-Path -Path $scriptRoot -ChildPath "..\logs" # Assuming logs dir relative to scriptroot parent
$playerMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "player_matches.json"
function send-discord {
param (
$content
)
$payload = [PSCustomObject]@{
content = $content
}
Invoke-RestMethod -Uri $webhookurl -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json'
}
function send-discord-losers {
param (
$content
)
$payload = [PSCustomObject]@{
content = $content
}
Invoke-RestMethod -Uri $webhookurl_losers -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json'
}
$map_map = @{
"Baltic_Main" = "Erangel"
"Chimera_Main" = "Paramo"
"Desert_Main" = "Miramar"
"DihorOtok_Main" = "Vikendi"
"Erangel_Main" = "Erangel"
"Heaven_Main" = "Haven"
"Kiki_Main" = "Deston"
"Range_Main" = "Camp Jackal"
"Savage_Main" = "Sanhok"
"Summerland_Main" = "Karakin"
"Tiger_Main" = "Taego"
"Neon_Main" = "Rondo"
}
try {
$player_matches = get-content "$scriptroot/../data/player_matches.json" | convertfrom-json -Depth 100
}
catch {
Write-Output 'Unable to read file exitin'
}
Write-Output $player_matches
Write-Output $new_win_matches
$new_win_matches = $player_matches[-1].new_win_matches
# Gebruik nu de lijst van nieuwe verloren matches uit het JSON-bestand
$new_loss_matches = $player_matches[-1].new_loss_matches
# Post verloren matches naar #losers kanaal
foreach ($lossid in $new_loss_matches) {
$lossmatch = $player_matches.player_matches | Where-Object { $_.id -eq $lossid }
if ($null -eq $lossmatch) { continue }
if ($lossmatch[0].gameMode -eq 'tdm') { continue }
# Fetch detailed match stats and telemetry for the loss
$loss_match_stats = $null
$loss_telemetry = $null
# Ensure Log directory exists
if (-not (Test-Path -Path $logDir -PathType Container)) {
Write-Warning "Log directory not found at '$logDir'. Attempting to create."
try {
$loss_match_stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/matches/$lossid" -Method GET -Headers $headers
$loss_telemetry = (invoke-webrequest @($lossmatch.telemetry_url)[0]).content | convertfrom-json | where-object { ($_._T -eq 'LOGPLAYERTAKEDAMAGE') -or ($_._T -eq 'LOGPLAYERKILLV2') }
New-Item -Path $logDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Successfully created log directory."
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Failed to fetch API/telemetry data for loss match {0}: {1}" -f $lossid, $errorMessage)
# Log to console if transcript path fails
Write-Error "Failed to create log directory '$logDir'. Please check permissions. Error: $($_.Exception.Message)"
# Continue without transcript if log dir fails? Or exit? For now, continue.
}
$loss_stats_table = @()
$loss_victims = @() # For team damage
# Iterate through players found in the locally stored match data for this loss
foreach ($player_stat in $lossmatch[0].stats) {
$player_name = $player_stat.name
# Find the corresponding detailed stats from the API response
$detailed_player_stats = $null
if ($null -ne $loss_match_stats) {
$detailed_player_stats = $loss_match_stats.included | Where-Object {$_.type -eq 'participant'} | ForEach-Object {$_.attributes.stats} | Where-Object { $_.name -eq $player_name }
}
if ($null -eq $detailed_player_stats) {
Write-Warning "Could not find detailed stats for player $player_name in loss match $lossid. Using basic stats."
# Fallback to basic stats if detailed stats are missing
$loss_stats_table += [PSCustomObject]@{
Name = $player_name
'Human dmg' = "N/A"
'Human Kills' = "N/A"
'Dmg' = "$([math]::Round($player_stat.damageDealt))" # Use basic stat
'Kills' = "$($player_stat.kills)" # Use basic stat
'alive' = "$([math]::Round(($player_stat.timeSurvived / 60)))" # Use basic stat
}
continue # Skip telemetry processing if detailed stats failed
}
# Calculate stats (similar to win stats calculation)
$human_dmg = "N/A"
$human_kills = "N/A"
if ($null -ne $loss_telemetry) {
try {
$human_dmg = [math]::Round(($loss_telemetry | Where-Object { $_._T -eq 'LOGPLAYERTAKEDAMAGE' -and $_.attacker.name -eq $player_name -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId } | Measure-Object -Property damage -Sum).Sum)
$human_kills = ($loss_telemetry | Where-Object { $_._T -eq 'LOGPLAYERKILLV2' -and $_.killer.name -eq $player_name -and $_.victim.accountId -notlike "ai.*" }).count
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Error processing telemetry stats for {0} in loss {1}: {2}" -f $player_name, $lossid, $errorMessage)
}
}
$loss_stats_table += [PSCustomObject]@{
Name = $player_name
'Human dmg' = "$human_dmg"
'Human Kills' = "$human_kills"
'Dmg' = "$([math]::Round($detailed_player_stats.damageDealt))"
'Kills' = "$($detailed_player_stats.kills)"
'alive' = "$([math]::Round(($detailed_player_stats.timeSurvived / 60)))"
}
# Calculate team damage
if ($null -ne $loss_telemetry) {
try {
$teamdmg = $loss_telemetry | Where-Object {
$_._T -eq 'LOGPLAYERTAKEDAMAGE' -and
$_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and
$_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $player_name
}
if ($teamdmg.count -ge 1) {
foreach ($victim_name in ($teamdmg.victim.name | Select-Object -Unique)) {
$loss_victims += [PSCustomObject]@{
attacker = $player_name
victim = $victim_name
Damage = "$([math]::Round((($teamdmg | Where-Object { $_.victim.name -eq $victim_name }).damage | Measure-Object -Sum).Sum))"
}
}
}
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Error processing team damage for {0} in loss {1}: {2}" -f $player_name, $lossid, $errorMessage)
}
}
}
# Format the stats table
$content_lossstats = ""
if ($loss_stats_table.Count -gt 0) {
$content_lossstats = '```' + ($loss_stats_table | Format-Table -AutoSize | Out-String) + '```'
}
# Format team damage table
$content_loss_victims = ""
if ($loss_victims.Count -gt 0) {
$content_loss_victims = ":skull::skull: Team Damage :skull::skull:`n" + '```' + ($loss_victims | Format-Table -AutoSize | Out-String) + '```'
}
# Original message construction variables
$losers = $lossmatch[0].stats.name -join ', ' # Join names for display
$map = $map_map[$lossmatch[0].mapName]
$place = ($lossmatch[0].stats | Select-Object -First 1).winPlace # Get placement from the first player stat
$first_player_name = ($lossmatch[0].stats | Select-Object -First 1).name
$replay_url = $lossmatch[0].telemetry_url -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg'
$replay_url = $replay_url -replace '-telemetry.json', ''
$replay_url = $replay_url + "?follow=$first_player_name" # Follow the first player
$match_settings = @"
``````
match mode $($lossmatch[0].gameMode)
match type $($lossmatch[0].matchType)
map $($map_map[$lossmatch[0].mapName])
id $($lossmatch[0].id)
``````
"@
send-discord-losers -content "We hebben een LOSERT! Geen Kip voor jou! :skull::skull:"
send-discord-losers -content ":partying_face::partying_face::partying_face: Helaas, $($losers) :partying_face::partying_face::partying_face:"
send-discord-losers -content $match_settings
send-discord-losers -content $content_lossstats
send-discord-losers -content $content_loss_victims
send-discord-losers -content "[2D replay](<$replay_url>)"
send-discord-losers -content "Meer match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$($lossmatch[0].id)>)"
}
# Start Transcript (use calculated $logDir)
$transcriptPath = Join-Path -Path $logDir -ChildPath "report_new_matches_$logPrefix.log"
try {
Start-Transcript -Path $transcriptPath -Append -ErrorAction Stop
Write-Output "Starting report_new_matches script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
Write-Output "Transcript logging to: $transcriptPath"
} catch {
Write-Error "Failed to start transcript at '$transcriptPath'. Error: $($_.Exception.Message)"
# Exit if transcript is critical? For now, continue.
}
foreach ($winid in $new_win_matches) {
# --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
if ($transcriptPath) { Stop-Transcript }
exit 1
}
. $lockFilePath
New-Lock -by "report_new_matches" -ErrorAction Stop # Stop if locking fails
$win_stats = @()
$victims = @()
if ($null -eq $winid) { continue }
$winmatches = $player_matches.player_matches | Where-Object { $_.id -eq $winid }
$telemetry = (invoke-webrequest @($winmatches.telemetry_url)[0]).content | convertfrom-json | where-object { ($_._T -eq 'LOGPLAYERTAKEDAMAGE') -or ($_._T -eq 'LOGPLAYERKILLV2') }
$winners = @(($winmatches | where-object { $_.stats.winPlace -eq 1 }).stats.name)
$2D_replay_url = @($winmatches.telemetry_url)[0] -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg'
$2D_replay_url = $2D_replay_url -replace '-telemetry.json', ''
$2D_replay_url = $2D_replay_url + "?follow=$($winners[0])"
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Configuration Loading ---
$apiKey = $null
$webhookUrl = $null
$webhookUrlLosers = $null
$match_stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/matches/$winid" -Method GET -Headers $headers
if ($winmatches[0].gameMode -eq 'tdm' ) {
continue
} #skip tdm matches
if ($winmatches[0].matchType -eq 'custom') {
$players_to_report = $match_stats.included.attributes.stats | where-object { $_.playerId -notlike "ai.*" }
# Load API Key from main config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') { $apiKey = $matches[1]; Write-Verbose "API Key loaded." }
else { Write-Warning "API Key pattern not found in '$phpConfigPath'." }
} catch { Write-Warning "Failed to read '$phpConfigPath': $($_.Exception.Message)" }
} else { Write-Warning "Main config file not found at '$phpConfigPath'." }
# Load Webhook URLs from discord/config.php
if (Test-Path -Path $discordConfigPath -PathType Leaf) {
try {
$discordFileContent = Get-Content -Path $discordConfigPath -Raw -ErrorAction Stop
if ($discordFileContent -match '^\s*\$webhookurl\s*=\s*''([^'']+)''') { $webhookUrl = $matches[1]; Write-Verbose "Main webhook URL loaded." }
else { Write-Warning "Main webhook URL pattern not found in '$discordConfigPath'." }
if ($discordFileContent -match '^\s*\$webhookurl_losers\s*=\s*''([^'']+)''') { $webhookUrlLosers = $matches[1]; Write-Verbose "Losers webhook URL loaded." }
else { Write-Warning "Losers webhook URL pattern not found in '$discordConfigPath'." }
} catch { Write-Warning "Failed to read '$discordConfigPath': $($_.Exception.Message)" }
} else { Write-Warning "Discord config file not found at '$discordConfigPath'." }
# Validate required config
if (-not $apiKey) { Write-Error "API Key missing."; throw "Missing API Key" }
if (-not $webhookUrl) { Write-Error "Main Discord webhook URL missing."; throw "Missing Webhook URL" }
if (-not $webhookUrlLosers) { Write-Error "Losers Discord webhook URL missing."; throw "Missing Losers Webhook URL" }
# --- API Headers ---
$apiHeaders = @{
'accept' = 'application/vnd.api+json'
'Authorization' = "Bearer $apiKey"
}
else {
$players_to_report = $match_stats.included.attributes.stats | where-object { $_.winplace -eq 1 }
# --- Helper Function for API Calls (Copied from update_clan_members.ps1) ---
function Invoke-PubgApi {
param(
[Parameter(Mandatory=$true)][string]$Uri, [Parameter(Mandatory=$true)][hashtable]$Headers,
[int]$RetryCount = 1, [int]$RetryDelaySeconds = 61
)
for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
try {
Write-Verbose "API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
if ($null -ne $response) { Write-Verbose "API call successful."; return $response }
else { Write-Warning "API call to $Uri returned null."; return $null }
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping $RetryDelaySeconds sec..."; Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) { Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"; return $null }
else { Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"; return $null }
}
}
return $null
}
if ($new_win_matches.count -le 10) {
#fail safe
send-discord -content ":chicken: :chicken: **WINNER WINNER CHICKEN DINNER!!** :chicken: :chicken:"
send-discord -content ":partying_face::partying_face::partying_face: Gefeliciteerd $($winners -join ', ') :partying_face::partying_face::partying_face:"
$match_settings = @"
``````
match mode $($winmatches[0].gameMode)
match type $($winmatches[0].matchType)
map $($map_map[$winmatches[0].mapName])
id $($winmatches[0].id)
``````
"@
send-discord -content $match_settings
# --- Helper Function for Telemetry Download/Parse ---
function Get-TelemetryData {
param([string]$TelemetryUrl)
if (-not $TelemetryUrl) { Write-Warning "Get-TelemetryData: No Telemetry URL provided."; return $null }
$telemetryFileName = $TelemetryUrl.Split('/')[-1]
$telemetryCacheFilePath = Join-Path -Path $telemetryCachePath -ChildPath $telemetryFileName
# Try cache first
if (Test-Path -Path $telemetryCacheFilePath -PathType Leaf) {
Write-Verbose "Loading telemetry from cache: $telemetryCacheFilePath"
try {
$telemetry = Get-Content -Path $telemetryCacheFilePath | ConvertFrom-Json -ErrorAction Stop
if ($null -ne $telemetry) { return $telemetry }
else { Write-Warning "Failed to parse cached telemetry: $telemetryCacheFilePath" }
} catch { Write-Warning "Error reading cached telemetry '$telemetryCacheFilePath': $($_.Exception.Message)" }
}
# Download if not cached or cache failed
Write-Output "Downloading telemetry: $TelemetryUrl"
try {
$webHeaders = @{ 'Accept-Encoding' = 'gzip' }
$response = Invoke-WebRequest -Uri $TelemetryUrl -Headers $webHeaders -UseBasicParsing -ErrorAction Stop
$telemetryContent = $response.Content
$telemetryContent | Out-File -FilePath $telemetryCacheFilePath -Encoding UTF8 -ErrorAction SilentlyContinue
$telemetry = $telemetryContent | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $telemetry) { Write-Warning "Failed to parse downloaded telemetry from $TelemetryUrl." }
return $telemetry
} catch {
$errorMessage = $_.Exception.Message
Write-Warning "Failed to download/save telemetry from $TelemetryUrl. Error: $errorMessage"
return $null
}
}
else {
write-output "Something went wrong (more then 10 matches to report)"
}
foreach ($player in $players_to_report.name) {
if ($null -eq $player) { continue }
write-output "creating table for player $player"
$win_stats += [PSCustomObject]@{
Name = $player
'Human dmg' = "$([math]::Round(($telemetry | Where-Object { $_._T -eq 'LOGPLAYERTAKEDAMAGE' -and $_.attacker.name -eq $player -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId } | Measure-Object -Property damage -Sum).Sum))"
'Human Kills' = "$(($telemetry | Where-Object { $_._T -eq 'LOGPLAYERKILLV2' -and $_.killer.name -eq $player -and $_.victim.accountId -notlike "ai.*" }).count)"
'Dmg' = "$([math]::Round(($players_to_report | Where-Object { $_.name -eq $player }).damageDealt))"
'Kills' = "$(($players_to_report | Where-Object { $_.name -eq $player }).kills)"
'alive' = "$([math]::Round((($players_to_report | Where-Object { $_.name -eq $player }).timeSurvived /60 )))"
# --- Discord Sending Functions (with Error Handling) ---
function Send-DiscordMessage {
param([string]$Webhook, [string]$Content)
if (-not $Webhook -or -not $Content) { Write-Warning "Send-DiscordMessage: Missing Webhook or Content."; return }
$payload = @{ content = $Content } | ConvertTo-Json -Depth 3
try {
Invoke-RestMethod -Uri $Webhook -Method Post -Body $payload -ContentType 'application/json' -ErrorAction Stop
Write-Verbose "Successfully sent message to Discord."
} catch {
$errorMessage = $_.Exception.Message
Write-Error "Failed to send message to Discord ($Webhook). Error: $errorMessage"
}
$teamdmg = $telemetry | Where-Object {
$_._T -eq 'LOGPLAYERTAKEDAMAGE' -and
$_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and
$_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $player
}
function Send-DiscordWin($Content) { Send-DiscordMessage -Webhook $webhookUrl -Content $Content }
function Send-DiscordLoss($Content) { Send-DiscordMessage -Webhook $webhookUrlLosers -Content $Content }
# --- Map Definitions ---
$mapNameLookup = @{ # Renamed from $map_map
"Baltic_Main" = "Erangel"
"Chimera_Main" = "Paramo"
"Desert_Main" = "Miramar"
"DihorOtok_Main" = "Vikendi"
"Erangel_Main" = "Erangel" # Duplicate key, might be intentional?
"Heaven_Main" = "Haven"
"Kiki_Main" = "Deston"
"Range_Main" = "Camp Jackal"
"Savage_Main" = "Sanhok"
"Summerland_Main" = "Karakin"
"Tiger_Main" = "Taego"
"Neon_Main" = "Rondo"
}
# --- Load Player Matches Data ---
$playerMatchesData = $null
if (Test-Path -Path $playerMatchesJsonPath -PathType Leaf) {
try {
$playerMatchesData = Get-Content -Path $playerMatchesJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $playerMatchesData -or -not ($playerMatchesData -is [array])) {
Write-Error "Invalid structure in '$playerMatchesJsonPath'. Expected array. Cannot proceed."
throw "Invalid player matches data."
}
Write-Output "Successfully loaded player matches data."
} catch {
Write-Error "Error reading '$playerMatchesJsonPath': $($_.Exception.Message). Cannot proceed."
throw $_
}
} else {
Write-Error "Player matches file not found at '$playerMatchesJsonPath'. Cannot proceed."
throw "Missing player matches file."
}
# --- Extract New Wins and Losses ---
# Find the special entries added by get_matches.ps1
$newWinEntry = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -eq 'new_win_matches' } | Select-Object -First 1
$newLossEntry = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -eq 'new_loss_matches' } | Select-Object -First 1
$newWinMatchIds = if ($null -ne $newWinEntry -and $newWinEntry.new_win_matches -is [array]) { $newWinEntry.new_win_matches } else { @() }
$newLossMatchIds = if ($null -ne $newLossEntry -and $newLossEntry.new_loss_matches -is [array]) { $newLossEntry.new_loss_matches } else { @() }
Write-Output "Found $($newWinMatchIds.Count) new win match IDs and $($newLossMatchIds.Count) new loss match IDs to report."
# Extract actual match data (excluding the special entries)
$actualMatchEntries = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -ne 'new_win_matches' -and $_.PSObject.Properties.Name -ne 'new_loss_matches' }
# --- Process and Report New Losses ---
Write-Output "Processing $($newLossMatchIds.Count) new losses..."
foreach ($lossId in $newLossMatchIds) {
if (-not $lossId) { continue }
Write-Output "Processing loss match ID: $lossId"
# Find all player entries related to this loss match ID in the loaded data
$lossMatchPlayerEntries = $actualMatchEntries.player_matches | Where-Object { $_.id -eq $lossId }
if ($null -eq $lossMatchPlayerEntries -or $lossMatchPlayerEntries.Count -eq 0) {
Write-Warning "Could not find match details for loss ID $lossId in player_matches.json data."
continue
}
if ($teamdmg.count -ge 1) {
foreach ($victim in ($teamdmg.victim.name | Select-Object -Unique)) {
$victims += [PSCustomObject]@{
attacker = $player
victim = $victim
Damage = "$([math]::Round((($teamdmg | Where-Object { $_.victim.name -eq $victim }).damage | Measure-Object -Sum).Sum))"
}
# Use the first entry for common match details (assuming they are consistent)
$firstLossEntry = $lossMatchPlayerEntries[0]
$lossGameMode = $firstLossEntry.gameMode
$lossMatchType = $firstLossEntry.matchType
$lossMapNameRaw = $firstLossEntry.mapName
$lossTelemetryUrl = $firstLossEntry.telemetry_url
# Skip TDM matches
if ($lossGameMode -eq 'tdm') { Write-Output "Skipping TDM loss match $lossId."; continue }
# Fetch Full Match Stats from API (needed for all participants' details)
$lossMatchApiStats = Invoke-PubgApi -Uri "https://api.pubg.com/shards/steam/matches/$lossId" -Headers $apiHeaders
# Get Telemetry Data
$lossTelemetryEvents = Get-TelemetryData -TelemetryUrl $lossTelemetryUrl
$relevantLossTelemetry = $null
if ($lossTelemetryEvents -is [array]) {
$relevantLossTelemetry = $lossTelemetryEvents | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -or $_._T -eq 'LogPlayerKillV2' }
} else { Write-Warning "Invalid or missing telemetry data for loss match $lossId." }
# Prepare data tables
$lossStatsTable = @()
$lossTeamDamageVictims = @()
# Iterate through players involved in this loss from our data
$involvedPlayerNames = $lossMatchPlayerEntries.stats.name | Select-Object -Unique
foreach ($playerName in $involvedPlayerNames) {
$playerLossStats = $lossMatchPlayerEntries | Where-Object { $_.stats.name -eq $playerName } | Select-Object -First 1 -ExpandProperty stats
# Try to get more detailed stats from the full API response if available
$detailedPlayerStats = $null
if ($null -ne $lossMatchApiStats -and $lossMatchApiStats.included -is [array]) {
$detailedPlayerStats = $lossMatchApiStats.included |
Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.name -eq $playerName } |
Select-Object -First 1 -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
# Use detailed stats if found, otherwise fallback to basic stats from player_matches.json
$statsToUse = if ($null -ne $detailedPlayerStats) { $detailedPlayerStats } else { $playerLossStats }
if ($null -eq $statsToUse) { Write-Warning "Could not find any stats for $playerName in loss $lossId."; continue }
# Calculate Human Kills/Damage from Telemetry
$humanDmg = "N/A"
$humanKills = "N/A"
if ($null -ne $relevantLossTelemetry) {
try {
$humanDmgEvents = $relevantLossTelemetry | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -and $_.attacker.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId }
if ($humanDmgEvents) { $humanDmg = [math]::Round(($humanDmgEvents | Measure-Object -Property damage -Sum).Sum) }
$humanKillEvents = $relevantLossTelemetry | Where-Object { $_._T -eq 'LogPlayerKillV2' -and $_.killer.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" }
$humanKills = $humanKillEvents.Count
} catch { Write-Warning ("Error processing telemetry stats for {0} in loss {1}: {2}" -f $playerName, $lossId, $_.Exception.Message) }
}
# Add to stats table
$lossStatsTable += [PSCustomObject]@{
Name = $playerName
'Human dmg' = "$humanDmg"
'Human Kills' = "$humanKills"
'Dmg' = "$([math]::Round($statsToUse.damageDealt))"
'Kills' = "$($statsToUse.kills)"
'alive (min)' = "$([math]::Round(($statsToUse.timeSurvived / 60)))" # Clarify unit
}
# Calculate team damage from Telemetry
if ($null -ne $relevantLossTelemetry) {
try {
$teamDmgEvents = $relevantLossTelemetry | Where-Object {
$_._T -eq 'LogPlayerTakeDamage' -and $_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and $_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $playerName
}
if ($teamDmgEvents) {
foreach ($victimEntry in ($teamDmgEvents | Group-Object victim.name)) {
$lossTeamDamageVictims += [PSCustomObject]@{
attacker = $playerName
victim = $victimEntry.Name
Damage = "$([math]::Round(($victimEntry.Group.damage | Measure-Object -Sum).Sum))"
}
}
}
} catch { Write-Warning ("Error processing team damage for {0} in loss {1}: {2}" -f $playerName, $lossId, $_.Exception.Message) }
}
} # End foreach playerName in loss
# Format tables for Discord message
$contentLossStats = if ($lossStatsTable.Count -gt 0) { '```' + ($lossStatsTable | Format-Table -AutoSize | Out-String) + '```' } else { "" }
$contentLossVictims = if ($lossTeamDamageVictims.Count -gt 0) { ":skull::skull: Team Damage :skull::skull:`n" + '```' + ($lossTeamDamageVictims | Format-Table -AutoSize | Out-String) + '```' } else { "" }
# Construct and Send Loss Message
$losersString = $involvedPlayerNames -join ', '
$mapDisplayName = if ($mapNameLookup.ContainsKey($lossMapNameRaw)) { $mapNameLookup[$lossMapNameRaw] } else { $lossMapNameRaw }
$firstPlayerName = $involvedPlayerNames[0] # Use first player for replay link
$replayUrl = $lossTelemetryUrl -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg' -replace '-telemetry.json', ''
$replayUrl += "?follow=$firstPlayerName"
$matchSettings = @"
``````
Match Mode : $lossGameMode
Match Type : $lossMatchType
Map : $mapDisplayName
Match ID : $lossId
``````
"@
Send-DiscordLoss -Content "We hebben een LOSERT! Geen Kip voor jou! :skull::skull:"
Send-DiscordLoss -Content ":partying_face::partying_face: Helaas, **$($losersString)** :partying_face::partying_face:"
Send-DiscordLoss -Content $matchSettings
if ($contentLossStats) { Send-DiscordLoss -Content $contentLossStats }
if ($contentLossVictims) { Send-DiscordLoss -Content $contentLossVictims }
Send-DiscordLoss -Content "[2D Replay](<$replayUrl>)"
Send-DiscordLoss -Content "Meer match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$lossId>)"
Write-Output "Sent loss report for match $lossId."
Start-Sleep -Seconds 2 # Small delay between messages
} # End foreach lossId
# --- Process and Report New Wins ---
Write-Output "Processing $($newWinMatchIds.Count) new wins..."
foreach ($winId in $newWinMatchIds) {
if (-not $winId) { continue }
Write-Output "Processing win match ID: $winId"
# Find all player entries related to this win match ID
$winMatchPlayerEntries = $actualMatchEntries.player_matches | Where-Object { $_.id -eq $winId }
if ($null -eq $winMatchPlayerEntries -or $winMatchPlayerEntries.Count -eq 0) {
Write-Warning "Could not find match details for win ID $winId in player_matches.json data."
continue
}
# Use the first entry for common details
$firstWinEntry = $winMatchPlayerEntries[0]
$winGameMode = $firstWinEntry.gameMode
$winMatchType = $firstWinEntry.matchType
$winMapNameRaw = $firstWinEntry.mapName
$winTelemetryUrl = $firstWinEntry.telemetry_url
# Skip TDM
if ($winGameMode -eq 'tdm') { Write-Output "Skipping TDM win match $winId."; continue }
# Get Telemetry
$winTelemetryEvents = Get-TelemetryData -TelemetryUrl $winTelemetryUrl
$relevantWinTelemetry = $null
if ($winTelemetryEvents -is [array]) {
$relevantWinTelemetry = $winTelemetryEvents | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -or $_._T -eq 'LogPlayerKillV2' }
} else { Write-Warning "Invalid or missing telemetry data for win match $winId." }
# Get Full Match Stats from API
$winMatchApiStats = Invoke-PubgApi -Uri "https://api.pubg.com/shards/steam/matches/$winId" -Headers $apiHeaders
# Determine players to report (winners or all non-AI in custom)
$playersToReportStats = @()
if ($null -ne $winMatchApiStats -and $winMatchApiStats.included -is [array]) {
if ($winMatchType -eq 'custom') {
$playersToReportStats = $winMatchApiStats.included | Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.playerId -notlike "ai.*" } | Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
} else {
$playersToReportStats = $winMatchApiStats.included | Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.winPlace -eq 1 } | Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
} else {
Write-Warning "Could not get full match stats from API for win $winId. Reporting might be incomplete."
# Fallback: Use players from our loaded data who won
$playersToReportStats = $winMatchPlayerEntries | Where-Object { $_.stats.winPlace -eq 1 } | Select-Object -ExpandProperty stats
}
if ($playersToReportStats.Count -eq 0) { Write-Warning "No winning players found to report for match $winId."; continue }
$winnerNames = $playersToReportStats.name
$winnersString = $winnerNames -join ', '
# Prepare data tables
$winStatsTable = @()
$winTeamDamageVictims = @()
# Fail-safe check (from original script) - Limit number of reports if too many new wins detected at once?
# This might indicate an issue with the comparison logic or first run.
if ($newWinMatchIds.Count -gt 10) {
Write-Warning "More than 10 new win matches detected ($($newWinMatchIds.Count)). This might indicate an issue. Reporting only the first 10."
# Optionally break or limit the loop here if desired
# For now, just log the warning.
}
# Send initial win messages
Send-DiscordWin -Content ":chicken::chicken: **WINNER WINNER CHICKEN DINNER!!** :chicken::chicken:"
Send-DiscordWin -Content ":partying_face::partying_face: Gefeliciteerd **$($winnersString)** :partying_face::partying_face:"
$mapDisplayName = if ($mapNameLookup.ContainsKey($winMapNameRaw)) { $mapNameLookup[$winMapNameRaw] } else { $winMapNameRaw }
$matchSettings = @"
``````
Match Mode : $winGameMode
Match Type : $winMatchType
Map : $mapDisplayName
Match ID : $winId
``````
"@
Send-DiscordWin -Content $matchSettings
# Calculate stats for each reported player
foreach ($playerStat in $playersToReportStats) {
$playerName = $playerStat.name
if (-not $playerName) { continue }
Write-Verbose "Creating stats table entry for winner $playerName in match $winId"
# Calculate Human Kills/Damage from Telemetry
$humanDmg = "N/A"
$humanKills = "N/A"
if ($null -ne $relevantWinTelemetry) {
try {
$humanDmgEvents = $relevantWinTelemetry | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -and $_.attacker.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId }
if ($humanDmgEvents) { $humanDmg = [math]::Round(($humanDmgEvents | Measure-Object -Property damage -Sum).Sum) }
$humanKillEvents = $relevantWinTelemetry | Where-Object { $_._T -eq 'LogPlayerKillV2' -and $_.killer.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" }
$humanKills = $humanKillEvents.Count
} catch { Write-Warning ("Error processing telemetry stats for {0} in win {1}: {2}" -f $playerName, $winId, $_.Exception.Message) }
}
# Add to stats table
$winStatsTable += [PSCustomObject]@{
Name = $playerName
'Human dmg' = "$humanDmg"
'Human Kills' = "$humanKills"
'Dmg' = "$([math]::Round($playerStat.damageDealt))"
'Kills' = "$($playerStat.kills)"
'alive (min)' = "$([math]::Round(($playerStat.timeSurvived / 60)))"
}
# Calculate team damage from Telemetry
if ($null -ne $relevantWinTelemetry) {
try {
$teamDmgEvents = $relevantWinTelemetry | Where-Object {
$_._T -eq 'LogPlayerTakeDamage' -and $_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and $_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $playerName
}
if ($teamDmgEvents) {
foreach ($victimEntry in ($teamDmgEvents | Group-Object victim.name)) {
$winTeamDamageVictims += [PSCustomObject]@{
attacker = $playerName
victim = $victimEntry.Name
Damage = "$([math]::Round(($victimEntry.Group.damage | Measure-Object -Sum).Sum))"
}
}
}
} catch { Write-Warning ("Error processing team damage for {0} in win {1}: {2}" -f $playerName, $winId, $_.Exception.Message) }
}
} # End foreach playerStat
# Format and send stats tables
$contentWinStats = if ($winStatsTable.Count -gt 0) { '```' + ($winStatsTable | Format-Table -AutoSize | Out-String) + '```' } else { "" }
if ($contentWinStats) { Send-DiscordWin -Content $contentWinStats }
$contentWinVictims = if ($winTeamDamageVictims.Count -gt 0) { ":skull::skull: Team Damage Report :skull::skull:`n" + '```' + ($winTeamDamageVictims | Format-Table -AutoSize | Out-String) + '```' } else { "" }
if ($contentWinVictims) { Send-DiscordWin -Content $contentWinVictims }
# Send Replay and Details Links
$firstWinnerName = $winnerNames[0]
$replayUrl = $winTelemetryUrl -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg' -replace '-telemetry.json', ''
$replayUrl += "?follow=$firstWinnerName"
Send-DiscordWin -Content "[2D Replay](<$replayUrl>)"
Send-DiscordWin -Content "More match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$winId>)"
Write-Output "Sent win report for match $winId."
Start-Sleep -Seconds 2 # Small delay between messages
} # End foreach winId
# --- Clear New Match Lists in Data File ---
Write-Output "Clearing new win/loss lists in player matches data file..."
$updatedPlayerMatchesData = $playerMatchesData # Start with the loaded data
$winListCleared = $false
$lossListCleared = $false
# Iterate through the array to find and modify the special entries
for ($i = 0; $i -lt $updatedPlayerMatchesData.Count; $i++) {
$item = $updatedPlayerMatchesData[$i]
if ($item -is [PSCustomObject]) {
if ($item.PSObject.Properties.Name -eq 'new_win_matches') {
$item.new_win_matches = @() # Clear the list
$winListCleared = $true
}
if ($item.PSObject.Properties.Name -eq 'new_loss_matches') {
$item.new_loss_matches = @() # Clear the list
$lossListCleared = $true
}
}
# Stop if both found and cleared
if ($winListCleared -and $lossListCleared) { break }
}
write-output "New win matches:"
$new_win_matches
if ($new_win_matches.count -le 10) {
$content_winstats = '```' + ($win_stats | Format-Table | out-string) + '```'
send-discord -content $content_winstats
if ($victims.count -ge 1) {
send-discord -content ":skull::skull: Helaas hebben we deze keer ook team killers :skull::skull: "
$content_victims = '```' + ($victims | Format-Table | out-string) + '```'
send-discord -content $content_victims
}
send-discord -content "[2D replay](<$2D_replay_url>)"
send-discord -content "More match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$($winmatches[0].id)>)"
}
else {
write-output "Something went wrong (more then 10 matches to report)"
}
$legenda = '
```
dmg_h = Schade aangericht aan echte spelers
dmg = Totale schade (aan zowel echte spelers als AI)
k_h = Aantal echte spelers die je hebt geelimineerd
K_a = Totale aantal eliminaties (inclusief AI)
t_serv = Overleefde tijd (in minuten)
k_t = Team eliminaties
```
'
#send-discord -content $legenda
}
foreach ($item in $player_matches) {
if ($item.PSObject.Properties.Name -contains "new_win_matches") {
$item.new_win_matches = $null
$item.new_loss_matches = $null
if ($winListCleared -or $lossListCleared) {
try {
$updatedPlayerMatchesData | ConvertTo-Json -Depth 100 | Out-File -FilePath $playerMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Successfully cleared new match lists in '$playerMatchesJsonPath'."
} catch {
Write-Error "Failed to save player matches data after clearing lists. '$playerMatchesJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Could not find 'new_win_matches' or 'new_loss_matches' entries to clear in '$playerMatchesJsonPath'."
}
}
# Convert back to JSON (optional)
$newJson = $player_matches | ConvertTo-Json -Depth 100
# Display the updated JSON
$newJson | out-file "$scriptroot/../data/player_matches.json"
remove-lock
Stop-Transcript
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
if ($transcriptPath -and (Get-Transcript | Select-Object -ExpandProperty Path) -eq $transcriptPath) {
Stop-Transcript
}
}

View file

@ -1,60 +1,139 @@
Start-Transcript -Path '/var/log/report_new_matches.log' -Append
# --- Script Setup ---
# Note: Transcript path seems incorrect, should likely be specific to this script.
# Consider changing to '/var/log/dtch/report_to_discord.log' or similar.
Start-Transcript -Path '/var/log/dtch/report_to_discord.log' -Append
Write-Output "Starting report_to_discord script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
if ($PSScriptRoot.length -eq 0) {
$scriptroot = Get-Location
# Determine script root directory reliably
if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
}
else {
$scriptroot = $PSScriptRoot
}
. $scriptroot\..\includes\ps1\lockfile.ps1
new-lock -by "report_to_discord"
function IsValidEntry($entry) {
return ($entry.KD_H -ne 'NaN' -and $entry.KD_ALL -ne 'NaN') -and
($entry.KD_H -ne 'Infinity' -and $entry.KD_ALL -ne 'Infinity')
}
$fileContent = Get-Content -Path "$scriptroot/../discord/config.php" -Raw
Write-Output "Script root identified as: $scriptRoot"
# Use regex to match the apiKey value
if ($fileContent -match "\`$webhookurl\s*=\s*\'([^\']+)\'") {
$webhookurl = $matches[1]
}
else {
Write-Output "API Key not found"
}
# Define paths using Join-Path
$includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$discordConfigPath = Join-Path -Path $scriptRoot -ChildPath "config.php" # Config is in the same dir
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
$lastStatsJsonPath = Join-Path -Path $dataPath -ChildPath "player_last_stats.json"
$stats = get-content "$scriptroot/../data/player_last_stats.json" | convertfrom-json
# --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
exit 1
}
. $lockFilePath
New-Lock -by "report_to_discord" -ErrorAction Stop # Stop if locking fails
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Helper Function ---
# Checks if K/D values are valid numbers (not NaN or Infinity)
function Test-IsValidKdEntry {
param($entry)
# Check if properties exist and are numeric before comparison
$kdHValid = $entry.PSObject.Properties.Name -contains 'KD_H' -and $entry.KD_H -is [double] -and -not [double]::IsNaN($entry.KD_H) -and -not [double]::IsInfinity($entry.KD_H)
$kdAllValid = $entry.PSObject.Properties.Name -contains 'KD_ALL' -and $entry.KD_ALL -is [double] -and -not [double]::IsNaN($entry.KD_ALL) -and -not [double]::IsInfinity($entry.KD_ALL)
return $kdHValid -and $kdAllValid
}
$filteredData = @{
all = $stats.all | Where-Object { IsValidEntry $_ }
}
# --- Configuration Loading ---
$webhookUrl = $null
if (Test-Path -Path $discordConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $discordConfigPath -Raw -ErrorAction Stop
# Corrected regex for webhookurl
if ($fileContent -match '^\s*\$webhookurl\s*=\s*''([^'']+)''') {
$webhookUrl = $matches[1]
Write-Output "Discord webhook URL loaded successfully."
} else {
Write-Warning "Webhook URL pattern not found in '$discordConfigPath'."
}
} catch {
Write-Warning "Failed to read '$discordConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Discord config file not found at '$discordConfigPath'."
}
$most_kills = @{
'name' = (($filteredData.all | Sort-Object kills -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object kills -Descending)[0].kills)
}
$most_deaths = @{
'name' = (($filteredData.all | Sort-Object deaths -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object deaths -Descending)[0].deaths)
}
$most_humankills = @{
'name' = (($filteredData.all | Sort-Object humankills -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object humankills -Descending)[0].humankills)
}
$most_KD_H = @{
'name' = (($filteredData.all | Sort-Object KD_H -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object KD_H -Descending)[0].KD_H)
}
$most_KD_ALL = @{
'name' = (($filteredData.all | Sort-Object KD_ALL -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object KD_ALL -Descending)[0].KD_ALL)
}
$most_matches = @{
'name' = (($filteredData.all | Sort-Object matches -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object matches -Descending)[0].matches)
}
if (-not $webhookUrl) {
Write-Error "Discord webhook URL could not be loaded. Cannot send report."
throw "Missing Webhook URL" # Throw to trigger finally block
}
$content = "
# --- Load Stats Data ---
$statsData = $null
if (Test-Path -Path $lastStatsJsonPath -PathType Leaf) {
try {
$statsData = Get-Content -Path $lastStatsJsonPath | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $statsData) {
Write-Error "Failed to parse stats data from '$lastStatsJsonPath'. Cannot generate report."
throw "Invalid Stats Data"
}
Write-Output "Successfully loaded player stats data."
} catch {
Write-Error "Error reading '$lastStatsJsonPath': $($_.Exception.Message). Cannot generate report."
throw $_
}
} else {
Write-Error "Player stats file not found at '$lastStatsJsonPath'. Cannot generate report."
throw "Missing Stats File"
}
# --- Process Stats ---
Write-Output "Processing stats..."
# Safely access the 'all' category and filter valid entries
$filteredAllStats = @()
if ($statsData.PSObject.Properties.Name -contains 'all' -and $statsData.all -is [array]) {
$filteredAllStats = $statsData.all | Where-Object { Test-IsValidKdEntry $_ }
Write-Output "Found $($filteredAllStats.Count) valid entries in 'all' category."
} else {
Write-Warning "Stats data does not contain a valid 'all' array. Report might be incomplete."
}
# --- Find Top Players (Handle Empty/Null Cases) ---
# Helper to safely get top player stat
function Get-TopStat {
param(
[array]$Data,
[string]$Property,
[string]$DefaultName = "N/A",
[string]$DefaultStat = "N/A"
)
$sorted = $Data | Sort-Object $Property -Descending -ErrorAction SilentlyContinue
if ($sorted -and $sorted.Count -gt 0) {
# Ensure the property exists on the top object before accessing
$topEntry = $sorted[0]
$playerName = if ($topEntry.PSObject.Properties.Name -contains 'playername') { $topEntry.playername } else { $DefaultName }
$statValue = if ($topEntry.PSObject.Properties.Name -contains $Property) { $topEntry.$Property } else { $DefaultStat }
# Format numeric stats if needed (e.g., K/D)
if ($Property -like "KD*" -and $statValue -is [double]) { $statValue = "{0:N2}" -f $statValue }
return @{ 'name' = $playerName; 'stat' = $statValue }
} else {
return @{ 'name' = $DefaultName; 'stat' = $DefaultStat }
}
}
$mostKills = Get-TopStat -Data $filteredAllStats -Property 'kills'
$mostDeaths = Get-TopStat -Data $filteredAllStats -Property 'deaths'
$mostHumanKills = Get-TopStat -Data $filteredAllStats -Property 'humankills'
$mostKdH = Get-TopStat -Data $filteredAllStats -Property 'KD_H'
$mostKdAll = Get-TopStat -Data $filteredAllStats -Property 'KD_ALL'
$mostMatches = Get-TopStat -Data $filteredAllStats -Property 'matches'
Write-Output "Determined top players for report."
# --- Format Discord Message ---
# Using PowerShell multi-line string with variable substitution
$reportContent = @"
:rocket: Het maandelijks raportje :rocket:
Hey toppers!
@ -62,22 +141,22 @@ Hey toppers!
Laten we eens duiken in de cijfers van onze supergamers van de afgelopen maand:
:dart: Meeste Kills:
Hats off voor **$($most_kills['name'])**! Met **$($most_kills['stat'])** kills is hij/zij onze scherpschutter van de maand!
Hats off voor **$($mostKills.name)**! Met **$($mostKills.stat)** kills is hij/zij onze scherpschutter van de maand!
:skull_crossbones: Meeste Deaths:
Oei, oei, oei... **$($most_deaths['name'])** is helaas het vaakst naar het hiernamaals gestuurd met **$($most_deaths['stat'])** deaths. Kop op, volgende keer beter!
Oei, oei, oei... **$($mostDeaths.name)** is helaas het vaakst naar het hiernamaals gestuurd met **$($mostDeaths.stat)** deaths. Kop op, volgende keer beter!
:robot: Meeste Humankills:
Watch out! We hebben een Terminator onder ons. Hoedje af voor **$($most_humankills['name'])** met **$($most_humankills['stat'])** humankills!
Watch out! We hebben een Terminator onder ons. Hoedje af voor **$($mostHumanKills.name)** met **$($mostHumanKills.stat)** humankills!
:bar_chart: Beste KD Ratio (Alle vijanden):
De onevenaarbare **$($most_KD_ALL['name'])** heeft een KD van **$($most_KD_ALL['stat'])** ! Niet slecht, toch? 😉
De onevenaarbare **$($mostKdAll.name)** heeft een KD van **$($mostKdAll.stat)** ! Niet slecht, toch? 😉
:adult: Beste KD Ratio (Alleen menselijke spelers):
Opgelet, gamers! **$($most_KD_H['name'])** heeft een KD van **$($most_KD_H['stat'])** tegen andere spelers! Wie daagt hem/haar uit?
Opgelet, gamers! **$($mostKdH.name)** heeft een KD van **$($mostKdH.stat)** tegen andere spelers! Wie daagt hem/haar uit?
:video_game: Meeste Matches:
Onze meest toegewijde gamer, **$($most_matches['name'])**, heeft maar liefst **$($most_matches['stat'])** matches gespeeld. Ga zo door!
Onze meest toegewijde gamer, **$($mostMatches.name)**, heeft maar liefst **$($mostMatches.stat)** matches gespeeld. Ga zo door!
Da's het voor nu, gamers! Blijf schieten, blijf lachen en tot het volgende rapportje!
@ -85,18 +164,33 @@ High fives en knuffels (virtueel, natuurlijk),
Het Gaming Team
Meer stats zijn hier te vinden : https://lanta.eu/DTCH
"
"@
$content
Write-Output "Formatted Discord message content."
# Write-Verbose $reportContent # Uncomment to see the full message in verbose logs
# --- Send to Discord ---
$payload = @{
content = $reportContent
# username = "Stats Bot" # Optional: Override bot name
# avatar_url = "" # Optional: Override bot avatar
} | ConvertTo-Json -Depth 3 # Depth 3 should be sufficient
Write-Output "Sending report to Discord webhook..."
try {
Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $payload -ContentType 'application/json' -ErrorAction Stop
Write-Output "Report successfully sent to Discord."
} catch {
$errorMessage = $_.Exception.Message
Write-Error "Failed to send report to Discord. Error: $errorMessage"
# Consider logging the failed payload or response details if available
# Write-Warning "Payload: $payload"
}
$payload = [PSCustomObject]@{
content = $content
}
Invoke-RestMethod -Uri $webhookurl -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json'
remove-lock
Stop-Transcript
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
Stop-Transcript
}

View file

@ -1,255 +1,567 @@
import json
# -*- coding: utf-8 -*-
"""
Discord bot for team creation, stats reporting, and event logging.
"""
import json
import os
import discord
import random
import asyncio
import re
import logging
from discord.ext import commands
# --- Configuration ---
def get_token():
with open("config.php", "r") as file:
content = file.read()
match = re.search(r"bottoken\s*=\s*'(.+?)'", content)
if match:
return match.group(1)
return None
# TODO: Move hardcoded channel names to a config file or environment variables
TEAMIFY_VOICE_CHANNEL = "teamify"
TEAMIFY_TEXT_CHANNEL = "teamify"
TEMP_CATEGORY_NAME = "Temporary Teams"
LOGGING_CHANNEL = "logging"
WELCOME_CHANNEL = "raadhuisplein"
GOD_CHANNEL = "GOD_CHANNEL" # Channel to ignore for voice logging
token = get_token()
# TODO: Move stats file path to config
STATS_FILE_PATH = os.path.join("..", "data", "player_last_stats.json")
CONFIG_PHP_PATH = "config.php" # Path relative to this script's location
# --- Logging Setup ---
# Basic logging to console
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
logger = logging.getLogger('discord_bot')
# --- Helper Functions ---
def get_token_from_php(config_path):
"""
Reads the bot token from a PHP config file.
WARNING: This method of storing secrets is insecure.
Consider using environment variables or a dedicated secrets management solution.
"""
try:
with open(config_path, "r", encoding="utf-8") as file:
content = file.read()
# Regex to find $bottoken = 'TOKEN_VALUE';
match = re.search(r"\$bottoken\s*=\s*'(.+?)'", content)
if match:
logger.info("Successfully extracted bot token from %s", config_path)
return match.group(1)
else:
logger.error("Bot token pattern not found in %s", config_path)
return None
except FileNotFoundError:
logger.error("Config file not found at %s", config_path)
return None
except Exception as e:
logger.exception("Error reading or parsing config file %s: %s", config_path, e)
return None
async def cleanup_empty_channels(guild, temp_channels, ctx_channel):
"""
Periodically checks and removes empty temporary voice channels.
"""
while temp_channels:
await asyncio.sleep(60) # Check every 60 seconds
for channel in temp_channels[:]: # Iterate over a copy
try:
# Refresh channel state
current_channel = guild.get_channel(channel.id)
if current_channel and isinstance(current_channel, discord.VoiceChannel):
if len(current_channel.members) == 0:
logger.info("Deleting empty temporary channel: %s", current_channel.name)
await current_channel.delete(reason="Channel empty")
temp_channels.remove(channel)
# Optionally notify in the original context channel
# await ctx_channel.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
elif current_channel is None:
# Channel might have been deleted manually
logger.warning("Temporary channel %s (ID: %d) not found, removing from tracking list.", channel.name, channel.id)
temp_channels.remove(channel)
except discord.Forbidden:
logger.error("Missing permissions to delete channel %s", channel.name)
# Remove from list to avoid repeated attempts if permissions are missing
temp_channels.remove(channel)
except discord.NotFound:
logger.warning("Attempted to delete channel %s but it was already gone.", channel.name)
if channel in temp_channels:
temp_channels.remove(channel)
except Exception as e:
logger.exception("Error during temporary channel cleanup for %s: %s", channel.name, e)
# Remove from list to avoid potential infinite loops on persistent errors
if channel in temp_channels:
temp_channels.remove(channel)
# --- Bot Setup ---
token = get_token_from_php(CONFIG_PHP_PATH)
if not token:
raise ValueError("Bot token niet gevonden in config.php")
logger.critical("Bot token not found or could not be read. Exiting.")
exit() # Exit if token is essential
# Intents instellen
# Define Intents
intents = discord.Intents.default()
intents.voice_states = True
intents.guilds = True
intents.messages = True
intents.message_content = True
intents.presences = True # Nodig als de bot presences moet zien
intents.members = True # Nodig om leden in een voice channel te zien
intents.presences = True # Required if the bot needs to see presences
intents.members = True # Required to see members in voice channels and for join/remove events
bot = commands.Bot(command_prefix="!", intents=intents)
# --- Events ---
@bot.event
async def on_ready():
print(f'Bot is ingelogd als {bot.user}')
"""Called when the bot is ready and connected."""
logger.info('Bot is ingelogd als %s (ID: %s)', bot.user, bot.user.id)
print(f'Bot is ingelogd als {bot.user}') # Keep console output for convenience
@bot.event
async def on_command_error(ctx, error):
"""Global command error handler."""
if isinstance(error, commands.CommandNotFound):
# Optionally ignore 'Command not found' errors or send a message
# await ctx.send("Onbekend commando.")
logger.warning("Command not found: %s", ctx.message.content)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send(f"Je mist een argument: {error.param.name}")
elif isinstance(error, commands.BadArgument):
await ctx.send("Ongeldig argument opgegeven.")
elif isinstance(error, commands.CheckFailure):
await ctx.send("Je hebt geen permissies voor dit commando of kanaal.")
else:
# Log other errors
logger.exception("Unhandled command error in '%s': %s", ctx.command, error)
try:
# Attempt to notify user about unexpected error
await ctx.send("Er is een onverwachte fout opgetreden.")
except discord.HTTPException:
pass # Ignore if we can't send the error message
@bot.event
async def on_voice_state_update(member, before, after):
"""Logs user movements between voice channels."""
# Ignore bots
if member.bot:
return
logging_channel = discord.utils.get(member.guild.text_channels, name=LOGGING_CHANNEL)
if not logging_channel:
logger.warning("Logging channel '%s' not found.", LOGGING_CHANNEL)
return
# Ignore movements involving the GOD_CHANNEL
if (before.channel and before.channel.name == GOD_CHANNEL) or \
(after.channel and after.channel.name == GOD_CHANNEL):
return
try:
if before.channel is None and after.channel is not None:
# Member joins a voice channel
logger.info("%s joined voice channel: %s", member.display_name, after.channel.name)
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
elif before.channel is not None and after.channel is None:
# Member leaves a voice channel
logger.info("%s left voice channel: %s", member.display_name, before.channel.name)
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
elif before.channel != after.channel:
# Member switches voice channels
logger.info("%s switched from %s to %s", member.display_name, before.channel.name, after.channel.name)
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
except discord.Forbidden:
logger.error("Missing permissions to send message in logging channel '%s'", LOGGING_CHANNEL)
except Exception as e:
logger.exception("Error during on_voice_state_update: %s", e)
@bot.event
async def on_member_join(member):
"""Welcomes new members."""
if member.bot: return # Ignore bots
welcome_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL)
if welcome_channel:
logger.info("Member joined: %s", member.display_name)
try:
await welcome_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
except discord.Forbidden:
logger.error("Missing permissions to send message in welcome channel '%s'", WELCOME_CHANNEL)
except Exception as e:
logger.exception("Error during on_member_join: %s", e)
else:
logger.warning("Welcome channel '%s' not found.", WELCOME_CHANNEL)
@bot.event
async def on_member_remove(member):
"""Logs when members leave."""
if member.bot: return # Ignore bots
goodbye_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL) # Using same channel as welcome
if goodbye_channel:
logger.info("Member left: %s", member.display_name)
try:
await goodbye_channel.send(f"😢 {member.display_name} heeft de server verlaten. We zullen je missen!") # Use display_name as mention won't work
except discord.Forbidden:
logger.error("Missing permissions to send message in goodbye channel '%s'", WELCOME_CHANNEL)
except Exception as e:
logger.exception("Error during on_member_remove: %s", e)
else:
logger.warning("Goodbye channel '%s' not found.", WELCOME_CHANNEL)
# --- Commands ---
@bot.command()
async def test(ctx):
"""A simple test command."""
await ctx.send("Test geslaagd!")
@bot.command()
async def teamify(ctx, *args):
for arg in args:
if arg.lower() == "help":
help_message = (
"**Gebruik van !teamify:**\n"
"`!teamify` - Verdeel spelers in teams van max. 4 personen.\n"
"`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n"
"`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze naar tijdelijke voice-kanalen.\n"
"`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke voice-kanalen."
)
await ctx.send(help_message)
return
"""
Splits members in the 'teamify' voice channel into random teams.
# Beperk het commando tot alleen het kanaal "teamify"
if ctx.channel.name != "teamify":
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify kanaal.")
return
guild = ctx.guild
voice_channel = discord.utils.get(guild.voice_channels, name="teamify")
if not voice_channel or len(voice_channel.members) == 0:
await ctx.send("Er zijn geen mensen in het kanaal 'teamify' om teams van te maken!")
return
members = voice_channel.members
random.shuffle(members)
# Standaardwaarden
num_teams = 0
move_players = False
# Verwerk argumenten
for arg in args:
if arg.isdigit(): # Als het een getal is, gebruik het als het aantal teams
num_teams = int(arg)
elif arg.lower() == "move": # Als 'true' is opgegeven, verplaats spelers
move_players = True
# Bepaal het aantal teams als niet opgegeven
if num_teams <= 0:
num_teams = len(members) // 4 if len(members) >= 4 else 1
num_teams = min(num_teams, len(members))
teams = [[] for _ in range(num_teams)]
for i, member in enumerate(members):
teams[i % num_teams].append(member)
# Zoek het tekstkanaal "teamify"
text_channel = discord.utils.get(guild.text_channels, name="teamify")
if not text_channel:
await ctx.send("Het kanaal 'teamify' bestaat niet!")
return
message = f"Willekeurige teams uit {voice_channel.name}:\n\n"
category = discord.utils.get(guild.categories, name="Temporary Teams")
if not category:
category = await guild.create_category("Temporary Teams")
temp_channels = []
for i, team in enumerate(teams, start=1):
team_names = ', '.join([member.mention for member in team])
message += f"**Team {i}:** {team_names}\n"
if move_players:
temp_channel = await guild.create_voice_channel(f"Squad {i}", category=category)
temp_channels.append(temp_channel)
for member in team:
await member.move_to(temp_channel)
await text_channel.send(message)
await ctx.send("Teams zijn gepost in #teamify!" + (" Spelers zijn verplaatst naar tijdelijke kanalen." if move_players else ""))
# Controleer continu of de kanalen leeg zijn en verwijder ze
if move_players:
while temp_channels:
await asyncio.sleep(60) # Controleer elke minuut
for channel in temp_channels[:]:
if len(channel.members) == 0:
if channel in guild.voice_channels: # Controleer of het kanaal nog bestaat
await channel.delete()
temp_channels.remove(channel)
await ctx.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
@bot.command()
async def whoisbest(ctx, category="Casual", matchesback=18):
if category.lower() == "help":
Usage:
!teamify - Auto-split into teams of 4.
!teamify <num_teams> - Split into a specific number of teams.
!teamify move - Auto-split and move to temporary channels.
!teamify <num_teams> move - Split into specific teams and move.
!teamify help - Show help message.
"""
# Handle help argument first
if "help" in [arg.lower() for arg in args]:
help_message = (
"**Gebruik van het commando `whoisbest`:**\n"
"`!whoisbest [category] [matchesback]`\n\n"
"**Parameters:**\n"
"`category` - De categorie van de stats, bijv. 'Casual' of 'Ranked'. Niet hoofdlettergevoelig.\n"
"`matchesback` - Het minimum aantal matches dat een speler gespeeld moet hebben om mee te tellen (standaard 18).\n\n"
"**Voorbeeld:**\n"
"`!whoisbest Casual 18`\n"
"Laat de top 3 spelers zien op basis van winratio en gemiddelde damage in de Casual categorie met minimaal 18 matches.\n\n"
"Typ `!whoisbest help` om deze uitleg opnieuw te zien."
"**Gebruik van !teamify:**\n"
f"`!teamify` - Verdeel spelers in `{TEAMIFY_VOICE_CHANNEL}` in teams van max. 4.\n"
"`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n"
"`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke kanalen.\n"
"`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze.\n"
"`!teamify help` - Toon dit bericht."
)
await ctx.send(help_message)
return
# Check if command is used in the correct text channel
if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} kanaal.")
return
# Bestandspad
file_path = os.path.join("..", "data", "player_last_stats.json")
guild = ctx.guild
source_voice_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
if not source_voice_channel:
await ctx.send(f"Voice kanaal '{TEAMIFY_VOICE_CHANNEL}' niet gevonden!")
return
if not source_voice_channel.members:
await ctx.send(f"Er zijn geen mensen in het kanaal '{TEAMIFY_VOICE_CHANNEL}' om teams van te maken!")
return
members = list(source_voice_channel.members) # Get a mutable list
random.shuffle(members)
member_count = len(members)
# Parse arguments
num_teams_arg = 0
move_players = False
for arg in args:
if arg.isdigit():
num_teams_arg = int(arg)
elif arg.lower() == "move":
move_players = True
# Determine number of teams
if num_teams_arg <= 0:
# Auto-calculate based on team size 4, minimum 1 team
num_teams = (member_count + 3) // 4 if member_count > 0 else 1
else:
num_teams = num_teams_arg
# Ensure num_teams is valid
num_teams = max(1, min(num_teams, member_count)) # At least 1 team, max members count
# Create teams
teams = [[] for _ in range(num_teams)]
for i, member in enumerate(members):
teams[i % num_teams].append(member)
# Find the output text channel
output_text_channel = discord.utils.get(guild.text_channels, name=TEAMIFY_TEXT_CHANNEL)
if not output_text_channel:
# Fallback to context channel if specific channel not found
logger.warning("Output text channel '%s' not found, using context channel.", TEAMIFY_TEXT_CHANNEL)
output_text_channel = ctx.channel
# await ctx.send(f"Tekst kanaal '{TEAMIFY_TEXT_CHANNEL}' bestaat niet! Resultaten worden hier gepost.")
# Prepare message and temporary channels if moving
message = f"Willekeurige teams uit {source_voice_channel.mention}:\n\n"
temp_channels = []
category = None
if move_players:
try:
category = discord.utils.get(guild.categories, name=TEMP_CATEGORY_NAME)
if not category:
logger.info("Creating category '%s'", TEMP_CATEGORY_NAME)
category = await guild.create_category(TEMP_CATEGORY_NAME, reason="Teamify temporary channels")
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om de categorie '{TEMP_CATEGORY_NAME}' te maken.")
move_players = False # Disable moving if category creation fails
logger.error("Missing permissions to create category '%s'", TEMP_CATEGORY_NAME)
except Exception as e:
await ctx.send("Fout bij het maken/vinden van de categorie.")
move_players = False
logger.exception("Error finding/creating category '%s': %s", TEMP_CATEGORY_NAME, e)
# Build message and move players
move_tasks = []
for i, team in enumerate(teams, start=1):
team_names = ', '.join([member.mention for member in team])
message += f"**Team {i}:** {team_names}\n"
if move_players and category:
try:
channel_name = f"Squad {i}"
logger.info("Creating temporary voice channel: %s", channel_name)
temp_channel = await guild.create_voice_channel(channel_name, category=category, reason="Teamify command")
temp_channels.append(temp_channel)
for member in team:
# Add move operation to a list to run concurrently
move_tasks.append(member.move_to(temp_channel))
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om voice kanaal 'Squad {i}' te maken of leden te verplaatsen.")
logger.error("Missing permissions to create/move for Squad %d", i)
# Optionally stop creating more channels if one fails
except Exception as e:
await ctx.send(f"Fout bij het maken van kanaal 'Squad {i}'.")
logger.exception("Error creating channel 'Squad %d': %s", i, e)
# Send the team message
try:
# JSON-bestand lezen
with open(file_path, "r", encoding="utf-8") as file:
await output_text_channel.send(message)
logger.info("Posted team message to #%s", output_text_channel.name)
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om berichten te sturen in #{output_text_channel.name}.")
logger.error("Missing permissions to send message in #%s", output_text_channel.name)
except Exception as e:
await ctx.send("Fout bij het versturen van het team bericht.")
logger.exception("Error sending team message: %s", e)
# Execute moves concurrently
if move_tasks:
logger.info("Moving %d members to temporary channels...", len(move_tasks))
results = await asyncio.gather(*move_tasks, return_exceptions=True)
moved_count = 0
for i, result in enumerate(results):
member_to_move = move_tasks[i].__self__ # Get the member object from the coroutine
if isinstance(result, Exception):
logger.error("Failed to move member %s: %s", member_to_move.display_name, result)
try:
# Try to notify in context channel about failed move
await ctx.send(f"Kon {member_to_move.mention} niet verplaatsen: {result}")
except discord.HTTPException: pass # Ignore if sending fails
else:
moved_count += 1
logger.info("Finished moving members. Successful moves: %d", moved_count)
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}! {moved_count} speler(s) verplaatst.")
elif move_players: # If move was intended but failed (e.g., category issue)
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}. Kon spelers niet verplaatsen.")
else:
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}.")
# Start background task for cleaning up empty channels if channels were created
if temp_channels:
logger.info("Starting background task to clean up %d temporary channels.", len(temp_channels))
bot.loop.create_task(cleanup_empty_channels(guild, temp_channels, ctx.channel))
@bot.command()
async def whoisbest(ctx, category="Casual", matchesback: int = 18):
"""
Shows top 3 players by winrate and AHD for a given category.
Usage: !whoisbest [category] [min_matches]
Example: !whoisbest Ranked 10
"""
if category.lower() == "help":
help_message = (
"**Gebruik van `!whoisbest`:**\n"
"`!whoisbest [category] [min_matches]`\n\n"
"**Parameters:**\n"
"`category` - Stats categorie (bijv. 'Casual', 'Ranked', 'Intense'). Niet hoofdlettergevoelig. Standaard: 'Casual'.\n"
"`min_matches` - Minimum aantal matches gespeeld. Standaard: 18.\n\n"
"**Voorbeeld:** `!whoisbest Ranked 10`\n"
"Toont top 3 Win% en AHD voor Ranked met minimaal 10 matches.\n"
)
await ctx.send(help_message)
return
logger.info("Whoisbest command invoked: category=%s, matchesback=%d", category, matchesback)
try:
# Read JSON file
if not os.path.exists(STATS_FILE_PATH):
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
logger.error("Stats file not found: %s", STATS_FILE_PATH)
return
with open(STATS_FILE_PATH, "r", encoding="utf-8") as file:
data = json.load(file)
# Mapping maken (lowercase -> originele categorie)
category_mapping = {cat.lower(): cat for cat in data.keys()}
# Create case-insensitive mapping for categories
category_mapping = {cat.lower(): cat for cat in data.keys() if isinstance(data[cat], list)} # Only map list categories
# Zet de opgegeven category om naar lowercase
lower_category = category.lower()
if lower_category not in category_mapping:
available_categories = ', '.join(data.keys())
await ctx.send(f"Ongeldige categorie '{category}'! Beschikbare categorieën: {available_categories}")
available_categories = ', '.join(category_mapping.values()) # Show original names
await ctx.send(f"Ongeldige categorie '{category}'. Beschikbaar: {available_categories}")
logger.warning("Invalid category requested: %s", category)
return
# Gebruik de juiste (originele) categorie naam uit de mapping
actual_category = category_mapping[lower_category]
logger.info("Processing category: %s", actual_category)
players = [player for player in data.get(actual_category, []) if player.get("matches", 0) >= matchesback]
# Filter players based on minimum matches
players_in_category = data.get(actual_category, [])
if not isinstance(players_in_category, list):
await ctx.send(f"Data voor categorie '{actual_category}' is niet in het verwachte formaat.")
logger.error("Invalid data format for category '%s'", actual_category)
return
if not players:
await ctx.send(f"Geen spelersstatistieken gevonden voor '{actual_category}' met minimaal {matchesback} gespeelde matches!")
filtered_players = [
player for player in players_in_category
if isinstance(player, dict) and player.get("matches", 0) >= matchesback
]
logger.info("Found %d players meeting criteria (>=%d matches) in %s", len(filtered_players), matchesback, actual_category)
if not filtered_players:
await ctx.send(f"Geen spelers gevonden voor '{actual_category}' met ≥ {matchesback} matches.")
return
# Sorteer spelers op winratio (aflopend)
top_winratio = sorted(players, key=lambda x: x.get("winratio", 0), reverse=True)[:3]
# Safely sort players, handling potential missing keys or non-numeric data
def safe_get_float(player_dict, key, default=0.0):
val = player_dict.get(key)
if isinstance(val, (int, float)):
return float(val)
# Try converting comma decimal separator if needed
if isinstance(val, str):
try: return float(val.replace(',', '.'))
except ValueError: return default
return default
# Sorteer spelers op gemiddelde damage (aflopend)
top_ahd = sorted(players, key=lambda x: x.get("ahd", 0), reverse=True)[:3]
top_winratio = sorted(filtered_players, key=lambda x: safe_get_float(x, "winratio"), reverse=True)[:3]
top_ahd = sorted(filtered_players, key=lambda x: safe_get_float(x, "ahd"), reverse=True)[:3]
# Bouw het bericht op
message = f"**\U0001F3C6 Top 3 Winratio ({actual_category})**\n"
for i, player in enumerate(top_winratio, start=1):
message += f"{i}. **{player['playername']}** - {player['winratio']:.2f}%\n"
# Build the response message
response_message = f"**📊 Top Stats voor '{actual_category}' (min {matchesback} matches)**\n\n"
message += f"\n**\U0001F480 Top 3 AHD ({actual_category})**\n"
for i, player in enumerate(top_ahd, start=1):
message += f"{i}. **{player['playername']}** - {player['ahd']:.2f}\n"
response_message += "**\U0001F3C6 Top 3 Winratio**\n"
if top_winratio:
for i, player in enumerate(top_winratio, start=1):
player_name = player.get('playername', 'Onbekend')
win_ratio = safe_get_float(player, 'winratio')
response_message += f"{i}. **{player_name}** - {win_ratio:.2f}%\n"
else:
response_message += "_Geen data beschikbaar_\n"
await ctx.send(message)
response_message += f"\n**\U0001F4A5 Top 3 Average Human Damage (AHD)**\n" # Changed emoji
if top_ahd:
for i, player in enumerate(top_ahd, start=1):
player_name = player.get('playername', 'Onbekend')
ahd_val = safe_get_float(player, 'ahd')
response_message += f"{i}. **{player_name}** - {ahd_val:.2f}\n"
else:
response_message += "_Geen data beschikbaar_\n"
await ctx.send(response_message)
logger.info("Successfully sent whoisbest stats for %s", actual_category)
except FileNotFoundError:
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
logger.error("Stats file not found: %s", STATS_FILE_PATH)
except json.JSONDecodeError:
await ctx.send("Fout bij het lezen van de statistieken (ongeldig JSON).")
logger.exception("JSONDecodeError reading stats file: %s", STATS_FILE_PATH)
except Exception as e:
await ctx.send(f"Fout bij het laden van de statistieken: {str(e)}")
await ctx.send(f"Onverwachte fout bij het ophalen van statistieken: {e}")
logger.exception("Error in whoisbest command: %s", e)
@bot.event
async def on_voice_state_update(member, before, after):
logging_channel = discord.utils.get(member.guild.text_channels, name="logging")
if not logging_channel:
return
if (before.channel and before.channel.name == "GOD_CHANNEL") or (after.channel and after.channel.name == "GOD_CHANNEL"):
return # Als het God_channel is, doe niks
if before.channel is None and after.channel is not None:
# Lid joint een voice channel
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
elif before.channel is not None and after.channel is None:
# Lid verlaat een voice channel
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
elif before.channel != after.channel:
# Lid switched van voice kanaal
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
@bot.event
async def on_member_join(member):
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
if logging_channel:
await logging_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
@bot.event
async def on_member_remove(member):
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
if logging_channel:
await logging_channel.send(f"😢 {member.name} heeft de server verlaten. We zullen je missen!")
@bot.command()
@commands.has_permissions(move_members=True) # Add permission check
async def moveall(ctx):
# Controleer of het commando in het juiste kanaal wordt uitgevoerd
if ctx.channel.name != "teamify":
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify tekstkanaal.")
"""Moves all members from other voice channels to the 'teamify' channel."""
# Check if command is used in the correct text channel
if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} tekstkanaal.")
return
guild = ctx.guild
teamify_channel = discord.utils.get(guild.voice_channels, name="teamify")
if not teamify_channel:
await ctx.send("Het teamify voice-kanaal bestaat niet!")
target_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
if not target_channel:
await ctx.send(f"Het '{TEAMIFY_VOICE_CHANNEL}' voice-kanaal bestaat niet!")
logger.error("Target voice channel '%s' not found for moveall.", TEAMIFY_VOICE_CHANNEL)
return
moved_members = 0
moved_members_count = 0
move_tasks = []
members_to_move = []
logger.info("Moveall command initiated by %s", ctx.author.display_name)
for channel in guild.voice_channels:
if channel != teamify_channel:
for member in channel.members:
try:
await member.move_to(teamify_channel)
moved_members += 1
except Exception as e:
await ctx.send(f"Kon {member.mention} niet verplaatsen: {e}")
if moved_members > 0:
await ctx.send(f"{moved_members} speler(s) zijn verplaatst naar het teamify kanaal.")
# Skip the target channel itself and channels with no members
if channel == target_channel or not channel.members:
continue
for member in channel.members:
# Avoid moving bots unless intended
# if member.bot: continue
members_to_move.append(member)
move_tasks.append(member.move_to(target_channel))
if not move_tasks:
await ctx.send("Er waren geen spelers in andere kanalen om te verplaatsen.")
logger.info("Moveall: No members found in other channels.")
return
logger.info("Attempting to move %d members to #%s", len(move_tasks), target_channel.name)
results = await asyncio.gather(*move_tasks, return_exceptions=True)
for i, result in enumerate(results):
member = members_to_move[i]
if isinstance(result, Exception):
logger.error("Failed to move member %s: %s", member.display_name, result)
try:
await ctx.send(f"Kon {member.mention} niet verplaatsen: {result}")
except discord.HTTPException: pass
else:
moved_members_count += 1
logger.info("Moveall finished. Successfully moved %d members.", moved_members_count)
if moved_members_count > 0:
await ctx.send(f"{moved_members_count} speler(s) zijn verplaatst naar het {target_channel.mention} kanaal.")
else:
await ctx.send("Er waren geen spelers om te verplaatsen.")
bot.run(token)
await ctx.send("Kon geen spelers verplaatsen (controleer permissies?).")
@moveall.error
async def moveall_error(ctx, error):
"""Error handler specific to moveall command."""
if isinstance(error, commands.MissingPermissions):
await ctx.send("Je hebt geen permissies om leden te verplaatsen.")
else:
# Log and notify for other errors related to moveall
logger.exception("Error in moveall command: %s", error)
await ctx.send("Er is een fout opgetreden bij het uitvoeren van `moveall`.")
# --- Run Bot ---
if __name__ == "__main__":
logger.info("Starting bot...")
bot.run(token)