From 27e709c41f0ad82e95fb8201024ebae4b752c3f9 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Thu, 13 Jul 2023 17:05:48 +1000 Subject: [PATCH 01/10] 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): From 769592f2f370ade17a6e874c61eea6e50764840f Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Tue, 21 Nov 2023 10:24:48 +1000 Subject: [PATCH 02/10] Added detail to advent help menu --- tests/test_advent.py | 0 tests/testfiles/test_advent.json | 8003 ------------------------------ uqcsbot/advent.py | 256 +- 3 files changed, 198 insertions(+), 8061 deletions(-) delete mode 100644 tests/test_advent.py delete mode 100644 tests/testfiles/test_advent.json diff --git a/tests/test_advent.py b/tests/test_advent.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/testfiles/test_advent.json b/tests/testfiles/test_advent.json deleted file mode 100644 index 6637548a..00000000 --- a/tests/testfiles/test_advent.json +++ /dev/null @@ -1,8003 +0,0 @@ -{ - "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 53d2c390..5fc68ed5 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -120,20 +120,20 @@ def get_total_star1_time(self, default: int = 0) -> int: 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: + def get_total_star2_time(self, default: int = 0) -> int: """ - Returns the total time working on stars 1 and 2 for all challenges in a year. + 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 = self.get_total_star1_time() + self.get_total_star2_time() + total = sum(self.times[day].get(2, 0) for day in ADVENT_DAYS) return total if total != 0 else default - def get_total_star2_time(self, default: int = 0) -> int: + def get_total_time(self, default: int = 0) -> int: """ - Returns the total time working on just star 2 for all challenges in a year. + 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 = sum(self.times[day].get(2, 0) for day in ADVENT_DAYS) + total = self.get_total_star1_time() + self.get_total_star2_time() return total if total != 0 else default def get_discord_userid(self, bot: UQCSBot) -> Optional[int]: @@ -305,22 +305,138 @@ def __init__(self, bot: UQCSBot): name="advent", description="Commands for Advent of Code" ) + Command = Literal[ + "help", + "leaderboard", + "register", + "register-force", + "unregister", + "unregister-force", + "previous-winners", + "new-winner", + "remove-winner", + "leaderboard_style", + ] + @advent_command_group.command(name="help") - async def help_command(self, interaction: discord.Interaction): + @app_commands.describe(command="The command you want to view help about.") + async def help_command( + self, interaction: discord.Interaction, command: Optional[Command] = None + ): """ Print a help message about advent of code. """ - 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." - ) + match command: + case None: + 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. + +To 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. + +For more help, you can use `/advent help ` to get information about a specific command. + """ + ) + case "help": + await interaction.response.send_message( + """ +`/advent help` is a help menu for all the Advent of Code commands. If you use `/advent help ` you can see details of a particular command. Not much else to say here, try another command. + """ + ) + case "leaderboard": + await interaction.response.send_message( + """ +`/advent leaderboard` displays a leaderboard for the Advent of Code challenges. There are two types of leaderboard: for a single day, and for the entire month. These are selected by either providing the `day` option or not. You can also display the leaderboard for a past year or another leaderboard (say another private leaderboard that you have). + +There are 6 different sorting options, which do slightly different things depending on whether the leaderboard is for a single day or an entire month. The default sorting method changes on which type of leaderboard you want. + `Star 1 Time ` - For single-day leaderboards, this sorts by the shortest time to get star 1 for the given problem. For monthly leaderboards, this sorts by the shortest total star 1 time for all problems. + `Star 2 Time ` - For single-day leaderboards, this sorts by the shortest time to get just star 2 for the given problem. For monthly leaderboards, this sorts by the shortest total star 2 time for all problems. + `Star 1 & 2 Time` - For single-day leaderboards, this sorts by the shortest time to get both stars 1 and 2 for the given problem. For monthly leaderboards, this sorts by the shortest total time working on all the problems. + `Total Time ` - This sorts by the sortest total time working on all the problems. For monthly leaderboards, this is the same as `Star 1 & 2 Time`. + `Total Stars ` - This sorts by the largest number of total stars collected over the month. + `Global ` - This sorts by users global score. Note that this will only show users with global score. + +You can also style the leaderboard (i.e. change the columns). The default style will change depending on whether the leaderboard is for a single-day or the entire month, and depending on the sorting method. Styles consist of a string, with each character representing a column. Use `/advent help leaderboard-style` to see the possoble characters. + """ + ) + case "leaderboard_style": + await interaction.response.send_message( + """ +Not a command, but an option given to the command `/advent leaderboard` controling the columns in the leaderboard. Each character in the given string represents a certain column. The possible characters are: +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 (daily 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. + """ + ) + case "register": + await interaction.response.send_message( + """ +`/advent register` links an Advent of Code account and a discord user so that you are eligble for prizes. Each Advent of Code account and discord account can only be linked to one other account each year. Note that registrations last for only the current year. If you are having any issues with this, message committee to help. + """ + ) + case "register-force": + await interaction.response.send_message( + """ +`/advent register-force` is an admin-only command to force a registration (i.e. create a registration between any Advent of Code account and Discord user). This can be used for moderation, if someone is having difficulties registering or if you want to register someone for a previous year. This command can break things (such as creating duplicate registrations), so be careful. Exactly one of `aoc_name` or `aoc_id` should be given. Also note that you need to use the Discord ID, not the discord username. If you have developer options enables on your account, this can be found by right clicking on the user and selecting `Copy User ID`. + """ + ) + case "unregister": + await interaction.response.send_message( + """ +`/advent unregister` unlinks your discord account from the currently linked Advent of Code account. Message committee if you need any help. + """ + ) + case "unregister-force": + await interaction.response.send_message( + """ +`/advent unregister-force` is an admin-only command that removes a registration from the database. This can be used as a moderation tool, to remove someone who has registered to an Advent of Code account that isn't there. Note that you need to use the Discord ID, not the discord username. If you have developer options enables on your account, this can be found by right clicking on the user and selecting `Copy User ID`. + """ + ) + case "previous-winners": + await interaction.response.send_message( + """ +`/advent previous-winners` displays the previous winners for a particular year. Note that the records for year prior to 2022 may not be accurate, as the current system was not in use then. + """ + ) + case "new-winner": + await interaction.response.send_message( + """ +`/advent add-winner` is an admin-only command that allows you to either manually or randomly select winners. Participants will only be eligible to win if they have completed at least one star within the given times. For manual selection, provide an Advent of Code user ID (note that this is not the same as their Advent of Code name), otherwise a random winner will be drawn. + +The arguments for the command have a bit of nuance. They are as follow: + `prize ` - A description of the prize to be given. This will be displayed when the winner is selected and if `/advent previous-winners` is used. + `start` & `end ` - The initial and final dates (inclusive) of the time range of the prize. To be eligible to win, participants need to get a star from ode of these days. The weights of the selected winner are determined from this range as well. + `number_of_winners ` - The number of winners to randomly select. + `allow_repeat_winners ` - This allows participants to win multiple times from the same selection if `number_of_winners` is greater than 1. Note that regardless of this option, someone can win multiple times in a year, just not in a single selection. + `allow_unregistered_users` - This allows Advent of Code accounts that do not have a linked discord account to win. Note that it can be difficult to give out prizes to users that do not have a linked discord. + `year ` - The year the prize is for. + """ + ) + case "remove-winner": + await interaction.response.send_message( + """ +`/advent remove-winner` is an admin-only command that removes a winner from the database. It uses the database ID (which is distinct from the Advent of code user ID and the Discord user ID). You can find these ids by running `/advent previous-winners show_ids:True`. + """ + ) @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.", + day="Day of the leaderboard [1-25]. If not given (default), the entire month leaderboard is given.", 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 + sortby="The method to sort the leaderboard.", + leaderboard_style="The display format of the leaderboard. See the help menu for more information.", ) async def leaderboard_command( self, @@ -332,8 +448,13 @@ async def leaderboard_command( leaderboard_style: Optional[str] = None, ): """ - Display the advent of code leaderboard. + Display an advent of code leaderboard. """ + if (not day is None) and (day not in ADVENT_DAYS): + await interaction.response.send_message( + "The day given is not a valid advent of code day." + ) + return await interaction.response.defer(thinking=True) @@ -384,8 +505,8 @@ async def leaderboard_command( scoreboard_file = io.BytesIO( bytes( - print_leaderboard( - parse_leaderboard_column_string(leaderboard_style, self.bot), + _print_leaderboard( + _parse_leaderboard_column_string(leaderboard_style, self.bot), members, day, ), @@ -431,23 +552,24 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str member = member[0] AOC_id = member.id - if ( - query := db_session.query(AOCRegistrations) + query = ( + db_session.query(AOCRegistrations) .filter( and_( AOCRegistrations.year == year, AOCRegistrations.aoc_userid == AOC_id ) ) .one_or_none() - ) is not None: + ) + if query 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 discord_id = interaction.user.id - if ( - query := db_session.query(AOCRegistrations) + query = ( + db_session.query(AOCRegistrations) .filter( and_( AOCRegistrations.year == year, @@ -455,7 +577,8 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str ) ) .one_or_none() - ) is not None: + ) + if query 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." ) @@ -477,7 +600,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str @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.", + discord_id="The discord ID number of the user. Note that this is not their username.", aoc_name="The name shown on Advent of Code.", aoc_id="The AOC id of the user.", ) @@ -490,7 +613,7 @@ async def register_admin_command( 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. + Forces a registration entry to be created. For admin use only. 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 @@ -520,15 +643,16 @@ async def register_admin_command( member = member[0] aoc_id = member.id - if ( - query := db_session.query(AOCRegistrations) + query = ( + db_session.query(AOCRegistrations) .filter( and_( AOCRegistrations.year == year, AOCRegistrations.aoc_userid == aoc_id ) ) .one_or_none() - ) is not None: + ) + if query is not None: await interaction.edit_original_response( content=f"Advent of Code name `{aoc_name}` is already registered to <@{query.discord_userid}>." ) @@ -565,7 +689,7 @@ async def unregister_command(self, interaction: discord.Interaction): ) 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." + content=f"Your discord account (<@{discord_id}>) is already unregistered for this year." ) return @@ -587,7 +711,8 @@ 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. + Forces a registration entry to be removed. + For admin use only; assumes you know what you are doing. """ await interaction.response.defer(thinking=True) @@ -614,16 +739,21 @@ async def unregister_admin_command( @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.", + year="Year to find the previous listed winners for. Defaults to the current year.", + show_ids="Whether to show the database ids. Mainly for debugging purposes. Defaults to false.", ) async def previous_winners_command( - self, interaction: discord.Interaction, year: int, show_ids: bool = False + self, + interaction: discord.Interaction, + year: Optional[int] = None, + show_ids: bool = False, ): """ List the previous winners of Advent of Code. """ await interaction.response.defer(thinking=True) + if year is None: + year = datetime.now().year db_session = self.bot.create_db_session() prev_winners = list( @@ -671,14 +801,14 @@ async def previous_winners_command( @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).", + start="The initial date (inclusive) to base the weights on. Defaults to 1.", + end="The final date (includive) to base the weights on. Defaults to 25.", + number_of_winners="The number of winners to select. Defaults to 1.", + weights='How to bias the winner selection. Defaults to "Equal"', + allow_repeat_winners="Allow for winners to be selected multiple times. Defaults to False", + allow_unregistered_users="Allow winners to be selected from unregistered users. Defaults to False.", 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.", + aoc_id="The AOC id of the winner to add, if selecting a winner. Use only if manually selecting a winner.", ) async def add_winners_command( self, @@ -688,7 +818,7 @@ async def add_winners_command( end: int = 25, number_of_winners: int = 1, weights: Literal["Stars", "Equal"] = "Equal", - allow_repeat_winners: bool = True, + allow_repeat_winners: bool = False, allow_unregistered_users: bool = False, year: Optional[int] = None, aoc_id: Optional[int] = None, @@ -707,6 +837,7 @@ async def add_winners_command( year, prize, ) + # Note that this message is a bit more dull, as it should only be used for admin reasons. await interaction.edit_original_response( content=f"The user with AOC id {aoc_id} has been recorded as winning a prize: {prize}" ) @@ -727,9 +858,11 @@ async def add_winners_command( if member.id in registered_AOC_ids ] - required_number_of_potential_winners = ( - 1 if allow_repeat_winners else number_of_winners - ) + if allow_repeat_winners: + required_number_of_potential_winners = 1 + else: + required_number_of_potential_winners = 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)." @@ -792,7 +925,8 @@ async def add_winners_command( ) 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. + Remove an AOC winner from the database. + The show_ids option for previous-winners can get the id. """ await interaction.response.defer(thinking=True) @@ -843,7 +977,9 @@ 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. + 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 @@ -932,6 +1068,10 @@ def _add_winners(self, winners: List[Member], year: int, prize: str): def _random_choices_without_repition( self, population: List[Member], weights: List[int], k: int ) -> List[Member]: + """ + Selects k people from a list of members, weighted by weights. + The weight of a person is like how many tickets they have for the lottery. + """ result: List[Member] = [] for _ in range(k): if sum(weights) == 0: @@ -1115,20 +1255,20 @@ def padding_column(): return LeaderboardColumn(title=(" ", " "), calculation=lambda _, __, ___: " ") -def parse_leaderboard_column_string(s: str, bot: UQCSBot) -> List[LeaderboardColumn]: +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 + # - 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 """ @@ -1203,7 +1343,7 @@ def _get_member_star_progress_bar(member: Member): return "".join(_star_char(len(member.times[day])) for day in ADVENT_DAYS) -def print_leaderboard( +def _print_leaderboard( columns: List[LeaderboardColumn], members: List[Member], day: Optional[Day] ): """ From 582826b53ec98f335ce8690c447f208e64fe21e0 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Tue, 21 Nov 2023 10:32:22 +1000 Subject: [PATCH 03/10] Added weights to advent help menu --- uqcsbot/advent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index 5fc68ed5..ea222daf 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -417,6 +417,7 @@ async def help_command( The arguments for the command have a bit of nuance. They are as follow: `prize ` - A description of the prize to be given. This will be displayed when the winner is selected and if `/advent previous-winners` is used. `start` & `end ` - The initial and final dates (inclusive) of the time range of the prize. To be eligible to win, participants need to get a star from ode of these days. The weights of the selected winner are determined from this range as well. + `weights ` - How the winners are selected. For "Equal", each eligible participant has an equal probability of winning. For "Stars", it is as if each user gets a "raffle ticket" for each star they completed within the timeframe, meaning more stars provides a greater chance of winning. `number_of_winners ` - The number of winners to randomly select. `allow_repeat_winners ` - This allows participants to win multiple times from the same selection if `number_of_winners` is greater than 1. Note that regardless of this option, someone can win multiple times in a year, just not in a single selection. `allow_unregistered_users` - This allows Advent of Code accounts that do not have a linked discord account to win. Note that it can be difficult to give out prizes to users that do not have a linked discord. From 9a983655996536db32d97dd41adb791d3b93a233 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Tue, 21 Nov 2023 16:18:57 +1000 Subject: [PATCH 04/10] Used the proper discord mention property and refactored a little --- uqcsbot/advent.py | 81 +++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index ea222daf..16cd4b9b 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -330,7 +330,7 @@ async def help_command( case None: 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. +[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 private leaderboard with prizes on offer. To 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. @@ -563,8 +563,13 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str .one_or_none() ) if query is not None: + discord_user = self.bot.uqcs_server.get_member(query.discord_userid) + if discord_user: + discord_ping = discord_user.mention + else: + discord_ping = f"someone who doesn't seem to be in the server (discord id = {query.discord_userid})" 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." + content=f"Advent of Code name `{aoc_name}` is already registered to {discord_ping}. Please contact committee if this is your Advent of Code name." ) return @@ -581,7 +586,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str ) if query 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." + content=f"Your discord account ({interaction.user.mention}) is already registered to the Advent of Code name `{query.aoc_userid}`. You'll need to unregister to change name." ) return @@ -594,10 +599,10 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str db_session.close() await interaction.edit_original_response( - content=f"Advent of Code name `{aoc_name}` is now registered to <@{discord_id}>." + content=f"Advent of Code name `{aoc_name}` is now registered to {interaction.user.mention}." ) - @app_commands.default_permissions(manage_guild=True) + @app_commands.checks.has_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.", @@ -654,8 +659,13 @@ async def register_admin_command( .one_or_none() ) if query is not None: + discord_user = self.bot.uqcs_server.get_member(query.discord_userid) + if discord_user: + discord_ping = discord_user.mention + else: + discord_ping = f"someone who doesn't seem to be in the server (discord id = {query.discord_userid})" await interaction.edit_original_response( - content=f"Advent of Code name `{aoc_name}` is already registered to <@{query.discord_userid}>." + content=f"Advent of Code name `{aoc_name}` is already registered to {discord_ping}." ) return @@ -667,8 +677,13 @@ async def register_admin_command( db_session.commit() db_session.close() + discord_user = self.bot.uqcs_server.get_member(discord_id) + if discord_user: + discord_ping = discord_user.mention + else: + discord_ping = f"someone who doesn't seem to be in the server (discord id = {discord_id})" await interaction.edit_original_response( - content=f"Advent of Code name `{aoc_name}` is now registered to <@{discord_id}> (for {year})." + content=f"Advent of Code name `{aoc_name}` is now registered to {discord_ping} (for {year})." ) @advent_command_group.command(name="unregister") @@ -690,7 +705,7 @@ async def unregister_command(self, interaction: discord.Interaction): ) if (query.one_or_none()) is None: await interaction.edit_original_response( - content=f"Your discord account (<@{discord_id}>) is already unregistered for this year." + content=f"Your discord account ({interaction.user.mention}) is already unregistered for this year." ) return @@ -699,10 +714,10 @@ async def unregister_command(self, interaction: discord.Interaction): db_session.close() await interaction.edit_original_response( - content=f"<@{discord_id}> is no longer registered to win Advent of Code prizes." + content=f"{interaction.user.mention} is no longer registered to win Advent of Code prizes." ) - @app_commands.default_permissions(manage_guild=True) + @app_commands.checks.has_permissions(manage_guild=True) @advent_command_group.command(name="unregister-force") @app_commands.describe( year="Year that the registration is for", @@ -716,6 +731,7 @@ async def unregister_admin_command( For admin use only; assumes you know what you are doing. """ await interaction.response.defer(thinking=True) + discord_user = self.bot.uqcs_server.get_member(discord_id) db_session = self.bot.create_db_session() query = db_session.query(AOCRegistrations).filter( @@ -725,8 +741,12 @@ async def unregister_admin_command( ) ) if (query.one_or_none()) is None: + if discord_user: + discord_ping = discord_user.mention + else: + discord_ping = f"who does not seem to be in the server; id = {discord_id}" 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." + content=f"This discord account ({discord_ping}) is already unregistered for this year. Ensure that you enter the users discord id, not discord name or nickname." ) return @@ -734,8 +754,12 @@ async def unregister_admin_command( db_session.commit() db_session.close() + if discord_user: + discord_ping = discord_user.mention + else: + discord_ping = f"A user who does not seem to be in the server (id = {discord_id})" await interaction.edit_original_response( - content=f"<@{discord_id}> is no longer registered to win Advent of Code prizes for {year}." + content=f"{discord_ping} is no longer registered to win Advent of Code prizes for {year}." ) @advent_command_group.command(name="previous-winners") @@ -789,16 +813,18 @@ async def previous_winners_command( 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( + discord_user = self.bot.uqcs_server.get_member( [user.discord_userid for user in registrations][0] ) - message += f"{name[0]} (@{discord_user.display_name}) - {winner.prize}" + discord_ping = f" ({discord_user.display_name})" if discord_user else "" + # Don't actually ping as this may be called many times + message += f"{name[0]}{discord_ping} - {winner.prize}" db_session.commit() db_session.close() await interaction.edit_original_response(content=message) - @app_commands.default_permissions(manage_guild=True) + @app_commands.checks.has_permissions(manage_guild=True) @advent_command_group.command(name="add-winners") @app_commands.describe( prize="A description of the prize that is being awarded.", @@ -895,31 +921,29 @@ async def add_winners_command( self._add_winners(winners, year, prize) 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}" - ) - return 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 "" + discord_user = self.bot.uqcs_server.get_member(discord_id) if discord_id else None + discord_ping = f" ({discord_user.mention})" if discord_user 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 " + if len(distinct_winners) == 1: + pass + elif i < len(distinct_winners) - 1: + winners_message += ", " + else: + winners_message += " 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}" ) - @app_commands.default_permissions(manage_guild=True) + @app_commands.checks.has_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." @@ -1242,9 +1266,10 @@ def name_column(bot: UQCSBot): 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)): + if not (discord_user := bot.uqcs_server.get_member(discord_userid)): return member.name - return f"{member.name} (@{discord_user.name})" + # Don't actually ping as leaderboard is called many times + return f"{member.name} (@{discord_user.display_name})" return LeaderboardColumn(title=("", ""), calculation=format_name) From a33719830374e9bbf79b423b769578c0e2cca4fe Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Tue, 21 Nov 2023 18:29:43 +1000 Subject: [PATCH 05/10] Minor style changes --- uqcsbot/advent.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index 16cd4b9b..2cf237db 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -565,7 +565,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str if query is not None: discord_user = self.bot.uqcs_server.get_member(query.discord_userid) if discord_user: - discord_ping = discord_user.mention + discord_ping = discord_user.mention else: discord_ping = f"someone who doesn't seem to be in the server (discord id = {query.discord_userid})" await interaction.edit_original_response( @@ -661,7 +661,7 @@ async def register_admin_command( if query is not None: discord_user = self.bot.uqcs_server.get_member(query.discord_userid) if discord_user: - discord_ping = discord_user.mention + discord_ping = discord_user.mention else: discord_ping = f"someone who doesn't seem to be in the server (discord id = {query.discord_userid})" await interaction.edit_original_response( @@ -679,7 +679,7 @@ async def register_admin_command( discord_user = self.bot.uqcs_server.get_member(discord_id) if discord_user: - discord_ping = discord_user.mention + discord_ping = discord_user.mention else: discord_ping = f"someone who doesn't seem to be in the server (discord id = {discord_id})" await interaction.edit_original_response( @@ -744,7 +744,9 @@ async def unregister_admin_command( if discord_user: discord_ping = discord_user.mention else: - discord_ping = f"who does not seem to be in the server; id = {discord_id}" + discord_ping = ( + f"who does not seem to be in the server; id = {discord_id}" + ) await interaction.edit_original_response( content=f"This discord account ({discord_ping}) is already unregistered for this year. Ensure that you enter the users discord id, not discord name or nickname." ) @@ -757,7 +759,9 @@ async def unregister_admin_command( if discord_user: discord_ping = discord_user.mention else: - discord_ping = f"A user who does not seem to be in the server (id = {discord_id})" + discord_ping = ( + f"A user who does not seem to be in the server (id = {discord_id})" + ) await interaction.edit_original_response( content=f"{discord_ping} is no longer registered to win Advent of Code prizes for {year}." ) @@ -925,7 +929,9 @@ async def add_winners_command( winners_message = "" for i, winner in enumerate(distinct_winners): discord_id = winner.get_discord_userid(self.bot) - discord_user = self.bot.uqcs_server.get_member(discord_id) if discord_id else None + discord_user = ( + self.bot.uqcs_server.get_member(discord_id) if discord_id else None + ) discord_ping = f" ({discord_user.mention})" if discord_user else "" number_of_prizes = len( [member for member in winners if member.id == winner.id] From 9e02f86910fb352a0ba7114dd61515b7ebc12351 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Sun, 26 Nov 2023 19:30:54 +1000 Subject: [PATCH 06/10] Refactored AOC into a utils file --- uqcsbot/advent.py | 739 ++++++++-------------------------- uqcsbot/bot.py | 1 + uqcsbot/models.py | 4 +- uqcsbot/utils/advent_utils.py | 384 ++++++++++++++++++ 4 files changed, 560 insertions(+), 568 deletions(-) create mode 100644 uqcsbot/utils/advent_utils.py diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index 2cf237db..c6b0ffb1 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -1,10 +1,9 @@ import io import logging import os -from datetime import datetime, timedelta -from pytz import timezone +from datetime import datetime from random import choices -from typing import Any, Callable, Dict, Iterable, List, Optional, Literal +from typing import Callable, Dict, Iterable, List, Optional, Literal import requests from requests.exceptions import RequestException from sqlalchemy.sql.expression import and_ @@ -16,6 +15,7 @@ from uqcsbot.bot import UQCSBot from uqcsbot.models import AOCRegistrations, AOCWinners from uqcsbot.utils.err_log_utils import FatalErrorWithLog +from uqcsbot.utils.advent_utils import Member, Day, Json, InvalidHTTPSCode, ADVENT_DAYS, CACHE_TIME, parse_leaderboard_column_string, print_leaderboard # Leaderboard API URL with placeholders for year and code. LEADERBOARD_URL = "https://adventofcode.com/{year}/leaderboard/private/view/{code}.json" @@ -23,135 +23,9 @@ # UQCS leaderboard ID. UQCS_LEADERBOARD = 989288 -# Days in Advent of Code. List of numbers 1 to 25. -ADVENT_DAYS = list(range(1, 25 + 1)) - -# Puzzles are unlocked at midnight EST. -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) - # 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 = Literal[1, 2] -Seconds = int -Times = Dict[Star, Seconds] -Delta = Optional[Seconds] -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, 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 - # 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_ - - # 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: Json, year: int) -> "Member": - """ - Constructs a Member from the API response. - - Times and delta are calculated for the given year and day. - """ - - member = cls( - data["id"], - data["name"], - data["local_score"], - data["stars"], - data["global_score"], - ) - - for d, day_data in data["completion_day_level"].items(): - day = int(d) - times = member.times[day] - - # timestamp of puzzle unlock, rounded to whole seconds - DAY_START = int(datetime(year, 12, day, tzinfo=EST_TIMEZONE).timestamp()) - - 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 - - return member - - 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 - - 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 - - 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_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_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 - - 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 - - # --- 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. @@ -268,6 +142,23 @@ class Advent(commands.Cog): /advent remove-winner - Remove a winner for the database """ + advent_command_group = app_commands.Group( + name="advent", description="Commands for Advent of Code" + ) + + Command = Literal[ + "help", + "leaderboard", + "register", + "register-force", + "unregister", + "unregister-force", + "previous-winners", + "new-winner", + "remove-winner", + "leaderboard_style", + ] + def __init__(self, bot: UQCSBot): self.bot = bot self.bot.schedule_task( @@ -301,22 +192,156 @@ def __init__(self, bot: UQCSBot): bot, "Unable to find AoC session ID. Not loading advent cog." ) - advent_command_group = app_commands.Group( - name="advent", description="Commands for Advent of Code" - ) + 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.bot.AOC_CNAME + ) + 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" + ) + return + role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) + if role is None: + logging.warning(f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle pre-release ping.") + # Still return a message, as it is better to message and not ping than to not message at all. + ping = "" + else: + ping = f"{role.mention} " + await channel.send(f"{ping}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.bot.AOC_CNAME + ) + 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" + ) + return + role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) + if role is None: + logging.warning(f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle release ping.") + # Still return a message, as it is better to message and not ping than to not message at all. + ping = "" + else: + ping = f"{role.mention} " + await channel.send( + f"{ping}Today's Advent of Code puzzle has been released. Good luck!" + ) + + 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(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, prize: str): + """ + Add all members within the list to the database + """ + for winner in winners: + db_session = self.bot.create_db_session() + db_session.add( + AOCWinners(aoc_userid=winner.id, year=year, prize=prize) + ) + db_session.commit() + db_session.close() + + def _random_choices_without_repition( + self, population: List[Member], weights: List[int], k: int + ) -> List[Member]: + """ + Selects k people from a list of members, weighted by weights. + The weight of a person is like how many tickets they have for the lottery. + """ + result: List[Member] = [] + for _ in range(k): + if sum(weights) == 0: + return [] + + result.append(choices(population, weights)[0]) + index = population.index(result[-1]) + population.pop(index) + weights.pop(index) + + return result - Command = Literal[ - "help", - "leaderboard", - "register", - "register-force", - "unregister", - "unregister-force", - "previous-winners", - "new-winner", - "remove-winner", - "leaderboard_style", - ] @advent_command_group.command(name="help") @app_commands.describe(command="The command you want to view help about.") @@ -506,8 +531,8 @@ async def leaderboard_command( scoreboard_file = io.BytesIO( bytes( - _print_leaderboard( - _parse_leaderboard_column_string(leaderboard_style, self.bot), + print_leaderboard( + parse_leaderboard_column_string(leaderboard_style, self.bot), members, day, ), @@ -535,7 +560,6 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str # TODO: Check UQCS membership await interaction.response.defer(thinking=True) - id = self._get_unused_registration_id() db_session = self.bot.create_db_session() year = datetime.now().year @@ -592,7 +616,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str db_session.add( AOCRegistrations( - id=id, aoc_userid=AOC_id, year=year, discord_userid=discord_id + aoc_userid=AOC_id, year=year, discord_userid=discord_id ) ) db_session.commit() @@ -631,7 +655,6 @@ async def register_admin_command( await interaction.response.defer(thinking=True) - id = self._get_unused_registration_id() db_session = self.bot.create_db_session() if aoc_name: @@ -671,7 +694,7 @@ async def register_admin_command( db_session.add( AOCRegistrations( - id=id, aoc_userid=aoc_id, year=year, discord_userid=discord_id + aoc_userid=aoc_id, year=year, discord_userid=discord_id ) ) db_session.commit() @@ -978,422 +1001,6 @@ async def remove_winner_command(self, interaction: discord.Interaction, id: int) 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.bot.AOC_CNAME - ) - 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" - ) - 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.bot.AOC_CNAME - ) - 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" - ) - return - await channel.send( - "Today's Advent of Code puzzle has been released. Good luck!" - ) - - 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(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, prize: str): - """ - Add all members within the list to the database - """ - for winner in winners: - 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: List[Member], weights: List[int], k: int - ) -> List[Member]: - """ - Selects k people from a list of members, weighted by weights. - The weight of a person is like how many tickets they have for the lottery. - """ - result: List[Member] = [] - for _ in range(k): - if sum(weights) == 0: - return [] - - result.append(choices(population, weights)[0]) - index = population.index(result[-1]) - population.pop(index) - weights.pop(index) - - return result - - 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(): - """ - 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}", - ) - - @staticmethod - def star2_column(): - """ - 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}", - ) - - @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}", - ) - - @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}", - ) - - @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}", - ) - - @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}", - ) - - @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}", - ) - - @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}", - ) - - @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}", - ) - - @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.uqcs_server.get_member(discord_userid)): - return member.name - # Don't actually ping as leaderboard is called many times - return f"{member.name} (@{discord_user.display_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 3a1af1c2..77887a92 100644 --- a/uqcsbot/bot.py +++ b/uqcsbot/bot.py @@ -30,6 +30,7 @@ def __init__(self, *args: Any, **kwargs: Any): self.ADMIN_ALERTS_CNAME = "admin-alerts" self.GENERAL_CNAME = "general" self.AOC_CNAME = "contests" + self.AOC_ROLE = "CPG" self.BOT_TIMEZONE = timezone("Australia/Brisbane") self.uqcs_server: discord.Guild diff --git a/uqcsbot/models.py b/uqcsbot/models.py index ab8227a4..4d7f884d 100644 --- a/uqcsbot/models.py +++ b/uqcsbot/models.py @@ -19,7 +19,7 @@ class Base(DeclarativeBase): class AOCWinners(Base): __tablename__ = "aoc_winners" - id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False) + id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False, autoincrement=True) 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) @@ -28,7 +28,7 @@ class AOCWinners(Base): class AOCRegistrations(Base): __tablename__ = "aoc_registrations" - id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False) + id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False, autoincrement=True) 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( diff --git a/uqcsbot/utils/advent_utils.py b/uqcsbot/utils/advent_utils.py new file mode 100644 index 00000000..ee7e0810 --- /dev/null +++ b/uqcsbot/utils/advent_utils.py @@ -0,0 +1,384 @@ +from typing import Any, List, Literal, Dict, Optional, Callable +from datetime import datetime, timedelta +from pytz import timezone + +from uqcsbot.bot import UQCSBot +from uqcsbot.models import AOCRegistrations + +# Days in Advent of Code. List of numbers 1 to 25. +ADVENT_DAYS = list(range(1, 25 + 1)) + +# type aliases for documentation purposes. +Day = int # from 1 to 25 +Star = Literal[1, 2] +Seconds = int +Times = Dict[Star, Seconds] +Delta = Optional[Seconds] +Json = Dict[str, Any] + +# Puzzles are unlocked at midnight EST. +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) + +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, 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 + # 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_ + + # 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: Json, year: int) -> "Member": + """ + Constructs a Member from the API response. + + Times and delta are calculated for the given year and day. + """ + + member = cls( + data["id"], + data["name"], + data["local_score"], + data["stars"], + data["global_score"], + ) + + for d, day_data in data["completion_day_level"].items(): + day = int(d) + times = member.times[day] + + # timestamp of puzzle unlock, rounded to whole seconds + DAY_START = int(datetime(year, 12, day, tzinfo=EST_TIMEZONE).timestamp()) + + 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 + + return member + + 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 + + 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 + + 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_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_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 + + 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 + + +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) + +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(): + """ + 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}", + ) + + @staticmethod + def star2_column(): + """ + 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}", + ) + + @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}", + ) + + @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}", + ) + + @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}", + ) + + @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}", + ) + + @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}", + ) + + @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}", + ) + + @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}", + ) + + @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.uqcs_server.get_member(discord_userid)): + return member.name + # Don't actually ping as leaderboard is called many times + return f"{member.name} (@{discord_user.display_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 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 + From 5e2cd0857ff17bf9cc0860b2c865d306eb64b865 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Sun, 26 Nov 2023 19:51:29 +1000 Subject: [PATCH 07/10] Small style changes for black --- uqcsbot/advent.py | 38 +++++++++++++++++++++-------------- uqcsbot/models.py | 8 ++++++-- uqcsbot/utils/advent_utils.py | 6 ++++-- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index c6b0ffb1..cb3754fc 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -1,7 +1,7 @@ import io import logging import os -from datetime import datetime +from datetime import datetime from random import choices from typing import Callable, Dict, Iterable, List, Optional, Literal import requests @@ -15,7 +15,16 @@ from uqcsbot.bot import UQCSBot from uqcsbot.models import AOCRegistrations, AOCWinners from uqcsbot.utils.err_log_utils import FatalErrorWithLog -from uqcsbot.utils.advent_utils import Member, Day, Json, InvalidHTTPSCode, ADVENT_DAYS, CACHE_TIME, parse_leaderboard_column_string, print_leaderboard +from uqcsbot.utils.advent_utils import ( + Member, + Day, + Json, + InvalidHTTPSCode, + ADVENT_DAYS, + CACHE_TIME, + parse_leaderboard_column_string, + print_leaderboard, +) # Leaderboard API URL with placeholders for year and code. LEADERBOARD_URL = "https://adventofcode.com/{year}/leaderboard/private/view/{code}.json" @@ -267,12 +276,16 @@ async def reminder_fifteen_minutes(self): return role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) if role is None: - logging.warning(f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle pre-release ping.") + logging.warning( + f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle pre-release ping." + ) # Still return a message, as it is better to message and not ping than to not message at all. ping = "" else: ping = f"{role.mention} " - await channel.send(f"{ping}Today's Advent of Code puzzle is released in 15 minutes.") + await channel.send( + f"{ping}Today's Advent of Code puzzle is released in 15 minutes." + ) async def reminder_released(self): """ @@ -291,7 +304,9 @@ async def reminder_released(self): return role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) if role is None: - logging.warning(f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle release ping.") + logging.warning( + f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle release ping." + ) # Still return a message, as it is better to message and not ping than to not message at all. ping = "" else: @@ -317,9 +332,7 @@ def _add_winners(self, winners: List[Member], year: int, prize: str): """ for winner in winners: db_session = self.bot.create_db_session() - db_session.add( - AOCWinners(aoc_userid=winner.id, year=year, prize=prize) - ) + db_session.add(AOCWinners(aoc_userid=winner.id, year=year, prize=prize)) db_session.commit() db_session.close() @@ -342,7 +355,6 @@ def _random_choices_without_repition( return result - @advent_command_group.command(name="help") @app_commands.describe(command="The command you want to view help about.") async def help_command( @@ -615,9 +627,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str return db_session.add( - AOCRegistrations( - aoc_userid=AOC_id, year=year, discord_userid=discord_id - ) + AOCRegistrations(aoc_userid=AOC_id, year=year, discord_userid=discord_id) ) db_session.commit() db_session.close() @@ -693,9 +703,7 @@ async def register_admin_command( return db_session.add( - AOCRegistrations( - aoc_userid=aoc_id, year=year, discord_userid=discord_id - ) + AOCRegistrations(aoc_userid=aoc_id, year=year, discord_userid=discord_id) ) db_session.commit() db_session.close() diff --git a/uqcsbot/models.py b/uqcsbot/models.py index 4d7f884d..4b39fd8d 100644 --- a/uqcsbot/models.py +++ b/uqcsbot/models.py @@ -19,7 +19,9 @@ class Base(DeclarativeBase): class AOCWinners(Base): __tablename__ = "aoc_winners" - id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False, autoincrement=True) + id: Mapped[int] = mapped_column( + "id", BigInteger, primary_key=True, nullable=False, autoincrement=True + ) 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) @@ -28,7 +30,9 @@ class AOCWinners(Base): class AOCRegistrations(Base): __tablename__ = "aoc_registrations" - id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True, nullable=False, autoincrement=True) + id: Mapped[int] = mapped_column( + "id", BigInteger, primary_key=True, nullable=False, autoincrement=True + ) 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( diff --git a/uqcsbot/utils/advent_utils.py b/uqcsbot/utils/advent_utils.py index ee7e0810..b527ceff 100644 --- a/uqcsbot/utils/advent_utils.py +++ b/uqcsbot/utils/advent_utils.py @@ -22,8 +22,9 @@ # 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) + class InvalidHTTPSCode(Exception): - def __init__(self, message, request_code): + def __init__(self, message: str, request_code: int): super().__init__(message) self.request_code = request_code @@ -166,6 +167,7 @@ def _format_seconds_long(seconds: Optional[int]): def _get_member_star_progress_bar(member: Member): return "".join(_star_char(len(member.times[day])) for day in ADVENT_DAYS) + 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. @@ -363,6 +365,7 @@ def parse_leaderboard_column_string(s: str, bot: UQCSBot) -> List[LeaderboardCol columns.append(LeaderboardColumn.name_column(bot)) return columns + def print_leaderboard( columns: List[LeaderboardColumn], members: List[Member], day: Optional[Day] ): @@ -381,4 +384,3 @@ def print_leaderboard( ) return leaderboard - From fbe25a67cfd5aa50f792f0c838069b4104de1b7b Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Sun, 26 Nov 2023 20:21:31 +1000 Subject: [PATCH 08/10] Fixed autoincrement for the SQL database --- uqcsbot/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uqcsbot/models.py b/uqcsbot/models.py index 4b39fd8d..2a8fb5b1 100644 --- a/uqcsbot/models.py +++ b/uqcsbot/models.py @@ -20,7 +20,7 @@ class AOCWinners(Base): __tablename__ = "aoc_winners" id: Mapped[int] = mapped_column( - "id", BigInteger, primary_key=True, nullable=False, autoincrement=True + "id", Integer, primary_key=True, nullable=False, autoincrement=True ) aoc_userid: Mapped[int] = mapped_column("aoc_userid", Integer, nullable=False) year: Mapped[int] = mapped_column("year", Integer, nullable=False) @@ -31,7 +31,7 @@ class AOCRegistrations(Base): __tablename__ = "aoc_registrations" id: Mapped[int] = mapped_column( - "id", BigInteger, primary_key=True, nullable=False, autoincrement=True + "id", Integer, primary_key=True, nullable=False, autoincrement=True ) aoc_userid: Mapped[int] = mapped_column("aoc_userid", Integer, nullable=False) year: Mapped[int] = mapped_column("year", Integer, nullable=False) From c8539ee094db6bb2c4c7638bd4bace5d8b8df48b Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Wed, 29 Nov 2023 21:36:33 +1000 Subject: [PATCH 09/10] Removed ids from registration db & removed advent from pyright exception list --- pyproject.toml | 1 - uqcsbot/advent.py | 15 ++++++++++----- uqcsbot/models.py | 7 ++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18bc2814..2989fa34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ build-backend = "poetry.core.masonry.api" [tool.pyright] strict = ["**"] exclude = [ - "**/advent.py", "**/bot.py", "**/error_handler.py", "**/events.py", diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index cb3754fc..641d5d4e 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -640,7 +640,7 @@ async def register_command(self, interaction: discord.Interaction, aoc_name: str @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. Note that this is not their username.", + discord_id_str="The discord ID number of the user. Note that this is not their username.", aoc_name="The name shown on Advent of Code.", aoc_id="The AOC id of the user.", ) @@ -648,13 +648,14 @@ async def register_admin_command( self, interaction: discord.Interaction, year: int, - discord_id: int, + discord_id_str: str, # str as discord can't handle integers this big aoc_name: Optional[str] = None, aoc_id: Optional[int] = None, ): """ Forces a registration entry to be created. For admin use only. Either aoc_name or aoc_id should be given. """ + discord_id = int(discord_id_str) if (aoc_name is None and aoc_id is None) or ( aoc_name is not None and aoc_id is not None ): @@ -752,15 +753,16 @@ async def unregister_command(self, interaction: discord.Interaction): @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.", + discord_id_str="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 + self, interaction: discord.Interaction, year: int, discord_id_str: str ): """ Forces a registration entry to be removed. For admin use only; assumes you know what you are doing. """ + discord_id = int(discord_id_str) await interaction.response.defer(thinking=True) discord_user = self.bot.uqcs_server.get_member(discord_id) @@ -930,6 +932,9 @@ async def add_winners_command( 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 + number_of_potential_winners = len( + potential_winners + ) # potential winners will be changed ahead, so we store this value for the award message match weights: case "Stars": @@ -977,7 +982,7 @@ async def add_winners_command( winners_message += " 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}" + content=f"The results are in! Out of {number_of_potential_winners} potential participants, {winners_message} have recieved a prize from participating in Advent of Code: {prize}" ) @app_commands.checks.has_permissions(manage_guild=True) diff --git a/uqcsbot/models.py b/uqcsbot/models.py index 2a8fb5b1..87744b74 100644 --- a/uqcsbot/models.py +++ b/uqcsbot/models.py @@ -30,14 +30,11 @@ class AOCWinners(Base): class AOCRegistrations(Base): __tablename__ = "aoc_registrations" - id: Mapped[int] = mapped_column( - "id", Integer, primary_key=True, nullable=False, autoincrement=True + discord_userid: Mapped[int] = mapped_column( + "discord_userid", 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): From 5cc377e6ffbcbf86c8dbb6d1320465d30b8306d5 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Wed, 29 Nov 2023 22:44:29 +1000 Subject: [PATCH 10/10] Removed type-checking machinery --- uqcsbot/advent.py | 70 +++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/uqcsbot/advent.py b/uqcsbot/advent.py index 641d5d4e..92cdbdbc 100644 --- a/uqcsbot/advent.py +++ b/uqcsbot/advent.py @@ -1,5 +1,4 @@ import io -import logging import os from datetime import datetime from random import choices @@ -201,6 +200,27 @@ def __init__(self, bot: UQCSBot): bot, "Unable to find AoC session ID. Not loading advent cog." ) + @commands.Cog.listener() + async def on_ready(self): + channel = discord.utils.get( + self.bot.uqcs_server.channels, name=self.bot.AOC_CNAME + ) + if isinstance(channel, discord.TextChannel): + self.channel = channel + else: + raise FatalErrorWithLog( + self.bot, + f"Could not find channel #{self.bot.AOC_CNAME} for advent of code cog.", + ) + role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) + if isinstance(role, discord.Role): + self.role = role + else: + raise FatalErrorWithLog( + self.bot, + f"Could not find role @{self.bot.AOC_ROLE} for advent of code cog", + ) + def _get_leaderboard_json(self, year: int, code: int) -> Json: """ Returns a json dump of the leaderboard @@ -263,56 +283,16 @@ 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.bot.AOC_CNAME - ) - 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" - ) - return - role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) - if role is None: - logging.warning( - f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle pre-release ping." - ) - # Still return a message, as it is better to message and not ping than to not message at all. - ping = "" - else: - ping = f"{role.mention} " - await channel.send( - f"{ping}Today's Advent of Code puzzle is released in 15 minutes." + await self.channel.send( + f"{self.role.mention}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.bot.AOC_CNAME - ) - 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" - ) - return - role = discord.utils.get(self.bot.uqcs_server.roles, name=self.bot.AOC_ROLE) - if role is None: - logging.warning( - f"The role @{self.bot.AOC_ROLE} could not be found for an Advent of Code puzzle release ping." - ) - # Still return a message, as it is better to message and not ping than to not message at all. - ping = "" - else: - ping = f"{role.mention} " - await channel.send( - f"{ping}Today's Advent of Code puzzle has been released. Good luck!" + await self.channel.send( + f"{self.role.mention}Today's Advent of Code puzzle has been released. Good luck!" ) def _get_previous_winner_aoc_ids(self, year: int) -> List[int]: