|
1 | 1 | from django.http import HttpRequest |
2 | 2 | from django.core.mail import send_mail |
| 3 | +from django.utils import timezone |
| 4 | +import logging |
3 | 5 |
|
4 | 6 | from .models import Score, CleanCodeSubmission, ExemptedIP |
5 | 7 | from .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 |
7 | 10 |
|
8 | 11 | from typing import Callable, Union |
9 | 12 | from Crypto.Cipher import AES |
10 | 13 | from urllib.request import urlopen, Request |
| 14 | +import json |
11 | 15 |
|
12 | 16 | 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' |
13 | 17 |
|
|
21 | 25 | WRONG_AUTO_OR_TELEOP_MESSAGE = 'Incorrect choice for control mode! Ensure you are submitting to the correct leaderboard for autonomous or tele-operated play.' |
22 | 26 |
|
23 | 27 |
|
| 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(f"Discord webhook failed with status: {response.status}") |
| 120 | + |
| 121 | + except Exception as e: |
| 122 | + logging.error(f"Failed to send Discord webhook: {e}") |
| 123 | + |
| 124 | + |
| 125 | +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: |
| 126 | + """Test function for Discord webhook - returns True if successful""" |
| 127 | + if not DISCORD_WEBHOOK_URL: |
| 128 | + logging.error("Discord webhook URL not configured") |
| 129 | + return False |
| 130 | + |
| 131 | + try: |
| 132 | + embed = { |
| 133 | + "title": "🧪 TEST WORLD RECORD NOTIFICATION 🧪", |
| 134 | + "description": f"**{player_name}** has set a new world record! *(This is a test)*", |
| 135 | + "color": 0x00FF00, # Green color for test |
| 136 | + "fields": [ |
| 137 | + { |
| 138 | + "name": "🎮 Game & Robot", |
| 139 | + "value": f"**{game}**\n`{robot}`", |
| 140 | + "inline": True |
| 141 | + }, |
| 142 | + { |
| 143 | + "name": "🎯 Score", |
| 144 | + "value": f"**{score:,} points**", |
| 145 | + "inline": True |
| 146 | + }, |
| 147 | + { |
| 148 | + "name": "⏱️ Previous Record", |
| 149 | + "value": f"**{previous_player}**'s record ({previous_score:,} points) stood for **{duration}**", |
| 150 | + "inline": False |
| 151 | + }, |
| 152 | + { |
| 153 | + "name": "📎 Proof", |
| 154 | + "value": "[Test Submission](https://secondrobotics.org)", |
| 155 | + "inline": False |
| 156 | + } |
| 157 | + ], |
| 158 | + "footer": { |
| 159 | + "text": f"TEST - Record set on {timezone.now().strftime('%B %d, %Y at %I:%M %p UTC')}", |
| 160 | + "icon_url": "https://cdn.discordapp.com/emojis/1306393882618114139.png" |
| 161 | + }, |
| 162 | + "author": { |
| 163 | + "name": "Second Robotics Competition (TEST MODE)", |
| 164 | + "url": "https://secondrobotics.org", |
| 165 | + "icon_url": "https://secondrobotics.org/static/images/logo.png" |
| 166 | + }, |
| 167 | + "timestamp": timezone.now().isoformat() |
| 168 | + } |
| 169 | + |
| 170 | + payload = { |
| 171 | + "embeds": [embed], |
| 172 | + "username": "World Record Bot (TEST)" |
| 173 | + } |
| 174 | + |
| 175 | + data = json.dumps(payload).encode('utf-8') |
| 176 | + req = Request(DISCORD_WEBHOOK_URL, data=data, headers={ |
| 177 | + 'Content-Type': 'application/json', |
| 178 | + 'User-Agent': USER_AGENT |
| 179 | + }) |
| 180 | + |
| 181 | + response = urlopen(req) |
| 182 | + return response.status == 204 |
| 183 | + |
| 184 | + except Exception as e: |
| 185 | + logging.error(f"Failed to send test Discord webhook: {e}") |
| 186 | + return False |
| 187 | + |
| 188 | + |
24 | 189 | def submit_score(score_obj: Score, clean_code_check_func: Callable[[Score], Union[str, None]]) -> Union[str, None]: |
25 | 190 | # Check to ensure image / video is proper |
26 | 191 | res = submission_screenshot_check(score_obj) |
@@ -151,13 +316,32 @@ def extract_form_data(form: ScoreForm, request: HttpRequest) -> Score: |
151 | 316 |
|
152 | 317 |
|
153 | 318 | def approve_score(score_obj: Score, prev_submissions): |
154 | | - # Delete previous submissions with lower or equal scores |
| 319 | + # Check if this is a new world record before deleting previous submissions |
| 320 | + current_world_record = Score.objects.filter( |
| 321 | + leaderboard=score_obj.leaderboard, |
| 322 | + approved=True |
| 323 | + ).order_by('-score', 'time_set').first() |
| 324 | + |
| 325 | + is_world_record = (current_world_record is None or |
| 326 | + score_obj.score > current_world_record.score) |
| 327 | + |
| 328 | + # Delete previous submissions with lower or equal scores in the category |
155 | 329 | prev_submissions.filter(score__lte=score_obj.score).delete() |
156 | 330 |
|
157 | 331 | # Save the new submission |
158 | 332 | score_obj.approved = True |
159 | 333 | score_obj.save() |
160 | 334 |
|
| 335 | + # Send Discord webhook if this is a world record |
| 336 | + if is_world_record: |
| 337 | + if not DEBUG: |
| 338 | + try: |
| 339 | + send_world_record_webhook(score_obj, current_world_record) |
| 340 | + except Exception as e: |
| 341 | + logging.error(f"Failed to send world record webhook: {e}") |
| 342 | + else: |
| 343 | + 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)") |
| 344 | + |
161 | 345 | code_obj = CleanCodeSubmission() |
162 | 346 | code_obj.clean_code = score_obj.clean_code |
163 | 347 | code_obj.player = score_obj.player |
|
0 commit comments