Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: 🔨 Set up Python 3.9.20
- name: 🔨 Set up Python 3.12
uses: actions/setup-python@v2
with:
python-version: 3.9.20
python-version: 3.12

- name: Install dependencies
run: |
Expand Down
3 changes: 3 additions & 0 deletions SRCweb/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,6 @@

SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 # 30 days
MAX_UPLOAD_SIZE = "5242880"

# Discord Webhook URL for Highscores
DISCORD_WEBHOOK_URL = os.getenv("HIGHSCORES_WEBHOOK_URL")
188 changes: 186 additions & 2 deletions highscores/lib.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from django.http import HttpRequest
from django.core.mail import send_mail
from django.utils import timezone
import logging

from .models import Score, CleanCodeSubmission, ExemptedIP
from .forms import ScoreForm
from SRCweb.settings import NEW_AES_KEY, DEBUG, ADMIN_EMAILS, EMAIL_HOST_USER
from SRCweb.settings import NEW_AES_KEY, DEBUG, ADMIN_EMAILS, EMAIL_HOST_USER, DISCORD_WEBHOOK_URL
import os

from typing import Callable, Union
from Crypto.Cipher import AES
from urllib.request import urlopen, Request
import json

USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'

Expand All @@ -21,6 +25,167 @@
WRONG_AUTO_OR_TELEOP_MESSAGE = 'Incorrect choice for control mode! Ensure you are submitting to the correct leaderboard for autonomous or tele-operated play.'


def send_world_record_webhook(new_score: Score, previous_record: Score = None) -> None:
"""Send Discord webhook notification for new world record"""
if not DISCORD_WEBHOOK_URL:
logging.error("Discord webhook URL not configured")
return

try:
# Calculate duration and get previous record holder info
if previous_record is None:
# Get the current world record (which will become the previous one)
previous_record = Score.objects.filter(
leaderboard=new_score.leaderboard,
approved=True
).order_by('-score', 'time_set').first()

if previous_record and previous_record.score < new_score.score:
# Calculate how long the previous record stood
duration_diff = new_score.time_set - previous_record.time_set

# Calculate duration in a readable format
total_seconds = int(duration_diff.total_seconds())
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60

if days > 0:
duration_text = f"{days} day{'s' if days != 1 else ''}, {hours} hour{'s' if hours != 1 else ''}"
elif hours > 0:
duration_text = f"{hours} hour{'s' if hours != 1 else ''}, {minutes} minute{'s' if minutes != 1 else ''}"
elif minutes > 0:
duration_text = f"{minutes} minute{'s' if minutes != 1 else ''}"
else:
duration_text = "less than a minute"

previous_record_info = f"**{previous_record.player.username}**'s record ({previous_record.score:,} points) stood for **{duration_text}**"
else:
previous_record_info = "**First record set for this category!**"

# Create the embed message
embed = {
"title": "🏆 NEW WORLD RECORD ACHIEVED! 🏆",
"description": f"**{new_score.player.username}** has set a new world record!",
"color": 0xFFD700, # Gold color
"fields": [
{
"name": "🎮 Game & Robot",
"value": f"**{new_score.leaderboard.game}**\n`{new_score.leaderboard.name}`",
"inline": True
},
{
"name": "🎯 Score",
"value": f"**{new_score.score:,} points**",
"inline": True
},
{
"name": "⏱️ Previous Record",
"value": previous_record_info,
"inline": False
},
{
"name": "📎 Proof",
"value": f"[View Submission]({new_score.source})",
"inline": False
}
],
"footer": {
"text": f"Record set on {new_score.time_set.strftime('%B %d, %Y at %I:%M %p UTC')}",
"icon_url": "https://cdn.discordapp.com/emojis/1306393882618114139.png"
},
"author": {
"name": "Second Robotics Competition",
"url": "https://secondrobotics.org",
"icon_url": "https://secondrobotics.org/static/images/logo.png"
},
"timestamp": new_score.time_set.isoformat()
}

payload = {
"embeds": [embed],
"username": "World Record Bot"
}

# Send the webhook
data = json.dumps(payload).encode('utf-8')
req = Request(DISCORD_WEBHOOK_URL, data=data, headers={
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
})

response = urlopen(req)
if response.status != 204:
logging.error(f"Discord webhook failed with status: {response.status}")

except Exception as e:
logging.error(f"Failed to send Discord webhook: {e}")


def test_world_record_webhook(player_name: str, score: int, game: str, robot: str, previous_player: str = "TestPlayer", previous_score: int = 95000, duration: str = "2 days, 3 hours") -> bool:
"""Test function for Discord webhook - returns True if successful"""
if not DISCORD_WEBHOOK_URL:
logging.error("Discord webhook URL not configured")
return False

try:
embed = {
"title": "🧪 TEST WORLD RECORD NOTIFICATION 🧪",
"description": f"**{player_name}** has set a new world record! *(This is a test)*",
"color": 0x00FF00, # Green color for test
"fields": [
{
"name": "🎮 Game & Robot",
"value": f"**{game}**\n`{robot}`",
"inline": True
},
{
"name": "🎯 Score",
"value": f"**{score:,} points**",
"inline": True
},
{
"name": "⏱️ Previous Record",
"value": f"**{previous_player}**'s record ({previous_score:,} points) stood for **{duration}**",
"inline": False
},
{
"name": "📎 Proof",
"value": "[Test Submission](https://secondrobotics.org)",
"inline": False
}
],
"footer": {
"text": f"TEST - Record set on {timezone.now().strftime('%B %d, %Y at %I:%M %p UTC')}",
"icon_url": "https://cdn.discordapp.com/emojis/1306393882618114139.png"
},
"author": {
"name": "Second Robotics Competition (TEST MODE)",
"url": "https://secondrobotics.org",
"icon_url": "https://secondrobotics.org/static/images/logo.png"
},
"timestamp": timezone.now().isoformat()
}

