11from django .http import HttpRequest
22from django .core .mail import send_mail
3+ from django .utils import timezone
4+ import logging
35
46from .models import Score , CleanCodeSubmission , ExemptedIP
57from .forms import ScoreForm
6- from SRCweb .settings import NEW_AES_KEY , DEBUG , ADMIN_EMAILS , EMAIL_HOST_USER
8+ from SRCweb .settings import NEW_AES_KEY , DEBUG , ADMIN_EMAILS , EMAIL_HOST_USER , DISCORD_WEBHOOK_URL
9+ import os
710
811from typing import Callable , Union
912from Crypto .Cipher import AES
1013from urllib .request import urlopen , Request
14+ import json
1115
1216USER_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'
1317
2125WRONG_AUTO_OR_TELEOP_MESSAGE = 'Incorrect choice for control mode! Ensure you are submitting to the correct leaderboard for autonomous or tele-operated play.'
2226
2327
28+ def send_world_record_webhook (new_score : Score , previous_record : Score = None ) -> None :
29+ """Send Discord webhook notification for new world record"""
30+ if not DISCORD_WEBHOOK_URL :
31+ logging .error ("Discord webhook URL not configured" )
32+ return
33+
34+ try :
35+ # Calculate duration and get previous record holder info
36+ if previous_record is None :
37+ # Get the current world record (which will become the previous one)
38+ previous_record = Score .objects .filter (
39+ leaderboard = new_score .leaderboard ,
40+ approved = True
41+ ).order_by ('-score' , 'time_set' ).first ()
42+
43+ if previous_record and previous_record .score < new_score .score :
44+ # Calculate how long the previous record stood
45+ duration_diff = new_score .time_set - previous_record .time_set
46+
47+ # Calculate duration in a readable format
48+ total_seconds = int (duration_diff .total_seconds ())
49+ days = total_seconds // 86400
50+ hours = (total_seconds % 86400 ) // 3600
51+ minutes = (total_seconds % 3600 ) // 60
52+
53+ if days > 0 :
54+ duration_text = f"{ days } day{ 's' if days != 1 else '' } , { hours } hour{ 's' if hours != 1 else '' } "
55+ elif hours > 0 :
56+ duration_text = f"{ hours } hour{ 's' if hours != 1 else '' } , { minutes } minute{ 's' if minutes != 1 else '' } "
57+ elif minutes > 0 :
58+ duration_text = f"{ minutes } minute{ 's' if minutes != 1 else '' } "
59+ else :
60+ duration_text = "less than a minute"
61+
62+ previous_record_info = f"**{ previous_record .player .username } **'s record ({ previous_record .score :,} points) stood for **{ duration_text } **"
63+ else :
64+ previous_record_info = "**First record set for this category!**"
65+
66+ # Create the embed message
67+ embed = {
68+ "title" : "🏆 NEW WORLD RECORD ACHIEVED! 🏆" ,
69+ "description" : f"**{ new_score .player .username } ** has set a new world record!" ,
70+ "color" : 0xFFD700 , # Gold color
71+ "fields" : [
72+ {
73+ "name" : "🎮 Game & Robot" ,
74+ "value" : f"**{ new_score .leaderboard .game } **\n `{ new_score .leaderboard .name } `" ,
75+ "inline" : True
76+ },
77+ {
78+ "name" : "🎯 Score" ,
79+ "value" : f"**{ new_score .score :,} points**" ,
80+ "inline" : True
81+ },
82+ {
83+ "name" : "⏱️ Previous Record" ,
84+ "value" : previous_record_info ,
85+ "inline" : False
86+ },
87+ {
88+ "name" : "📎 Proof" ,
89+ "value" : f"[View Submission]({ new_score .source } )" ,
90+ "inline" : False
91+ }
92+ ],
93+ "footer" : {
94+ "text" : f"Record set on { new_score .time_set .strftime ('%B %d, %Y at %I:%M %p UTC' )} " ,
95+ "icon_url" : "https://cdn.discordapp.com/emojis/1306393882618114139.png"
96+ },
97+ "author" : {
98+ "name" : "Second Robotics Competition" ,
99+ "url" : "https://secondrobotics.org" ,
100+ "icon_url" : "https://secondrobotics.org/static/images/logo.png"
101+ },
102+ "timestamp" : new_score .time_set .isoformat ()
103+ }
104+
105+ payload = {
106+ "embeds" : [embed ],
107+ "username" : "World Record Bot"
108+ }
109+
110+ # Send the webhook
111+ data = json .dumps (payload ).encode ('utf-8' )
112+ req = Request (DISCORD_WEBHOOK_URL , data = data , headers = {
113+ 'Content-Type' : 'application/json' ,
114+ 'User-Agent' : USER_AGENT
115+ })
116+
117+ response = urlopen (req )
118+ if response .status != 204 :
119+ logging .error (
120+ f"Discord webhook failed with status: { response .status } " )
121+
122+ except Exception as e :
123+ logging .error (f"Failed to send Discord webhook: { e } " )
124+
125+
126+ 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 :
127+ """Test function for Discord webhook - returns True if successful"""
128+ if not DISCORD_WEBHOOK_URL :
129+ logging .error ("Discord webhook URL not configured" )
130+ return False
131+
132+ try :
133+ embed = {
134+ "title" : "🧪 TEST WORLD RECORD NOTIFICATION 🧪" ,
135+ "description" : f"**{ player_name } ** has set a new world record! *(This is a test)*" ,
136+ "color" : 0x00FF00 , # Green color for test
137+ "fields" : [
138+ {
139+ "name" : "🎮 Game & Robot" ,
140+ "value" : f"**{ game } **\n `{ robot } `" ,
141+ "inline" : True
142+ },
143+ {
144+ "name" : "🎯 Score" ,
145+ "value" : f"**{ score :,} points**" ,
146+ "inline" : True
147+ },
148+ {
149+ "name" : "⏱️ Previous Record" ,
150+ "value" : f"**{ previous_player } **'s record ({ previous_score :,} points) stood for **{ duration } **" ,
151+ "inline" : False
152+ },
153+ {
154+ "name" : "📎 Proof" ,
155+ "value" : "[Test Submission](https://secondrobotics.org)" ,
156+ "inline" : False
157+ }
158+ ],
159+ "footer" : {
160+ "text" : f"TEST - Record set on { timezone .now ().strftime ('%B %d, %Y at %I:%M %p UTC' )} " ,
161+ "icon_url" : "https://cdn.discordapp.com/emojis/1306393882618114139.png"
162+ },
163+ "author" : {
164+ "name" : "Second Robotics Competition (TEST MODE)" ,
165+ "url" : "https://secondrobotics.org" ,
166+ "icon_url" : "https://secondrobotics.org/static/images/logo.png"
167+ },
168+ "timestamp" : timezone .now ().isoformat ()
169+ }
170+
171+ payload = {
172+ "embeds" : [embed ],
173+ "username" : "World Record Bot (TEST)"
174+ }
175+
176+ data = json .dumps (payload ).encode ('utf-8' )
177+ req = Request (DISCORD_WEBHOOK_URL , data = data , headers = {
178+ 'Content-Type' : 'application/json' ,
179+ 'User-Agent' : USER_AGENT
180+ })
181+
182+ response = urlopen (req )
183+ return response .status == 204
184+
185+ except Exception as e :
186+ logging .error (f"Failed to send test Discord webhook: { e } " )
187+ return False
188+
189+
24190def submit_score (score_obj : Score , clean_code_check_func : Callable [[Score ], Union [str , None ]]) -> Union [str , None ]:
25191 # Check to ensure image / video is proper
26192 res = submission_screenshot_check (score_obj )
@@ -118,6 +284,10 @@ def submit_reefscape(score_obj: Score) -> Union[str, None]:
118284 return submit_score (score_obj , reefscape_clean_code_check )
119285
120286
287+ def submit_push_back (score_obj : Score ) -> Union [str , None ]:
288+ return submit_score (score_obj , push_back_clean_code_check )
289+
290+
121291def decode_time_data (in_string : str ) -> str :
122292 out_bytes = ""
123293
@@ -158,17 +328,22 @@ def extract_form_data(form: ScoreForm, request: HttpRequest) -> Score:
158328
159329
160330def approve_score (score_obj : Score , prev_submissions , time_data_issue = None ):
161- # Delete previous submissions with lower or equal scores
331+ # Check if this is a new world record before deleting previous submissions
332+ current_world_record = Score .objects .filter (
333+ leaderboard = score_obj .leaderboard ,
334+ approved = True
335+ ).order_by ('-score' , 'time_set' ).first ()
336+
337+ is_world_record = (current_world_record is None or
338+ score_obj .score > current_world_record .score )
339+
340+ # Delete previous submissions with lower or equal scores in the category
162341 prev_submissions .filter (score__lte = score_obj .score ).delete ()
163342
164343 # Save the new submission
165344 score_obj .approved = True
166345 score_obj .save ()
167346
168- # Send time data warning email if there was an issue
169- if time_data_issue :
170- send_time_data_warning_email (score_obj , time_data_issue )
171-
172347 code_obj = CleanCodeSubmission ()
173348 code_obj .clean_code = score_obj .clean_code
174349 code_obj .player = score_obj .player
@@ -337,6 +512,10 @@ def reefscape_clean_code_check(score_obj: Score) -> Union[str, None]:
337512 return clean_code_check (score_obj , check_reefscape_game_settings , check_subtraction_score )
338513
339514
515+ def push_back_clean_code_check (score_obj : Score ) -> Union [str , None ]:
516+ return clean_code_check (score_obj , check_push_back_game_settings , check_skills_challenge_score )
517+
518+
340519def extract_clean_code_info (score_obj : Score ) -> tuple [str , list [str ], str , str , str , str , str , str ]:
341520 """ Extracts the relevant information from the clean code.
342521 :param score_obj: Score object to extract from
@@ -606,6 +785,18 @@ def check_reefscape_game_settings(game_options: list, restart_option: str, game_
606785 return None # No error
607786
608787
788+ def check_push_back_game_settings (game_options : list , restart_option : str , game_index : str ) -> Union [str , None ]:
789+ """ Checks if the Push Back game settings are valid.
790+ :return: None if the settings are valid, or a response with an error message if they are not.
791+ """
792+ if (game_index != '20' ):
793+ return 'Wrong game! This form is for Push Back.'
794+ if (restart_option != '2' ):
795+ return 'You must use restart option 2 (skills challenge) for Push Back high score submissions.'
796+
797+ return None # No error
798+
799+
609800def check_robot_type (score_obj : Score , robot_model : str ) -> Union [str , None ]:
610801 """ Checks if the robot model is valid.
611802 :return: None if the robot model is valid, or a response with an error message if it is not.
@@ -795,6 +986,7 @@ def get_game_length(game_index: str):
795986 "ro" : submit_rover_ruckus ,
796987 "ss" : submit_skystone ,
797988 "rs" : submit_reefscape ,
989+ "pb" : submit_push_back ,
798990}
799991
800992game_to_submit_func = {
@@ -814,4 +1006,5 @@ def get_game_length(game_index: str):
8141006 "Rover Ruckus" : submit_rover_ruckus ,
8151007 "Skystone" : submit_skystone ,
8161008 "REEFSCAPE" : submit_reefscape ,
1009+ "Push Back" : submit_push_back ,
8171010}
0 commit comments