From 27e709c41f0ad82e95fb8201024ebae4b752c3f9 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Thu, 13 Jul 2023 17:05:48 +1000 Subject: [PATCH] Reworked advent of code. Refreshed all of advent of code to work with the new slash commands. This includes: - Adding a database to record AOC accounts and discord users - Adding a database to record previous winners - Reworking the command to select winners - Reworking the leaderboard display system --- tests/test_advent.py | 0 tests/testfiles/test_advent.json | 8003 ++++++++++++++++++++++++++++++ uqcsbot/advent.py | 1395 ++++-- uqcsbot/bot.py | 1 + uqcsbot/models.py | 16 +- 5 files changed, 9051 insertions(+), 364 deletions(-) create mode 100644 tests/test_advent.py create mode 100644 tests/testfiles/test_advent.json diff --git a/tests/test_advent.py b/tests/test_advent.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/testfiles/test_advent.json b/tests/testfiles/test_advent.json new file mode 100644 index 00000000..6637548a --- /dev/null +++ b/tests/testfiles/test_advent.json @@ -0,0 +1,8003 @@ +{ + "members": { + "54678": { + "global_score": 0, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669959459, + "star_index": 291924 + }, + "2": { + "star_index": 292129, + "get_star_ts": 1669959482 + } + }, + "2": { + "1": { + "star_index": 297909, + "get_star_ts": 1669960330 + }, + "2": { + "star_index": 298862, + "get_star_ts": 1669960494 + } + }, + "3": { + "1": { + "get_star_ts": 1670064320, + "star_index": 670772 + }, + "2": { + "star_index": 671789, + "get_star_ts": 1670064573 + } + }, + "9": { + "1": { + "star_index": 2225125, + "get_star_ts": 1670579906 + }, + "2": { + "get_star_ts": 1670581378, + "star_index": 2228871 + } + } + }, + "local_score": 506, + "last_star_ts": 1670581378, + "name": "TRManderson", + "id": 54678, + "stars": 8 + }, + "68379": { + "global_score": 0, + "local_score": 3347, + "completion_day_level": { + "1": { + "1": { + "star_index": 9527, + "get_star_ts": 1669871448 + }, + "2": { + "get_star_ts": 1669871507, + "star_index": 10250 + } + }, + "2": { + "1": { + "star_index": 913590, + "get_star_ts": 1670143274 + }, + "2": { + "get_star_ts": 1670144073, + "star_index": 916984 + } + }, + "3": { + "1": { + "star_index": 922384, + "get_star_ts": 1670145287 + }, + "2": { + "star_index": 927170, + "get_star_ts": 1670146329 + } + }, + "4": { + "1": { + "star_index": 942716, + "get_star_ts": 1670149511 + }, + "2": { + "star_index": 946510, + "get_star_ts": 1670150265 + } + }, + "5": { + "1": { + "star_index": 1436008, + "get_star_ts": 1670301547 + }, + "2": { + "get_star_ts": 1670302005, + "star_index": 1436525 + } + }, + "6": { + "1": { + "star_index": 1446135, + "get_star_ts": 1670303296 + }, + "2": { + "star_index": 1470496, + "get_star_ts": 1670305148 + } + }, + "7": { + "1": { + "get_star_ts": 1670906897, + "star_index": 2838161 + }, + "2": { + "star_index": 2840889, + "get_star_ts": 1670908822 + } + }, + "8": { + "1": { + "star_index": 1959055, + "get_star_ts": 1670476500 + }, + "2": { + "star_index": 1972412, + "get_star_ts": 1670478597 + } + }, + "9": { + "1": { + "star_index": 2180432, + "get_star_ts": 1670564146 + }, + "2": { + "star_index": 2192491, + "get_star_ts": 1670567231 + } + }, + "10": { + "1": { + "get_star_ts": 1670658881, + "star_index": 2384774 + }, + "2": { + "star_index": 2429070, + "get_star_ts": 1670676821 + } + }, + "11": { + "1": { + "get_star_ts": 1670744462, + "star_index": 2551606 + }, + "2": { + "star_index": 2556685, + "get_star_ts": 1670747538 + } + }, + "12": { + "1": { + "get_star_ts": 1670825835, + "star_index": 2705255 + }, + "2": { + "star_index": 2706527, + "get_star_ts": 1670826411 + } + }, + "13": { + "1": { + "star_index": 2889886, + "get_star_ts": 1670936056 + }, + "2": { + "get_star_ts": 1670937454, + "star_index": 2892067 + } + }, + "14": { + "1": { + "get_star_ts": 1670997080, + "star_index": 2973905 + }, + "2": { + "get_star_ts": 1670998017, + "star_index": 2976471 + } + }, + "15": { + "1": { + "star_index": 3086767, + "get_star_ts": 1671082672 + }, + "2": { + "star_index": 3277460, + "get_star_ts": 1671279633 + } + }, + "16": { + "1": { + "star_index": 3264158, + "get_star_ts": 1671262478 + }, + "2": { + "get_star_ts": 1671274466, + "star_index": 3272985 + } + }, + "17": { + "1": { + "get_star_ts": 1671364797, + "star_index": 3353098 + }, + "2": { + "get_star_ts": 1671373828, + "star_index": 3362330 + } + }, + "18": { + "1": { + "star_index": 3527762, + "get_star_ts": 1671597817 + }, + "2": { + "get_star_ts": 1671621156, + "star_index": 3553430 + } + }, + "21": { + "1": { + "star_index": 3535115, + "get_star_ts": 1671602478 + }, + "2": { + "star_index": 3541250, + "get_star_ts": 1671608555 + } + }, + "22": { + "1": { + "get_star_ts": 1671691872, + "star_index": 3603418 + }, + "2": { + "star_index": 3620508, + "get_star_ts": 1671717239 + } + }, + "23": { + "1": { + "get_star_ts": 1671780346, + "star_index": 3662014 + }, + "2": { + "star_index": 3662123, + "get_star_ts": 1671780479 + } + }, + "25": { + "1": { + "get_star_ts": 1672719976, + "star_index": 3954159 + } + } + }, + "last_star_ts": 1672719976, + "name": "Aidan Goldthorpe", + "id": 68379, + "stars": 43 + }, + "69921": { + "stars": 0, + "id": 69921, + "name": "gricey432", + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "global_score": 0 + }, + "148240": { + "stars": 38, + "id": 148240, + "name": "mcoot", + "last_star_ts": 1672464283, + "completion_day_level": { + "1": { + "1": { + "star_index": 10194, + "get_star_ts": 1669871503 + }, + "2": { + "get_star_ts": 1669871597, + "star_index": 11302 + } + }, + "2": { + "1": { + "star_index": 292427, + "get_star_ts": 1669959519 + }, + "2": { + "get_star_ts": 1669959843, + "star_index": 294825 + } + }, + "3": { + "1": { + "get_star_ts": 1670059635, + "star_index": 650890 + }, + "2": { + "star_index": 652328, + "get_star_ts": 1670059997 + } + }, + "4": { + "1": { + "get_star_ts": 1670130486, + "star_index": 851795 + }, + "2": { + "get_star_ts": 1670130533, + "star_index": 852695 + } + }, + "5": { + "1": { + "get_star_ts": 1670220580, + "star_index": 1164362 + }, + "2": { + "star_index": 1165369, + "get_star_ts": 1670220773 + } + }, + "6": { + "1": { + "star_index": 1539635, + "get_star_ts": 1670318531 + }, + "2": { + "star_index": 1540550, + "get_star_ts": 1670318717 + } + }, + "7": { + "1": { + "get_star_ts": 1670414159, + "star_index": 1815102 + }, + "2": { + "get_star_ts": 1670414717, + "star_index": 1816480 + } + }, + "8": { + "1": { + "star_index": 1979681, + "get_star_ts": 1670480129 + }, + "2": { + "get_star_ts": 1670500536, + "star_index": 2042396 + } + }, + "9": { + "1": { + "star_index": 2195232, + "get_star_ts": 1670568183 + }, + "2": { + "get_star_ts": 1670583316, + "star_index": 2233799 + } + }, + "10": { + "1": { + "get_star_ts": 1670651821, + "star_index": 2368248 + }, + "2": { + "get_star_ts": 1670665732, + "star_index": 2400841 + } + }, + "11": { + "1": { + "star_index": 2544484, + "get_star_ts": 1670740677 + }, + "2": { + "star_index": 2558333, + "get_star_ts": 1670748500 + } + }, + "12": { + "1": { + "star_index": 3112753, + "get_star_ts": 1671100194 + }, + "2": { + "get_star_ts": 1671196046, + "star_index": 3208605 + } + }, + "13": { + "1": { + "get_star_ts": 1671238840, + "star_index": 3250192 + }, + "2": { + "star_index": 3250446, + "get_star_ts": 1671239342 + } + }, + "14": { + "1": { + "star_index": 3346274, + "get_star_ts": 1671358499 + }, + "2": { + "get_star_ts": 1671361190, + "star_index": 3349189 + } + }, + "15": { + "1": { + "get_star_ts": 1671364412, + "star_index": 3352725 + }, + "2": { + "star_index": 3482576, + "get_star_ts": 1671535360 + } + }, + "16": { + "1": { + "star_index": 3278061, + "get_star_ts": 1671280337 + } + }, + "18": { + "1": { + "get_star_ts": 1671341351, + "star_index": 3329076 + }, + "2": { + "get_star_ts": 1671348359, + "star_index": 3336563 + } + }, + "20": { + "1": { + "star_index": 3612826, + "get_star_ts": 1671705911 + }, + "2": { + "get_star_ts": 1671710568, + "star_index": 3615963 + } + }, + "21": { + "1": { + "star_index": 3854824, + "get_star_ts": 1672199865 + }, + "2": { + "get_star_ts": 1672211256, + "star_index": 3856466 + } + }, + "22": { + "1": { + "get_star_ts": 1672464283, + "star_index": 3917341 + } + } + }, + "local_score": 2921, + "global_score": 0 + }, + "152729": { + "global_score": 0, + "id": 152729, + "stars": 45, + "last_star_ts": 1671871319, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669872676, + "star_index": 18655 + }, + "2": { + "get_star_ts": 1669872830, + "star_index": 19371 + } + }, + "2": { + "1": { + "get_star_ts": 1670051573, + "star_index": 621892 + }, + "2": { + "star_index": 625456, + "get_star_ts": 1670052660 + } + }, + "3": { + "1": { + "get_star_ts": 1670045656, + "star_index": 596989 + }, + "2": { + "star_index": 607249, + "get_star_ts": 1670047419 + } + }, + "4": { + "1": { + "get_star_ts": 1670130908, + "star_index": 859120 + }, + "2": { + "star_index": 863464, + "get_star_ts": 1670131239 + } + }, + "5": { + "1": { + "get_star_ts": 1670219084, + "star_index": 1155299 + }, + "2": { + "get_star_ts": 1670219505, + "star_index": 1158252 + } + }, + "6": { + "1": { + "get_star_ts": 1670321226, + "star_index": 1552248 + }, + "2": { + "get_star_ts": 1670321484, + "star_index": 1553355 + } + }, + "7": { + "1": { + "get_star_ts": 1670411267, + "star_index": 1807497 + }, + "2": { + "get_star_ts": 1670411530, + "star_index": 1808233 + } + }, + "8": { + "1": { + "get_star_ts": 1670662955, + "star_index": 2393815 + }, + "2": { + "get_star_ts": 1670755608, + "star_index": 2573035 + } + }, + "9": { + "1": { + "star_index": 2579387, + "get_star_ts": 1670758402 + }, + "2": { + "star_index": 2581652, + "get_star_ts": 1670759380 + } + }, + "10": { + "1": { + "get_star_ts": 1670658810, + "star_index": 2384637 + }, + "2": { + "get_star_ts": 1670660358, + "star_index": 2387951 + } + }, + "11": { + "1": { + "star_index": 2537899, + "get_star_ts": 1670738317 + }, + "2": { + "star_index": 2544246, + "get_star_ts": 1670740569 + } + }, + "12": { + "1": { + "star_index": 2757625, + "get_star_ts": 1670854329 + }, + "2": { + "get_star_ts": 1670854705, + "star_index": 2758342 + } + }, + "13": { + "1": { + "get_star_ts": 1670939061, + "star_index": 2894586 + }, + "2": { + "get_star_ts": 1670941789, + "star_index": 2899010 + } + }, + "14": { + "1": { + "star_index": 3015381, + "get_star_ts": 1671022680 + }, + "2": { + "star_index": 3015842, + "get_star_ts": 1671023010 + } + }, + "15": { + "1": { + "get_star_ts": 1671348852, + "star_index": 3336978 + }, + "2": { + "get_star_ts": 1671365566, + "star_index": 3353886 + } + }, + "16": { + "1": { + "star_index": 3255649, + "get_star_ts": 1671251403 + }, + "2": { + "get_star_ts": 1671278800, + "star_index": 3276702 + } + }, + "17": { + "1": { + "star_index": 3357717, + "get_star_ts": 1671369315 + }, + "2": { + "star_index": 3359635, + "get_star_ts": 1671371321 + } + }, + "18": { + "1": { + "get_star_ts": 1671340350, + "star_index": 3326554 + }, + "2": { + "star_index": 3331006, + "get_star_ts": 1671342520 + } + }, + "20": { + "1": { + "star_index": 3480194, + "get_star_ts": 1671532508 + }, + "2": { + "star_index": 3480720, + "get_star_ts": 1671533097 + } + }, + "21": { + "1": { + "star_index": 3646835, + "get_star_ts": 1671753019 + }, + "2": { + "star_index": 3648308, + "get_star_ts": 1671756085 + } + }, + "22": { + "1": { + "star_index": 3649802, + "get_star_ts": 1671760239 + } + }, + "23": { + "1": { + "get_star_ts": 1671793581, + "star_index": 3671480 + }, + "2": { + "star_index": 3671608, + "get_star_ts": 1671793756 + } + }, + "24": { + "1": { + "get_star_ts": 1671870624, + "star_index": 3719386 + }, + "2": { + "get_star_ts": 1671871319, + "star_index": 3719711 + } + } + }, + "local_score": 3497, + "name": "jrgold" + }, + "153295": { + "global_score": 0, + "stars": 0, + "id": 153295, + "name": "Max Bo", + "local_score": 0, + "completion_day_level": {}, + "last_star_ts": 0 + }, + "198125": { + "global_score": 0, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669870893, + "star_index": 78 + }, + "2": { + "get_star_ts": 1669871019, + "star_index": 2086 + } + }, + "2": { + "1": { + "get_star_ts": 1669957592, + "star_index": 268387 + }, + "2": { + "star_index": 271168, + "get_star_ts": 1669957805 + } + }, + "3": { + "1": { + "star_index": 587101, + "get_star_ts": 1670044670 + }, + "2": { + "get_star_ts": 1670044890, + "star_index": 589683 + } + }, + "4": { + "1": { + "get_star_ts": 1670130537, + "star_index": 852803 + }, + "2": { + "get_star_ts": 1670130593, + "star_index": 853837 + } + }, + "5": { + "1": { + "star_index": 1140838, + "get_star_ts": 1670217378 + }, + "2": { + "get_star_ts": 1670217605, + "star_index": 1142787 + } + }, + "6": { + "1": { + "star_index": 1439925, + "get_star_ts": 1670303061 + }, + "2": { + "star_index": 1442104, + "get_star_ts": 1670303148 + } + }, + "7": { + "1": { + "get_star_ts": 1670394385, + "star_index": 1756817 + }, + "2": { + "star_index": 1757633, + "get_star_ts": 1670394584 + } + }, + "8": { + "1": { + "get_star_ts": 1670487308, + "star_index": 2002948 + }, + "2": { + "star_index": 2041197, + "get_star_ts": 1670500080 + } + }, + "9": { + "1": { + "star_index": 2200745, + "get_star_ts": 1670570361 + }, + "2": { + "star_index": 2203536, + "get_star_ts": 1670571517 + } + }, + "10": { + "1": { + "get_star_ts": 1670717000, + "star_index": 2512847 + }, + "2": { + "star_index": 2514476, + "get_star_ts": 1670718270 + } + }, + "11": { + "1": { + "star_index": 2586354, + "get_star_ts": 1670761454 + }, + "2": { + "get_star_ts": 1670828696, + "star_index": 2711026 + } + }, + "12": { + "1": { + "get_star_ts": 1670822497, + "star_index": 2695398 + }, + "2": { + "star_index": 2695925, + "get_star_ts": 1670822640 + } + }, + "13": { + "1": { + "get_star_ts": 1670909096, + "star_index": 2841899 + }, + "2": { + "get_star_ts": 1670910729, + "star_index": 2846893 + } + }, + "14": { + "1": { + "get_star_ts": 1671000250, + "star_index": 2981327 + }, + "2": { + "get_star_ts": 1671000488, + "star_index": 2981773 + } + }, + "15": { + "1": { + "get_star_ts": 1671082092, + "star_index": 3085505 + }, + "2": { + "get_star_ts": 1671085993, + "star_index": 3093773 + } + }, + "17": { + "1": { + "star_index": 3264171, + "get_star_ts": 1671262507 + }, + "2": { + "star_index": 3270107, + "get_star_ts": 1671270901 + } + }, + "18": { + "1": { + "get_star_ts": 1671348086, + "star_index": 3336323 + }, + "2": { + "star_index": 3336715, + "get_star_ts": 1671348544 + } + }, + "20": { + "1": { + "star_index": 3599332, + "get_star_ts": 1671687214 + }, + "2": { + "star_index": 3599852, + "get_star_ts": 1671687770 + } + }, + "21": { + "1": { + "star_index": 3597021, + "get_star_ts": 1671681093 + }, + "2": { + "get_star_ts": 1671683134, + "star_index": 3597671 + } + }, + "22": { + "1": { + "get_star_ts": 1671692464, + "star_index": 3603846 + } + }, + "23": { + "1": { + "star_index": 3660309, + "get_star_ts": 1671778154 + }, + "2": { + "get_star_ts": 1671778338, + "star_index": 3660482 + } + }, + "24": { + "1": { + "get_star_ts": 1671878817, + "star_index": 3723983 + }, + "2": { + "star_index": 3724679, + "get_star_ts": 1671879928 + } + }, + "25": { + "1": { + "star_index": 3798363, + "get_star_ts": 1672017749 + } + } + }, + "local_score": 3771, + "last_star_ts": 1672017749, + "name": "Thomas Hines", + "id": 198125, + "stars": 44 + }, + "208737": { + "global_score": 0, + "stars": 5, + "id": 208737, + "name": "Tom Richardson", + "completion_day_level": { + "1": { + "1": { + "star_index": 3728613, + "get_star_ts": 1671886951 + }, + "2": { + "star_index": 3729322, + "get_star_ts": 1671888250 + } + }, + "2": { + "1": { + "get_star_ts": 1671928549, + "star_index": 3750292 + }, + "2": { + "star_index": 3753057, + "get_star_ts": 1671939195 + } + }, + "3": { + "1": { + "get_star_ts": 1671963853, + "star_index": 3769590 + } + } + }, + "last_star_ts": 1671963853, + "local_score": 209 + }, + "246889": { + "global_score": 0, + "id": 246889, + "stars": 50, + "completion_day_level": { + "1": { + "1": { + "star_index": 3825671, + "get_star_ts": 1672099004 + }, + "2": { + "star_index": 3825690, + "get_star_ts": 1672099040 + } + }, + "2": { + "1": { + "star_index": 3825760, + "get_star_ts": 1672099283 + }, + "2": { + "star_index": 3825813, + "get_star_ts": 1672099425 + } + }, + "3": { + "1": { + "star_index": 3825876, + "get_star_ts": 1672099653 + }, + "2": { + "get_star_ts": 1672099802, + "star_index": 3825939 + } + }, + "4": { + "1": { + "get_star_ts": 1672100001, + "star_index": 3826001 + }, + "2": { + "get_star_ts": 1672100076, + "star_index": 3826018 + } + }, + "5": { + "1": { + "get_star_ts": 1672100646, + "star_index": 3826213 + }, + "2": { + "get_star_ts": 1672100666, + "star_index": 3826220 + } + }, + "6": { + "1": { + "star_index": 3826285, + "get_star_ts": 1672100919 + }, + "2": { + "get_star_ts": 1672101019, + "star_index": 3826316 + } + }, + "7": { + "1": { + "get_star_ts": 1672101980, + "star_index": 3826555 + }, + "2": { + "star_index": 3826626, + "get_star_ts": 1672102212 + } + }, + "8": { + "1": { + "star_index": 3826834, + "get_star_ts": 1672103118 + }, + "2": { + "get_star_ts": 1672103645, + "star_index": 3826961 + } + }, + "9": { + "1": { + "get_star_ts": 1672104516, + "star_index": 3827197 + }, + "2": { + "get_star_ts": 1672104829, + "star_index": 3827270 + } + }, + "10": { + "1": { + "get_star_ts": 1672105438, + "star_index": 3827378 + }, + "2": { + "get_star_ts": 1672105989, + "star_index": 3827505 + } + }, + "11": { + "1": { + "get_star_ts": 1672107080, + "star_index": 3827731 + }, + "2": { + "star_index": 3827796, + "get_star_ts": 1672107456 + } + }, + "12": { + "1": { + "get_star_ts": 1672108268, + "star_index": 3827970 + }, + "2": { + "star_index": 3827992, + "get_star_ts": 1672108347 + } + }, + "13": { + "1": { + "get_star_ts": 1672110049, + "star_index": 3828323 + }, + "2": { + "star_index": 3828377, + "get_star_ts": 1672110319 + } + }, + "14": { + "1": { + "star_index": 3828501, + "get_star_ts": 1672110996 + }, + "2": { + "get_star_ts": 1672111133, + "star_index": 3828532 + } + }, + "15": { + "1": { + "get_star_ts": 1672112375, + "star_index": 3828838 + }, + "2": { + "get_star_ts": 1672112866, + "star_index": 3828944 + } + }, + "16": { + "1": { + "get_star_ts": 1672114756, + "star_index": 3829314 + }, + "2": { + "star_index": 3829541, + "get_star_ts": 1672115999 + } + }, + "17": { + "1": { + "star_index": 3829863, + "get_star_ts": 1672117941 + }, + "2": { + "star_index": 3830214, + "get_star_ts": 1672120088 + } + }, + "18": { + "1": { + "star_index": 3830260, + "get_star_ts": 1672120321 + }, + "2": { + "star_index": 3830493, + "get_star_ts": 1672121912 + } + }, + "19": { + "1": { + "get_star_ts": 1672128207, + "star_index": 3831561 + }, + "2": { + "get_star_ts": 1672128760, + "star_index": 3831665 + } + }, + "20": { + "1": { + "get_star_ts": 1672129780, + "star_index": 3831867 + }, + "2": { + "star_index": 3831946, + "get_star_ts": 1672130212 + } + }, + "21": { + "1": { + "star_index": 3832008, + "get_star_ts": 1672130527 + }, + "2": { + "get_star_ts": 1672131600, + "star_index": 3832260 + } + }, + "22": { + "1": { + "star_index": 3832611, + "get_star_ts": 1672133052 + }, + "2": { + "star_index": 3833172, + "get_star_ts": 1672135283 + } + }, + "23": { + "1": { + "star_index": 3833783, + "get_star_ts": 1672137531 + }, + "2": { + "star_index": 3833823, + "get_star_ts": 1672137678 + } + }, + "24": { + "1": { + "get_star_ts": 1672139325, + "star_index": 3834325 + }, + "2": { + "star_index": 3834413, + "get_star_ts": 1672139631 + } + }, + "25": { + "1": { + "get_star_ts": 1672140353, + "star_index": 3834673 + }, + "2": { + "get_star_ts": 1672140356, + "star_index": 3834676 + } + } + }, + "last_star_ts": 1672140356, + "local_score": 3453, + "name": "Cameron Aavik" + }, + "309191": { + "local_score": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "name": "Bennett Hardwick", + "id": 309191, + "stars": 0, + "global_score": 0 + }, + "380404": { + "global_score": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "name": "jsutton101", + "id": 380404, + "stars": 0 + }, + "381066": { + "id": 381066, + "stars": 32, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871239, + "star_index": 6403 + }, + "2": { + "get_star_ts": 1669871444, + "star_index": 9465 + } + }, + "2": { + "1": { + "get_star_ts": 1669957814, + "star_index": 271313 + }, + "2": { + "star_index": 276107, + "get_star_ts": 1669958108 + } + }, + "3": { + "1": { + "get_star_ts": 1670049254, + "star_index": 614360 + }, + "2": { + "star_index": 615400, + "get_star_ts": 1670049572 + } + }, + "4": { + "1": { + "get_star_ts": 1670130951, + "star_index": 859714 + }, + "2": { + "star_index": 860110, + "get_star_ts": 1670130977 + } + }, + "5": { + "1": { + "star_index": 1147144, + "get_star_ts": 1670218075 + }, + "2": { + "star_index": 1147821, + "get_star_ts": 1670218151 + } + }, + "6": { + "1": { + "get_star_ts": 1670303227, + "star_index": 1444269 + }, + "2": { + "get_star_ts": 1670303259, + "star_index": 1445140 + } + }, + "7": { + "1": { + "get_star_ts": 1670652776, + "star_index": 2371359 + }, + "2": { + "get_star_ts": 1670653226, + "star_index": 2372670 + } + }, + "8": { + "1": { + "star_index": 1963164, + "get_star_ts": 1670477115 + }, + "2": { + "get_star_ts": 1670478028, + "star_index": 1969219 + } + }, + "9": { + "1": { + "get_star_ts": 1670564025, + "star_index": 2179825 + }, + "2": { + "get_star_ts": 1670565590, + "star_index": 2186917 + } + }, + "10": { + "1": { + "get_star_ts": 1670649205, + "star_index": 2353279 + }, + "2": { + "get_star_ts": 1670651021, + "star_index": 2364860 + } + }, + "11": { + "1": { + "star_index": 2536378, + "get_star_ts": 1670737878 + }, + "2": { + "star_index": 2537325, + "get_star_ts": 1670738159 + } + }, + "12": { + "1": { + "star_index": 2705626, + "get_star_ts": 1670826005 + }, + "2": { + "get_star_ts": 1670826122, + "star_index": 2705872 + } + }, + "13": { + "1": { + "star_index": 2843996, + "get_star_ts": 1670909746 + }, + "2": { + "get_star_ts": 1670972637, + "star_index": 2949858 + } + }, + "14": { + "1": { + "star_index": 3418359, + "get_star_ts": 1671452106 + }, + "2": { + "get_star_ts": 1671452666, + "star_index": 3418774 + } + }, + "15": { + "1": { + "star_index": 3420475, + "get_star_ts": 1671454947 + }, + "2": { + "get_star_ts": 1671458009, + "star_index": 3422846 + } + }, + "20": { + "1": { + "star_index": 3469671, + "get_star_ts": 1671519567 + }, + "2": { + "star_index": 3469734, + "get_star_ts": 1671519637 + } + } + }, + "local_score": 2648, + "last_star_ts": 1671519637, + "name": "Leo Orpilla III", + "global_score": 0 + }, + "390776": { + "global_score": 115, + "id": 390776, + "stars": 50, + "local_score": 4702, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669870938, + "star_index": 611 + }, + "2": { + "star_index": 1845, + "get_star_ts": 1669871008 + } + }, + "2": { + "1": { + "get_star_ts": 1669957452, + "star_index": 267319 + }, + "2": { + "star_index": 268244, + "get_star_ts": 1669957578 + } + }, + "3": { + "1": { + "star_index": 577224, + "get_star_ts": 1670043826 + }, + "2": { + "get_star_ts": 1670043941, + "star_index": 577969 + } + }, + "4": { + "1": { + "get_star_ts": 1670130462, + "star_index": 851330 + }, + "2": { + "get_star_ts": 1670130487, + "star_index": 851800 + } + }, + "5": { + "1": { + "get_star_ts": 1670216890, + "star_index": 1137920 + }, + "2": { + "star_index": 1138171, + "get_star_ts": 1670216957 + } + }, + "6": { + "1": { + "star_index": 1438061, + "get_star_ts": 1670302959 + }, + "2": { + "star_index": 1438721, + "get_star_ts": 1670303004 + } + }, + "7": { + "1": { + "get_star_ts": 1670390462, + "star_index": 1739399 + }, + "2": { + "get_star_ts": 1670390857, + "star_index": 1741104 + } + }, + "8": { + "1": { + "star_index": 1956341, + "get_star_ts": 1670475864 + }, + "2": { + "star_index": 1959796, + "get_star_ts": 1670476626 + } + }, + "9": { + "1": { + "get_star_ts": 1670562956, + "star_index": 2174055 + }, + "2": { + "get_star_ts": 1670563221, + "star_index": 2175340 + } + }, + "10": { + "1": { + "get_star_ts": 1670649079, + "star_index": 2352537 + }, + "2": { + "star_index": 2357028, + "get_star_ts": 1670649725 + } + }, + "11": { + "1": { + "star_index": 2528961, + "get_star_ts": 1670735881 + }, + "2": { + "get_star_ts": 1670736391, + "star_index": 2530385 + } + }, + "12": { + "1": { + "star_index": 2693750, + "get_star_ts": 1670821981 + }, + "2": { + "get_star_ts": 1670822137, + "star_index": 2694206 + } + }, + "13": { + "1": { + "get_star_ts": 1670908459, + "star_index": 2839775 + }, + "2": { + "star_index": 2841908, + "get_star_ts": 1670909099 + } + }, + "14": { + "1": { + "get_star_ts": 1670995100, + "star_index": 2967023 + }, + "2": { + "star_index": 2968252, + "get_star_ts": 1670995473 + } + }, + "15": { + "1": { + "star_index": 3083978, + "get_star_ts": 1671081247 + }, + "2": { + "star_index": 3088381, + "get_star_ts": 1671083298 + } + }, + "16": { + "1": { + "get_star_ts": 1671172336, + "star_index": 3188680 + }, + "2": { + "star_index": 3192373, + "get_star_ts": 1671176909 + } + }, + "17": { + "1": { + "star_index": 3257282, + "get_star_ts": 1671255114 + }, + "2": { + "get_star_ts": 1671257728, + "star_index": 3260235 + } + }, + "18": { + "1": { + "star_index": 3324810, + "get_star_ts": 1671339817 + }, + "2": { + "star_index": 3327127, + "get_star_ts": 1671340532 + } + }, + "19": { + "1": { + "star_index": 3403775, + "get_star_ts": 1671430929 + }, + "2": { + "get_star_ts": 1671431076, + "star_index": 3403885 + } + }, + "20": { + "1": { + "get_star_ts": 1671514007, + "star_index": 3463148 + }, + "2": { + "get_star_ts": 1671514088, + "star_index": 3463255 + } + }, + "21": { + "1": { + "star_index": 3528297, + "get_star_ts": 1671599061 + }, + "2": { + "star_index": 3528753, + "get_star_ts": 1671599280 + } + }, + "22": { + "1": { + "get_star_ts": 1671687881, + "star_index": 3599960 + }, + "2": { + "get_star_ts": 1671693032, + "star_index": 3604260 + } + }, + "23": { + "1": { + "star_index": 3654881, + "get_star_ts": 1671773819 + }, + "2": { + "star_index": 3655110, + "get_star_ts": 1671773952 + } + }, + "24": { + "1": { + "star_index": 3711611, + "get_star_ts": 1671859878 + }, + "2": { + "get_star_ts": 1671860332, + "star_index": 3712086 + } + }, + "25": { + "1": { + "star_index": 3755023, + "get_star_ts": 1671945153 + }, + "2": { + "get_star_ts": 1671945157, + "star_index": 3755029 + } + } + }, + "last_star_ts": 1671945157, + "name": "bradleysigma" + }, + "399258": { + "global_score": 0, + "last_star_ts": 1671946528, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669870899, + "star_index": 126 + }, + "2": { + "star_index": 556, + "get_star_ts": 1669870934 + } + }, + "2": { + "1": { + "get_star_ts": 1669957521, + "star_index": 267731 + }, + "2": { + "star_index": 269936, + "get_star_ts": 1669957719 + } + }, + "3": { + "1": { + "star_index": 671744, + "get_star_ts": 1670064561 + }, + "2": { + "star_index": 672501, + "get_star_ts": 1670064745 + } + }, + "4": { + "1": { + "get_star_ts": 1670152704, + "star_index": 958228 + }, + "2": { + "star_index": 959662, + "get_star_ts": 1670153033 + } + }, + "5": { + "1": { + "star_index": 1176405, + "get_star_ts": 1670223352 + }, + "2": { + "get_star_ts": 1670223437, + "star_index": 1176692 + } + }, + "6": { + "1": { + "star_index": 1540499, + "get_star_ts": 1670318705 + }, + "2": { + "get_star_ts": 1670318796, + "star_index": 1540931 + } + }, + "7": { + "1": { + "star_index": 1779181, + "get_star_ts": 1670401788 + }, + "2": { + "get_star_ts": 1670401957, + "star_index": 1779658 + } + }, + "8": { + "1": { + "get_star_ts": 1670510181, + "star_index": 2068700 + }, + "2": { + "star_index": 2207287, + "get_star_ts": 1670573043 + } + }, + "9": { + "1": { + "star_index": 2217373, + "get_star_ts": 1670577025 + }, + "2": { + "get_star_ts": 1670578275, + "star_index": 2220772 + } + }, + "10": { + "1": { + "star_index": 2381702, + "get_star_ts": 1670657359 + }, + "2": { + "star_index": 2382983, + "get_star_ts": 1670657984 + } + }, + "11": { + "1": { + "get_star_ts": 1670736269, + "star_index": 2529977 + }, + "2": { + "get_star_ts": 1670736479, + "star_index": 2530672 + } + }, + "12": { + "1": { + "get_star_ts": 1670836244, + "star_index": 2724730 + }, + "2": { + "star_index": 2725819, + "get_star_ts": 1670836827 + } + }, + "13": { + "1": { + "star_index": 2852647, + "get_star_ts": 1670913150 + }, + "2": { + "star_index": 2853241, + "get_star_ts": 1670913460 + } + }, + "14": { + "1": { + "get_star_ts": 1671001689, + "star_index": 2983782 + }, + "2": { + "star_index": 2985045, + "get_star_ts": 1671002534 + } + }, + "15": { + "1": { + "star_index": 3119117, + "get_star_ts": 1671105318 + }, + "2": { + "get_star_ts": 1671106302, + "star_index": 3120232 + } + }, + "16": { + "1": { + "get_star_ts": 1671275696, + "star_index": 3273940 + }, + "2": { + "get_star_ts": 1671377956, + "star_index": 3366393 + } + }, + "17": { + "1": { + "get_star_ts": 1671256175, + "star_index": 3258480 + }, + "2": { + "get_star_ts": 1671260808, + "star_index": 3262910 + } + }, + "18": { + "1": { + "get_star_ts": 1671361446, + "star_index": 3349498 + }, + "2": { + "get_star_ts": 1671363098, + "star_index": 3351331 + } + }, + "19": { + "1": { + "star_index": 3665927, + "get_star_ts": 1671786274 + }, + "2": { + "star_index": 3667242, + "get_star_ts": 1671788057 + } + }, + "20": { + "1": { + "star_index": 3463996, + "get_star_ts": 1671514549 + }, + "2": { + "get_star_ts": 1671515580, + "star_index": 3465411 + } + }, + "21": { + "1": { + "star_index": 3530109, + "get_star_ts": 1671599775 + }, + "2": { + "get_star_ts": 1671602383, + "star_index": 3534996 + } + }, + "22": { + "1": { + "star_index": 3600263, + "get_star_ts": 1671688213 + }, + "2": { + "star_index": 3602284, + "get_star_ts": 1671690469 + } + }, + "23": { + "1": { + "star_index": 3654324, + "get_star_ts": 1671773445 + }, + "2": { + "star_index": 3654726, + "get_star_ts": 1671773717 + } + }, + "24": { + "1": { + "star_index": 3713886, + "get_star_ts": 1671862084 + }, + "2": { + "get_star_ts": 1671862413, + "star_index": 3714187 + } + }, + "25": { + "1": { + "get_star_ts": 1671946520, + "star_index": 3757510 + }, + "2": { + "get_star_ts": 1671946528, + "star_index": 3757531 + } + } + }, + "local_score": 4152, + "name": "katrinafyi", + "id": 399258, + "stars": 50 + }, + "417621": { + "global_score": 0, + "stars": 0, + "id": 417621, + "name": "James Dearlove", + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0 + }, + "435026": { + "id": 435026, + "stars": 0, + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "name": "Tom", + "global_score": 0 + }, + "435132": { + "global_score": 200, + "last_star_ts": 1670736782, + "completion_day_level": { + "1": { + "1": { + "star_index": 0, + "get_star_ts": 1669870883 + }, + "2": { + "star_index": 386, + "get_star_ts": 1669870922 + } + }, + "2": { + "1": { + "get_star_ts": 1669957514, + "star_index": 267686 + }, + "2": { + "star_index": 273502, + "get_star_ts": 1669957949 + } + }, + "3": { + "1": { + "get_star_ts": 1670088850, + "star_index": 761016 + }, + "2": { + "star_index": 761545, + "get_star_ts": 1670089017 + } + }, + "4": { + "1": { + "get_star_ts": 1670130264, + "star_index": 847919 + }, + "2": { + "get_star_ts": 1670130305, + "star_index": 848473 + } + }, + "5": { + "1": { + "get_star_ts": 1670217089, + "star_index": 1138787 + }, + "2": { + "star_index": 1140330, + "get_star_ts": 1670217315 + } + }, + "6": { + "1": { + "star_index": 1437483, + "get_star_ts": 1670302861 + }, + "2": { + "get_star_ts": 1670302878, + "star_index": 1437517 + } + }, + "8": { + "1": { + "get_star_ts": 1670475982, + "star_index": 1956632 + }, + "2": { + "get_star_ts": 1670476509, + "star_index": 1959104 + } + }, + "9": { + "1": { + "get_star_ts": 1670563307, + "star_index": 2175846 + }, + "2": { + "star_index": 2185092, + "get_star_ts": 1670565130 + } + }, + "11": { + "1": { + "star_index": 2531909, + "get_star_ts": 1670736782 + } + } + }, + "local_score": 1516, + "name": "Matthew Low", + "id": 435132, + "stars": 17 + }, + "475612": { + "global_score": 0, + "last_star_ts": 1672015283, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669870966, + "star_index": 1071 + }, + "2": { + "get_star_ts": 1669871024, + "star_index": 2197 + } + }, + "2": { + "1": { + "get_star_ts": 1669957831, + "star_index": 271557 + }, + "2": { + "star_index": 275975, + "get_star_ts": 1669958099 + } + }, + "3": { + "1": { + "get_star_ts": 1670044366, + "star_index": 583016 + }, + "2": { + "star_index": 585866, + "get_star_ts": 1670044574 + } + }, + "4": { + "1": { + "get_star_ts": 1670130473, + "star_index": 851533 + }, + "2": { + "star_index": 855750, + "get_star_ts": 1670130698 + } + }, + "5": { + "1": { + "star_index": 1161474, + "get_star_ts": 1670220051 + }, + "2": { + "get_star_ts": 1670220286, + "star_index": 1162837 + } + }, + "6": { + "1": { + "get_star_ts": 1670303461, + "star_index": 1450125 + }, + "2": { + "get_star_ts": 1670303709, + "star_index": 1455140 + } + }, + "7": { + "1": { + "star_index": 1766925, + "get_star_ts": 1670397505 + }, + "2": { + "star_index": 1769396, + "get_star_ts": 1670398366 + } + }, + "8": { + "1": { + "star_index": 1958673, + "get_star_ts": 1670476441 + }, + "2": { + "get_star_ts": 1670476691, + "star_index": 1960243 + } + }, + "9": { + "1": { + "get_star_ts": 1670564091, + "star_index": 2180180 + }, + "2": { + "star_index": 2183001, + "get_star_ts": 1670564670 + } + }, + "10": { + "1": { + "get_star_ts": 1670649228, + "star_index": 2353415 + }, + "2": { + "star_index": 2362242, + "get_star_ts": 1670650531 + } + }, + "11": { + "1": { + "get_star_ts": 1670819439, + "star_index": 2691340 + }, + "2": { + "get_star_ts": 1670827778, + "star_index": 2709369 + } + }, + "12": { + "1": { + "star_index": 2703645, + "get_star_ts": 1670825154 + }, + "2": { + "get_star_ts": 1670825719, + "star_index": 2704997 + } + }, + "13": { + "1": { + "star_index": 2873333, + "get_star_ts": 1670925287 + }, + "2": { + "star_index": 2875379, + "get_star_ts": 1670926560 + } + }, + "14": { + "1": { + "star_index": 2970516, + "get_star_ts": 1670996077 + }, + "2": { + "get_star_ts": 1670996472, + "star_index": 2971898 + } + }, + "15": { + "1": { + "get_star_ts": 1671081681, + "star_index": 3084688 + }, + "2": { + "get_star_ts": 1671086518, + "star_index": 3094645 + } + }, + "16": { + "1": { + "get_star_ts": 1671170200, + "star_index": 3186958 + }, + "2": { + "star_index": 3190265, + "get_star_ts": 1671174231 + } + }, + "17": { + "1": { + "star_index": 3258645, + "get_star_ts": 1671256310 + }, + "2": { + "get_star_ts": 1671264010, + "star_index": 3265174 + } + }, + "18": { + "1": { + "star_index": 3351541, + "get_star_ts": 1671363308 + }, + "2": { + "get_star_ts": 1671366014, + "star_index": 3354353 + } + }, + "19": { + "1": { + "get_star_ts": 1671437893, + "star_index": 3408506 + }, + "2": { + "star_index": 3417129, + "get_star_ts": 1671450397 + } + }, + "20": { + "1": { + "star_index": 3468272, + "get_star_ts": 1671518098 + }, + "2": { + "star_index": 3468676, + "get_star_ts": 1671518475 + } + }, + "21": { + "1": { + "star_index": 3530471, + "get_star_ts": 1671599906 + }, + "2": { + "get_star_ts": 1671606107, + "star_index": 3538960 + } + }, + "22": { + "1": { + "get_star_ts": 1671691136, + "star_index": 3602794 + }, + "2": { + "star_index": 3609095, + "get_star_ts": 1671700521 + } + }, + "23": { + "1": { + "star_index": 3659855, + "get_star_ts": 1671777655 + }, + "2": { + "star_index": 3661523, + "get_star_ts": 1671779701 + } + }, + "24": { + "1": { + "star_index": 3797557, + "get_star_ts": 1672014936 + }, + "2": { + "get_star_ts": 1672015263, + "star_index": 3797652 + } + }, + "25": { + "1": { + "get_star_ts": 1671976210, + "star_index": 3776610 + }, + "2": { + "get_star_ts": 1672015283, + "star_index": 3797658 + } + } + }, + "local_score": 4372, + "name": "Brian S", + "id": 475612, + "stars": 50 + }, + "488043": { + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669872241, + "star_index": 16331 + }, + "2": { + "star_index": 20083, + "get_star_ts": 1669872993 + } + }, + "2": { + "1": { + "star_index": 273697, + "get_star_ts": 1669957961 + }, + "2": { + "get_star_ts": 1669958284, + "star_index": 278906 + } + }, + "3": { + "1": { + "get_star_ts": 1670044964, + "star_index": 590473 + }, + "2": { + "get_star_ts": 1670046829, + "star_index": 604452 + } + }, + "4": { + "1": { + "get_star_ts": 1670141861, + "star_index": 907998 + }, + "2": { + "star_index": 913208, + "get_star_ts": 1670143177 + } + }, + "5": { + "1": { + "star_index": 1154667, + "get_star_ts": 1670218999 + }, + "2": { + "star_index": 1156172, + "get_star_ts": 1670219204 + } + }, + "6": { + "1": { + "get_star_ts": 1670303235, + "star_index": 1444529 + }, + "2": { + "star_index": 1445516, + "get_star_ts": 1670303272 + } + }, + "7": { + "1": { + "get_star_ts": 1670391166, + "star_index": 1742478 + }, + "2": { + "star_index": 1747676, + "get_star_ts": 1670392206 + } + }, + "8": { + "1": { + "get_star_ts": 1670476550, + "star_index": 1959350 + }, + "2": { + "star_index": 1965332, + "get_star_ts": 1670477427 + } + }, + "9": { + "1": { + "star_index": 2285061, + "get_star_ts": 1670604484 + }, + "2": { + "get_star_ts": 1670640361, + "star_index": 2344883 + } + }, + "10": { + "1": { + "get_star_ts": 1670651622, + "star_index": 2367488 + }, + "2": { + "get_star_ts": 1670652454, + "star_index": 2370355 + } + }, + "11": { + "1": { + "star_index": 2536359, + "get_star_ts": 1670737873 + }, + "2": { + "star_index": 2543947, + "get_star_ts": 1670740441 + } + }, + "12": { + "1": { + "get_star_ts": 1670824778, + "star_index": 2702668 + }, + "2": { + "get_star_ts": 1670825812, + "star_index": 2705205 + } + }, + "13": { + "1": { + "star_index": 2898246, + "get_star_ts": 1670941333 + }, + "2": { + "star_index": 2900329, + "get_star_ts": 1670942555 + } + }, + "14": { + "1": { + "star_index": 3011128, + "get_star_ts": 1671019616 + }, + "2": { + "get_star_ts": 1671020451, + "star_index": 3012260 + } + }, + "15": { + "1": { + "get_star_ts": 1671083693, + "star_index": 3089389 + }, + "2": { + "get_star_ts": 1671111342, + "star_index": 3126536 + } + }, + "21": { + "1": { + "star_index": 3563216, + "get_star_ts": 1671632517 + }, + "2": { + "get_star_ts": 1671672374, + "star_index": 3594153 + } + } + }, + "last_star_ts": 1671672374, + "local_score": 2609, + "name": "thatsokay", + "id": 488043, + "stars": 32, + "global_score": 0 + }, + "675530": { + "completion_day_level": { + "1": { + "1": { + "star_index": 4377340, + "get_star_ts": 1688275612 + } + } + }, + "last_star_ts": 1688275612, + "local_score": 30, + "name": "joshua-morris", + "id": 675530, + "stars": 1, + "global_score": 0 + }, + "693745": { + "global_score": 0, + "name": "DavidMcGovern", + "last_star_ts": 1676946494, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669880623, + "star_index": 44688 + }, + "2": { + "get_star_ts": 1669884517, + "star_index": 59773 + } + }, + "2": { + "1": { + "get_star_ts": 1669962355, + "star_index": 307911 + }, + "2": { + "get_star_ts": 1669970327, + "star_index": 347394 + } + }, + "3": { + "1": { + "star_index": 830335, + "get_star_ts": 1670114788 + }, + "2": { + "get_star_ts": 1670117370, + "star_index": 833609 + } + }, + "4": { + "1": { + "star_index": 877224, + "get_star_ts": 1670133123 + }, + "2": { + "get_star_ts": 1670135432, + "star_index": 886204 + } + }, + "5": { + "1": { + "get_star_ts": 1670306033, + "star_index": 1475850 + }, + "2": { + "star_index": 1499934, + "get_star_ts": 1670311046 + } + }, + "6": { + "1": { + "star_index": 1505849, + "get_star_ts": 1670312174 + }, + "2": { + "get_star_ts": 1670312271, + "star_index": 1506404 + } + }, + "7": { + "1": { + "star_index": 1920594, + "get_star_ts": 1670451310 + }, + "2": { + "get_star_ts": 1670464306, + "star_index": 1943543 + } + }, + "8": { + "1": { + "get_star_ts": 1670479605, + "star_index": 1977443 + }, + "2": { + "get_star_ts": 1670483582, + "star_index": 1991363 + } + }, + "9": { + "1": { + "star_index": 2193042, + "get_star_ts": 1670567417 + }, + "2": { + "get_star_ts": 1670571917, + "star_index": 2204528 + } + }, + "10": { + "1": { + "get_star_ts": 1670656349, + "star_index": 2379671 + }, + "2": { + "get_star_ts": 1670657272, + "star_index": 2381512 + } + }, + "11": { + "1": { + "star_index": 2742837, + "get_star_ts": 1670846186 + }, + "2": { + "get_star_ts": 1670846714, + "star_index": 2743778 + } + }, + "12": { + "1": { + "star_index": 2853741, + "get_star_ts": 1670913694 + }, + "2": { + "star_index": 2860977, + "get_star_ts": 1670917951 + } + }, + "13": { + "1": { + "get_star_ts": 1670931821, + "star_index": 2883524 + }, + "2": { + "star_index": 2885913, + "get_star_ts": 1670933447 + } + }, + "14": { + "1": { + "get_star_ts": 1671074221, + "star_index": 3079889 + }, + "2": { + "star_index": 3080409, + "get_star_ts": 1671075106 + } + }, + "15": { + "1": { + "get_star_ts": 1671091371, + "star_index": 3101228 + }, + "2": { + "star_index": 3107234, + "get_star_ts": 1671095981 + } + }, + "16": { + "1": { + "star_index": 3355595, + "get_star_ts": 1671367219 + } + }, + "17": { + "1": { + "star_index": 3327298, + "get_star_ts": 1671340585 + }, + "2": { + "star_index": 3592404, + "get_star_ts": 1671667988 + } + }, + "18": { + "1": { + "star_index": 3332220, + "get_star_ts": 1671343492 + }, + "2": { + "get_star_ts": 1671345534, + "star_index": 3334174 + } + }, + "19": { + "1": { + "get_star_ts": 1671509821, + "star_index": 3461120 + }, + "2": { + "get_star_ts": 1671512311, + "star_index": 3462030 + } + }, + "20": { + "1": { + "star_index": 3472627, + "get_star_ts": 1671523274 + }, + "2": { + "star_index": 3482494, + "get_star_ts": 1671535254 + } + }, + "21": { + "1": { + "get_star_ts": 1671602808, + "star_index": 3535506 + }, + "2": { + "get_star_ts": 1671616211, + "star_index": 3548748 + } + }, + "22": { + "1": { + "get_star_ts": 1671713323, + "star_index": 3617770 + } + }, + "23": { + "1": { + "star_index": 4178836, + "get_star_ts": 1676944255 + }, + "2": { + "star_index": 4178879, + "get_star_ts": 1676946494 + } + }, + "25": { + "1": { + "star_index": 3756936, + "get_star_ts": 1671946156 + } + } + }, + "local_score": 3439, + "stars": 45, + "id": 693745 + }, + "727309": { + "global_score": 0, + "name": "harryvanroy", + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "stars": 0, + "id": 727309 + }, + "790186": { + "global_score": 0, + "id": 790186, + "stars": 16, + "completion_day_level": { + "1": { + "1": { + "star_index": 3958018, + "get_star_ts": 1672751554 + }, + "2": { + "get_star_ts": 1672753278, + "star_index": 3958290 + } + }, + "2": { + "1": { + "get_star_ts": 1672755244, + "star_index": 3958668 + }, + "2": { + "get_star_ts": 1672755827, + "star_index": 3958777 + } + }, + "3": { + "1": { + "get_star_ts": 1673094933, + "star_index": 4000680 + }, + "2": { + "star_index": 4000719, + "get_star_ts": 1673095221 + } + }, + "4": { + "1": { + "get_star_ts": 1673096302, + "star_index": 4000843 + }, + "2": { + "get_star_ts": 1673096488, + "star_index": 4000863 + } + }, + "5": { + "1": { + "get_star_ts": 1673098624, + "star_index": 4001102 + }, + "2": { + "get_star_ts": 1673098703, + "star_index": 4001110 + } + }, + "6": { + "1": { + "get_star_ts": 1673179711, + "star_index": 4008738 + }, + "2": { + "get_star_ts": 1673180077, + "star_index": 4008778 + } + }, + "7": { + "1": { + "star_index": 4016917, + "get_star_ts": 1673264474 + }, + "2": { + "get_star_ts": 1673264897, + "star_index": 4016950 + } + }, + "8": { + "1": { + "star_index": 4017238, + "get_star_ts": 1673267951 + }, + "2": { + "get_star_ts": 1673269417, + "star_index": 4017393 + } + } + }, + "local_score": 828, + "last_star_ts": 1673269417, + "name": "Matt" + }, + "825180": { + "name": "kennoath", + "local_score": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "stars": 0, + "id": 825180, + "global_score": 0 + }, + "851485": { + "stars": 0, + "id": 851485, + "name": "rowboat1", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "global_score": 0 + }, + "854164": { + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "name": "Zachary Thomas", + "id": 854164, + "stars": 0, + "global_score": 0 + }, + "957566": { + "stars": 3, + "id": 957566, + "name": "gamesfreak26", + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1670306179, + "star_index": 1476659 + }, + "2": { + "get_star_ts": 1670310716, + "star_index": 1498346 + } + }, + "2": { + "1": { + "get_star_ts": 1670472280, + "star_index": 1952564 + } + } + }, + "local_score": 137, + "last_star_ts": 1670472280, + "global_score": 0 + }, + "978227": { + "global_score": 0, + "name": "jtpashley", + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "stars": 0, + "id": 978227 + }, + "989288": { + "global_score": 0, + "id": 989288, + "stars": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "name": "UQ Computing Society" + }, + "990370": { + "id": 990370, + "stars": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "name": "nathan-wien", + "global_score": 0 + }, + "990546": { + "id": 990546, + "stars": 2, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669875891, + "star_index": 29787 + }, + "2": { + "get_star_ts": 1669876266, + "star_index": 30970 + } + } + }, + "local_score": 134, + "last_star_ts": 1669876266, + "name": "sanni ☀️", + "global_score": 0 + }, + "996197": { + "stars": 0, + "id": 996197, + "name": "Jason Hassell", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "global_score": 0 + }, + "996620": { + "stars": 6, + "id": 996620, + "name": "alyssadev", + "completion_day_level": { + "1": { + "1": { + "star_index": 118378, + "get_star_ts": 1669900640 + }, + "2": { + "star_index": 118591, + "get_star_ts": 1669900701 + } + }, + "2": { + "1": { + "star_index": 301978, + "get_star_ts": 1669961076 + }, + "2": { + "star_index": 306018, + "get_star_ts": 1669961952 + } + }, + "3": { + "1": { + "star_index": 815746, + "get_star_ts": 1670107181 + }, + "2": { + "star_index": 817482, + "get_star_ts": 1670107883 + } + } + }, + "last_star_ts": 1670107883, + "local_score": 363, + "global_score": 0 + }, + "1079351": { + "id": 1079351, + "stars": 26, + "last_star_ts": 1671253457, + "completion_day_level": { + "1": { + "1": { + "star_index": 54495, + "get_star_ts": 1669883248 + }, + "2": { + "get_star_ts": 1669886486, + "star_index": 68214 + } + }, + "2": { + "1": { + "get_star_ts": 1669958319, + "star_index": 279486 + }, + "2": { + "get_star_ts": 1669959577, + "star_index": 292869 + } + }, + "3": { + "1": { + "star_index": 606739, + "get_star_ts": 1670047312 + }, + "2": { + "get_star_ts": 1670048277, + "star_index": 610735 + } + }, + "4": { + "1": { + "star_index": 858709, + "get_star_ts": 1670130882 + }, + "2": { + "get_star_ts": 1670131023, + "star_index": 860748 + } + }, + "5": { + "1": { + "get_star_ts": 1670223905, + "star_index": 1178415 + }, + "2": { + "star_index": 1178998, + "get_star_ts": 1670224059 + } + }, + "6": { + "1": { + "get_star_ts": 1670304369, + "star_index": 1463973 + }, + "2": { + "star_index": 1465693, + "get_star_ts": 1670304548 + } + }, + "7": { + "1": { + "get_star_ts": 1670549920, + "star_index": 2161106 + }, + "2": { + "get_star_ts": 1670550509, + "star_index": 2161668 + } + }, + "8": { + "1": { + "star_index": 2452081, + "get_star_ts": 1670686475 + }, + "2": { + "star_index": 2452673, + "get_star_ts": 1670686731 + } + }, + "9": { + "1": { + "star_index": 2197819, + "get_star_ts": 1670569173 + }, + "2": { + "get_star_ts": 1670575596, + "star_index": 2213621 + } + }, + "10": { + "1": { + "star_index": 2373116, + "get_star_ts": 1670653404 + }, + "2": { + "star_index": 2377527, + "get_star_ts": 1670655326 + } + }, + "11": { + "1": { + "star_index": 2543002, + "get_star_ts": 1670740068 + }, + "2": { + "get_star_ts": 1670742125, + "star_index": 2547482 + } + }, + "12": { + "1": { + "star_index": 3256095, + "get_star_ts": 1671252543 + }, + "2": { + "star_index": 3256459, + "get_star_ts": 1671253457 + } + }, + "13": { + "1": { + "star_index": 2899222, + "get_star_ts": 1670941902 + }, + "2": { + "star_index": 2902248, + "get_star_ts": 1670943701 + } + } + }, + "local_score": 1892, + "name": "Jake Moss", + "global_score": 0 + }, + "1081824": { + "global_score": 0, + "name": "Anti Matter", + "local_score": 83, + "completion_day_level": { + "1": { + "1": { + "star_index": 1724776, + "get_star_ts": 1670377707 + }, + "2": { + "star_index": 1725357, + "get_star_ts": 1670378161 + } + } + }, + "last_star_ts": 1670378161, + "stars": 2, + "id": 1081824 + }, + "1089509": { + "local_score": 3693, + "completion_day_level": { + "1": { + "1": { + "star_index": 11238, + "get_star_ts": 1669871591 + }, + "2": { + "get_star_ts": 1669871827, + "star_index": 13470 + } + }, + "2": { + "1": { + "star_index": 276176, + "get_star_ts": 1669958111 + }, + "2": { + "get_star_ts": 1669958375, + "star_index": 280302 + } + }, + "3": { + "1": { + "get_star_ts": 1670044777, + "star_index": 588366 + }, + "2": { + "star_index": 595021, + "get_star_ts": 1670045422 + } + }, + "4": { + "1": { + "get_star_ts": 1670131192, + "star_index": 862942 + }, + "2": { + "star_index": 867078, + "get_star_ts": 1670131590 + } + }, + "5": { + "1": { + "get_star_ts": 1670217718, + "star_index": 1143758 + }, + "2": { + "star_index": 1145291, + "get_star_ts": 1670217880 + } + }, + "6": { + "1": { + "get_star_ts": 1670303916, + "star_index": 1458361 + }, + "2": { + "get_star_ts": 1670304058, + "star_index": 1460288 + } + }, + "7": { + "1": { + "get_star_ts": 1670391627, + "star_index": 1744748 + }, + "2": { + "star_index": 1746820, + "get_star_ts": 1670392028 + } + }, + "8": { + "1": { + "get_star_ts": 1670477300, + "star_index": 1964441 + }, + "2": { + "get_star_ts": 1670477777, + "star_index": 1967626 + } + }, + "9": { + "1": { + "star_index": 2172814, + "get_star_ts": 1670562601 + }, + "2": { + "get_star_ts": 1670564749, + "star_index": 2183430 + } + }, + "10": { + "1": { + "get_star_ts": 1670649526, + "star_index": 2355592 + }, + "2": { + "get_star_ts": 1670650034, + "star_index": 2359207 + } + }, + "11": { + "1": { + "get_star_ts": 1670736525, + "star_index": 2530824 + }, + "2": { + "get_star_ts": 1670736855, + "star_index": 2532233 + } + }, + "12": { + "1": { + "star_index": 2695258, + "get_star_ts": 1670822457 + }, + "2": { + "get_star_ts": 1670822555, + "star_index": 2695610 + } + }, + "13": { + "1": { + "star_index": 2853385, + "get_star_ts": 1670913524 + }, + "2": { + "star_index": 2854401, + "get_star_ts": 1670914050 + } + }, + "14": { + "1": { + "get_star_ts": 1670995690, + "star_index": 2969075 + }, + "2": { + "get_star_ts": 1670995896, + "star_index": 2969859 + } + }, + "15": { + "1": { + "star_index": 3554843, + "get_star_ts": 1671622786 + }, + "2": { + "star_index": 3560389, + "get_star_ts": 1671629469 + } + }, + "16": { + "1": { + "star_index": 3575967, + "get_star_ts": 1671646761 + }, + "2": { + "get_star_ts": 1671647945, + "star_index": 3576928 + } + }, + "17": { + "1": { + "star_index": 3718761, + "get_star_ts": 1671869419 + } + }, + "18": { + "1": { + "get_star_ts": 1671946259, + "star_index": 3757110 + } + }, + "20": { + "1": { + "get_star_ts": 1671950450, + "star_index": 3761498 + }, + "2": { + "star_index": 3761937, + "get_star_ts": 1671951194 + } + }, + "21": { + "1": { + "get_star_ts": 1671952331, + "star_index": 3762630 + } + }, + "22": { + "1": { + "star_index": 3601047, + "get_star_ts": 1671689101 + } + }, + "23": { + "1": { + "star_index": 3655625, + "get_star_ts": 1671774305 + }, + "2": { + "get_star_ts": 1671774365, + "star_index": 3655726 + } + }, + "24": { + "1": { + "get_star_ts": 1671862248, + "star_index": 3714036 + }, + "2": { + "star_index": 3714253, + "get_star_ts": 1671862502 + } + }, + "25": { + "1": { + "get_star_ts": 1671945532, + "star_index": 3755729 + } + } + }, + "last_star_ts": 1671952331, + "name": "b-paul", + "id": 1089509, + "stars": 43, + "global_score": 0 + }, + "1098155": { + "global_score": 0, + "id": 1098155, + "stars": 31, + "last_star_ts": 1671207144, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669881251, + "star_index": 46997 + }, + "2": { + "get_star_ts": 1669881566, + "star_index": 48172 + } + }, + "2": { + "1": { + "get_star_ts": 1669959000, + "star_index": 287853 + }, + "2": { + "star_index": 291345, + "get_star_ts": 1669959385 + } + }, + "3": { + "1": { + "star_index": 587849, + "get_star_ts": 1670044733 + }, + "2": { + "get_star_ts": 1670045494, + "star_index": 595639 + } + }, + "4": { + "1": { + "star_index": 858181, + "get_star_ts": 1670130848 + }, + "2": { + "get_star_ts": 1670131036, + "star_index": 860931 + } + }, + "5": { + "1": { + "star_index": 1168085, + "get_star_ts": 1670221311 + }, + "2": { + "get_star_ts": 1670221603, + "star_index": 1169389 + } + }, + "6": { + "1": { + "star_index": 1468746, + "get_star_ts": 1670304917 + }, + "2": { + "get_star_ts": 1670305087, + "star_index": 1470068 + } + }, + "7": { + "1": { + "get_star_ts": 1670424333, + "star_index": 1842681 + }, + "2": { + "get_star_ts": 1670425498, + "star_index": 1846172 + } + }, + "8": { + "1": { + "star_index": 2053208, + "get_star_ts": 1670504662 + }, + "2": { + "star_index": 2073341, + "get_star_ts": 1670511823 + } + }, + "9": { + "1": { + "star_index": 2187047, + "get_star_ts": 1670565621 + }, + "2": { + "get_star_ts": 1670566921, + "star_index": 2191602 + } + }, + "10": { + "1": { + "star_index": 2373771, + "get_star_ts": 1670653654 + }, + "2": { + "get_star_ts": 1670654887, + "star_index": 2376612 + } + }, + "11": { + "1": { + "star_index": 2530557, + "get_star_ts": 1670736444 + }, + "2": { + "star_index": 2533151, + "get_star_ts": 1670737062 + } + }, + "12": { + "1": { + "star_index": 2700448, + "get_star_ts": 1670823995 + }, + "2": { + "star_index": 2705899, + "get_star_ts": 1670826135 + } + }, + "13": { + "1": { + "get_star_ts": 1670912819, + "star_index": 2851946 + }, + "2": { + "star_index": 2855708, + "get_star_ts": 1670914812 + } + }, + "14": { + "1": { + "star_index": 3104203, + "get_star_ts": 1671093682 + }, + "2": { + "get_star_ts": 1671098157, + "star_index": 3110157 + } + }, + "15": { + "1": { + "get_star_ts": 1671124314, + "star_index": 3143896 + }, + "2": { + "star_index": 3191627, + "get_star_ts": 1671175958 + } + }, + "16": { + "1": { + "star_index": 3220183, + "get_star_ts": 1671207144 + } + } + }, + "local_score": 2430, + "name": "Luna" + }, + "1302538": { + "global_score": 0, + "stars": 0, + "id": 1302538, + "name": "Strayy", + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0 + }, + "1468791": { + "name": "fulminatingmoat", + "local_score": 1161, + "completion_day_level": { + "1": { + "1": { + "star_index": 33037, + "get_star_ts": 1669876972 + }, + "2": { + "star_index": 33334, + "get_star_ts": 1669877070 + } + }, + "2": { + "1": { + "get_star_ts": 1669990133, + "star_index": 439084 + }, + "2": { + "star_index": 442508, + "get_star_ts": 1669990927 + } + }, + "3": { + "1": { + "star_index": 642805, + "get_star_ts": 1670057567 + }, + "2": { + "star_index": 644854, + "get_star_ts": 1670058098 + } + }, + "4": { + "1": { + "star_index": 1040656, + "get_star_ts": 1670173461 + }, + "2": { + "star_index": 1041781, + "get_star_ts": 1670173789 + } + }, + "5": { + "1": { + "get_star_ts": 1670252025, + "star_index": 1294095 + }, + "2": { + "get_star_ts": 1670252336, + "star_index": 1295358 + } + }, + "6": { + "1": { + "star_index": 1548615, + "get_star_ts": 1670320392 + }, + "2": { + "get_star_ts": 1670320423, + "star_index": 1548735 + } + }, + "7": { + "1": { + "get_star_ts": 1670396013, + "star_index": 1762420 + }, + "2": { + "get_star_ts": 1670396382, + "star_index": 1763560 + } + }, + "8": { + "1": { + "star_index": 1976475, + "get_star_ts": 1670479395 + }, + "2": { + "get_star_ts": 1670481243, + "star_index": 1983819 + } + }, + "9": { + "1": { + "star_index": 2278098, + "get_star_ts": 1670601579 + } + } + }, + "last_star_ts": 1670601579, + "stars": 17, + "id": 1468791, + "global_score": 0 + }, + "1507456": { + "last_star_ts": 1671950229, + "completion_day_level": { + "1": { + "1": { + "star_index": 75378, + "get_star_ts": 1669888161 + }, + "2": { + "star_index": 76230, + "get_star_ts": 1669888363 + } + }, + "2": { + "1": { + "star_index": 268995, + "get_star_ts": 1669957647 + }, + "2": { + "star_index": 274237, + "get_star_ts": 1669957993 + } + }, + "3": { + "1": { + "get_star_ts": 1670044407, + "star_index": 583589 + }, + "2": { + "star_index": 587207, + "get_star_ts": 1670044679 + } + }, + "4": { + "1": { + "get_star_ts": 1670130648, + "star_index": 854838 + }, + "2": { + "star_index": 863685, + "get_star_ts": 1670131258 + } + }, + "5": { + "1": { + "star_index": 1144826, + "get_star_ts": 1670217832 + }, + "2": { + "star_index": 1146691, + "get_star_ts": 1670218025 + } + }, + "6": { + "1": { + "get_star_ts": 1670303375, + "star_index": 1448077 + }, + "2": { + "star_index": 1449052, + "get_star_ts": 1670303415 + } + }, + "7": { + "1": { + "get_star_ts": 1670391921, + "star_index": 1746257 + }, + "2": { + "star_index": 1747796, + "get_star_ts": 1670392230 + } + }, + "8": { + "1": { + "get_star_ts": 1670477344, + "star_index": 1964747 + }, + "2": { + "star_index": 1979920, + "get_star_ts": 1670480189 + } + }, + "9": { + "1": { + "get_star_ts": 1670563254, + "star_index": 2175541 + }, + "2": { + "get_star_ts": 1670564513, + "star_index": 2182316 + } + }, + "10": { + "1": { + "get_star_ts": 1670649078, + "star_index": 2352527 + }, + "2": { + "star_index": 2355668, + "get_star_ts": 1670649536 + } + }, + "11": { + "1": { + "get_star_ts": 1670737152, + "star_index": 2533527 + }, + "2": { + "get_star_ts": 1670739088, + "star_index": 2540345 + } + }, + "12": { + "1": { + "star_index": 2700994, + "get_star_ts": 1670824169 + }, + "2": { + "get_star_ts": 1670824460, + "star_index": 2701853 + } + }, + "13": { + "1": { + "get_star_ts": 1670911065, + "star_index": 2847826 + }, + "2": { + "get_star_ts": 1670911686, + "star_index": 2849371 + } + }, + "14": { + "1": { + "get_star_ts": 1670996830, + "star_index": 2973047 + }, + "2": { + "star_index": 2975144, + "get_star_ts": 1670997498 + } + }, + "15": { + "1": { + "get_star_ts": 1671083937, + "star_index": 3089986 + }, + "2": { + "get_star_ts": 1671090138, + "star_index": 3099662 + } + }, + "16": { + "1": { + "star_index": 3188566, + "get_star_ts": 1671172212 + }, + "2": { + "star_index": 3192601, + "get_star_ts": 1671177201 + } + }, + "17": { + "1": { + "get_star_ts": 1671258082, + "star_index": 3260589 + }, + "2": { + "star_index": 3264188, + "get_star_ts": 1671262541 + } + }, + "18": { + "1": { + "star_index": 3329226, + "get_star_ts": 1671341429 + }, + "2": { + "get_star_ts": 1671343145, + "star_index": 3331829 + } + }, + "19": { + "1": { + "get_star_ts": 1671593012, + "star_index": 3526162 + }, + "2": { + "star_index": 3526268, + "get_star_ts": 1671593328 + } + }, + "20": { + "1": { + "get_star_ts": 1671526671, + "star_index": 3475324 + }, + "2": { + "get_star_ts": 1671526975, + "star_index": 3475545 + } + }, + "21": { + "1": { + "star_index": 3532303, + "get_star_ts": 1671600703 + }, + "2": { + "star_index": 3536000, + "get_star_ts": 1671603249 + } + }, + "22": { + "1": { + "get_star_ts": 1671688420, + "star_index": 3600451 + }, + "2": { + "get_star_ts": 1671711470, + "star_index": 3616532 + } + }, + "23": { + "1": { + "star_index": 3671356, + "get_star_ts": 1671793425 + }, + "2": { + "star_index": 3671616, + "get_star_ts": 1671793767 + } + }, + "24": { + "1": { + "get_star_ts": 1671867924, + "star_index": 3717937 + }, + "2": { + "get_star_ts": 1671868869, + "star_index": 3718459 + } + }, + "25": { + "1": { + "get_star_ts": 1671950181, + "star_index": 3761294 + }, + "2": { + "star_index": 3761333, + "get_star_ts": 1671950229 + } + } + }, + "local_score": 4370, + "name": "latewend", + "id": 1507456, + "stars": 50, + "global_score": 0 + }, + "1508233": { + "global_score": 0, + "stars": 0, + "id": 1508233, + "name": "Zernoxi", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0 + }, + "1510484": { + "global_score": 0, + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "name": "james-seymour", + "id": 1510484, + "stars": 0 + }, + "1528744": { + "global_score": 0, + "name": "Linden Wells", + "last_star_ts": 1671349061, + "completion_day_level": { + "1": { + "1": { + "star_index": 23272, + "get_star_ts": 1669873825 + }, + "2": { + "get_star_ts": 1669873949, + "star_index": 23727 + } + }, + "2": { + "1": { + "star_index": 315868, + "get_star_ts": 1669964162 + }, + "2": { + "get_star_ts": 1669964995, + "star_index": 319500 + } + }, + "8": { + "1": { + "get_star_ts": 1670477823, + "star_index": 1967930 + }, + "2": { + "get_star_ts": 1670643209, + "star_index": 2346974 + } + }, + "9": { + "1": { + "star_index": 2518394, + "get_star_ts": 1670721977 + }, + "2": { + "star_index": 2523881, + "get_star_ts": 1670728977 + } + }, + "13": { + "1": { + "star_index": 2963273, + "get_star_ts": 1670990107 + }, + "2": { + "star_index": 2965516, + "get_star_ts": 1670993752 + } + }, + "18": { + "1": { + "star_index": 3337150, + "get_star_ts": 1671349061 + } + } + }, + "local_score": 770, + "stars": 11, + "id": 1528744 + }, + "1530990": { + "completion_day_level": { + "1": { + "1": { + "star_index": 13725, + "get_star_ts": 1669871859 + }, + "2": { + "get_star_ts": 1669873103, + "star_index": 20560 + } + }, + "2": { + "1": { + "get_star_ts": 1669960661, + "star_index": 299832 + }, + "2": { + "star_index": 319895, + "get_star_ts": 1669965085 + } + }, + "3": { + "1": { + "get_star_ts": 1670046605, + "star_index": 603273 + }, + "2": { + "get_star_ts": 1670048281, + "star_index": 610756 + } + }, + "4": { + "1": { + "star_index": 863302, + "get_star_ts": 1670131224 + }, + "2": { + "star_index": 870375, + "get_star_ts": 1670131992 + } + }, + "5": { + "1": { + "star_index": 1174159, + "get_star_ts": 1670222764 + }, + "2": { + "star_index": 1199572, + "get_star_ts": 1670229134 + } + }, + "6": { + "1": { + "get_star_ts": 1670388361, + "star_index": 1736325 + }, + "2": { + "star_index": 1736862, + "get_star_ts": 1670388922 + } + } + }, + "local_score": 841, + "last_star_ts": 1670388922, + "name": "ThatSealgair", + "id": 1530990, + "stars": 12, + "global_score": 0 + }, + "1531333": { + "global_score": 0, + "id": 1531333, + "stars": 0, + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "name": "Paul Clarke" + }, + "1533251": { + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "name": "Nicole27597", + "id": 1533251, + "stars": 0, + "global_score": 0 + }, + "1533800": { + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "name": "Cameron Badman", + "id": 1533800, + "stars": 0, + "global_score": 0 + }, + "1544827": { + "name": "courtneyzhan", + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669886783, + "star_index": 69553 + }, + "2": { + "star_index": 69670, + "get_star_ts": 1669886811 + } + }, + "2": { + "1": { + "get_star_ts": 1669962135, + "star_index": 306864 + }, + "2": { + "star_index": 312093, + "get_star_ts": 1669963315 + } + }, + "3": { + "1": { + "get_star_ts": 1672021783, + "star_index": 3799443 + }, + "2": { + "get_star_ts": 1672022605, + "star_index": 3799633 + } + }, + "4": { + "1": { + "star_index": 3798462, + "get_star_ts": 1672018110 + }, + "2": { + "star_index": 3799038, + "get_star_ts": 1672020289 + } + }, + "6": { + "1": { + "star_index": 3799707, + "get_star_ts": 1672022971 + }, + "2": { + "get_star_ts": 1672023024, + "star_index": 3799721 + } + } + }, + "local_score": 574, + "last_star_ts": 1672023024, + "stars": 10, + "id": 1544827, + "global_score": 0 + }, + "1545148": { + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669875160, + "star_index": 27546 + }, + "2": { + "get_star_ts": 1669875341, + "star_index": 28086 + } + }, + "2": { + "1": { + "get_star_ts": 1669959371, + "star_index": 291220 + }, + "2": { + "get_star_ts": 1669959728, + "star_index": 293989 + } + }, + "3": { + "1": { + "star_index": 582196, + "get_star_ts": 1670044302 + }, + "2": { + "get_star_ts": 1670044598, + "star_index": 586184 + } + }, + "4": { + "1": { + "star_index": 852838, + "get_star_ts": 1670130539 + }, + "2": { + "star_index": 854842, + "get_star_ts": 1670130648 + } + }, + "5": { + "1": { + "get_star_ts": 1670218259, + "star_index": 1148744 + }, + "2": { + "star_index": 1148862, + "get_star_ts": 1670218273 + } + }, + "6": { + "1": { + "star_index": 1480207, + "get_star_ts": 1670306882 + }, + "2": { + "star_index": 1480563, + "get_star_ts": 1670306953 + } + }, + "7": { + "1": { + "star_index": 1747223, + "get_star_ts": 1670392114 + }, + "2": { + "star_index": 1748685, + "get_star_ts": 1670392415 + } + }, + "8": { + "1": { + "star_index": 1959876, + "get_star_ts": 1670476637 + }, + "2": { + "star_index": 1966374, + "get_star_ts": 1670477585 + } + }, + "9": { + "1": { + "star_index": 2174131, + "get_star_ts": 1670562975 + }, + "2": { + "get_star_ts": 1670563223, + "star_index": 2175358 + } + }, + "10": { + "1": { + "star_index": 2350976, + "get_star_ts": 1670648717 + }, + "2": { + "star_index": 2352196, + "get_star_ts": 1670649022 + } + }, + "11": { + "1": { + "get_star_ts": 1670737175, + "star_index": 2533630 + }, + "2": { + "star_index": 2535148, + "get_star_ts": 1670737559 + } + }, + "12": { + "1": { + "star_index": 2755035, + "get_star_ts": 1670852993 + }, + "2": { + "get_star_ts": 1670852999, + "star_index": 2755048 + } + }, + "13": { + "1": { + "star_index": 2841520, + "get_star_ts": 1670908996 + }, + "2": { + "get_star_ts": 1670909660, + "star_index": 2843692 + } + }, + "14": { + "1": { + "get_star_ts": 1670995861, + "star_index": 2969718 + }, + "2": { + "get_star_ts": 1670998798, + "star_index": 2978441 + } + }, + "15": { + "1": { + "get_star_ts": 1671101276, + "star_index": 3114144 + }, + "2": { + "get_star_ts": 1671103716, + "star_index": 3117260 + } + }, + "16": { + "1": { + "get_star_ts": 1671250250, + "star_index": 3255160 + }, + "2": { + "star_index": 3255924, + "get_star_ts": 1671252129 + } + }, + "18": { + "1": { + "star_index": 3326775, + "get_star_ts": 1671340427 + }, + "2": { + "get_star_ts": 1671341531, + "star_index": 3329434 + } + } + }, + "last_star_ts": 1671341531, + "local_score": 2898, + "name": "tomstephen", + "id": 1545148, + "stars": 34, + "global_score": 51 + }, + "1570615": { + "global_score": 0, + "stars": 3, + "id": 1570615, + "name": "dcpais", + "completion_day_level": { + "1": { + "1": { + "star_index": 3268530, + "get_star_ts": 1671268783 + }, + "2": { + "star_index": 3269931, + "get_star_ts": 1671270682 + } + }, + "2": { + "1": { + "get_star_ts": 1671273887, + "star_index": 3272517 + } + } + }, + "last_star_ts": 1671273887, + "local_score": 116 + }, + "1604451": { + "stars": 0, + "id": 1604451, + "name": "mamatmania-iidx", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "global_score": 0 + }, + "1754593": { + "global_score": 0, + "id": 1754593, + "stars": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "local_score": 0, + "name": "Ravsterv" + }, + "1796011": { + "global_score": 0, + "id": 1796011, + "stars": 0, + "completion_day_level": {}, + "local_score": 0, + "last_star_ts": 0, + "name": "Lewis Luck" + }, + "1803780": { + "stars": 0, + "id": 1803780, + "name": "h4sh5", + "local_score": 0, + "completion_day_level": {}, + "last_star_ts": 0, + "global_score": 0 + }, + "1839414": { + "global_score": 0, + "stars": 0, + "id": 1839414, + "name": "Thomas Malcolm", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0 + }, + "1842429": { + "id": 1842429, + "stars": 0, + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "name": "Riley Bowyer", + "global_score": 0 + }, + "1868482": { + "name": "JamieKats", + "last_star_ts": 1670029210, + "completion_day_level": { + "1": { + "1": { + "star_index": 84431, + "get_star_ts": 1669890457 + }, + "2": { + "star_index": 87026, + "get_star_ts": 1669891143 + } + }, + "2": { + "1": { + "get_star_ts": 1670027351, + "star_index": 559706 + }, + "2": { + "star_index": 562169, + "get_star_ts": 1670029210 + } + } + }, + "local_score": 221, + "stars": 4, + "id": 1868482, + "global_score": 0 + }, + "1904138": { + "global_score": 0, + "stars": 0, + "id": 1904138, + "name": "Anwealso", + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0 + }, + "1962133": { + "global_score": 0, + "id": 1962133, + "stars": 32, + "last_star_ts": 1671183403, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871067, + "star_index": 3084 + }, + "2": { + "star_index": 8061, + "get_star_ts": 1669871342 + } + }, + "2": { + "1": { + "get_star_ts": 1669957837, + "star_index": 271671 + }, + "2": { + "star_index": 279139, + "get_star_ts": 1669958299 + } + }, + "3": { + "1": { + "get_star_ts": 1670044696, + "star_index": 587432 + }, + "2": { + "star_index": 589804, + "get_star_ts": 1670044900 + } + }, + "4": { + "1": { + "star_index": 853113, + "get_star_ts": 1670130554 + }, + "2": { + "get_star_ts": 1670130630, + "star_index": 854504 + } + }, + "5": { + "1": { + "get_star_ts": 1670219081, + "star_index": 1155270 + }, + "2": { + "get_star_ts": 1670219165, + "star_index": 1155894 + } + }, + "6": { + "1": { + "star_index": 1443679, + "get_star_ts": 1670303205 + }, + "2": { + "star_index": 1447585, + "get_star_ts": 1670303355 + } + }, + "7": { + "1": { + "get_star_ts": 1670394787, + "star_index": 1758335 + }, + "2": { + "star_index": 1761270, + "get_star_ts": 1670395656 + } + }, + "8": { + "1": { + "star_index": 1958008, + "get_star_ts": 1670476317 + }, + "2": { + "star_index": 1964367, + "get_star_ts": 1670477287 + } + }, + "9": { + "1": { + "star_index": 2175994, + "get_star_ts": 1670563334 + }, + "2": { + "star_index": 2180208, + "get_star_ts": 1670564097 + } + }, + "10": { + "1": { + "star_index": 2357730, + "get_star_ts": 1670649821 + }, + "2": { + "get_star_ts": 1670651792, + "star_index": 2368131 + } + }, + "11": { + "1": { + "star_index": 2529978, + "get_star_ts": 1670736270 + }, + "2": { + "get_star_ts": 1670738023, + "star_index": 2536884 + } + }, + "12": { + "1": { + "star_index": 2701185, + "get_star_ts": 1670824237 + }, + "2": { + "get_star_ts": 1670824497, + "star_index": 2701957 + } + }, + "13": { + "1": { + "get_star_ts": 1670914822, + "star_index": 2855720 + }, + "2": { + "get_star_ts": 1670915378, + "star_index": 2856677 + } + }, + "14": { + "1": { + "star_index": 2978123, + "get_star_ts": 1670998657 + }, + "2": { + "star_index": 2979215, + "get_star_ts": 1670999153 + } + }, + "15": { + "1": { + "star_index": 3087676, + "get_star_ts": 1671083035 + }, + "2": { + "get_star_ts": 1671084097, + "star_index": 3090302 + } + }, + "16": { + "1": { + "star_index": 3189579, + "get_star_ts": 1671173388 + }, + "2": { + "star_index": 3197694, + "get_star_ts": 1671183403 + } + } + }, + "local_score": 2805, + "name": "LimaoC" + }, + "1971747": { + "id": 1971747, + "stars": 4, + "last_star_ts": 1669965547, + "completion_day_level": { + "1": { + "1": { + "star_index": 20578, + "get_star_ts": 1669873108 + }, + "2": { + "star_index": 21257, + "get_star_ts": 1669873273 + } + }, + "2": { + "1": { + "get_star_ts": 1669964174, + "star_index": 315913 + }, + "2": { + "star_index": 322041, + "get_star_ts": 1669965547 + } + } + }, + "local_score": 274, + "name": "the_batfish", + "global_score": 0 + }, + "2068870": { + "global_score": 0, + "id": 2068870, + "stars": 0, + "last_star_ts": 0, + "completion_day_level": {}, + "local_score": 0, + "name": "Campbell McFadden" + }, + "2214964": { + "global_score": 0, + "stars": 0, + "id": 2214964, + "name": "Iain Jensen", + "local_score": 0, + "completion_day_level": {}, + "last_star_ts": 0 + }, + "2247216": { + "name": "Quinn Horton", + "local_score": 4242, + "completion_day_level": { + "1": { + "1": { + "star_index": 15303, + "get_star_ts": 1669872079 + }, + "2": { + "star_index": 16992, + "get_star_ts": 1669872356 + } + }, + "2": { + "1": { + "get_star_ts": 1669958916, + "star_index": 286995 + }, + "2": { + "get_star_ts": 1669959382, + "star_index": 291325 + } + }, + "3": { + "1": { + "get_star_ts": 1670044316, + "star_index": 582375 + }, + "2": { + "star_index": 589962, + "get_star_ts": 1670044914 + } + }, + "4": { + "1": { + "star_index": 857606, + "get_star_ts": 1670130812 + }, + "2": { + "get_star_ts": 1670131033, + "star_index": 860890 + } + }, + "5": { + "1": { + "star_index": 1154077, + "get_star_ts": 1670218919 + }, + "2": { + "get_star_ts": 1670219274, + "star_index": 1156682 + } + }, + "6": { + "1": { + "get_star_ts": 1670304029, + "star_index": 1459921 + }, + "2": { + "get_star_ts": 1670304136, + "star_index": 1461336 + } + }, + "7": { + "1": { + "star_index": 1766702, + "get_star_ts": 1670397432 + }, + "2": { + "get_star_ts": 1670398444, + "star_index": 1769628 + } + }, + "8": { + "1": { + "star_index": 1962292, + "get_star_ts": 1670476990 + }, + "2": { + "get_star_ts": 1670478185, + "star_index": 1970108 + } + }, + "9": { + "1": { + "get_star_ts": 1670563992, + "star_index": 2179658 + }, + "2": { + "star_index": 2189063, + "get_star_ts": 1670566162 + } + }, + "10": { + "1": { + "get_star_ts": 1670649912, + "star_index": 2358353 + }, + "2": { + "get_star_ts": 1670651037, + "star_index": 2364919 + } + }, + "11": { + "1": { + "get_star_ts": 1670740992, + "star_index": 2545219 + }, + "2": { + "star_index": 2545947, + "get_star_ts": 1670741319 + } + }, + "12": { + "1": { + "get_star_ts": 1670858910, + "star_index": 2766577 + }, + "2": { + "star_index": 2768503, + "get_star_ts": 1670859889 + } + }, + "13": { + "1": { + "star_index": 2854056, + "get_star_ts": 1670913857 + }, + "2": { + "star_index": 2855282, + "get_star_ts": 1670914562 + } + }, + "14": { + "1": { + "get_star_ts": 1670999726, + "star_index": 2980381 + }, + "2": { + "get_star_ts": 1671000191, + "star_index": 2981221 + } + }, + "15": { + "1": { + "get_star_ts": 1671103549, + "star_index": 3117035 + }, + "2": { + "star_index": 3124531, + "get_star_ts": 1671109828 + } + }, + "16": { + "1": { + "star_index": 3204336, + "get_star_ts": 1671191264 + }, + "2": { + "star_index": 3206726, + "get_star_ts": 1671194139 + } + }, + "17": { + "1": { + "get_star_ts": 1671264208, + "star_index": 3265338 + }, + "2": { + "star_index": 3279975, + "get_star_ts": 1671282683 + } + }, + "18": { + "1": { + "star_index": 3326007, + "get_star_ts": 1671340174 + }, + "2": { + "get_star_ts": 1671345438, + "star_index": 3334080 + } + }, + "19": { + "1": { + "star_index": 3457349, + "get_star_ts": 1671499906 + }, + "2": { + "get_star_ts": 1671500956, + "star_index": 3457777 + } + }, + "20": { + "1": { + "star_index": 3470955, + "get_star_ts": 1671521148 + }, + "2": { + "get_star_ts": 1671524589, + "star_index": 3473649 + } + }, + "21": { + "1": { + "get_star_ts": 1671600215, + "star_index": 3531246 + }, + "2": { + "get_star_ts": 1671605128, + "star_index": 3537983 + } + }, + "22": { + "1": { + "star_index": 3600583, + "get_star_ts": 1671688541 + }, + "2": { + "star_index": 3610126, + "get_star_ts": 1671702037 + } + }, + "23": { + "1": { + "star_index": 3707600, + "get_star_ts": 1671846234 + }, + "2": { + "star_index": 3708049, + "get_star_ts": 1671847677 + } + }, + "24": { + "1": { + "star_index": 3765822, + "get_star_ts": 1671957908 + }, + "2": { + "star_index": 3765958, + "get_star_ts": 1671958155 + } + }, + "25": { + "1": { + "star_index": 3778430, + "get_star_ts": 1671979463 + }, + "2": { + "get_star_ts": 1671979540, + "star_index": 3778470 + } + } + }, + "last_star_ts": 1671979540, + "stars": 50, + "id": 2247216, + "global_score": 0 + }, + "2273060": { + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669907400, + "star_index": 143091 + }, + "2": { + "get_star_ts": 1669907555, + "star_index": 143684 + } + }, + "2": { + "1": { + "get_star_ts": 1669997261, + "star_index": 470624 + }, + "2": { + "star_index": 476420, + "get_star_ts": 1669998674 + } + }, + "3": { + "1": { + "star_index": 633363, + "get_star_ts": 1670055016 + }, + "2": { + "get_star_ts": 1670057805, + "star_index": 643702 + } + }, + "4": { + "1": { + "get_star_ts": 1670131446, + "star_index": 865685 + }, + "2": { + "get_star_ts": 1670131849, + "star_index": 869289 + } + }, + "5": { + "1": { + "get_star_ts": 1670221226, + "star_index": 1167662 + }, + "2": { + "star_index": 1168125, + "get_star_ts": 1670221320 + } + }, + "6": { + "1": { + "star_index": 1465294, + "get_star_ts": 1670304502 + }, + "2": { + "get_star_ts": 1670305163, + "star_index": 1470606 + } + }, + "7": { + "1": { + "star_index": 2830163, + "get_star_ts": 1670896198 + }, + "2": { + "star_index": 2832877, + "get_star_ts": 1670899726 + } + }, + "8": { + "1": { + "get_star_ts": 1670482370, + "star_index": 1987584 + }, + "2": { + "get_star_ts": 1670553789, + "star_index": 2164723 + } + }, + "9": { + "1": { + "star_index": 2194585, + "get_star_ts": 1670567963 + }, + "2": { + "star_index": 2199388, + "get_star_ts": 1670569804 + } + }, + "10": { + "1": { + "star_index": 2370632, + "get_star_ts": 1670652540 + }, + "2": { + "star_index": 2379070, + "get_star_ts": 1670656050 + } + }, + "11": { + "1": { + "get_star_ts": 1670923940, + "star_index": 2871019 + }, + "2": { + "get_star_ts": 1670924845, + "star_index": 2872581 + } + }, + "12": { + "1": { + "star_index": 3250152, + "get_star_ts": 1671238759 + }, + "2": { + "star_index": 3319884, + "get_star_ts": 1671327743 + } + }, + "13": { + "1": { + "get_star_ts": 1670932229, + "star_index": 2884097 + }, + "2": { + "get_star_ts": 1670933904, + "star_index": 2886563 + } + }, + "18": { + "1": { + "get_star_ts": 1671345797, + "star_index": 3334387 + }, + "2": { + "get_star_ts": 1671411793, + "star_index": 3395758 + } + }, + "19": { + "1": { + "get_star_ts": 1671675556, + "star_index": 3595278 + }, + "2": { + "get_star_ts": 1671676353, + "star_index": 3595519 + } + }, + "21": { + "1": { + "get_star_ts": 1671604230, + "star_index": 3537095 + }, + "2": { + "get_star_ts": 1671611573, + "star_index": 3544089 + } + } + }, + "last_star_ts": 1671676353, + "local_score": 2335, + "name": "Yiwen Jiang", + "id": 2273060, + "stars": 32, + "global_score": 0 + }, + "2273770": { + "global_score": 0, + "name": "Ganesh S", + "last_star_ts": 1669894363, + "completion_day_level": { + "1": { + "1": { + "star_index": 92472, + "get_star_ts": 1669892663 + }, + "2": { + "star_index": 98043, + "get_star_ts": 1669894363 + } + } + }, + "local_score": 108, + "stars": 2, + "id": 2273770 + }, + "2285288": { + "global_score": 0, + "last_star_ts": 1670772955, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871789, + "star_index": 13133 + }, + "2": { + "star_index": 15954, + "get_star_ts": 1669872178 + } + }, + "2": { + "1": { + "star_index": 276225, + "get_star_ts": 1669958113 + }, + "2": { + "star_index": 294980, + "get_star_ts": 1669959863 + } + }, + "3": { + "1": { + "star_index": 589019, + "get_star_ts": 1670044830 + }, + "2": { + "star_index": 600221, + "get_star_ts": 1670046095 + } + }, + "4": { + "1": { + "get_star_ts": 1670131462, + "star_index": 865852 + }, + "2": { + "get_star_ts": 1670131502, + "star_index": 866259 + } + }, + "5": { + "1": { + "get_star_ts": 1670220598, + "star_index": 1164451 + }, + "2": { + "star_index": 1166281, + "get_star_ts": 1670220956 + } + }, + "6": { + "1": { + "star_index": 1455198, + "get_star_ts": 1670303713 + }, + "2": { + "star_index": 1458368, + "get_star_ts": 1670303917 + } + }, + "7": { + "1": { + "star_index": 2051616, + "get_star_ts": 1670504078 + }, + "2": { + "get_star_ts": 1670505050, + "star_index": 2054255 + } + }, + "8": { + "1": { + "star_index": 2065272, + "get_star_ts": 1670509009 + }, + "2": { + "star_index": 2071623, + "get_star_ts": 1670511207 + } + }, + "9": { + "1": { + "get_star_ts": 1670564032, + "star_index": 2179863 + }, + "2": { + "star_index": 2344435, + "get_star_ts": 1670639747 + } + }, + "10": { + "1": { + "get_star_ts": 1670766935, + "star_index": 2598745 + }, + "2": { + "star_index": 2612636, + "get_star_ts": 1670772955 + } + } + }, + "local_score": 1516, + "name": "Ryan McNeilly", + "id": 2285288, + "stars": 20 + }, + "2300395": { + "id": 2300395, + "stars": 12, + "completion_day_level": { + "1": { + "1": { + "star_index": 115488, + "get_star_ts": 1669899769 + }, + "2": { + "star_index": 116619, + "get_star_ts": 1669900110 + } + }, + "2": { + "1": { + "get_star_ts": 1669960543, + "star_index": 299164 + }, + "2": { + "star_index": 302059, + "get_star_ts": 1669961090 + } + }, + "3": { + "1": { + "star_index": 603091, + "get_star_ts": 1670046570 + }, + "2": { + "star_index": 614472, + "get_star_ts": 1670049287 + } + }, + "4": { + "1": { + "get_star_ts": 1670132917, + "star_index": 876144 + }, + "2": { + "get_star_ts": 1670133057, + "star_index": 876920 + } + }, + "5": { + "1": { + "star_index": 1159618, + "get_star_ts": 1670219729 + }, + "2": { + "get_star_ts": 1670220088, + "star_index": 1161689 + } + }, + "6": { + "1": { + "get_star_ts": 1670313003, + "star_index": 1510270 + }, + "2": { + "get_star_ts": 1670316592, + "star_index": 1529711 + } + } + }, + "local_score": 826, + "last_star_ts": 1670316592, + "name": "Hasakev", + "global_score": 0 + }, + "2300436": { + "global_score": 0, + "name": "KingJulienXIV", + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871289, + "star_index": 7294 + }, + "2": { + "get_star_ts": 1669873333, + "star_index": 21492 + } + }, + "2": { + "1": { + "get_star_ts": 1669972208, + "star_index": 358486 + }, + "2": { + "get_star_ts": 1669972566, + "star_index": 360508 + } + }, + "3": { + "1": { + "get_star_ts": 1670045707, + "star_index": 597403 + }, + "2": { + "get_star_ts": 1670046896, + "star_index": 604806 + } + }, + "4": { + "1": { + "get_star_ts": 1670130636, + "star_index": 854611 + }, + "2": { + "star_index": 864797, + "get_star_ts": 1670131359 + } + }, + "5": { + "1": { + "star_index": 1166088, + "get_star_ts": 1670220918 + }, + "2": { + "get_star_ts": 1670221375, + "star_index": 1168375 + } + }, + "6": { + "1": { + "star_index": 1452056, + "get_star_ts": 1670303549 + }, + "2": { + "get_star_ts": 1670305109, + "star_index": 1470233 + } + }, + "7": { + "1": { + "get_star_ts": 1670454628, + "star_index": 1928980 + }, + "2": { + "get_star_ts": 1670455515, + "star_index": 1930725 + } + }, + "8": { + "1": { + "star_index": 2144193, + "get_star_ts": 1670538444 + }, + "2": { + "get_star_ts": 1670541089, + "star_index": 2149631 + } + }, + "9": { + "1": { + "get_star_ts": 1670565969, + "star_index": 2188383 + }, + "2": { + "star_index": 2333618, + "get_star_ts": 1670629223 + } + }, + "10": { + "1": { + "get_star_ts": 1670800651, + "star_index": 2673647 + }, + "2": { + "get_star_ts": 1670803593, + "star_index": 2677563 + } + }, + "11": { + "1": { + "get_star_ts": 1670899341, + "star_index": 2832572 + }, + "2": { + "get_star_ts": 1670899975, + "star_index": 2833071 + } + } + }, + "local_score": 1608, + "last_star_ts": 1670899975, + "stars": 22, + "id": 2300436 + }, + "2315131": { + "global_score": 0, + "name": "Yutong Ji", + "completion_day_level": { + "1": { + "1": { + "star_index": 36011, + "get_star_ts": 1669877938 + }, + "2": { + "star_index": 36556, + "get_star_ts": 1669878103 + } + }, + "2": { + "1": { + "get_star_ts": 1669958628, + "star_index": 283684 + }, + "2": { + "star_index": 290874, + "get_star_ts": 1669959329 + } + }, + "3": { + "1": { + "get_star_ts": 1670045090, + "star_index": 591838 + }, + "2": { + "star_index": 596041, + "get_star_ts": 1670045540 + } + }, + "4": { + "1": { + "star_index": 855406, + "get_star_ts": 1670130678 + }, + "2": { + "get_star_ts": 1670130868, + "star_index": 858506 + } + }, + "5": { + "1": { + "get_star_ts": 1670218074, + "star_index": 1147129 + }, + "2": { + "star_index": 1151606, + "get_star_ts": 1670218598 + } + }, + "6": { + "1": { + "star_index": 1444488, + "get_star_ts": 1670303234 + }, + "2": { + "get_star_ts": 1670304077, + "star_index": 1460540 + } + }, + "7": { + "1": { + "get_star_ts": 1670398778, + "star_index": 1770540 + }, + "2": { + "get_star_ts": 1670399415, + "star_index": 1772377 + } + }, + "8": { + "1": { + "star_index": 1982328, + "get_star_ts": 1670480827 + }, + "2": { + "star_index": 1986902, + "get_star_ts": 1670482148 + } + }, + "9": { + "1": { + "get_star_ts": 1670565332, + "star_index": 2185945 + }, + "2": { + "get_star_ts": 1670571244, + "star_index": 2202895 + } + }, + "10": { + "1": { + "get_star_ts": 1670652077, + "star_index": 2369164 + }, + "2": { + "star_index": 2377969, + "get_star_ts": 1670655516 + } + }, + "11": { + "1": { + "star_index": 2533445, + "get_star_ts": 1670737130 + }, + "2": { + "star_index": 2572577, + "get_star_ts": 1670755407 + } + }, + "12": { + "1": { + "star_index": 3083201, + "get_star_ts": 1671080107 + }, + "2": { + "get_star_ts": 1671176218, + "star_index": 3191831 + } + }, + "13": { + "1": { + "star_index": 2848643, + "get_star_ts": 1670911397 + }, + "2": { + "get_star_ts": 1670915290, + "star_index": 2856520 + } + }, + "14": { + "1": { + "get_star_ts": 1670996824, + "star_index": 2973026 + }, + "2": { + "get_star_ts": 1670998122, + "star_index": 2976774 + } + }, + "15": { + "1": { + "get_star_ts": 1671082787, + "star_index": 3087016 + }, + "2": { + "get_star_ts": 1671084366, + "star_index": 3090836 + } + }, + "16": { + "1": { + "star_index": 3256130, + "get_star_ts": 1671252635 + } + }, + "17": { + "1": { + "get_star_ts": 1671328709, + "star_index": 3320314 + } + }, + "18": { + "1": { + "star_index": 3329531, + "get_star_ts": 1671341579 + }, + "2": { + "star_index": 3334351, + "get_star_ts": 1671345754 + } + }, + "19": { + "1": { + "star_index": 3710028, + "get_star_ts": 1671855202 + } + }, + "20": { + "1": { + "star_index": 3527627, + "get_star_ts": 1671597421 + }, + "2": { + "get_star_ts": 1671597617, + "star_index": 3527693 + } + }, + "21": { + "1": { + "get_star_ts": 1671602215, + "star_index": 3534776 + }, + "2": { + "get_star_ts": 1671607360, + "star_index": 3540072 + } + }, + "23": { + "1": { + "get_star_ts": 1671777345, + "star_index": 3659545 + }, + "2": { + "star_index": 3660184, + "get_star_ts": 1671778021 + } + }, + "25": { + "1": { + "get_star_ts": 1671946762, + "star_index": 3757886 + } + } + }, + "local_score": 3476, + "last_star_ts": 1671946762, + "stars": 42, + "id": 2315131 + }, + "2316198": { + "stars": 10, + "id": 2316198, + "name": "santiago rodrigues", + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669952965, + "star_index": 262129 + }, + "2": { + "star_index": 263246, + "get_star_ts": 1669953942 + } + }, + "2": { + "1": { + "get_star_ts": 1669958269, + "star_index": 278673 + }, + "2": { + "get_star_ts": 1669958437, + "star_index": 281161 + } + }, + "3": { + "1": { + "star_index": 841328, + "get_star_ts": 1670124538 + }, + "2": { + "star_index": 842362, + "get_star_ts": 1670125563 + } + }, + "4": { + "1": { + "star_index": 1144506, + "get_star_ts": 1670217798 + }, + "2": { + "get_star_ts": 1670217984, + "star_index": 1146290 + } + }, + "5": { + "1": { + "star_index": 1157062, + "get_star_ts": 1670219327 + }, + "2": { + "star_index": 1160748, + "get_star_ts": 1670219926 + } + } + }, + "last_star_ts": 1670219926, + "local_score": 661, + "global_score": 0 + }, + "2343027": { + "global_score": 0, + "stars": 50, + "id": 2343027, + "name": "William Barnett", + "completion_day_level": { + "1": { + "1": { + "star_index": 1899, + "get_star_ts": 1669871010 + }, + "2": { + "star_index": 2935, + "get_star_ts": 1669871061 + } + }, + "2": { + "1": { + "star_index": 267709, + "get_star_ts": 1669957518 + }, + "2": { + "get_star_ts": 1669957666, + "star_index": 269239 + } + }, + "3": { + "1": { + "star_index": 577511, + "get_star_ts": 1670043877 + }, + "2": { + "get_star_ts": 1670044123, + "star_index": 579888 + } + }, + "4": { + "1": { + "get_star_ts": 1670130242, + "star_index": 847656 + }, + "2": { + "star_index": 849784, + "get_star_ts": 1670130381 + } + }, + "5": { + "1": { + "star_index": 1149162, + "get_star_ts": 1670218308 + }, + "2": { + "star_index": 1150867, + "get_star_ts": 1670218508 + } + }, + "6": { + "1": { + "star_index": 1483267, + "get_star_ts": 1670307533 + }, + "2": { + "star_index": 1483408, + "get_star_ts": 1670307560 + } + }, + "7": { + "1": { + "get_star_ts": 1670390462, + "star_index": 1739394 + }, + "2": { + "get_star_ts": 1670390640, + "star_index": 1740101 + } + }, + "8": { + "1": { + "get_star_ts": 1670475978, + "star_index": 1956618 + }, + "2": { + "get_star_ts": 1670476347, + "star_index": 1958172 + } + }, + "9": { + "1": { + "star_index": 2174074, + "get_star_ts": 1670562960 + }, + "2": { + "star_index": 2179297, + "get_star_ts": 1670563931 + } + }, + "10": { + "1": { + "star_index": 2352085, + "get_star_ts": 1670649003 + }, + "2": { + "get_star_ts": 1670649476, + "star_index": 2355225 + } + }, + "11": { + "1": { + "star_index": 2528887, + "get_star_ts": 1670735844 + }, + "2": { + "star_index": 2529594, + "get_star_ts": 1670736139 + } + }, + "12": { + "1": { + "star_index": 2693888, + "get_star_ts": 1670822038 + }, + "2": { + "get_star_ts": 1670822153, + "star_index": 2694251 + } + }, + "13": { + "1": { + "star_index": 2850144, + "get_star_ts": 1670912002 + }, + "2": { + "get_star_ts": 1670912527, + "star_index": 2851312 + } + }, + "14": { + "1": { + "get_star_ts": 1670995269, + "star_index": 2967492 + }, + "2": { + "get_star_ts": 1670995631, + "star_index": 2968857 + } + }, + "15": { + "1": { + "get_star_ts": 1671082366, + "star_index": 3086102 + }, + "2": { + "star_index": 3090143, + "get_star_ts": 1671084014 + } + }, + "16": { + "1": { + "get_star_ts": 1671175194, + "star_index": 3190947 + }, + "2": { + "get_star_ts": 1671253708, + "star_index": 3256558 + } + }, + "17": { + "1": { + "get_star_ts": 1671255256, + "star_index": 3257380 + }, + "2": { + "star_index": 3262966, + "get_star_ts": 1671260879 + } + }, + "18": { + "1": { + "get_star_ts": 1671339972, + "star_index": 3325305 + }, + "2": { + "get_star_ts": 1671340656, + "star_index": 3327515 + } + }, + "19": { + "1": { + "get_star_ts": 1671434638, + "star_index": 3406323 + }, + "2": { + "get_star_ts": 1671440363, + "star_index": 3410109 + } + }, + "20": { + "1": { + "star_index": 3468537, + "get_star_ts": 1671518341 + }, + "2": { + "star_index": 3468984, + "get_star_ts": 1671518802 + } + }, + "21": { + "1": { + "get_star_ts": 1671599114, + "star_index": 3528383 + }, + "2": { + "get_star_ts": 1671600709, + "star_index": 3532320 + } + }, + "22": { + "1": { + "star_index": 3598941, + "get_star_ts": 1671686685 + }, + "2": { + "get_star_ts": 1671692530, + "star_index": 3603897 + } + }, + "23": { + "1": { + "get_star_ts": 1671775321, + "star_index": 3657101 + }, + "2": { + "star_index": 3657248, + "get_star_ts": 1671775418 + } + }, + "24": { + "1": { + "get_star_ts": 1671859618, + "star_index": 3711388 + }, + "2": { + "get_star_ts": 1671859781, + "star_index": 3711510 + } + }, + "25": { + "1": { + "get_star_ts": 1671945106, + "star_index": 3754947 + }, + "2": { + "star_index": 3754972, + "get_star_ts": 1671945120 + } + } + }, + "last_star_ts": 1671945120, + "local_score": 4595 + }, + "2343082": { + "global_score": 0, + "id": 2343082, + "stars": 16, + "last_star_ts": 1670480594, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669874144, + "star_index": 24367 + }, + "2": { + "get_star_ts": 1669874335, + "star_index": 24999 + } + }, + "2": { + "1": { + "get_star_ts": 1669957983, + "star_index": 274070 + }, + "2": { + "get_star_ts": 1669958193, + "star_index": 277488 + } + }, + "3": { + "1": { + "get_star_ts": 1670374924, + "star_index": 1721470 + }, + "2": { + "get_star_ts": 1670375908, + "star_index": 1722740 + } + }, + "4": { + "1": { + "get_star_ts": 1670378038, + "star_index": 1725188 + }, + "2": { + "get_star_ts": 1670378210, + "star_index": 1725416 + } + }, + "5": { + "1": { + "get_star_ts": 1670222908, + "star_index": 1174736 + }, + "2": { + "star_index": 1178303, + "get_star_ts": 1670223878 + } + }, + "6": { + "1": { + "star_index": 1469572, + "get_star_ts": 1670305024 + }, + "2": { + "star_index": 1482443, + "get_star_ts": 1670307355 + } + }, + "7": { + "1": { + "get_star_ts": 1670474973, + "star_index": 1955386 + }, + "2": { + "get_star_ts": 1670476391, + "star_index": 1958399 + } + }, + "8": { + "1": { + "star_index": 1978007, + "get_star_ts": 1670479739 + }, + "2": { + "star_index": 1981465, + "get_star_ts": 1670480594 + } + } + }, + "local_score": 1129, + "name": "andrewj-brown" + }, + "2343864": { + "name": "monomino _", + "last_star_ts": 1671947508, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669873444, + "star_index": 21946 + }, + "2": { + "star_index": 22349, + "get_star_ts": 1669873552 + } + }, + "2": { + "1": { + "star_index": 280176, + "get_star_ts": 1669958366 + }, + "2": { + "get_star_ts": 1669958668, + "star_index": 284173 + } + }, + "3": { + "1": { + "star_index": 595120, + "get_star_ts": 1670045433 + }, + "2": { + "get_star_ts": 1670046283, + "star_index": 601387 + } + }, + "4": { + "1": { + "star_index": 856823, + "get_star_ts": 1670130764 + }, + "2": { + "star_index": 865137, + "get_star_ts": 1670131390 + } + }, + "5": { + "1": { + "get_star_ts": 1670221986, + "star_index": 1171006 + }, + "2": { + "get_star_ts": 1670226442, + "star_index": 1188276 + } + }, + "6": { + "1": { + "get_star_ts": 1670304083, + "star_index": 1460609 + }, + "2": { + "get_star_ts": 1670304218, + "star_index": 1462233 + } + }, + "7": { + "1": { + "get_star_ts": 1670455696, + "star_index": 1931065 + }, + "2": { + "star_index": 1932211, + "get_star_ts": 1670456269 + } + }, + "8": { + "1": { + "star_index": 1979335, + "get_star_ts": 1670480050 + }, + "2": { + "star_index": 1996671, + "get_star_ts": 1670485291 + } + }, + "9": { + "1": { + "get_star_ts": 1670564675, + "star_index": 2183035 + }, + "2": { + "star_index": 2189625, + "get_star_ts": 1670566317 + } + }, + "10": { + "1": { + "star_index": 2368946, + "get_star_ts": 1670652017 + }, + "2": { + "get_star_ts": 1670654378, + "star_index": 2375501 + } + }, + "11": { + "1": { + "get_star_ts": 1670741965, + "star_index": 2547176 + }, + "2": { + "star_index": 2569464, + "get_star_ts": 1670754053 + } + }, + "12": { + "1": { + "get_star_ts": 1670823227, + "star_index": 2698023 + }, + "2": { + "get_star_ts": 1670823735, + "star_index": 2699662 + } + }, + "13": { + "1": { + "star_index": 2872266, + "get_star_ts": 1670924668 + }, + "2": { + "star_index": 2872853, + "get_star_ts": 1670924996 + } + }, + "14": { + "1": { + "get_star_ts": 1671532817, + "star_index": 3480478 + }, + "2": { + "star_index": 3480768, + "get_star_ts": 1671533158 + } + }, + "15": { + "1": { + "star_index": 3111292, + "get_star_ts": 1671099014 + }, + "2": { + "star_index": 3112568, + "get_star_ts": 1671100039 + } + }, + "16": { + "1": { + "get_star_ts": 1671615302, + "star_index": 3547824 + }, + "2": { + "star_index": 3549615, + "get_star_ts": 1671617127 + } + }, + "17": { + "1": { + "star_index": 3595989, + "get_star_ts": 1671677796 + }, + "2": { + "get_star_ts": 1671684957, + "star_index": 3598251 + } + }, + "18": { + "1": { + "star_index": 3327955, + "get_star_ts": 1671340817 + }, + "2": { + "star_index": 3340663, + "get_star_ts": 1671353070 + } + }, + "19": { + "1": { + "star_index": 3405690, + "get_star_ts": 1671433676 + }, + "2": { + "star_index": 3405977, + "get_star_ts": 1671434114 + } + }, + "20": { + "1": { + "get_star_ts": 1671516378, + "star_index": 3466403 + }, + "2": { + "get_star_ts": 1671524353, + "star_index": 3473466 + } + }, + "21": { + "1": { + "get_star_ts": 1671600777, + "star_index": 3532455 + }, + "2": { + "star_index": 3538598, + "get_star_ts": 1671605740 + } + }, + "22": { + "1": { + "get_star_ts": 1671699422, + "star_index": 3608326 + }, + "2": { + "star_index": 3610159, + "get_star_ts": 1671702099 + } + }, + "23": { + "1": { + "star_index": 3661485, + "get_star_ts": 1671779649 + }, + "2": { + "star_index": 3663064, + "get_star_ts": 1671781889 + } + }, + "24": { + "1": { + "get_star_ts": 1671874074, + "star_index": 3721186 + }, + "2": { + "get_star_ts": 1671875013, + "star_index": 3721696 + } + }, + "25": { + "1": { + "star_index": 3758850, + "get_star_ts": 1671947473 + }, + "2": { + "get_star_ts": 1671947508, + "star_index": 3758900 + } + } + }, + "local_score": 4113, + "stars": 50, + "id": 2343864, + "global_score": 0 + }, + "2345794": { + "global_score": 0, + "stars": 12, + "id": 2345794, + "name": "antdon", + "completion_day_level": { + "1": { + "1": { + "star_index": 32189, + "get_star_ts": 1669876692 + }, + "2": { + "star_index": 34375, + "get_star_ts": 1669877413 + } + }, + "2": { + "1": { + "star_index": 289179, + "get_star_ts": 1669959140 + }, + "2": { + "get_star_ts": 1669960070, + "star_index": 296308 + } + }, + "3": { + "1": { + "get_star_ts": 1670056034, + "star_index": 637001 + }, + "2": { + "star_index": 642560, + "get_star_ts": 1670057508 + } + }, + "4": { + "1": { + "get_star_ts": 1670307108, + "star_index": 1481281 + }, + "2": { + "star_index": 1482413, + "get_star_ts": 1670307349 + } + }, + "5": { + "1": { + "get_star_ts": 1670221378, + "star_index": 1168386 + }, + "2": { + "star_index": 1434860, + "get_star_ts": 1670300542 + } + }, + "6": { + "1": { + "get_star_ts": 1670303525, + "star_index": 1451527 + }, + "2": { + "star_index": 1455407, + "get_star_ts": 1670303726 + } + } + }, + "local_score": 839, + "last_star_ts": 1670307349 + }, + "2346318": { + "global_score": 0, + "local_score": 153, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669872233, + "star_index": 16290 + }, + "2": { + "star_index": 21722, + "get_star_ts": 1669873388 + } + } + }, + "last_star_ts": 1669873388, + "name": "nhamid289", + "id": 2346318, + "stars": 2 + }, + "2346811": { + "name": "Al Holliday", + "last_star_ts": 1670548301, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669873442, + "star_index": 21939 + }, + "2": { + "get_star_ts": 1669875165, + "star_index": 27563 + } + }, + "2": { + "1": { + "get_star_ts": 1669961525, + "star_index": 304041 + }, + "2": { + "star_index": 313579, + "get_star_ts": 1669963646 + } + }, + "3": { + "1": { + "get_star_ts": 1670379912, + "star_index": 1727399 + }, + "2": { + "star_index": 1735793, + "get_star_ts": 1670387827 + } + }, + "5": { + "1": { + "get_star_ts": 1670290779, + "star_index": 1422031 + }, + "2": { + "star_index": 1423485, + "get_star_ts": 1670291831 + } + }, + "8": { + "1": { + "star_index": 2029802, + "get_star_ts": 1670495888 + }, + "2": { + "get_star_ts": 1670548301, + "star_index": 2159568 + } + } + }, + "local_score": 649, + "stars": 10, + "id": 2346811, + "global_score": 0 + }, + "2347573": { + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871412, + "star_index": 9043 + }, + "2": { + "get_star_ts": 1669871777, + "star_index": 13028 + } + } + }, + "local_score": 171, + "last_star_ts": 1669871777, + "name": "MattPChoy", + "id": 2347573, + "stars": 2, + "global_score": 0 + }, + "2348029": { + "id": 2348029, + "stars": 17, + "last_star_ts": 1670590074, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669871924, + "star_index": 14204 + }, + "2": { + "star_index": 17086, + "get_star_ts": 1669872374 + } + }, + "2": { + "1": { + "get_star_ts": 1669958252, + "star_index": 278435 + }, + "2": { + "star_index": 280149, + "get_star_ts": 1669958364 + } + }, + "3": { + "1": { + "star_index": 593606, + "get_star_ts": 1670045266 + }, + "2": { + "star_index": 601410, + "get_star_ts": 1670046287 + } + }, + "4": { + "1": { + "star_index": 958912, + "get_star_ts": 1670152858 + }, + "2": { + "star_index": 962813, + "get_star_ts": 1670153727 + } + }, + "5": { + "1": { + "get_star_ts": 1670222129, + "star_index": 1171537 + }, + "2": { + "get_star_ts": 1670222571, + "star_index": 1173348 + } + }, + "6": { + "1": { + "get_star_ts": 1670303430, + "star_index": 1449423 + }, + "2": { + "star_index": 1450917, + "get_star_ts": 1670303497 + } + }, + "7": { + "1": { + "star_index": 1759828, + "get_star_ts": 1670395215 + }, + "2": { + "get_star_ts": 1670395665, + "star_index": 1761302 + } + }, + "8": { + "1": { + "get_star_ts": 1670489909, + "star_index": 2011165 + }, + "2": { + "star_index": 2019980, + "get_star_ts": 1670492681 + } + }, + "9": { + "1": { + "star_index": 2249355, + "get_star_ts": 1670590074 + } + } + }, + "local_score": 1309, + "name": "uhhhhh", + "global_score": 0 + }, + "2348150": { + "completion_day_level": { + "1": { + "1": { + "star_index": 44430, + "get_star_ts": 1669880545 + }, + "2": { + "get_star_ts": 1669880632, + "star_index": 44717 + } + }, + "2": { + "1": { + "get_star_ts": 1669957671, + "star_index": 269292 + }, + "2": { + "star_index": 280690, + "get_star_ts": 1669958404 + } + }, + "3": { + "1": { + "star_index": 577067, + "get_star_ts": 1670043779 + }, + "2": { + "star_index": 578668, + "get_star_ts": 1670044015 + } + }, + "4": { + "1": { + "star_index": 850223, + "get_star_ts": 1670130404 + }, + "2": { + "get_star_ts": 1670130451, + "star_index": 851113 + } + }, + "5": { + "1": { + "get_star_ts": 1670217337, + "star_index": 1140500 + }, + "2": { + "star_index": 1141376, + "get_star_ts": 1670217442 + } + }, + "6": { + "1": { + "get_star_ts": 1670302911, + "star_index": 1437646 + }, + "2": { + "get_star_ts": 1670302981, + "star_index": 1438367 + } + }, + "7": { + "1": { + "get_star_ts": 1670390513, + "star_index": 1739603 + }, + "2": { + "get_star_ts": 1670390799, + "star_index": 1740830 + } + }, + "8": { + "1": { + "get_star_ts": 1670476466, + "star_index": 1958823 + }, + "2": { + "star_index": 1965540, + "get_star_ts": 1670477460 + } + }, + "9": { + "1": { + "get_star_ts": 1670563628, + "star_index": 2177626 + }, + "2": { + "star_index": 2178930, + "get_star_ts": 1670563866 + } + }, + "10": { + "1": { + "star_index": 2354971, + "get_star_ts": 1670649442 + }, + "2": { + "get_star_ts": 1670650797, + "star_index": 2363680 + } + }, + "11": { + "1": { + "get_star_ts": 1670736329, + "star_index": 2530177 + }, + "2": { + "get_star_ts": 1670736552, + "star_index": 2530922 + } + }, + "12": { + "1": { + "star_index": 2701893, + "get_star_ts": 1670824474 + }, + "2": { + "star_index": 2702495, + "get_star_ts": 1670824717 + } + }, + "13": { + "1": { + "star_index": 2962117, + "get_star_ts": 1670988427 + }, + "2": { + "get_star_ts": 1670989046, + "star_index": 2962528 + } + }, + "14": { + "1": { + "star_index": 2968063, + "get_star_ts": 1670995424 + }, + "2": { + "get_star_ts": 1670996253, + "star_index": 2971124 + } + }, + "15": { + "1": { + "star_index": 3086998, + "get_star_ts": 1671082778 + }, + "2": { + "get_star_ts": 1671088781, + "star_index": 3097916 + } + }, + "16": { + "1": { + "get_star_ts": 1671181902, + "star_index": 3196367 + }, + "2": { + "star_index": 3248451, + "get_star_ts": 1671235937 + } + }, + "17": { + "1": { + "star_index": 4030364, + "get_star_ts": 1673416544 + }, + "2": { + "get_star_ts": 1673474947, + "star_index": 4035451 + } + }, + "18": { + "1": { + "star_index": 4035658, + "get_star_ts": 1673476885 + }, + "2": { + "star_index": 4036985, + "get_star_ts": 1673500995 + } + }, + "19": { + "1": { + "star_index": 4043145, + "get_star_ts": 1673586304 + }, + "2": { + "get_star_ts": 1673586406, + "star_index": 4043148 + } + }, + "20": { + "1": { + "get_star_ts": 1673592120, + "star_index": 4043415 + }, + "2": { + "star_index": 4044084, + "get_star_ts": 1673605359 + } + }, + "21": { + "1": { + "get_star_ts": 1673607191, + "star_index": 4044227 + }, + "2": { + "get_star_ts": 1673608281, + "star_index": 4044280 + } + }, + "22": { + "1": { + "get_star_ts": 1673658923, + "star_index": 4048650 + }, + "2": { + "get_star_ts": 1673669289, + "star_index": 4049032 + } + }, + "23": { + "1": { + "get_star_ts": 1673671256, + "star_index": 4049109 + }, + "2": { + "star_index": 4049114, + "get_star_ts": 1673671400 + } + }, + "24": { + "1": { + "star_index": 4049184, + "get_star_ts": 1673673915 + }, + "2": { + "get_star_ts": 1673673974, + "star_index": 4049188 + } + }, + "25": { + "1": { + "get_star_ts": 1673680256, + "star_index": 4049434 + }, + "2": { + "star_index": 4049435, + "get_star_ts": 1673680273 + } + } + }, + "local_score": 4268, + "last_star_ts": 1673680273, + "name": "Lief", + "id": 2348150, + "stars": 50, + "global_score": 13 + }, + "2348758": { + "global_score": 0, + "name": "CNPCNPCNP", + "local_score": 2418, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669897758, + "star_index": 109000 + }, + "2": { + "get_star_ts": 1669898006, + "star_index": 109808 + } + }, + "2": { + "1": { + "get_star_ts": 1669964554, + "star_index": 317548 + }, + "2": { + "star_index": 319196, + "get_star_ts": 1669964926 + } + }, + "3": { + "1": { + "get_star_ts": 1670044961, + "star_index": 590447 + }, + "2": { + "star_index": 599210, + "get_star_ts": 1670045952 + } + }, + "4": { + "1": { + "get_star_ts": 1670130203, + "star_index": 847197 + }, + "2": { + "star_index": 850291, + "get_star_ts": 1670130408 + } + }, + "5": { + "1": { + "star_index": 1140966, + "get_star_ts": 1670217394 + }, + "2": { + "star_index": 1144010, + "get_star_ts": 1670217744 + } + }, + "6": { + "1": { + "star_index": 1445843, + "get_star_ts": 1670303284 + }, + "2": { + "star_index": 1446944, + "get_star_ts": 1670303328 + } + }, + "7": { + "1": { + "get_star_ts": 1670396887, + "star_index": 1765093 + }, + "2": { + "star_index": 1765724, + "get_star_ts": 1670397092 + } + }, + "8": { + "1": { + "star_index": 1965575, + "get_star_ts": 1670477465 + }, + "2": { + "star_index": 1969264, + "get_star_ts": 1670478036 + } + }, + "9": { + "1": { + "get_star_ts": 1670562678, + "star_index": 2173035 + }, + "2": { + "get_star_ts": 1670564809, + "star_index": 2183713 + } + }, + "10": { + "1": { + "star_index": 2511226, + "get_star_ts": 1670715805 + }, + "2": { + "get_star_ts": 1670717080, + "star_index": 2512967 + } + }, + "11": { + "1": { + "star_index": 2531045, + "get_star_ts": 1670736590 + }, + "2": { + "star_index": 2535484, + "get_star_ts": 1670737643 + } + }, + "12": { + "1": { + "get_star_ts": 1670885280, + "star_index": 2818140 + }, + "2": { + "star_index": 2818644, + "get_star_ts": 1670885574 + } + }, + "13": { + "1": { + "get_star_ts": 1670930351, + "star_index": 2881410 + }, + "2": { + "star_index": 2882329, + "get_star_ts": 1670930987 + } + }, + "14": { + "1": { + "get_star_ts": 1671060526, + "star_index": 3070573 + }, + "2": { + "get_star_ts": 1671061148, + "star_index": 3071195 + } + }, + "15": { + "1": { + "get_star_ts": 1671087678, + "star_index": 3096381 + }, + "2": { + "star_index": 3099819, + "get_star_ts": 1671090261 + } + } + }, + "last_star_ts": 1671090261, + "stars": 30, + "id": 2348758 + }, + "2349575": { + "stars": 7, + "id": 2349575, + "name": "Jackie", + "completion_day_level": { + "1": { + "1": { + "star_index": 23264, + "get_star_ts": 1669873822 + }, + "2": { + "star_index": 24325, + "get_star_ts": 1669874131 + } + }, + "2": { + "1": { + "star_index": 308701, + "get_star_ts": 1669962536 + }, + "2": { + "star_index": 308845, + "get_star_ts": 1669962567 + } + }, + "3": { + "1": { + "star_index": 843442, + "get_star_ts": 1670126671 + }, + "2": { + "get_star_ts": 1670127892, + "star_index": 844577 + } + }, + "4": { + "1": { + "star_index": 890332, + "get_star_ts": 1670136672 + } + } + }, + "last_star_ts": 1670136672, + "local_score": 462, + "global_score": 0 + }, + "2351664": { + "global_score": 0, + "name": "shirei220", + "completion_day_level": { + "1": { + "1": { + "star_index": 23692, + "get_star_ts": 1669873940 + }, + "2": { + "star_index": 24509, + "get_star_ts": 1669874186 + } + } + }, + "local_score": 141, + "last_star_ts": 1669874186, + "stars": 2, + "id": 2351664 + }, + "2375933": { + "id": 2375933, + "stars": 46, + "local_score": 3799, + "completion_day_level": { + "1": { + "1": { + "star_index": 56390, + "get_star_ts": 1669883702 + }, + "2": { + "star_index": 57603, + "get_star_ts": 1669884006 + } + }, + "2": { + "1": { + "star_index": 275518, + "get_star_ts": 1669958072 + }, + "2": { + "get_star_ts": 1669958436, + "star_index": 281152 + } + }, + "3": { + "1": { + "star_index": 581497, + "get_star_ts": 1670044251 + }, + "2": { + "get_star_ts": 1670044594, + "star_index": 586138 + } + }, + "4": { + "1": { + "star_index": 848446, + "get_star_ts": 1670130303 + }, + "2": { + "get_star_ts": 1670130470, + "star_index": 851477 + } + }, + "5": { + "1": { + "star_index": 1204536, + "get_star_ts": 1670230276 + }, + "2": { + "get_star_ts": 1670230463, + "star_index": 1205399 + } + }, + "6": { + "1": { + "star_index": 1537883, + "get_star_ts": 1670318182 + }, + "2": { + "get_star_ts": 1670318276, + "star_index": 1538353 + } + }, + "7": { + "1": { + "star_index": 1784324, + "get_star_ts": 1670403534 + }, + "2": { + "star_index": 1785620, + "get_star_ts": 1670403999 + } + }, + "8": { + "1": { + "star_index": 2011013, + "get_star_ts": 1670489865 + }, + "2": { + "get_star_ts": 1670494108, + "star_index": 2024455 + } + }, + "9": { + "1": { + "star_index": 2173851, + "get_star_ts": 1670562903 + }, + "2": { + "star_index": 2176039, + "get_star_ts": 1670563342 + } + }, + "10": { + "1": { + "star_index": 2353868, + "get_star_ts": 1670649295 + }, + "2": { + "get_star_ts": 1670650323, + "star_index": 2361031 + } + }, + "11": { + "1": { + "star_index": 2542487, + "get_star_ts": 1670739863 + }, + "2": { + "get_star_ts": 1670740599, + "star_index": 2544307 + } + }, + "12": { + "1": { + "get_star_ts": 1670844998, + "star_index": 2740864 + }, + "2": { + "star_index": 2741162, + "get_star_ts": 1670845176 + } + }, + "13": { + "1": { + "star_index": 2866134, + "get_star_ts": 1670921031 + }, + "2": { + "get_star_ts": 1670921845, + "star_index": 2867476 + } + }, + "14": { + "1": { + "star_index": 2993900, + "get_star_ts": 1671008222 + }, + "2": { + "get_star_ts": 1671009063, + "star_index": 2995268 + } + }, + "15": { + "1": { + "get_star_ts": 1671096877, + "star_index": 3108434 + }, + "2": { + "get_star_ts": 1671100497, + "star_index": 3113140 + } + }, + "16": { + "1": { + "star_index": 3200357, + "get_star_ts": 1671186423 + }, + "2": { + "star_index": 3214407, + "get_star_ts": 1671201840 + } + }, + "17": { + "1": { + "get_star_ts": 1671258768, + "star_index": 3261243 + }, + "2": { + "get_star_ts": 1671264308, + "star_index": 3265394 + } + }, + "18": { + "1": { + "get_star_ts": 1671346191, + "star_index": 3334735 + }, + "2": { + "star_index": 3354974, + "get_star_ts": 1671366636 + } + }, + "19": { + "1": { + "star_index": 3544105, + "get_star_ts": 1671611591 + }, + "2": { + "star_index": 3544310, + "get_star_ts": 1671611802 + } + }, + "20": { + "1": { + "star_index": 3476427, + "get_star_ts": 1671528037 + }, + "2": { + "get_star_ts": 1671528150, + "star_index": 3476531 + } + }, + "21": { + "1": { + "get_star_ts": 1671612664, + "star_index": 3545164 + }, + "2": { + "get_star_ts": 1671613785, + "star_index": 3546257 + } + }, + "22": { + "1": { + "star_index": 3613598, + "get_star_ts": 1671707014 + }, + "2": { + "get_star_ts": 1671756356, + "star_index": 3648415 + } + }, + "23": { + "1": { + "get_star_ts": 1671775169, + "star_index": 3656898 + }, + "2": { + "get_star_ts": 1671776681, + "star_index": 3658829 + } + } + }, + "last_star_ts": 1671776681, + "name": "wilszdev", + "global_score": 0 + }, + "2380289": { + "stars": 8, + "id": 2380289, + "name": "Tristan Duncombe", + "last_star_ts": 1670134838, + "completion_day_level": { + "1": { + "1": { + "star_index": 91842, + "get_star_ts": 1669892480 + }, + "2": { + "star_index": 93535, + "get_star_ts": 1669892987 + } + }, + "2": { + "1": { + "star_index": 338895, + "get_star_ts": 1669968838 + }, + "2": { + "star_index": 340770, + "get_star_ts": 1669969179 + } + }, + "3": { + "1": { + "star_index": 653621, + "get_star_ts": 1670060305 + }, + "2": { + "get_star_ts": 1670061837, + "star_index": 660113 + } + }, + "4": { + "1": { + "get_star_ts": 1670134619, + "star_index": 883402 + }, + "2": { + "get_star_ts": 1670134838, + "star_index": 884179 + } + } + }, + "local_score": 501, + "global_score": 0 + }, + "2469688": { + "name": "cassie", + "completion_day_level": { + "1": { + "1": { + "star_index": 225646, + "get_star_ts": 1669931447 + }, + "2": { + "star_index": 228790, + "get_star_ts": 1669932452 + } + }, + "2": { + "1": { + "get_star_ts": 1673632017, + "star_index": 4046421 + }, + "2": { + "get_star_ts": 1673632027, + "star_index": 4046423 + } + }, + "3": { + "1": { + "star_index": 4046430, + "get_star_ts": 1673632085 + }, + "2": { + "star_index": 4046432, + "get_star_ts": 1673632092 + } + }, + "4": { + "1": { + "get_star_ts": 1673632112, + "star_index": 4046437 + }, + "2": { + "get_star_ts": 1673632119, + "star_index": 4046439 + } + }, + "5": { + "1": { + "star_index": 4046442, + "get_star_ts": 1673632138 + }, + "2": { + "star_index": 4046443, + "get_star_ts": 1673632144 + } + }, + "6": { + "1": { + "star_index": 4046447, + "get_star_ts": 1673632163 + }, + "2": { + "get_star_ts": 1673632172, + "star_index": 4046449 + } + } + }, + "last_star_ts": 1673632172, + "local_score": 602, + "stars": 12, + "id": 2469688, + "global_score": 0 + }, + "2505600": { + "global_score": 0, + "name": "james-seymour-cubiko", + "completion_day_level": { + "1": { + "1": { + "star_index": 4308169, + "get_star_ts": 1684631858 + }, + "2": { + "get_star_ts": 1684632154, + "star_index": 4308175 + } + }, + "2": { + "1": { + "get_star_ts": 1684639344, + "star_index": 4308276 + } + }, + "3": { + "1": { + "star_index": 4308355, + "get_star_ts": 1684645480 + }, + "2": { + "star_index": 4308368, + "get_star_ts": 1684646371 + } + }, + "4": { + "1": { + "get_star_ts": 1684647575, + "star_index": 4308389 + }, + "2": { + "star_index": 4308392, + "get_star_ts": 1684647718 + } + }, + "6": { + "1": { + "get_star_ts": 1684654219, + "star_index": 4308465 + }, + "2": { + "star_index": 4308468, + "get_star_ts": 1684654355 + } + }, + "10": { + "1": { + "star_index": 4309816, + "get_star_ts": 1684716959 + } + } + }, + "local_score": 472, + "last_star_ts": 1684716959, + "stars": 10, + "id": 2505600 + }, + "2509967": { + "stars": 1, + "id": 2509967, + "name": "Abe", + "local_score": 48, + "completion_day_level": { + "1": { + "1": { + "star_index": 258611, + "get_star_ts": 1669949966 + } + } + }, + "last_star_ts": 1669949966, + "global_score": 0 + }, + "2518488": { + "id": 2518488, + "stars": 3, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1669963429, + "star_index": 312630 + }, + "2": { + "get_star_ts": 1669964618, + "star_index": 317824 + } + }, + "2": { + "1": { + "star_index": 321003, + "get_star_ts": 1669965318 + } + } + }, + "local_score": 150, + "last_star_ts": 1669965318, + "name": "Ninjaman10p", + "global_score": 0 + }, + "2633535": { + "global_score": 0, + "last_star_ts": 1671976793, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1670140048, + "star_index": 901364 + }, + "2": { + "star_index": 902476, + "get_star_ts": 1670140355 + } + }, + "2": { + "1": { + "star_index": 906242, + "get_star_ts": 1670141401 + }, + "2": { + "star_index": 1127724, + "get_star_ts": 1670207799 + } + }, + "3": { + "1": { + "get_star_ts": 1670298942, + "star_index": 1432919 + }, + "2": { + "star_index": 1434261, + "get_star_ts": 1670300051 + } + }, + "4": { + "1": { + "star_index": 3776953, + "get_star_ts": 1671976793 + } + }, + "5": { + "1": { + "star_index": 1194341, + "get_star_ts": 1670227924 + }, + "2": { + "star_index": 1425871, + "get_star_ts": 1670293581 + } + }, + "6": { + "1": { + "get_star_ts": 1670304984, + "star_index": 1469277 + }, + "2": { + "star_index": 1471531, + "get_star_ts": 1670305291 + } + } + }, + "local_score": 638, + "name": "Zheleznov", + "id": 2633535, + "stars": 11 + }, + "2680624": { + "id": 2680624, + "stars": 16, + "last_star_ts": 1670652837, + "completion_day_level": { + "1": { + "1": { + "star_index": 1547772, + "get_star_ts": 1670320213 + }, + "2": { + "get_star_ts": 1670320501, + "star_index": 1549088 + } + }, + "2": { + "1": { + "get_star_ts": 1670495140, + "star_index": 2027644 + }, + "2": { + "star_index": 2029014, + "get_star_ts": 1670495612 + } + }, + "3": { + "1": { + "star_index": 2034583, + "get_star_ts": 1670497522 + }, + "2": { + "get_star_ts": 1670502767, + "star_index": 2048155 + } + }, + "4": { + "1": { + "star_index": 2052212, + "get_star_ts": 1670504292 + }, + "2": { + "star_index": 2053464, + "get_star_ts": 1670504757 + } + }, + "6": { + "1": { + "star_index": 1578123, + "get_star_ts": 1670328125 + }, + "2": { + "star_index": 1578856, + "get_star_ts": 1670328354 + } + }, + "8": { + "1": { + "get_star_ts": 1670489236, + "star_index": 2008960 + }, + "2": { + "get_star_ts": 1670493490, + "star_index": 2022614 + } + }, + "9": { + "1": { + "star_index": 2183684, + "get_star_ts": 1670564803 + }, + "2": { + "star_index": 2330168, + "get_star_ts": 1670627015 + } + }, + "10": { + "1": { + "get_star_ts": 1670651132, + "star_index": 2365388 + }, + "2": { + "star_index": 2371551, + "get_star_ts": 1670652837 + } + } + }, + "local_score": 1002, + "name": "lachlanharnett", + "global_score": 0 + }, + "2747794": { + "id": 2747794, + "stars": 24, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1670497901, + "star_index": 2035590 + }, + "2": { + "get_star_ts": 1670498147, + "star_index": 2036235 + } + }, + "2": { + "1": { + "get_star_ts": 1670501120, + "star_index": 2043915 + }, + "2": { + "star_index": 2046077, + "get_star_ts": 1670501944 + } + }, + "3": { + "1": { + "star_index": 2054357, + "get_star_ts": 1670505092 + }, + "2": { + "get_star_ts": 1670506293, + "star_index": 2057594 + } + }, + "4": { + "1": { + "star_index": 2067939, + "get_star_ts": 1670509915 + }, + "2": { + "star_index": 2070665, + "get_star_ts": 1670510871 + } + }, + "5": { + "1": { + "get_star_ts": 1670834300, + "star_index": 2721072 + }, + "2": { + "get_star_ts": 1670840333, + "star_index": 2732542 + } + }, + "6": { + "1": { + "star_index": 2738287, + "get_star_ts": 1670843475 + }, + "2": { + "star_index": 2739392, + "get_star_ts": 1670844147 + } + }, + "7": { + "1": { + "star_index": 3462601, + "get_star_ts": 1671513469 + }, + "2": { + "get_star_ts": 1671514302, + "star_index": 3463592 + } + }, + "8": { + "1": { + "star_index": 3469456, + "get_star_ts": 1671519311 + }, + "2": { + "star_index": 3472435, + "get_star_ts": 1671523052 + } + }, + "9": { + "1": { + "get_star_ts": 1670570464, + "star_index": 2200982 + }, + "2": { + "get_star_ts": 1670830131, + "star_index": 2713542 + } + }, + "10": { + "1": { + "star_index": 3479989, + "get_star_ts": 1671532290 + }, + "2": { + "star_index": 3484967, + "get_star_ts": 1671538323 + } + }, + "11": { + "1": { + "star_index": 3524423, + "get_star_ts": 1671587923 + } + }, + "13": { + "1": { + "get_star_ts": 1670913732, + "star_index": 2853814 + }, + "2": { + "star_index": 2856774, + "get_star_ts": 1670915446 + } + }, + "15": { + "1": { + "star_index": 3098623, + "get_star_ts": 1671089320 + } + } + }, + "last_star_ts": 1671587923, + "local_score": 1491, + "name": "parker334", + "global_score": 0 + }, + "2770110": { + "global_score": 0, + "name": "averya25", + "last_star_ts": 1670604613, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1670507875, + "star_index": 2062133 + }, + "2": { + "star_index": 2065356, + "get_star_ts": 1670509039 + } + }, + "2": { + "1": { + "star_index": 2236340, + "get_star_ts": 1670584359 + }, + "2": { + "star_index": 2238849, + "get_star_ts": 1670585446 + } + }, + "3": { + "1": { + "get_star_ts": 1670589211, + "star_index": 2247361 + }, + "2": { + "get_star_ts": 1670601802, + "star_index": 2278636 + } + }, + "4": { + "1": { + "star_index": 2284402, + "get_star_ts": 1670604199 + }, + "2": { + "get_star_ts": 1670604613, + "star_index": 2285379 + } + } + }, + "local_score": 396, + "stars": 8, + "id": 2770110 + }, + "2801494": { + "global_score": 0, + "id": 2801494, + "stars": 1, + "last_star_ts": 1670721371, + "completion_day_level": { + "1": { + "1": { + "star_index": 2517787, + "get_star_ts": 1670721371 + } + } + }, + "local_score": 38, + "name": "Connor Geissmann" + }, + "2802261": { + "global_score": 0, + "last_star_ts": 1671275381, + "completion_day_level": { + "1": { + "1": { + "get_star_ts": 1671253426, + "star_index": 3256447 + }, + "2": { + "get_star_ts": 1671253996, + "star_index": 3256667 + } + }, + "2": { + "1": { + "get_star_ts": 1671256737, + "star_index": 3259181 + }, + "2": { + "star_index": 3259698, + "get_star_ts": 1671257203 + } + }, + "3": { + "1": { + "get_star_ts": 1671273231, + "star_index": 3271965 + }, + "2": { + "star_index": 3273689, + "get_star_ts": 1671275381 + } + } + }, + "local_score": 270, + "name": "lambdanon", + "id": 2802261, + "stars": 6 + }, + "2864811": { + "completion_day_level": { + "1": { + "1": { + "star_index": 3182950, + "get_star_ts": 1671162723 + }, + "2": { + "get_star_ts": 1671163354, + "star_index": 3183273 + } + }, + "2": { + "1": { + "get_star_ts": 1671170029, + "star_index": 3186836 + }, + "2": { + "get_star_ts": 1671282689, + "star_index": 3279979 + } + }, + "3": { + "1": { + "star_index": 3260941, + "get_star_ts": 1671258454 + }, + "2": { + "star_index": 3660944, + "get_star_ts": 1671778922 + } + }, + "4": { + "1": { + "get_star_ts": 1671864350, + "star_index": 3715752 + }, + "2": { + "star_index": 3716005, + "get_star_ts": 1671864698 + } + } + }, + "last_star_ts": 1671864698, + "local_score": 385, + "name": "Yash Talekar", + "id": 2864811, + "stars": 8, + "global_score": 0 + } + }, + "owner_id": 989288, + "event": "2022" + } \ No newline at end of file diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index d37f6563..53d2c390 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -1,20 +1,20 @@ import io import logging import os -from argparse import ArgumentParser, Namespace -from datetime import datetime, timedelta, timezone -from enum import Enum +from datetime import datetime, timedelta +from pytz import timezone from random import choices -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Optional, Literal +import requests +from requests.exceptions import RequestException +from sqlalchemy.sql.expression import and_ import discord -import requests +from discord import app_commands from discord.ext import commands -from requests.exceptions import RequestException from uqcsbot.bot import UQCSBot -from uqcsbot.models import AOCWinner -from uqcsbot.utils.command_utils import loading_status +from uqcsbot.models import AOCRegistrations, AOCWinners from uqcsbot.utils.err_log_utils import FatalErrorWithLog # Leaderboard API URL with placeholders for year and code. @@ -27,69 +27,47 @@ ADVENT_DAYS = list(range(1, 25 + 1)) # Puzzles are unlocked at midnight EST. -EST_TIMEZONE = timezone(timedelta(hours=-5)) - - -class SortMode(Enum): - """Options for sorting the leaderboard.""" - - PART_1 = "p1" - PART_2 = "p2" - DELTA = "delta" - LOCAL = "local" # SortMode.LOCAL is not shown to users - GLOBAL = "global" # SortMode.GLOBAL is not shown to users - - def __str__(self): - return self.value # needed so --help prints string values - - -# Map of sorting options to friendly name. -SORT_LABELS = { - SortMode.PART_1: "part 1 completion", - SortMode.PART_2: "part 2 completion", - SortMode.DELTA: "time delta", -} +EST_TIMEZONE = timezone("US/Eastern") +# The time to cache results to limit requests to adventofcode.com. Note that 15 minutes is the recomended minimum time. +CACHE_TIME = timedelta(minutes=15) -def sort_none_last(key): - """ - Given sort key function, returns new key function which can handle None. - - None values are sorted after non-None values. - """ - return lambda x: (key(x) is None, key(x)) - +# The maximum time in seconds that a person can complete a challenge in. Used as a maximum value to help with sorting when someone whas not attempted a day. +MAXIMUM_TIME_FOR_STAR = 365 * 24 * 60 * 60 # type aliases for documentation purposes. Day = int # from 1 to 25 -Star = int # 1 or 2 +Star = Literal[1, 2] Seconds = int Times = Dict[Star, Seconds] Delta = Optional[Seconds] -# TODO: make these types more specific with TypedDict and Literal when possible. +Json = Dict[str, Any] + + +class InvalidHTTPSCode(Exception): + def __init__(self, message, request_code): + super().__init__(message) + self.request_code = request_code class Member: - def __init__( - self, id: int, name: str, local: int, stars: int, global_: int - ) -> None: + def __init__(self, id: int, name: str, local: int, star_total: int, global_: int): + # The advent of code id self.id = id + # The advent of code name self.name = name + # The score of the user on the local leaderboard self.local = local - self.stars = stars + # The total number of stars the user has collected + self.star_total = star_total + # The score of the user on the global leaderboard self.global_ = global_ - self.all_times: Dict[Day, Times] = {d: {} for d in ADVENT_DAYS} - self.all_deltas: Dict[Day, Delta] = {d: None for d in ADVENT_DAYS} - - self.day: Optional[Day] = None - self.day_times: Times = {} - self.day_delta: Delta = None + # All of the Times. If no stars are collected, the Times dictionary is empty. + self.times: Dict[Day, Times] = {d: {} for d in ADVENT_DAYS} @classmethod - def from_member_data( - cls, data: Dict, year: int, day: Optional[int] = None - ) -> "Member": + def from_member_data(cls, data: Json, year: int) -> "Member": """ Constructs a Member from the API response. @@ -105,63 +83,190 @@ def from_member_data( ) for d, day_data in data["completion_day_level"].items(): - d = int(d) - times = member.all_times[d] + day = int(d) + times = member.times[day] # timestamp of puzzle unlock, rounded to whole seconds - DAY_START = int(datetime(year, 12, d, tzinfo=EST_TIMEZONE).timestamp()) + DAY_START = int(datetime(year, 12, day, tzinfo=EST_TIMEZONE).timestamp()) - for star, star_data in day_data.items(): - star = int(star) + for s, star_data in day_data.items(): + star = int(s) + # assert is for type checking + assert star == 1 or star == 2 times[star] = int(star_data["get_star_ts"]) - DAY_START assert times[star] >= 0 - if len(times) == 2: - part_1, part_2 = sorted(times.values()) - member.all_deltas[d] = part_2 - part_1 + return member - # if day is specified, save that day's information into the day_ fields. - if day: - member.day = day - member.day_times = member.all_times[day] - member.day_delta = member.all_deltas[day] + def get_time_delta(self, day: Day) -> Optional[Seconds]: + """ + Returns the number of seconds between the completion of the second star from the first, or None if the second star have not been completed. + """ + if len(self.times[day]) == 2: + return self.times[day][2] - self.times[day][1] + return None - return member + def attempted_day(self, day: Day) -> bool: + """ + Returns if a member completed at least the first star in the day + """ + return len(self.times[day]) >= 1 - @staticmethod - def sort_key(sort: SortMode) -> Callable[["Member"], Any]: - """ - Given sort mode, returns a key function which sorts members - by that option using the stored times and delta. - """ - - if sort == SortMode.LOCAL: - # sorts by local score, then stars, descending. - return lambda m: (-m.local, -m.stars) - if sort == SortMode.GLOBAL: - # sorts by global score, then local score, then stars, descending. - return lambda m: (-m.global_, -m.local, -m.stars) - - # these key functions sort in ascending order of the specified value. - # E731 advises using function definitions over lambdas which is unreasonable here - if sort == SortMode.PART_1: - key = lambda m: m.day_times.get(1) # noqa: E731 - elif sort == SortMode.PART_2: - key = lambda m: m.day_times.get(2) # noqa: E731 - elif sort == SortMode.DELTA: - key = lambda m: m.day_delta # noqa: E731 - else: - assert False + def get_total_star1_time(self, default: int = 0) -> int: + """ + Returns the total time working on just star 1 for all challenges in a year. + The argument default determines the returned value if the total is 0. + """ + total = sum(self.times[day].get(1, 0) for day in ADVENT_DAYS) + return total if total != 0 else default + + def get_total_time(self, default: int = 0) -> int: + """ + Returns the total time working on stars 1 and 2 for all challenges in a year. + The argument default determines the returned value if the total is 0. + """ + total = self.get_total_star1_time() + self.get_total_star2_time() + return total if total != 0 else default - return sort_none_last(key) + def get_total_star2_time(self, default: int = 0) -> int: + """ + Returns the total time working on just star 2 for all challenges in a year. + The argument default determines the returned value if the total is 0. + """ + total = sum(self.times[day].get(2, 0) for day in ADVENT_DAYS) + return total if total != 0 else default + def get_discord_userid(self, bot: UQCSBot) -> Optional[int]: + """ + Return the discord userid of this AOC member if one is registered in the database. + """ + db_session = bot.create_db_session() + registration = ( + db_session.query(AOCRegistrations) + .filter(AOCRegistrations.aoc_userid == self.id) + .one_or_none() + ) + db_session.close() + if registration: + return registration.discord_userid + return None -class Advent(commands.Cog): - CHANNEL_NAME = "contests" - # Session cookie (will expire in approx 30 days). - # See: https://github.com/UQComputingSociety/uqcsbot-discord/wiki/Tokens-and-Environment-Variables#aoc_session_id - SESSION_ID: str = "" +# --- Sorting Methods & Related Leaderboards --- + +# Star 1 Time: Time for just getting star 1. For the monthly leaderboard, this will be the total time spent on star 1 across all problems. +# Star 2 Time: Time for just getting star 2. Does not include the time to get star 1. For the monthly leaderboard, this will be the total time spent on star 2 across all problems. +# Star 1 & 2 Time: Time for getting both stars 1 and 2. +# Total Time: The total time spent on problems over the entire month. For the monthly leaderboard, this is the same as Star 1 & 2 Time. +# Total Stars: The total number of stars over the entire month. +# Global Rank: The users global rank over the month. This is not reasonable to be daily, as very few get a global ranking each day. +SortingMethod = Literal[ + "Star 1 Time", + "Star 2 Time", + "Star 1 & 2 Time", + "Total Time", + "Total Stars", + "Global Rank", +] + +# Note that a tuple is used so that there can be multiple sorting criterial +sorting_functions_for_day: Dict[ + SortingMethod, Callable[[Member, Day], tuple[int, ...]] +] = { + "Star 1 Time": lambda member, day: ( + member.times[day].get(1, MAXIMUM_TIME_FOR_STAR), + member.times[day].get(2, MAXIMUM_TIME_FOR_STAR), + ), + "Star 2 Time": lambda member, day: ( + member.times[day][2] - member.times[day][1] + if 2 in member.times[day] + else MAXIMUM_TIME_FOR_STAR, + member.times[day].get(1, MAXIMUM_TIME_FOR_STAR), + ), + "Star 1 & 2 Time": lambda member, day: ( + member.times[day].get(2, MAXIMUM_TIME_FOR_STAR), + member.times[day].get(1, MAXIMUM_TIME_FOR_STAR), + ), + "Total Time": lambda member, dat: ( + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + -member.star_total, + ), + "Total Stars": lambda member, day: ( + -member.star_total, + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + ), + "Global Rank": lambda member, day: ( + -member.global_, + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + ), +} + +# Each sorting method has its own leaderboard to show the most relevant details +leaderboards_for_day: Dict[SortingMethod, str] = { + "Star 1 Time": "# 1 2 3 ! @ T", + "Star 2 Time": "# 1 2 3 ! @ T", + "Star 1 & 2 Time": "# 1 2 3 ! @ T L", + "Total Time": "# T ! @ 1 2 3", + "Total Stars": "# * L 1 2 3", + "Global Rank": "# G L * 1 2 3", +} + +# These are used for the monthly leaderboard +sorting_functions_for_month: Dict[ + SortingMethod, Callable[[Member], tuple[int, ...]] +] = { + "Star 1 Time": lambda member: ( + member.get_total_star1_time(default=MAXIMUM_TIME_FOR_STAR), + member.get_total_star2_time(default=MAXIMUM_TIME_FOR_STAR), + ), + "Star 2 Time": lambda member: ( + member.get_total_star2_time(default=MAXIMUM_TIME_FOR_STAR), + member.get_total_star1_time(default=MAXIMUM_TIME_FOR_STAR), + ), + "Star 1 & 2 Time": lambda member: ( + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + -member.star_total, + member.get_total_star1_time(default=MAXIMUM_TIME_FOR_STAR), + ), + "Total Time": lambda member: ( + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + -member.star_total, + ), + "Total Stars": lambda member: ( + -member.star_total, + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + ), + "Global Rank": lambda member: ( + -member.global_, + member.get_total_time(default=MAXIMUM_TIME_FOR_STAR), + ), +} + +# Each sorting method has its own leaderboard to show the most relevant details +leaderboards_for_month: Dict[SortingMethod, str] = { + "Star 1 Time": "# ! @ T * L", + "Star 2 Time": "# ! @ T * L", + "Star 1 & 2 Time": "# L * T", + "Total Time": "# L * T ! @", + "Total Stars": "# L T B", + "Global Rank": "# G L * T", +} + + +class Advent(commands.Cog): + """ + All of the commands related to Advent of Code (AOC). + Commands: + /advent help - Display help menu + /advent leaderboard - Display a leaderboard. Many sorting options and different leaderboard styles + /advent register - Register an AOC id to the current discord username. Used for registrating for prizes + /advent register-force - Force a registration between an AOC id and a discord user. Used for moderation and admin reasons + /advent unregister - Unregister an AOC id to the current discord username. + /advent unregister-force - Force-remove a registration between an AOC id and a discord user. Used for moderation and admin reasons + /advent previous-winners - Show the previous winners from a year + /advent new-winner - Add a discord user as a winner (chosen directly or by random selection) for prizes + /advent remove-winner - Remove a winner for the database + """ def __init__(self, bot: UQCSBot): self.bot = bot @@ -183,296 +288,654 @@ def __init__(self, bot: UQCSBot): month=12, ) - if os.environ.get("AOC_SESSION_ID") is not None: - SESSION_ID = os.environ.get("AOC_SESSION_ID") + # A dictionary from a year to the list of members + self.members_cache: Dict[int, List[Member]] = {} + self.last_reload_time = datetime.now() + + if isinstance((session_id := os.environ.get("AOC_SESSION_ID")), str): + # Session cookie (will expire in approx 30 days). + # See: https://github.com/UQComputingSociety/uqcsbot-discord/wiki/Tokens-and-Environment-Variables#aoc_session_id + self.session_id: str = session_id else: raise FatalErrorWithLog( bot, "Unable to find AoC session ID. Not loading advent cog." ) - def star_char(self, num_stars: int): + advent_command_group = app_commands.Group( + name="advent", description="Commands for Advent of Code" + ) + + @advent_command_group.command(name="help") + async def help_command(self, interaction: discord.Interaction): """ - Given a number of stars (0, 1, or 2), returns its leaderboard - representation. + Print a help message about advent of code. """ - return " .*"[num_stars] + await interaction.response.send_message( + "[Advent of Code](https://adventofcode.com/) is a yearly coding competition that occurs during the first 25 days of december. Coding puzzles are released at 3pm AEST each day, with two stars available for each puzzle. You can spend as long as you like on each puzzle, but UQCS also has a provate leaderboard with prizes on offer. TODO.\n\nTo join, go to and sign in. The UQCS private leaderboard join code is `989288-0ff5a98d`. To be eligible for prizes, you will also have to link your discord account. This can be done by using the `/advent register` command. Reach out to committee if you are having any issues." + ) - def format_full_leaderboard(self, members: List[Member]) -> str: + @advent_command_group.command(name="leaderboard") + @app_commands.describe( + day="Day of the leaderboard [1-25]. If not given, the leaderboard for all days is given. Incompatable with global.", + year="Year of the leaderboard. Defaults to this year.", + code="The leaderboard code. Defaults to the UQCS leaderboard.", + sortby='The method to sort the leaderboard. Defaults to "Star 2 Time". Only works for single day leaderboards.', # TODO + leaderboard_style="The display format of the leaderboard.", # TODO + ) + async def leaderboard_command( + self, + interaction: discord.Interaction, + day: Optional[Day] = None, + year: Optional[int] = None, + code: int = UQCS_LEADERBOARD, + sortby: Optional[SortingMethod] = None, + leaderboard_style: Optional[str] = None, + ): """ - Returns a string representing the full leaderboard of the given list. - - Full leaderboard includes rank, points, stars (per day), and username. + Display the advent of code leaderboard. """ - # 3 4 25 - # |-| |--| |-----------------------| - # 1) 751 **************** Name - def format_member(i: int, m: Member): - stars = "".join(self.star_char(len(m.all_times[d])) for d in ADVENT_DAYS) - return f"{i:>3}) {m.local:>4} {stars} {m.name}" + await interaction.response.defer(thinking=True) - left = " " * (3 + 2 + 4 + 1) # chars before stars start - header = ( - f"{left} 1111111111222222\n" f"{left}1234567890123456789012345\n" - ) + if year is None: + year = datetime.now().year + if sortby is None: + sortby = "Star 1 & 2 Time" if day else "Total Stars" + if leaderboard_style is None: + leaderboard_style = ( + leaderboards_for_day[sortby] if day else leaderboards_for_month[sortby] + ) - return header + "\n".join(format_member(i, m) for i, m in enumerate(members, 1)) + try: + members = self._get_members(year, code) + except InvalidHTTPSCode: + await interaction.edit_original_response( + content="Error fetching leaderboard data. Check the leaderboard code and year." + ) + return + except AssertionError: + await interaction.edit_original_response( + content="Error parsing leaderboard data." + ) + return - def format_global_leaderboard(self, members: List[Member]) -> str: - """ - Returns a string representing the global leaderboard of the given list. + if code == UQCS_LEADERBOARD: + message = ":star: *Advent of Code UQCS Leaderboard* :trophy:" + else: + message = f":star: *Advent of Code Leaderboard {code}* :trophy:" - Full leaderboard includes rank, global points, and username. - """ + if day: + message += f"\n:calendar: *Day {day}* (Sorted By {sortby})" + members = [member for member in members if member.attempted_day(day)] + members.sort(key=lambda m: sorting_functions_for_day[sortby](m, day)) + else: + members = [ + member + for member in members + if any(member.attempted_day(day) for day in ADVENT_DAYS) + ] + members.sort(key=sorting_functions_for_month[sortby]) - # 3 4 - # |-| |--| - # 1) 751 Name - def format_member(i: int, m: Member): - return f"{i:>3}) {m.global_:>4} {m.name}" + if not members: + await interaction.edit_original_response( + content="This leaderboard contains no people." + ) + return - return "\n".join(format_member(i, m) for i, m in enumerate(members, 1)) + scoreboard_file = io.BytesIO( + bytes( + print_leaderboard( + parse_leaderboard_column_string(leaderboard_style, self.bot), + members, + day, + ), + "utf-8", + ) + ) + await interaction.edit_original_response( + content=message, + attachments=[ + discord.File( + scoreboard_file, + filename=f"advent_{code}_{year}_{day}.txt", + ) + ], + ) - def format_day_leaderboard(self, members: List[Member]) -> str: + @advent_command_group.command(name="register") + @app_commands.describe( + aoc_name="Your name shown on Advent of Code.", + ) + async def register_command(self, interaction: discord.Interaction, aoc_name: str): """ - Returns a string representing the leaderboard of the given members on - the given day. - - Full leaderboard includes rank, points, stars (per day), and username. + Register for prizes by linking your discord to an Advent of Code name. """ + # TODO: Check UQCS membership + await interaction.response.defer(thinking=True) - def format_seconds(seconds: Optional[int]) -> str: - if seconds is None: - return "" - delta = timedelta(seconds=seconds) - if delta > timedelta(hours=24): - return ">24h" - return str(delta) + id = self._get_unused_registration_id() + db_session = self.bot.create_db_session() + year = datetime.now().year - # 3 8 8 8 - # |-| |------| |------| |------| - # Part 1 Part 2 Delta - # 1) 0:00:00 0:00:00 0:00:00 Name 1 - # 2) >24h >24h >24h Name 2 - def format_member(i: int, m: Member) -> str: - assert m.day is not None - part_1 = format_seconds(m.day_times.get(1)) - part_2 = format_seconds(m.day_times.get(2)) - delta = format_seconds(m.day_delta) - return f"{i:>3}) {part_1:>8} {part_2:>8} {delta:>8} {m.name}" + members = self._get_members(year) + if aoc_name not in [member.name for member in members]: + await interaction.edit_original_response( + content=f"Could not find the Advent of Code name `{aoc_name}` within the UQCS leaderboard." + ) + return + member = [member for member in members if member.name == aoc_name] + if len(member) != 1: + await interaction.edit_original_response( + content=f"Could not find a unique Advent of Code name `{aoc_name}` within the UQCS leaderboard." + ) + member = member[0] + AOC_id = member.id + + if ( + query := db_session.query(AOCRegistrations) + .filter( + and_( + AOCRegistrations.year == year, AOCRegistrations.aoc_userid == AOC_id + ) + ) + .one_or_none() + ) is not None: + await interaction.edit_original_response( + content=f"Advent of Code name `{aoc_name}` is already registered to <@{query.discord_userid}>. Please contact committee if this is your Advent of Code name." + ) + return - header = " Part 1 Part 2 Delta\n" - return header + "\n".join(format_member(i, m) for i, m in enumerate(members, 1)) + discord_id = interaction.user.id + if ( + query := db_session.query(AOCRegistrations) + .filter( + and_( + AOCRegistrations.year == year, + AOCRegistrations.discord_userid == discord_id, + ) + ) + .one_or_none() + ) is not None: + await interaction.edit_original_response( + content=f"Your discord account (<@{discord_id}>) is already registered to the Advent of Code name `{query.aoc_userid}`. You'll need to unregister to change name." + ) + return - def format_advent_leaderboard( - self, members: List[Member], is_day: bool, is_global: bool, sort: SortMode - ) -> str: - """ - Returns a leaderboard for the given members with the given options. + db_session.add( + AOCRegistrations( + id=id, aoc_userid=AOC_id, year=year, discord_userid=discord_id + ) + ) + db_session.commit() + db_session.close() + + await interaction.edit_original_response( + content=f"Advent of Code name `{aoc_name}` is now registered to <@{discord_id}>." + ) - If full is True, leaderboard will show progress for all days, otherwise one - specific day is shown. + @app_commands.default_permissions(manage_guild=True) + @advent_command_group.command(name="register-force") + @app_commands.describe( + year="The year of Advent of Code this registration is for.", + discord_id="The discord ID number of the user.", + aoc_name="The name shown on Advent of Code.", + aoc_id="The AOC id of the user.", + ) + async def register_admin_command( + self, + interaction: discord.Interaction, + year: int, + discord_id: int, + aoc_name: Optional[str] = None, + aoc_id: Optional[int] = None, + ): + """ + Forces a registration entry for the given AOC name, year and discord ID (note this is not their username). For admin use only; assumes you know what you are doing. Either aoc_name or aoc_id should be given. """ + if (aoc_name is None and aoc_id is None) or ( + aoc_name is not None and aoc_id is not None + ): + await interaction.response.send_message( + "Exactly one of `aoc_name` and `aoc_id` must be given.", ephemeral=True + ) + return - if is_day: - # filter to users who have at least one star on this day. - members = [m for m in members if m.day_times] - members.sort(key=Member.sort_key(sort)) - return self.format_day_leaderboard(members) + await interaction.response.defer(thinking=True) - if is_global: - # filter to users who have global points. - members = [m for m in members if m.global_] - members.sort(key=Member.sort_key(SortMode.GLOBAL)) - return self.format_global_leaderboard(members) + id = self._get_unused_registration_id() + db_session = self.bot.create_db_session() - members.sort(key=Member.sort_key(SortMode.LOCAL)) - return self.format_full_leaderboard(members) + if aoc_name: + members = self._get_members(year, force_refresh=True) + if aoc_name not in [member.name for member in members]: + await interaction.edit_original_response( + content=f"Could not find the Advent of Code name `{aoc_name}` within the UQCS leaderboard." + ) + return + member = [member for member in members if member.name == aoc_name] + if len(member) != 1: + await interaction.edit_original_response( + content=f"Could not find a unique Advent of Code name `{aoc_name}` within the UQCS leaderboard." + ) + member = member[0] + aoc_id = member.id + + if ( + query := db_session.query(AOCRegistrations) + .filter( + and_( + AOCRegistrations.year == year, AOCRegistrations.aoc_userid == aoc_id + ) + ) + .one_or_none() + ) is not None: + await interaction.edit_original_response( + content=f"Advent of Code name `{aoc_name}` is already registered to <@{query.discord_userid}>." + ) + return - def parse_arguments(self, argv: List[str]) -> Namespace: - """ - Parses !advent arguments from the given list. + db_session.add( + AOCRegistrations( + id=id, aoc_userid=aoc_id, year=year, discord_userid=discord_id + ) + ) + db_session.commit() + db_session.close() - Returns namespace with argument values or throws UsageSyntaxException. - If an exception is thrown, its message should be shown to the user and - execution should NOT continue. + await interaction.edit_original_response( + content=f"Advent of Code name `{aoc_name}` is now registered to <@{discord_id}> (for {year})." + ) + + @advent_command_group.command(name="unregister") + async def unregister_command(self, interaction: discord.Interaction): + """ + Remove your registration for Advent of code prizes. """ - parser = ArgumentParser("!advent", add_help=False) + await interaction.response.defer(thinking=True) - parser.add_argument( - "day", - type=int, - default=0, - nargs="?", - help="Show leaderboard for specific day" + " (default: all days)", - ) - parser.add_argument( - "-g", - "--global", - action="store_true", - dest="global_", - help="Show global points", + db_session = self.bot.create_db_session() + year = datetime.now().year + + discord_id = interaction.user.id + query = db_session.query(AOCRegistrations).filter( + and_( + AOCRegistrations.year == year, + AOCRegistrations.discord_userid == discord_id, + ) ) - parser.add_argument( - "-y", - "--year", - type=int, - default=datetime.now().year, - help="Year of leaderboard (default: current year)", + if (query.one_or_none()) is None: + await interaction.edit_original_response( + content=f"This discord account (<@{discord_id}>) is already unregistered for this year." + ) + return + + query.delete(synchronize_session=False) + db_session.commit() + db_session.close() + + await interaction.edit_original_response( + content=f"<@{discord_id}> is no longer registered to win Advent of Code prizes." ) - parser.add_argument( - "-c", - "--code", - type=int, - default=UQCS_LEADERBOARD, - help="Leaderboard code (default: UQCS leaderboard)", + + @app_commands.default_permissions(manage_guild=True) + @advent_command_group.command(name="unregister-force") + @app_commands.describe( + year="Year that the registration is for", + discord_id="The discord id to remove. Note that this is not the username.", + ) + async def unregister_admin_command( + self, interaction: discord.Interaction, year: int, discord_id: int + ): + """ + Forces a registration entry to be removed. For admin use only; assumes you know what you are doing. + """ + await interaction.response.defer(thinking=True) + + db_session = self.bot.create_db_session() + query = db_session.query(AOCRegistrations).filter( + and_( + AOCRegistrations.year == year, + AOCRegistrations.discord_userid == discord_id, + ) ) - parser.add_argument( - "-s", - "--sort", - default=SortMode.PART_2, - type=SortMode, - choices=(SortMode.PART_1, SortMode.PART_2, SortMode.DELTA), - help="Sorting method when displaying one day" - + " (default: part 2 completion time)", + if (query.one_or_none()) is None: + await interaction.edit_original_response( + content=f"This discord account (<@{discord_id}>) is already unregistered for this year. Ensure that you enter the users discord id, not discord name or nickname." + ) + return + + query.delete(synchronize_session=False) + db_session.commit() + db_session.close() + + await interaction.edit_original_response( + content=f"<@{discord_id}> is no longer registered to win Advent of Code prizes for {year}." ) - parser.add_argument( - "-h", "--help", action="store_true", help="Prints this help message" + + @advent_command_group.command(name="previous-winners") + @app_commands.describe( + year="Year to find the previous listed winners for.", + show_ids="Whether to show the database ids. Mainly for debugging purposes.", + ) + async def previous_winners_command( + self, interaction: discord.Interaction, year: int, show_ids: bool = False + ): + """ + List the previous winners of Advent of Code. + """ + await interaction.response.defer(thinking=True) + + db_session = self.bot.create_db_session() + prev_winners = list( + db_session.query(AOCWinners).filter(AOCWinners.year == year) ) - # used to propagate usage errors out. - # somewhat hacky. typically, this should be done by subclassing ArgumentParser - def usage_error(message, *args, **kwargs): - raise ValueError(message) - - parser.error = usage_error + if not prev_winners: + await interaction.edit_original_response( + content=f"No Advent of Code winners are on record for {year}." + ) + return - args = parser.parse_args(argv) + registrations = self._get_registrations(year) + registered_AOC_ids = [member.aoc_userid for member in registrations] - if args.help: - raise ValueError("```\n" + parser.format_help() + "\n```") + # TODO would an embed be appropriate? + message = f"UQCS Advent of Code winners for {year}:" + for winner in prev_winners: + message += f"\n{winner.id} " if show_ids else "\n" - return args + name = [ + member.name + for member in self._get_members(year) + if member.id == winner.aoc_userid + ] + # There are three types of user: + # 1) Those who are not on the downloaded members list from AOC (error case) + # 2) Those who have not linked a discord account + # 3) Those who have linked a discord account + if len(name) != 1: + message += f"Unknown User (AOC id {winner.aoc_userid}) - {winner.prize}" + elif winner.aoc_userid not in registered_AOC_ids: + message += f"{name[0]} (unregisted discord) - {winner.prize}" + else: + discord_user = await self.bot.fetch_user( + [user.discord_userid for user in registrations][0] + ) + message += f"{name[0]} (@{discord_user.display_name}) - {winner.prize}" + db_session.commit() + db_session.close() - def get_leaderboard(self, year: int, code: int) -> Optional[Dict]: + await interaction.edit_original_response(content=message) + + @app_commands.default_permissions(manage_guild=True) + @advent_command_group.command(name="add-winners") + @app_commands.describe( + prize="A description of the prize that is being awarded.", + start="The initial date (inclusive) to base the weights on", + end="The final date (includive) to base the weights on", + number_of_winners="The number of winners to select", + weights="How to bias the winner selection.", + allow_repeat_winners="Allow for winners to be selected that already have won this year. Multiple selected winners will always be distinct.", + allow_unregistered_users="Allow winners to be selected from unregistered users (i.e. those who have not linked their discord).", + year="The year the prize is for. Defaults to the current year.", + aoc_id="The AOC id of the winner to add, if selecting a winner.", + ) + async def add_winners_command( + self, + interaction: discord.Interaction, + prize: str, + start: int = 1, + end: int = 25, + number_of_winners: int = 1, + weights: Literal["Stars", "Equal"] = "Equal", + allow_repeat_winners: bool = True, + allow_unregistered_users: bool = False, + year: Optional[int] = None, + aoc_id: Optional[int] = None, + ): """ - Returns a json dump of the leaderboard + Randomly choose (or select) winners from those who have completed challenges. """ - try: - response = requests.get( - LEADERBOARD_URL.format(year=year, code=code), - cookies={"session": self.SESSION_ID}, + + await interaction.response.defer(thinking=True) + if year is None: + year = datetime.now().year + + if aoc_id: + self._add_winners( + [member for member in self._get_members(year) if member.id == aoc_id], + year, + prize, ) - return response.json() - except ValueError as exception: # json.JSONDecodeError - # TODO: Handle the case when the response is ok but the contents - # are invalid (cannot be parsed as json) - raise exception - except RequestException as exception: - logging.error(exception.response.content) - pass - return None + await interaction.edit_original_response( + content=f"The user with AOC id {aoc_id} has been recorded as winning a prize: {prize}" + ) + return - @commands.command() - @loading_status - async def advent(self, ctx: commands.Context, *args): - """ - Prints the Advent of Code private leaderboard for UQCS. + registrations = self._get_registrations(year) + registered_AOC_ids = [member.aoc_userid for member in registrations] - !advent --help for additional help. - """ + potential_winners = [ + member + for member in self._get_members(year) + if any(member.attempted_day(day) for day in range(start, end + 1)) + ] + if not allow_unregistered_users: + potential_winners = [ + member + for member in potential_winners + if member.id in registered_AOC_ids + ] - try: - args = self.parse_arguments(args) - except ValueError as error: - await ctx.send(str(error)) + required_number_of_potential_winners = ( + 1 if allow_repeat_winners else number_of_winners + ) + if len(potential_winners) < required_number_of_potential_winners: + await interaction.edit_original_response( + content=f"There were not enough eligible users to select winners (at least {required_number_of_potential_winners} needed; only {len(potential_winners)} found)." + ) return - try: - leaderboard = self.get_leaderboard(args.year, args.code) - except ValueError: - await ctx.send( - "Error fetching leaderboard data. Check the leaderboard code, year, and day." + match weights: + case "Stars": + weight_values = [ + sum(len(member.times[day]) for day in range(start, end + 1)) + for member in potential_winners + ] + case "Equal": + weight_values = [1 for _ in potential_winners] + + if allow_repeat_winners: + winners = choices(potential_winners, weight_values, k=number_of_winners) + else: + winners = self._random_choices_without_repition( + potential_winners, weight_values, number_of_winners ) - raise - try: - members = [ - Member.from_member_data(data, args.year, args.day) - for data in leaderboard["members"].values() - ] - except Exception: - await ctx.send("Error parsing leaderboard data.") - raise + if not winners: + await interaction.edit_original_response( + content="There was some problem choosing the winners." + ) + return - # whether to show only one day - is_day = bool(args.day) - # whether to use global points - is_global = args.global_ + self._add_winners(winners, year, prize) - # header message - message = f":star: *Advent of Code Leaderboard {args.code}* :trophy:" - if is_day: - message += ( - f"\n:calendar: *Day {args.day}* (sorted by {SORT_LABELS[args.sort]})" + distinct_winners = set(winners) + if len(distinct_winners) == 1: + (winner,) = distinct_winners + discord_id = winner.get_discord_userid(self.bot) + discord_ping = f" (<@{discord_id})" if discord_id else "" + await interaction.edit_original_response( + content=f"The results are in! Out of {len(potential_winners)} potential participants, {winner.name}{discord_ping} has recieved a prize from participating in Advent of Code: {prize}" ) - elif is_global: - message += "\n:earth_asia: *Global Leaderboard Points*" + return - scoreboardFile = io.StringIO( - self.format_advent_leaderboard(members, is_day, is_global, args.sort) + winners_message = "" + for i, winner in enumerate(distinct_winners): + discord_id = winner.get_discord_userid(self.bot) + discord_ping = f" (<@{discord_id})" if discord_id else "" + number_of_prizes = len( + [member for member in winners if member.id == winner.id] + ) + prize_multiplier = f" (x{number_of_prizes})" if number_of_prizes > 1 else "" + winners_message += f"{winner.name}{discord_ping}{prize_multiplier}" + winners_message += ", " if i < len(distinct_winners) - 1 else " and " + + await interaction.edit_original_response( + content=f"The results are in! Out of {len(potential_winners)} potential participants, {winners_message} have recieved a prize from participating in Advent of Code: {prize}" ) - await ctx.send( - file=discord.File( - scoreboardFile, - filename=f"advent_{args.code}_{args.year}_{args.day}.txt", + + @app_commands.default_permissions(manage_guild=True) + @advent_command_group.command(name="remove-winner") + @app_commands.describe( + id="The database entry id for the winners database that should be deleted." + ) + async def remove_winner_command(self, interaction: discord.Interaction, id: int): + """ + Remove an AOC winner from the database. Use the show_ids option within previous-winners to get the id. + """ + await interaction.response.defer(thinking=True) + + db_session = self.bot.create_db_session() + + query = db_session.query(AOCWinners).filter(AOCWinners.id == id) + if query.one_or_none() is None: + await interaction.response.send_message( + f"No Advent of Code winners could be found with a database id of {id}." ) + return + + query.delete(synchronize_session=False) + db_session.commit() + db_session.close() + + await interaction.edit_original_response( + content=f"Removed the winners entry with id {id}." + ) + + def _get_leaderboard_json(self, year: int, code: int) -> Json: + """ + Returns a json dump of the leaderboard + """ + try: + response = requests.get( + LEADERBOARD_URL.format(year=year, code=code), + cookies={"session": self.session_id}, + ) + except RequestException as exception: + raise FatalErrorWithLog( + self.bot, + f"Could not get the leaderboard from Advent of Code. For more information {exception}", + ) + if response.status_code != 200: + raise InvalidHTTPSCode( + "Expected a HTTPS status code of 200.", response.status_code + ) + try: + return response.json() + except ValueError as exception: # json.JSONDecodeError + raise FatalErrorWithLog( + self.bot, + f"Could not interpret the JSON from Advent of Code (AOC). This suggests that AOC no longer provides JSON or something went very wrong. For more information: {exception}", + ) + + def _get_members( + self, year: int, code: int = UQCS_LEADERBOARD, force_refresh: bool = False + ): + """ + Returns the list of members in the leaderboard for the given year and leaderboard code. It will attempt to retrieve from a cache if 15 minutes has not passed. This can be overriden by setting force refresh. + """ + if ( + force_refresh + or (datetime.now() - self.last_reload_time >= CACHE_TIME) + or year not in self.members_cache + ): + leaderboard = self._get_leaderboard_json(year, code) + self.members_cache[year] = [ + Member.from_member_data(data, year) + for data in leaderboard["members"].values() + ] + return self.members_cache[year] + + def _get_registrations(self, year: int) -> Iterable[AOCRegistrations]: + """ + Get all registrations linking an AOC id to a discord account. + """ + db_session = self.bot.create_db_session() + registrations = db_session.query(AOCRegistrations).filter( + AOCRegistrations.year == year ) + db_session.commit() + db_session.close() + return registrations async def reminder_fifteen_minutes(self): + """ + The function used within the AOC reminder 15 minutes before each challenge starts. + """ channel = discord.utils.get( - self.bot.uqcs_server.channels, name=self.CHANNEL_NAME + self.bot.uqcs_server.channels, name=self.bot.AOC_CNAME ) - if channel is not None: - await channel.send( - "Today's Advent of Code puzzle is released in 15 minutes." + if channel is None: + logging.warning(f"Could not find required channel #{self.bot.AOC_CNAME}") + return + if not isinstance(channel, discord.TextChannel): + logging.warning( + f"Channel #{self.bot.AOC_CNAME} was expected to be a text channel, but was not" ) - else: - logging.warning(f"Could not find required channel #{self.CHANNEL_NAME}") + return + await channel.send("Today's Advent of Code puzzle is released in 15 minutes.") async def reminder_released(self): + """ + The function used within the AOC reminder when each challenge starts. + """ channel = discord.utils.get( - self.bot.uqcs_server.channels, name=self.CHANNEL_NAME + self.bot.uqcs_server.channels, name=self.bot.AOC_CNAME ) - if channel is not None: - await channel.send( - "Today's Advent of Code puzzle has been released. Good luck!" + if channel is None: + logging.warning(f"Could not find required channel #{self.bot.AOC_CNAME}") + return + if not isinstance(channel, discord.TextChannel): + logging.warning( + f"Channel #{self.bot.AOC_CNAME} was expected to be a text channel, but was not" ) - else: - logging.warning(f"Could not find required channel #{self.CHANNEL_NAME}") + return + await channel.send( + "Today's Advent of Code puzzle has been released. Good luck!" + ) - def _get_previous_winners(self, year: int): + def _get_previous_winner_aoc_ids(self, year: int) -> List[int]: + """ + Returns a list of all winner aoc ids for a year + """ db_session = self.bot.create_db_session() - prev_winners = db_session.query(AOCWinner).filter(AOCWinner.year == year) + prev_winners = db_session.query(AOCWinners).filter(AOCWinners.year == year) + db_session.commit() db_session.close() return [winner.aoc_userid for winner in prev_winners] - def _add_winners(self, winners: List[Member], year: int): - db_session = self.bot.create_db_session() - + def _add_winners(self, winners: List[Member], year: int, prize: str): + """ + Add all members within the list to the database + """ for winner in winners: - winner = AOCWinner(aoc_userid=winner.id, year=year) - db_session.add(winner) - - db_session.commit() - db_session.close() + id = self._get_unused_winner_id() + db_session = self.bot.create_db_session() + db_session.add( + AOCWinners(id=id, aoc_userid=winner.id, year=year, prize=prize) + ) + db_session.commit() + db_session.close() - def random_choices_without_repition(self, population, weights, k): - result = [] + def _random_choices_without_repition( + self, population: List[Member], weights: List[int], k: int + ) -> List[Member]: + result: List[Member] = [] for _ in range(k): if sum(weights) == 0: - return None + return [] result.append(choices(population, weights)[0]) index = population.index(result[-1]) @@ -481,76 +944,284 @@ def random_choices_without_repition(self, population, weights, k): return result - @commands.command() - @loading_status - async def advent_winners( - self, ctx: commands.Context, start: int, end: int, numberOfWinners: int, *args + def _get_unused_winner_id(self) -> int: + """Returns a AOCWinner id that is not currently in use""" + db_session = self.bot.create_db_session() + prev_winners = db_session.query(AOCWinners) + db_session.commit() + db_session.close() + winner_ids = [winner.id for winner in prev_winners] + i = 1 + while (id := i) in winner_ids: + i += 1 + return id + + def _get_unused_registration_id(self) -> int: + """Returns a AOCRegistration id that is not currently in use""" + db_session = self.bot.create_db_session() + prev_registrations = db_session.query(AOCRegistrations) + db_session.commit() + db_session.close() + registration_ids = [registration.id for registration in prev_registrations] + i = 1 + while (id := i) in registration_ids: + i += 1 + return id + + +class LeaderboardColumn: + """ + A column in a leaderboard. The title is the name of the column as 2 lines and the calculation is a function that determines what is printed for a given member, index and day. The title and calculation should have the same constant width. + """ + + def __init__( + self, + title: tuple[str, str], + calculation: Callable[[Member, int, Optional[Day]], str], ): + self.title = title + self.calculation = calculation + + @staticmethod + def ordering_column(): + """ + A column used at the right of leaderboards to indicate the overall order. Of the format "XXX)" where XXX is a left padded number of 3 characters. + """ + return LeaderboardColumn( + title=(" " * 4, " " * 4), # Empty spaces, as this does not need a heading + calculation=lambda _, index, __: f"{index:>3})", + ) + + @staticmethod + def star1_column(): """ - Determines winners for the AOC competition. Winners must be drawn by a member of the committee. + A column indicating the time taken to achieve the first star. Of the format "hh:mm:ss" or ">24h". Only applicable for particular days. + """ + return LeaderboardColumn( + title=(" " * 8, " Star 1 "), + calculation=lambda member, _, day: f"{_format_seconds(member.times[day].get(1, 0)) if day else '':>8}", + ) - !advent --help for additional help. + @staticmethod + def star2_column(): """ - if len([role for role in ctx.author.roles if role.name == "Committee"]) == 0: - await ctx.send("Only committee can select the winners") - return + A column indicating the time taken to achieve only the second star. Of the format "hh:mm:ss" or ">24h". Only applicable for particular days. + """ + return LeaderboardColumn( + title=(" " * 8, " Star 2 "), + calculation=lambda member, _, day: f"{_format_seconds(member.get_time_delta(day)) if day else '':>8}", + ) - try: - args = self.parse_arguments(args) - except ValueError as error: - await ctx.send(str(error)) - return + @staticmethod + def star1_and_2_column(): + """ + A column indicating the time taken to achieve both stars. Of the format "hh:mm:ss" or ">24h". Only applicable for particular days. + """ + return LeaderboardColumn( + title=(" " * 10, "Both Stars"), + calculation=lambda member, _, day: f"{_format_seconds(member.times[day].get(2, 0)) if day else '':>10}", + ) - try: - leaderboard = self.get_leaderboard(args.year, args.code) - except ValueError: - await ctx.send( - "Error fetching leaderboard data. Check the leaderboard code, year, and day." - ) - raise + @staticmethod + def total_time_column(): + """ + A column indicating the total time the user has spent on all stars. Of the format "hhhh:mm:ss" or ">30 days". + """ + return LeaderboardColumn( + title=(" " * 10, "Total Time"), + calculation=lambda member, _, __: f"{_format_seconds_long(member.get_total_time()):>10}", + ) - try: - members = [ - Member.from_member_data(data, args.year, args.day) - for data in leaderboard["members"].values() - ] - except Exception: - await ctx.send("Error parsing leaderboard data.") - raise + @staticmethod + def total_star1_time_column(): + """ + A column indicating the total time the user has spent on first stars. Of the format "hhhh:mm:ss" or ">30 days". + """ + return LeaderboardColumn( + title=("Total Star", " 1 Time "), + calculation=lambda member, _, __: f"{_format_seconds_long(member.get_total_star1_time()):>10}", + ) - previous_winners = self._get_previous_winners(args.year) - potential_winners = [ - member for member in members if int(member.id) not in previous_winners - ] - weights = [ - sum([1 for d in range(start, end + 1) if len(member.all_times[d]) > 0]) - for member in potential_winners - ] + @staticmethod + def total_star2_time_column(): + """ + A column indicating the total time the user has spent on second stars. Of the format "hhhh:mm:ss" or ">30 days". + """ + return LeaderboardColumn( + title=("Total Star", " 2 Time "), + calculation=lambda member, _, __: f"{_format_seconds_long(member.get_total_star2_time()):>10}", + ) - winners = self.random_choices_without_repition( - potential_winners, weights, numberOfWinners + @staticmethod + def stars_column(): + """ + A column indicating the total number of stars a user has. Of the format of a 5 character right-padded number. + """ + return LeaderboardColumn( + title=("Total", "Stars"), + calculation=lambda member, _, __: f"{member.star_total if member.star_total else '':>5}", ) - if winners == None: - await ctx.send( - f"Insufficient participants to be able to draw {numberOfWinners} winners." - ) - return + @staticmethod + def local_rank_column(): + """ + A column indicating the members local rank (of the UQCS leaderboard). Of the format of a 5 character right-padded number. + """ + return LeaderboardColumn( + title=("Local", "Order"), + calculation=lambda member, _, __: f"{member.local if member.local else '':>5}", + ) - self._add_winners(winners, args.year) + @staticmethod + def global_score_column(): + """ + A column indicating the members global score. Of the format of a 5 character right-padded number. + """ + return LeaderboardColumn( + title=("Global", "Score "), + calculation=lambda member, _, __: f"{member.global_ if member.global_ else '':>6}", + ) - await ctx.send( - "And the winners are:\n" - + "\n".join( - [ - winner.name - if (winner.name != None) - else "anonymous user #" + str(winner.id) - for winner in winners - ] - ) + @staticmethod + def star_bar_column(): + """ + A column with a progressbar of the stars that each person has. + """ + return LeaderboardColumn( + title=(" " * 9 + "1" * 10 + "2" * 6, "1234567890123456789012345"), + calculation=lambda member, _, __: _get_member_star_progress_bar(member), ) + @staticmethod + def name_column(bot: UQCSBot): + """ + A column listing each name. + """ + + def format_name(member: Member, _: int, __: Optional[int]) -> str: + if not (discord_userid := member.get_discord_userid(bot)): + return member.name + if not (discord_user := bot.get_user(discord_userid)): + return member.name + return f"{member.name} (@{discord_user.name})" + + return LeaderboardColumn(title=("", ""), calculation=format_name) + + @staticmethod + def padding_column(): + """ + A column that is of a single space character. + """ + return LeaderboardColumn(title=(" ", " "), calculation=lambda _, __, ___: " ") + + +def parse_leaderboard_column_string(s: str, bot: UQCSBot) -> List[LeaderboardColumn]: + """ + Create a list of columns corresponding to the given string. The characters in the string can be: + # - Provides a column of the form "XXX)" telling the order for the given leaderboard + 1 - The time for star 1 for the specific day (daily leaderboards only) + 2 - The time for star 2 for the specific day (daily leaderboards only) + 3 - The time for both stars for the specific day (dayly leaderboards only) + ! - The total time spent on first stars for the whole competition + @ - The total time spent on second stars for the whole competition + T - The total time spent overall for the whole competition + * - The total number of stars for the whole competition + L - The local ranking someone has within the UQCS leaderboard + G - The global score someone has + B - A progress bar of the stars each person has + space - A padding column of a single character + All other characters will be ignored + """ + columns: List[LeaderboardColumn] = [] + for c in s: + match c: + case "#": + columns.append(LeaderboardColumn.ordering_column()) + case "1": + columns.append(LeaderboardColumn.star1_column()) + case "2": + columns.append(LeaderboardColumn.star2_column()) + case "3": + columns.append(LeaderboardColumn.star1_and_2_column()) + case "!": + columns.append(LeaderboardColumn.total_star1_time_column()) + case "@": + columns.append(LeaderboardColumn.total_star2_time_column()) + case "T": + columns.append(LeaderboardColumn.total_time_column()) + case "*": + columns.append(LeaderboardColumn.stars_column()) + case "L": + columns.append(LeaderboardColumn.local_rank_column()) + case "G": + columns.append(LeaderboardColumn.global_score_column()) + case "B": + columns.append(LeaderboardColumn.star_bar_column()) + case " ": + columns.append(LeaderboardColumn.padding_column()) + case _: + pass + columns.append(LeaderboardColumn.padding_column()) + columns.append(LeaderboardColumn.name_column(bot)) + return columns + + +def _star_char(num_stars: int): + """ + Given a number of stars (0, 1, or 2), returns its leaderboard + representation. + """ + return " .*"[num_stars] + + +def _format_seconds(seconds: Optional[int]): + """ + Format seconds into the format "hh:mm:ss" or ">24h". + """ + if seconds is None or seconds == 0: + return "" + delta = timedelta(seconds=seconds) + if delta > timedelta(hours=24): + return ">24h" + return str(delta) + + +def _format_seconds_long(seconds: Optional[int]): + """ + Format seconds into the format "hhhh:mm:ss" or ">30 days". + """ + if seconds is None or seconds == 0: + return "-" + hours, remainder = divmod(seconds, 3600) + minutes, seconds = divmod(remainder, 60) + if hours >= 30 * 24: + return ">30 days" + return f"{hours}:{minutes:02}:{seconds:02}" + + +def _get_member_star_progress_bar(member: Member): + return "".join(_star_char(len(member.times[day])) for day in ADVENT_DAYS) + + +def print_leaderboard( + columns: List[LeaderboardColumn], members: List[Member], day: Optional[Day] +): + """ + Returns a string of the leaderboard of the given format. + """ + leaderboard = "".join(column.title[0] for column in columns) + leaderboard += "\n" + leaderboard += "".join(column.title[1] for column in columns) + + # Note that leaderboards start at 1, not 0 + for id, member in enumerate(members, start=1): + leaderboard += "\n" + leaderboard += "".join( + column.calculation(member, id, day) for column in columns + ) + + return leaderboard + async def setup(bot: UQCSBot): cog = Advent(bot) diff --git a/uqcsbot/bot.py b/uqcsbot/bot.py index e2b045cf..3a1af1c2 100644 --- a/uqcsbot/bot.py +++ b/uqcsbot/bot.py @@ -29,6 +29,7 @@ def __init__(self, *args: Any, **kwargs: Any): # Important channel names & constants go here self.ADMIN_ALERTS_CNAME = "admin-alerts" self.GENERAL_CNAME = "general" + self.AOC_CNAME = "contests" self.BOT_TIMEZONE = timezone("Australia/Brisbane") self.uqcs_server: discord.Guild diff --git a/uqcsbot/models.py b/uqcsbot/models.py index 44a5dd2a..ab8227a4 100644 --- a/uqcsbot/models.py +++ b/uqcsbot/models.py @@ -16,12 +16,24 @@ class Base(DeclarativeBase): pass -class AOCWinner(Base): - __tablename__ = "aoc_winner" +class AOCWinners(Base): + __tablename__ = "aoc_winners" id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False) aoc_userid: Mapped[int] = mapped_column("aoc_userid", Integer, nullable=False) year: Mapped[int] = mapped_column("year", Integer, nullable=False) + prize: Mapped[str] = mapped_column("prize", String, nullable=True) + + +class AOCRegistrations(Base): + __tablename__ = "aoc_registrations" + + id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False) + aoc_userid: Mapped[int] = mapped_column("aoc_userid", Integer, nullable=False) + year: Mapped[int] = mapped_column("year", Integer, nullable=False) + discord_userid: Mapped[int] = mapped_column( + "discord_userid", BigInteger, nullable=False + ) class MCWhitelist(Base):