99 IdentityType ,
1010 LeaderboardItem ,
1111 LeaderboardRankedEntry ,
12+ RateLimitItem ,
1213 RunItem ,
1314 SubmissionItem ,
1415)
@@ -276,8 +277,13 @@ def create_submission(
276277 code : str ,
277278 time : datetime .datetime ,
278279 user_name : str = None ,
280+ mode_category : str = None ,
279281 ) -> Optional [int ]:
280282 try :
283+ if time .tzinfo is None :
284+ time = time .astimezone ()
285+ time = time .astimezone (datetime .timezone .utc )
286+
281287 # check if we already have the code
282288 self .cursor .execute (
283289 """
@@ -323,10 +329,10 @@ def create_submission(
323329 self .cursor .execute (
324330 """
325331 INSERT INTO leaderboard.submission (leaderboard_id, file_name,
326- user_id, code_id, submission_time)
332+ user_id, code_id, submission_time, mode_category )
327333 VALUES (
328334 (SELECT id FROM leaderboard.leaderboard WHERE name = %s),
329- %s, %s, %s, %s)
335+ %s, %s, %s, %s, %s )
330336 RETURNING id
331337 """ ,
332338 (
@@ -335,6 +341,7 @@ def create_submission(
335341 user_id ,
336342 code_id ,
337343 time ,
344+ mode_category ,
338345 ),
339346 )
340347 submission_id = self .cursor .fetchone ()[0 ]
@@ -1438,6 +1445,154 @@ def validate_cli_id(self, cli_id: str) -> Optional[dict[str, str]]:
14381445 raise KernelBotError ("Error validating CLI ID" ) from e
14391446
14401447
1448+ def set_rate_limit (self , leaderboard_name : str , mode_category : str , max_per_hour : int ) -> RateLimitItem :
1449+ try :
1450+ self .cursor .execute (
1451+ """
1452+ INSERT INTO leaderboard.rate_limit (leaderboard_id, mode_category, max_submissions_per_hour)
1453+ VALUES (
1454+ (SELECT id FROM leaderboard.leaderboard WHERE name = %s),
1455+ %s, %s
1456+ )
1457+ ON CONFLICT (leaderboard_id, mode_category)
1458+ DO UPDATE SET max_submissions_per_hour = EXCLUDED.max_submissions_per_hour
1459+ RETURNING id, leaderboard_id, mode_category, max_submissions_per_hour
1460+ """ ,
1461+ (leaderboard_name , mode_category , max_per_hour ),
1462+ )
1463+ row = self .cursor .fetchone ()
1464+ if row is None :
1465+ raise LeaderboardDoesNotExist (leaderboard_name )
1466+ self .connection .commit ()
1467+ return RateLimitItem (
1468+ id = row [0 ],
1469+ leaderboard_id = row [1 ],
1470+ leaderboard_name = leaderboard_name ,
1471+ mode_category = row [2 ],
1472+ max_submissions_per_hour = row [3 ],
1473+ )
1474+ except psycopg2 .Error as e :
1475+ self .connection .rollback ()
1476+ if "null value in column" in str (e ):
1477+ raise LeaderboardDoesNotExist (leaderboard_name ) from e
1478+ logger .exception ("Error setting rate limit" , exc_info = e )
1479+ raise KernelBotError ("Error setting rate limit" ) from e
1480+
1481+ def get_rate_limits (self , leaderboard_name : str ) -> List [RateLimitItem ]:
1482+ try :
1483+ self .cursor .execute (
1484+ """
1485+ SELECT rl.id, rl.leaderboard_id, rl.mode_category, rl.max_submissions_per_hour
1486+ FROM leaderboard.rate_limit rl
1487+ JOIN leaderboard.leaderboard lb ON rl.leaderboard_id = lb.id
1488+ WHERE lb.name = %s
1489+ """ ,
1490+ (leaderboard_name ,),
1491+ )
1492+ return [
1493+ RateLimitItem (
1494+ id = row [0 ],
1495+ leaderboard_id = row [1 ],
1496+ leaderboard_name = leaderboard_name ,
1497+ mode_category = row [2 ],
1498+ max_submissions_per_hour = row [3 ],
1499+ )
1500+ for row in self .cursor .fetchall ()
1501+ ]
1502+ except psycopg2 .Error as e :
1503+ self .connection .rollback ()
1504+ logger .exception ("Error getting rate limits" , exc_info = e )
1505+ raise KernelBotError ("Error getting rate limits" ) from e
1506+
1507+ def delete_rate_limit (self , leaderboard_name : str , mode_category : str ) -> None :
1508+ try :
1509+ self .cursor .execute (
1510+ """
1511+ DELETE FROM leaderboard.rate_limit
1512+ WHERE leaderboard_id = (SELECT id FROM leaderboard.leaderboard WHERE name = %s)
1513+ AND mode_category = %s
1514+ """ ,
1515+ (leaderboard_name , mode_category ),
1516+ )
1517+ if self .cursor .rowcount == 0 :
1518+ raise KernelBotError (
1519+ f"No rate limit found for '{ leaderboard_name } ' with category '{ mode_category } '" ,
1520+ code = 404 ,
1521+ )
1522+ self .connection .commit ()
1523+ except KernelBotError :
1524+ raise
1525+ except psycopg2 .Error as e :
1526+ self .connection .rollback ()
1527+ logger .exception ("Error deleting rate limit" , exc_info = e )
1528+ raise KernelBotError ("Error deleting rate limit" ) from e
1529+
1530+ def check_rate_limit (
1531+ self , leaderboard_name : str , user_id : str , mode_category : str
1532+ ) -> Optional [dict ]:
1533+ """Check if a user has exceeded the rate limit for a leaderboard+category.
1534+
1535+ Returns None if no rate limit is configured, otherwise returns a dict with:
1536+ - allowed: bool
1537+ - current_count: int
1538+ - max_per_hour: int
1539+ - retry_after_seconds: int (0 if allowed)
1540+ """
1541+ try :
1542+ # Get the rate limit config
1543+ self .cursor .execute (
1544+ """
1545+ SELECT rl.max_submissions_per_hour
1546+ FROM leaderboard.rate_limit rl
1547+ JOIN leaderboard.leaderboard lb ON rl.leaderboard_id = lb.id
1548+ WHERE lb.name = %s AND rl.mode_category = %s
1549+ """ ,
1550+ (leaderboard_name , mode_category ),
1551+ )
1552+ row = self .cursor .fetchone ()
1553+ if row is None :
1554+ return None
1555+
1556+ max_per_hour = row [0 ]
1557+
1558+ # Count submissions in the last hour
1559+ self .cursor .execute (
1560+ """
1561+ SELECT COUNT(*), MIN(s.submission_time)
1562+ FROM leaderboard.submission s
1563+ JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id
1564+ WHERE lb.name = %s
1565+ AND s.user_id = %s
1566+ AND s.mode_category = %s
1567+ AND s.submission_time > NOW() - INTERVAL '1 hour'
1568+ """ ,
1569+ (leaderboard_name , user_id , mode_category ),
1570+ )
1571+ count_row = self .cursor .fetchone ()
1572+ current_count = count_row [0 ]
1573+ oldest_time = count_row [1 ]
1574+
1575+ allowed = current_count < max_per_hour
1576+ retry_after = 0
1577+ if not allowed and oldest_time is not None :
1578+ import datetime as dt
1579+
1580+ expiry = oldest_time + dt .timedelta (hours = 1 )
1581+ now = dt .datetime .now (dt .timezone .utc )
1582+ retry_after = max (0 , int ((expiry - now ).total_seconds ()))
1583+
1584+ return {
1585+ "allowed" : allowed ,
1586+ "current_count" : current_count ,
1587+ "max_per_hour" : max_per_hour ,
1588+ "retry_after_seconds" : retry_after ,
1589+ }
1590+ except psycopg2 .Error as e :
1591+ self .connection .rollback ()
1592+ logger .exception ("Error checking rate limit" , exc_info = e )
1593+ raise KernelBotError ("Error checking rate limit" ) from e
1594+
1595+
14411596class LeaderboardDoesNotExist (KernelBotError ):
14421597 def __init__ (self , name : str ):
14431598 super ().__init__ (message = f"Leaderboard `{ name } ` does not exist." , code = 404 )
0 commit comments