payload = {
"embeds": [embed],
"username": "World Record Bot (TEST)"
}

data = json.dumps(payload).encode('utf-8')
req = Request(DISCORD_WEBHOOK_URL, data=data, headers={
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
})

response = urlopen(req)
return response.status == 204

except Exception as e:
logging.error(f"Failed to send test Discord webhook: {e}")
return False


def submit_score(score_obj: Score, clean_code_check_func: Callable[[Score], Union[str, None]]) -> Union[str, None]:
# Check to ensure image / video is proper
res = submission_screenshot_check(score_obj)
Expand Down Expand Up @@ -151,13 +316,32 @@ def extract_form_data(form: ScoreForm, request: HttpRequest) -> Score:


def approve_score(score_obj: Score, prev_submissions):
# Delete previous submissions with lower or equal scores
# Check if this is a new world record before deleting previous submissions
current_world_record = Score.objects.filter(
leaderboard=score_obj.leaderboard,
approved=True
).order_by('-score', 'time_set').first()

is_world_record = (current_world_record is None or
score_obj.score > current_world_record.score)

# Delete previous submissions with lower or equal scores in the category
prev_submissions.filter(score__lte=score_obj.score).delete()

# Save the new submission
score_obj.approved = True
score_obj.save()

# Send Discord webhook if this is a world record
if is_world_record:
if not DEBUG:
try:
send_world_record_webhook(score_obj, current_world_record)
except Exception as e:
logging.error(f"Failed to send world record webhook: {e}")
else:
logging.info(f"DEBUG: World record detected for {score_obj.player.username} - {score_obj.score} on {score_obj.leaderboard.name} (webhook disabled in debug mode)")

code_obj = CleanCodeSubmission()
code_obj.clean_code = score_obj.clean_code
code_obj.player = score_obj.player
Expand Down
113 changes: 113 additions & 0 deletions highscores/templates/highscores/webhook_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{% extends 'home/base.html' %}
{% load static %}

{% block content %}
<div class="container mt-4">
<h1 class="mb-4">Discord Webhook Test</h1>
<p class="text-muted">Admin only - Test Discord world record notifications</p>

{% if success_message %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success_message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}

{% if error_message %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
{{ error_message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}

<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Send Test Webhook</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}

<div class="mb-3">
<label for="player_name" class="form-label">Player Name</label>
<input type="text" class="form-control" id="player_name" name="player_name" value="Test Player" required>
</div>

<div class="mb-3">
<label for="score" class="form-label">Score</label>
<input type="number" class="form-control" id="score" name="score" value="100000" required>
</div>

<div class="mb-3">
<label for="game" class="form-label">Game</label>
<select class="form-control" id="game" name="game">
<option value="Test Game">Test Game</option>
{% for game in games %}
<option value="{{ game }}">{{ game }}</option>
{% endfor %}
</select>
</div>

<div class="mb-3">
<label for="robot" class="form-label">Robot</label>
<select class="form-control" id="robot" name="robot">
<option value="Test Robot">Test Robot</option>
{% for robot in robots %}
<option value="{{ robot }}">{{ robot }}</option>
{% endfor %}
</select>
</div>

<div class="mb-3">
<label for="previous_player" class="form-label">Previous Record Holder</label>
<input type="text" class="form-control" id="previous_player" name="previous_player" value="PreviousPlayer" required>
</div>

<div class="mb-3">
<label for="previous_score" class="form-label">Previous Record Score</label>
<input type="number" class="form-control" id="previous_score" name="previous_score" value="95000" required>
</div>

<div class="mb-3">
<label for="duration" class="form-label">Previous Record Duration</label>
<input type="text" class="form-control" id="duration" name="duration" value="2 days, 3 hours" required>
<small class="form-text text-muted">Example: "2 days, 3 hours" or "45 minutes"</small>
</div>

<button type="submit" class="btn btn-primary">Send Test Webhook</button>
</form>
</div>
</div>
</div>

<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Information</h5>
</div>
<div class="card-body">
<p class="card-text">
This page allows administrators to test the Discord webhook system that sends notifications when world records are broken.
</p>
<p class="card-text">
<strong>What gets sent:</strong>
</p>
<ul>
<li>Player who got the record</li>
<li>Score achieved</li>
<li>Game name</li>
<li>Robot type</li>
<li>Duration the previous record stood</li>
<li>Link to submission source</li>
</ul>
<p class="card-text text-muted">
<small>Test messages will be clearly marked as tests in Discord.</small>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
13 changes: 7 additions & 6 deletions highscores/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@


urlpatterns = [
path('', views.home, name='home'),
# path("<str:game_slug>/submit/", views.submit_form, name="score submit"),
path("<str:game_slug>/combined/", views.leaderboard_combined,
name="game leaderboard"),
path("<str:game_slug>/<str:name>/",
views.leaderboard_robot, name="robot leaderboard"),
path('', views.home, name='home'),
# path("<str:game_slug>/submit/", views.submit_form, name="score submit"),
path("<str:game_slug>/combined/", views.leaderboard_combined,
name="game leaderboard"),
path("<str:game_slug>/<str:name>/",
views.leaderboard_robot, name="robot leaderboard"),
path('world-records/', views.world_records, name='world-records'),
path('overall/', views.overall_singleplayer_leaderboard, name='overall-singleplayer-leaderboard'),
path('webhook-test/', views.webhook_test, name='webhook-test'),
]
Loading