├── .eslintrc.json
├── logs.sh
├── src
├── components
│ └── App
│ │ ├── index.js
│ │ └── App.js
├── index.js
├── hooks
│ └── use-click-tracker.js
├── utils.js
├── global-styles.css
├── bitset.js
└── randomizedColors.js
├── public
├── favicon.png
├── favicon.png.old
├── favicon.svg
└── index.html
├── Makefile
├── hosts
├── static
└── fonts
│ ├── Sunset-Demi.woff
│ ├── apercu-bold-pro.ttf
│ ├── apercu-bold-pro.woff
│ ├── apercu-bold-pro.woff2
│ ├── apercu-italic-pro.ttf
│ ├── apercu-italic-pro.woff
│ ├── apercu-italic-pro.woff2
│ ├── apercu-regular-pro.ttf
│ └── apercu-regular-pro.woff
├── .parcelrc
├── start_gunicorn.sh
├── cleanup.sh
├── .gitignore
├── TODO
├── requirements.txt
├── gunicorn_restart.sh
├── cleanup_old_logs.py
├── LICENSE
├── compute-stats.sh
├── README.md
├── package.json
├── go.mod
├── deploy.sh
├── freeze_bits_and_compute_stats.py
├── server.py
├── go.sum
└── main.go
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
4 |
--------------------------------------------------------------------------------
/logs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cat hosts | xargs -n 1 -P 10 -I'{}' ssh root@{} "journalctl -f"
3 |
--------------------------------------------------------------------------------
/src/components/App/index.js:
--------------------------------------------------------------------------------
1 | export * from "./App";
2 | export { default } from "./App";
3 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | GOOS=linux GOARCH=amd64 go build -o /tmp/checkbox main.go
3 | run:
4 | go run main.go
5 |
6 |
--------------------------------------------------------------------------------
/public/favicon.png.old:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/public/favicon.png.old
--------------------------------------------------------------------------------
/hosts:
--------------------------------------------------------------------------------
1 | bak.onemil
2 | 2bak.onemil
3 | 3bak.onemil
4 | 4bak.onemil
5 | 5bak.onemil
6 | 6bak.onemil
7 | 7bak.onemil
8 | 8bak.onemil
--------------------------------------------------------------------------------
/static/fonts/Sunset-Demi.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/Sunset-Demi.woff
--------------------------------------------------------------------------------
/static/fonts/apercu-bold-pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-bold-pro.ttf
--------------------------------------------------------------------------------
/static/fonts/apercu-bold-pro.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-bold-pro.woff
--------------------------------------------------------------------------------
/static/fonts/apercu-bold-pro.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-bold-pro.woff2
--------------------------------------------------------------------------------
/static/fonts/apercu-italic-pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-italic-pro.ttf
--------------------------------------------------------------------------------
/static/fonts/apercu-italic-pro.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-italic-pro.woff
--------------------------------------------------------------------------------
/static/fonts/apercu-italic-pro.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-italic-pro.woff2
--------------------------------------------------------------------------------
/static/fonts/apercu-regular-pro.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-regular-pro.ttf
--------------------------------------------------------------------------------
/static/fonts/apercu-regular-pro.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nolenroyalty/one-million-checkboxes/HEAD/static/fonts/apercu-regular-pro.woff
--------------------------------------------------------------------------------
/.parcelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@parcel/config-default"
4 | ],
5 | "reporters": [
6 | "...",
7 | "parcel-reporter-static-files-copy"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import './global-styles.css';
5 | import App from './components/App';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render();
9 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/start_gunicorn.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | BASE_PORT=5001
4 |
5 | for i in 0 1 2 3
6 | do
7 | PORT=$((BASE_PORT + i))
8 | gunicorn --worker-class eventlet --workers 1 --threads 3 --bind 0.0.0.0:$PORT server:app &
9 | done
10 |
11 | PROCESS_JOBS='true' gunicorn --worker-class eventlet --workers 1 server:app &
12 |
13 | wait
14 |
--------------------------------------------------------------------------------
/cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Set up the environment
4 | export HOME=/home/ubuntu
5 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
6 |
7 | source /home/ubuntu/venv/bin/activate
8 | source /home/ubuntu/.redis-creds
9 | export PYTHONPATH=/home/ubuntu:$PYTHONPATH
10 | python /home/ubuntu/cleanup_old_logs.py
11 |
12 | # Deactivate the virtual environment
13 | deactivate
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 | .pnp
4 | .pnp.js
5 |
6 | # parcel-generated files
7 | dist
8 | .parcel-cache
9 |
10 | # misc
11 | .DS_Store
12 | .env.local
13 | .env.development.local
14 | .env.test.local
15 | .env.production.local
16 |
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 |
21 | *.swp
22 | venv/
23 |
24 | checkbox
25 | some-numbers.html
26 |
27 | __pycache__/
28 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | * set up logging
2 | * set up a job to disable shit randomly
3 |
4 | * ADD BASIC DEBOUNCING
5 | * issue an alert if you've clicked more than 10 boxes in a second
6 | * disable if you've clicked more than 100 boxes in 60 seconds (until you haven't)
7 | * server-side, do something similar but less extreme by IP
8 | * add a UUID generated client-side for this
9 | * uncheck boxes randomly IF lots have been checked recently
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | APScheduler==3.10.4
2 | bidict==0.23.1
3 | bitarray==2.9.2
4 | blinker==1.8.2
5 | click==8.1.7
6 | dnspython==2.6.1
7 | eventlet==0.36.1
8 | Flask==3.0.3
9 | Flask-Cors==4.0.1
10 | Flask-SocketIO==5.3.6
11 | Flask-WTF==1.2.1
12 | gevent==24.2.1
13 | greenlet==3.0.3
14 | gunicorn==22.0.0
15 | h11==0.14.0
16 | itsdangerous==2.2.0
17 | Jinja2==3.1.4
18 | MarkupSafe==2.1.5
19 | packaging==24.1
20 | python-engineio==4.9.1
21 | python-socketio==5.11.3
22 | pytz==2024.1
23 | redis==5.0.6
24 | setuptools==70.1.0
25 | simple-websocket==1.0.0
26 | six==1.16.0
27 | tzlocal==5.2
28 | Werkzeug==3.0.3
29 | wsproto==1.2.0
30 | WTForms==3.1.2
31 | zope.event==5.0
32 | zope.interface==6.4.post2
33 |
--------------------------------------------------------------------------------
/gunicorn_restart.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # jitter
4 | x=$((RANDOM % 300))
5 | echo "sleeping for $x"
6 | sleep "$x"
7 |
8 | count_running_servers() {
9 | ps aux | grep gunicorn | grep -oP 'bind \K0\.0\.0\.0:\d+' | sort | uniq | wc -l
10 | }
11 |
12 | running_servers=$(count_running_servers)
13 |
14 | if [ $running_servers -lt 3 ]; then
15 | echo "Less than 3 servers running. Restarting all servers..."
16 |
17 | sudo systemctl stop one-million-checkboxes.service
18 |
19 | sleep 10
20 |
21 | sudo systemctl start one-million-checkboxes.service
22 |
23 | echo "Servers restarted."
24 | else
25 | echo "At least 3 servers are running. No action needed."
26 | fi
27 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | One Million Checkboxes
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/cleanup_old_logs.py:
--------------------------------------------------------------------------------
1 | import redis
2 | from datetime import datetime, timedelta
3 | import os
4 |
5 | print(os.environ.get("REDIS_HOST"))
6 | redis_client = redis.Redis(
7 | host=os.environ.get('REDIS_HOST', 'localhost'),
8 | port=int(os.environ.get('REDIS_PORT', 6379)),
9 | username=os.environ.get('REDIS_USERNAME', 'default'),
10 | password=os.environ.get('REDIS_PASSWORD', ''),
11 | db=0,
12 | ssl=True
13 | )
14 | print("connected to redis")
15 |
16 | def cleanup_old_logs(days_to_keep=30):
17 | today = datetime.now().date()
18 | cutoff_date = today - timedelta(days=days_to_keep)
19 |
20 | print("before")
21 | keys = redis_client.keys("checkbox_logs:*")
22 | print("here")
23 |
24 | for key in keys:
25 | key_date = datetime.strptime(key.decode().split(':')[1], '%Y-%m-%d').date()
26 | if key_date < cutoff_date:
27 | redis_client.delete(key)
28 | else:
29 | # Keys are sorted, so if we find a key that's not old enough, we can stop
30 | break
31 |
32 | # Run this script daily
33 | cleanup_old_logs()
34 |
--------------------------------------------------------------------------------
/src/hooks/use-click-tracker.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef } from "react";
2 |
3 | function useClickTracker() {
4 | const clickCounts = useRef({
5 | oneSecond: 0,
6 | fifteenSeconds: 0,
7 | sixtySeconds: 0,
8 | });
9 |
10 | const clickTimestamps = useRef([]);
11 |
12 | const trackClick = useCallback(() => {
13 | const now = Date.now();
14 | clickTimestamps.current.push(now);
15 |
16 | // Remove timestamps older than 60 seconds
17 | const sixtySecondsAgo = now - 60000;
18 | clickTimestamps.current = clickTimestamps.current.filter(
19 | (timestamp) => timestamp > sixtySecondsAgo
20 | );
21 | clickCounts.current.sixtySeconds = clickTimestamps.current.length;
22 | clickCounts.current.fifteenSeconds = clickTimestamps.current.filter(
23 | (timestamp) => timestamp > now - 15000
24 | ).length;
25 | clickCounts.current.oneSecond = clickTimestamps.current.filter(
26 | (timestamp) => timestamp > now - 1000
27 | ).length;
28 | }, []);
29 |
30 | return [clickCounts, trackClick];
31 | }
32 |
33 | export default useClickTracker;
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Nolen Royalty
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/compute-stats.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 | set -x
5 |
6 | PYTHON_SCRIPT="/home/ubuntu/freeze_bits_and_compute_stats.py"
7 | TEMP_OUTPUT="/tmp/some-numbers.html"
8 | FINAL_OUTPUT="/var/www/generated-content/some-numbers.html"
9 | CONTENT_DIR="/var/www/generated-content"
10 | STAGING_OUTPUT="/tmp/some-numbers-staging.html"
11 | UBUNTU_USER="ubuntu"
12 | WWW_USER="www-data"
13 | WWW_GROUP="www-data"
14 | VENV_PATH="/home/ubuntu/venv/bin/activate"
15 |
16 | if [ "$(id -u)" -ne 0 ]; then
17 | echo "This script must be run as root" >&2
18 | exit 1
19 | fi
20 |
21 | cleanup() {
22 | rm -f "$TEMP_OUTPUT" "$STAGING_OUTPUT"
23 | }
24 | trap cleanup EXIT
25 |
26 | mkdir -p "$CONTENT_DIR"
27 | chown "$WWW_USER:$WWW_GROUP" "$CONTENT_DIR"
28 | chmod 775 "$CONTENT_DIR"
29 |
30 | sudo -u "$UBUNTU_USER" bash -c "
31 | source $VENV_PATH
32 | source /home/ubuntu/.redis-creds
33 | python $PYTHON_SCRIPT
34 | "
35 |
36 | if [ ! -f "$TEMP_OUTPUT" ]; then
37 | echo "Error: Python script did not generate the output file." >&2
38 | exit 1
39 | fi
40 |
41 | cp "$TEMP_OUTPUT" "$STAGING_OUTPUT"
42 |
43 | chown "$WWW_USER:$WWW_GROUP" "$STAGING_OUTPUT"
44 | chmod 644 "$STAGING_OUTPUT"
45 |
46 | mv "$STAGING_OUTPUT" "$FINAL_OUTPUT"
47 |
48 | echo "Stats file updated successfully at $FINAL_OUTPUT"
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # One Million Checkboxes
2 | The code that powered [One Million Checkboxes](https://en.wikipedia.org/wiki/One_Million_Checkboxes)
3 |
4 | ## THIS CODE IS MESSY
5 | I have made no attempt to clean up this code. My git hygiene was really bad. At many points I wrote awful hacky code. Sorry.
6 |
7 | If you're interested in reading this code, I recommend that you begin with [the code as it existed when I launched the site](https://github.com/nolenroyalty/one-million-checkboxes/tree/71b296ec74adbd941431559ff1d7358d817a03cb) and then observe how it evolved over time. I think that's a much better way of learning what I did than simply looking at the end result. There are a lot of problems with the end result! It's not good code.
8 |
9 | See [this blog post](https://eieio.games/essays/scaling-one-million-checkboxes/) for some details on the changes that I made to the site to scale it up.
10 |
11 | ## Want the data?
12 | The data from One Million Checkboxes is available [on the internet archive](https://archive.org/details/one-million-checkboxes-data). And a [separate repo of mine](https://github.com/nolenroyalty/one-million-checkboxes-data-scripts) provides scripts for working with the data.
13 |
14 |
15 | ## Thanks for everything
16 | It was really special to get to run this site. Thanks for playing :)
17 |
18 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function abbrNum(number, decPlaces) {
2 | let negate = false;
3 | if (number < 0) {
4 | negate = true;
5 | number = -1 * number;
6 | }
7 | // 2 decimal places => 100, 3 => 1000, etc
8 | decPlaces = Math.pow(10, decPlaces);
9 |
10 | // Enumerate number abbreviations
11 | var abbrev = ["K", "M", "B", "T"];
12 |
13 | // Go through the array backwards, so we do the largest first
14 | for (var i = abbrev.length - 1; i >= 0; i--) {
15 | // Convert array index to "1000", "1000000", etc
16 | var size = Math.pow(10, (i + 1) * 3);
17 |
18 | // If the number is bigger or equal do the abbreviation
19 | if (size <= number) {
20 | // Here, we multiply by decPlaces, round, and then divide by decPlaces.
21 | // This gives us nice rounding to a particular decimal place.
22 |
23 | number = Math.floor((number * decPlaces) / size) / decPlaces;
24 |
25 | // Handle special case where we round up to the next abbreviation
26 | if (number === 1000 && i < abbrev.length - 1) {
27 | number = 1;
28 | i++;
29 | }
30 |
31 | // Add the letter for the abbreviation
32 | number += abbrev[i];
33 |
34 | // We are done... stop
35 | break;
36 | }
37 | }
38 |
39 | return (negate ? "-" : "") + number;
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "one-million-checkboxes",
3 | "version": "1.0.0",
4 | "description": "One million checkboxes, synced for everyone.",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1",
7 | "predev": "rimraf .parcel-cache dist",
8 | "dev": "parcel public/index.html",
9 | "prebuild": "rimraf .parcel-cache dist",
10 | "build": "parcel build public/index.html",
11 | "new-component": "new-component"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "eslint": "^8.57.0",
18 | "eslint-config-react-app": "^7.0.1",
19 | "new-component": "^5.0.2",
20 | "parcel": "^2.12.0",
21 | "parcel-reporter-static-files-copy": "^1.5.3",
22 | "prettier": "^3.3.2",
23 | "process": "^0.11.10",
24 | "react": "^18.3.1",
25 | "react-dom": "^18.3.1",
26 | "react-feather": "^2.0.10",
27 | "react-use": "^17.5.0",
28 | "react-window": "^1.8.10",
29 | "rimraf": "^5.0.7",
30 | "socket.io-client": "^4.7.5",
31 | "styled-components": "^6.1.11"
32 | },
33 | "devDependencies": {
34 | "assert": "^2.1.0",
35 | "browserify-zlib": "^0.2.0",
36 | "buffer": "^6.0.3",
37 | "crypto-browserify": "^3.12.0",
38 | "events": "^3.3.0",
39 | "path-browserify": "^1.0.1",
40 | "punycode": "^1.4.1",
41 | "querystring-es3": "^0.2.1",
42 | "stream-browserify": "^3.0.0",
43 | "stream-http": "^3.2.0",
44 | "timers-browserify": "^2.0.12",
45 | "url": "^0.11.3",
46 | "util": "^0.12.5",
47 | "vm-browserify": "^1.1.2"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/global-styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | Stolen from Josh's Custom CSS Reset
3 | https://www.joshwcomeau.com/css/custom-css-reset/
4 | */
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: border-box;
9 | }
10 | * {
11 | margin: 0;
12 | }
13 | html,
14 | body {
15 | height: 100%;
16 | --blue: #3777ff;
17 | --dark: #181a21;
18 | /* --red: hsla(0, 80%, 70%, 1);
19 | */
20 | --red: hsla(6, 59%, 50%, 1);
21 | /* --gold: hsla(52, 80%, 70%, 1); */
22 | /* --gold: #c2b043; */
23 | --gold: hsla(47, 65%, 45%, 1);
24 | /* --green: hsla(103, 80%, 70%, 1); */
25 | /* --green: hsla(113, 65%, 45%, 1); */
26 | --green: hsla(128, 59%, 40%, 1);
27 | /* --blue: hsla(166, 80%, 70%, 1); */
28 | /* --purple: hsla(256, 80%, 70%, 1); */
29 | --purple: hsla(279, 59%, 50%, 1);
30 | /* --orange: hsla(24, 80%, 70%, 1); */
31 | --orange: hsla(28, 59%, 50%, 1);
32 |
33 | background-color: #ebeaea;
34 | }
35 | body {
36 | line-height: 1.5;
37 | -webkit-font-smoothing: antialiased;
38 | font-family: "Lato", sans-serif;
39 | color: var(--dark);
40 | }
41 | img,
42 | picture,
43 | video,
44 | canvas,
45 | svg {
46 | display: block;
47 | max-width: 100%;
48 | }
49 | input,
50 | button,
51 | textarea,
52 | select {
53 | font: inherit;
54 | }
55 | p,
56 | h1,
57 | h2,
58 | h3,
59 | h4,
60 | h5,
61 | h6 {
62 | overflow-wrap: break-word;
63 | }
64 | #root,
65 | #__next {
66 | isolation: isolate;
67 | }
68 |
69 | @font-face {
70 | font-family: "Sunset Demi";
71 | src: url("/static/fonts/Sunset-Demi.woff") format("woff");
72 | font-weight: normal;
73 | font-style: normal;
74 | }
75 |
76 | @font-face {
77 | font-family: "Apercu Regular Pro";
78 | src:
79 | url("/static/fonts/apercu-regular-pro.woff") format("woff"),
80 | url("/static/fonts/apercu-regular-pro.ttf") format("truetype");
81 | font-weight: normal;
82 | font-style: normal;
83 | }
84 |
85 | @font-face {
86 | font-family: "Apercu Italic Pro";
87 | src:
88 | url("/static/fonts/apercu-italic-pro.woff") format("woff"),
89 | url("/static/fonts/apercu-italic-pro.woff2") format("woff2"),
90 | url("/static/fonts/apercu-italic-pro.ttf") format("truetype");
91 | font-weight: normal;
92 | font-style: normal;
93 | }
94 |
--------------------------------------------------------------------------------
/src/bitset.js:
--------------------------------------------------------------------------------
1 | class BitSet {
2 | constructor({ base64String, count }) {
3 | const binaryString = atob(base64String);
4 | this.bytes = new Uint8Array(binaryString.length);
5 | this.checkCount = count;
6 | for (let i = 0; i < binaryString.length; i++) {
7 | this.bytes[i] = binaryString.charCodeAt(i);
8 | }
9 | }
10 |
11 | get(index) {
12 | const byteIndex = Math.floor(index / 8);
13 | const bitOffset = 7 - (index % 8);
14 | return (this.bytes[byteIndex] & (1 << bitOffset)) !== 0;
15 | }
16 |
17 | set(index, value) {
18 | if (typeof value === "boolean") {
19 | value = value ? 1 : 0;
20 | }
21 | const byteIndex = Math.floor(index / 8);
22 | const bitOffset = 7 - (index % 8);
23 | const current = this.bytes[byteIndex] & (1 << bitOffset);
24 | if (value) {
25 | this.bytes[byteIndex] |= 1 << bitOffset;
26 | if (current === 0) {
27 | this.checkCount++;
28 | }
29 | } else {
30 | this.bytes[byteIndex] &= ~(1 << bitOffset);
31 | if (current !== 0) {
32 | this.checkCount--;
33 | }
34 | }
35 | }
36 |
37 | toJSON() {
38 | return {
39 | base64String: this._toBase64String(),
40 | count: this.checkCount,
41 | };
42 | }
43 |
44 | static makeEmpty() {
45 | const bytes = new Uint8Array(125000);
46 | return new BitSet({
47 | base64String: BitSet._makeBase64String(bytes),
48 | count: 0,
49 | });
50 | }
51 |
52 | static makeFull() {
53 | const bytes = new Uint8Array(125000).fill(255);
54 | return new BitSet({
55 | base64String: BitSet._makeBase64String(bytes),
56 | count: 1000000,
57 | });
58 | }
59 |
60 | static _makeBase64String(bytes) {
61 | let binary = "";
62 | // const bytes = new Uint8Array(this.bytes.buffer);
63 | const len = bytes.byteLength;
64 | for (let i = 0; i < len; i++) {
65 | binary += String.fromCharCode(bytes[i]);
66 | }
67 | return btoa(binary);
68 | }
69 |
70 | _toBase64String() {
71 | return BitSet._makeBase64String(this.bytes);
72 | }
73 |
74 | static fromJSON({ base64String, count }) {
75 | return new BitSet({ base64String, count });
76 | }
77 |
78 | count() {
79 | return this.checkCount;
80 | }
81 |
82 | toggle(index) {
83 | if (this.get(index)) {
84 | this.set(index, 0);
85 | } else {
86 | this.set(index, 1);
87 | }
88 | }
89 | }
90 |
91 | export default BitSet;
92 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/nolenroyalty/one-million-checkboxes
2 |
3 | go 1.22.2
4 |
5 | toolchain go1.22.4
6 |
7 | require (
8 | github.com/alicebob/miniredis/v2 v2.33.0
9 | github.com/gin-contrib/static v1.1.2
10 | github.com/gin-gonic/gin v1.10.0
11 | github.com/googollee/go-socket.io v1.7.0
12 | github.com/puzpuzpuz/xsync v1.5.2
13 | github.com/redis/go-redis/v9 v9.5.3
14 | github.com/zishang520/socket.io/v2 v2.2.0
15 | )
16 |
17 | require (
18 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
19 | github.com/andybalholm/brotli v1.1.0 // indirect
20 | github.com/bytedance/sonic v1.11.6 // indirect
21 | github.com/bytedance/sonic/loader v0.1.1 // indirect
22 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
23 | github.com/cloudwego/base64x v0.1.4 // indirect
24 | github.com/cloudwego/iasm v0.2.0 // indirect
25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
26 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
27 | github.com/gin-contrib/sse v0.1.0 // indirect
28 | github.com/go-playground/locales v0.14.1 // indirect
29 | github.com/go-playground/universal-translator v0.18.1 // indirect
30 | github.com/go-playground/validator/v10 v10.20.0 // indirect
31 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
32 | github.com/goccy/go-json v0.10.2 // indirect
33 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
34 | github.com/gookit/color v1.5.4 // indirect
35 | github.com/gorilla/websocket v1.5.3 // indirect
36 | github.com/json-iterator/go v1.1.12 // indirect
37 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
38 | github.com/kr/text v0.1.0 // indirect
39 | github.com/leodido/go-urn v1.4.0 // indirect
40 | github.com/mattn/go-isatty v0.0.20 // indirect
41 | github.com/mitchellh/mapstructure v1.5.0 // indirect
42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
43 | github.com/modern-go/reflect2 v1.0.2 // indirect
44 | github.com/onsi/ginkgo/v2 v2.12.0 // indirect
45 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
46 | github.com/quic-go/qpack v0.4.0 // indirect
47 | github.com/quic-go/quic-go v0.44.0 // indirect
48 | github.com/quic-go/webtransport-go v0.8.0 // indirect
49 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
50 | github.com/ugorji/go/codec v1.2.12 // indirect
51 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
52 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
53 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
54 | github.com/yuin/gopher-lua v1.1.1 // indirect
55 | github.com/zishang520/engine.io-go-parser v1.2.5 // indirect
56 | github.com/zishang520/engine.io/v2 v2.1.1 // indirect
57 | github.com/zishang520/socket.io-go-parser/v2 v2.1.0 // indirect
58 | go.uber.org/mock v0.4.0 // indirect
59 | golang.org/x/arch v0.8.0 // indirect
60 | golang.org/x/crypto v0.23.0 // indirect
61 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
62 | golang.org/x/mod v0.17.0 // indirect
63 | golang.org/x/net v0.25.0 // indirect
64 | golang.org/x/sys v0.20.0 // indirect
65 | golang.org/x/text v0.15.0 // indirect
66 | golang.org/x/tools v0.21.0 // indirect
67 | google.golang.org/protobuf v1.34.1 // indirect
68 | gopkg.in/yaml.v3 v3.0.1 // indirect
69 | )
70 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Configuration
4 | REMOTE_USER="ubuntu"
5 | #REMOTE_HOST="142.93.76.97"
6 | REMOTE_DIR="/home/ubuntu/"
7 | WWW_DIR="/var/www/one-million-checkboxes"
8 | SSH_KEY="~/.ssh/id_rsa" # Path to your SSH key
9 |
10 | # Local directories and files to sync
11 | LOCAL_DIST="./dist"
12 | LOCAL_SERVER="./server.py"
13 | LOCAL_GUNICORN="./start_gunicorn.sh"
14 | LOCAL_REQUIREMENTS="./requirements.txt"
15 | LOCAL_CLEANUPSH="./cleanup.sh"
16 | LOCAL_CLEANUPPY="./cleanup_old_logs.py"
17 | CHECKBOX_BIN="/tmp/checkbox"
18 | echo "building..."
19 | GOOS=linux GOARCH=amd64 go build -o $CHECKBOX_BIN main.go
20 | echo $(md5sum $CHECKBOX_BIN)
21 | # Rsync options
22 | RSYNC_OPTS="-avz --delete"
23 | GO_SYS_UNIT="go-one-million.service"
24 |
25 | #for REMOTE_HOST in bak.onemil 2bak.onemil 3bak.onemil 4bak.onemil 5bak.onemil 6bak.onemil 7bak.onemil 8bak.onemil
26 | for REMOTE_HOST in onemil
27 | #for REMOTE_HOST in 2bak.onemil
28 | do
29 | echo $REMOTE_HOST
30 |
31 | #REMOTE_HOST="8bak.onemil"
32 |
33 | # Sync dist directory
34 | echo "Syncing dist directory..."
35 | rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_DIST/" "root@$REMOTE_HOST:$WWW_DIR/"
36 | ssh -i $SSH_KEY root@${REMOTE_HOST} -- chown -R www-data:www-data ${WWW_DIR}
37 | ssh -i $SSH_KEY root@${REMOTE_HOST} -- chmod -R 755 ${WWW_DIR}
38 |
39 | echo "syncing new server binary..."
40 |
41 | #ssh root@$REMOTE_HOST "systemctl stop $GO_SYS_UNIT"
42 | #rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$CHECKBOX_BIN" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
43 | #ssh root@$REMOTE_HOST "systemctl start $GO_SYS_UNIT"
44 |
45 | ### Sync freeze bit script
46 | #echo "syncing freeze_bits_and_compute_stats.py"
47 | #rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "freeze_bits_and_compute_stats.py" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
48 |
49 | #### Sync compute-stats script
50 | #echo "syncing compute_stats.sh"
51 | #rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "compute-stats.sh" "root@$REMOTE_HOST:/root/"
52 |
53 | ##Sync server.py
54 | #echo "Syncing server.py..."
55 | #rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_SERVER" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
56 |
57 | # ##Sync server.py
58 | # echo "Syncing server.py..."
59 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_SERVER" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
60 |
61 | # ### Sync cleanup.sh
62 | # echo "Syncing cleanup.sh..."
63 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_CLEANUPSH" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
64 |
65 | # #### Sync cleanup_old_logs.py
66 | # echo "Syncing cleanup_old_logs.py..."
67 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_CLEANUPPY" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
68 |
69 | # #### Sync start_gunicorn.sh
70 | # echo "Syncing start_gunicorn.sh..."
71 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_GUNICORN" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
72 |
73 | # #### Sync requirements.txt
74 | # echo "Syncing requirements.txt..."
75 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "$LOCAL_REQUIREMENTS" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
76 |
77 | # #### Sync gunicorn_restart
78 | # echo "Syncing gunicorn_restart..."
79 | # rsync $RSYNC_OPTS -e "ssh -i $SSH_KEY" "./gunicorn_restart.sh" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
80 |
81 | # echo "Sync completed!"
82 | # echo "Deployment finished!"
83 | done
84 |
--------------------------------------------------------------------------------
/freeze_bits_and_compute_stats.py:
--------------------------------------------------------------------------------
1 | import redis
2 | from redis import ConnectionPool
3 | import time
4 | import json
5 | from collections import defaultdict
6 | import os
7 |
8 | testing = not all(os.environ.get(var) for var in ['REDIS_HOST', 'REDIS_PORT', 'REDIS_USERNAME', 'REDIS_PASSWORD'])
9 |
10 | atomic_flip_script = """
11 | local frozen_bitset = KEYS[1]
12 | local frozen_count_key = KEYS[2]
13 | local index = tonumber(ARGV[1])
14 |
15 | local was_already_frozen = redis.call('GETBIT', frozen_bitset, index)
16 | if was_already_frozen == 0 then
17 | redis.call('SETBIT', frozen_bitset, index, 1)
18 | redis.call('INCR', frozen_count_key)
19 | return 1
20 | else
21 | return 0
22 | end
23 | """
24 |
25 | def get_redis_client():
26 | pool = None
27 | if testing:
28 | pool = ConnectionPool(
29 | host="127.0.0.1",
30 | port=58218,
31 | #username=os.environ.get('REDIS_USERNAME', 'default'),
32 | #password=os.environ.get('REDIS_PASSWORD', ''),
33 | db=0,
34 | #connection_class=redis.SSLConnection,
35 | #decode_responses=True,
36 | max_connections=1
37 | )
38 | else:
39 | pool = ConnectionPool(
40 | host=os.environ.get('REDIS_HOST', 'localhost'),
41 | port=int(os.environ.get('REDIS_PORT', 6379)),
42 | username=os.environ.get('REDIS_USERNAME', 'default'),
43 | password=os.environ.get('REDIS_PASSWORD', ''),
44 | db=0,
45 | connection_class=redis.SSLConnection,
46 | max_connections=1
47 | )
48 |
49 | return redis.Redis(connection_pool=pool)
50 |
51 |
52 | def get_time(r):
53 | seconds, microseconds = r.time()
54 | return seconds * 1000 + microseconds // 1000
55 |
56 | def get_freeze_time(r):
57 | t = r.get("freeze_time_ms")
58 | return int(t)
59 |
60 | def bytes_to_bits(byte_string):
61 | return ''.join(format(byte, '08b') for byte in byte_string)
62 |
63 | def find_dense_regions(bitstring, bit_kind="0", region_size=10000, top_n=3):
64 | regions = defaultdict(int)
65 | for i, bit in enumerate(bitstring):
66 | if bit == bit_kind: # Counting unfrozen/unchecked boxes
67 | regions[i // region_size] += 1
68 | return sorted(regions.items(), key=lambda x: x[1], reverse=True)[:top_n]
69 |
70 | def format_dense_regions(region):
71 | return ", ".join([":".join(map(str, x)) for x in region])
72 |
73 | def find_longest_streaks(bitstring):
74 | longest_0 = 0
75 | longest_1 = 0
76 | longest_0_idx = 0
77 | longest_1_idx = 0
78 | current_0 = 0
79 | current_1 = 0
80 | current_0_idx = 0
81 | current_1_idx = 0
82 |
83 | on_a_zero = True
84 |
85 | for idx, bit in enumerate(bitstring):
86 | if bit == '0':
87 | if not on_a_zero:
88 | on_a_zero = True
89 | current_0_idx = idx
90 |
91 | current_0 += 1
92 | current_1 = 0
93 | if current_0 > longest_0:
94 | longest_0 = current_0
95 | longest_0_idx = current_0_idx
96 | else:
97 | if on_a_zero:
98 | on_a_zero = False
99 | current_1_idx = idx
100 | current_1 += 1
101 | current_0 = 0
102 | if current_1 > longest_1:
103 | longest_1 = current_1
104 | longest_1_idx = current_1_idx
105 | return [[longest_0, longest_0_idx], [longest_1, longest_1_idx]]
106 |
107 | def freeze_bits(r, atomic_flip_hash):
108 | current_time = get_time(r)
109 | freeze_time = get_freeze_time(r)
110 | safety_buffer = freeze_time * 0.1
111 | threshold = current_time - freeze_time - safety_buffer
112 |
113 | chunk_size = 5000
114 | cursor = -1
115 | stats = {
116 | "total_checked": 0,
117 | "eligible_for_freezing": 0,
118 | "newly_frozen": 0,
119 | "already_frozen": 0
120 | }
121 |
122 | while cursor != 0:
123 | if cursor == -1: cursor = 0
124 | cursor, chunk = r.hscan("last_checked", cursor, count=chunk_size)
125 |
126 | for index, last_checked in chunk.items():
127 | index = int(index.decode("utf-8"))
128 | last_checked = int(last_checked.decode("utf-8"))
129 | stats["total_checked"] += 1
130 |
131 | if last_checked != 0 and last_checked < threshold:
132 | stats["eligible_for_freezing"] += 1
133 |
134 | did_freeze = r.evalsha(atomic_flip_hash, 2, "frozen_bitset", "frozen_count", index)
135 |
136 | if did_freeze == 1:
137 | stats["newly_frozen"] += 1
138 | r.publish("frozen_bit_channel", json.dumps([index]))
139 | else:
140 | stats["already_frozen"] += 1
141 |
142 | stats["frozen_count"] = stats["newly_frozen"] + stats["already_frozen"]
143 | stats["freeze_time_ms"] = freeze_time
144 | return stats
145 |
146 | if __name__ == "__main__":
147 | r = get_redis_client()
148 | atomic_flip_hash = r.script_load(atomic_flip_script)
149 | stats = freeze_bits(r, atomic_flip_hash)
150 | sunset_bitset = bytes_to_bits(r.get("sunset_bitset"))
151 | sunset_streaks = find_longest_streaks(sunset_bitset)
152 |
153 | frozen_bitset = bytes_to_bits(r.get("frozen_bitset"))
154 | frozen_streaks = find_longest_streaks(frozen_bitset)
155 |
156 | f = format_dense_regions
157 | dense_frozen_1s = f(find_dense_regions(frozen_bitset, bit_kind="1"))
158 | dense_frozen_0s = f(find_dense_regions(frozen_bitset, bit_kind="0"))
159 | dense_unchecked = f(find_dense_regions(sunset_bitset, bit_kind="0"))
160 | dense_checked = f(find_dense_regions(sunset_bitset, bit_kind="1"))
161 | print("0" in sunset_bitset)
162 | print(len(list(x for x in sunset_bitset if x == "0")))
163 | print(r.get("sunset_count"))
164 |
165 | html_content = f"""
166 |
167 |
168 |
182 |
183 |
184 |
185 |
some numbers, updated sometimes
186 |
187 | - {stats['frozen_count']}
188 | - {stats['freeze_time_ms']}
189 | - {" : ".join(map(str, sunset_streaks[0]))} | {" : ".join(map(str, sunset_streaks[1]))}
190 | - {" : ".join(map(str, frozen_streaks[0]))} | {" : ".join(map(str, frozen_streaks[1]))}
191 | - f {dense_frozen_1s}
192 | - f {dense_frozen_0s}
193 | - b {dense_checked}
194 | - b {dense_unchecked}
195 |
196 |
197 |
198 |
199 | """
200 |
201 | output_path = "/tmp/some-numbers.html" if not testing else "some-numbers.html"
202 |
203 | with open(output_path, "w") as f:
204 | f.write(html_content)
205 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | import eventlet
2 | eventlet.monkey_patch(thread=True, time=True)
3 |
4 | from flask import Flask, render_template, jsonify, request, send_from_directory, send_file
5 | from flask_socketio import SocketIO
6 | from flask_cors import CORS
7 | import os
8 | from apscheduler.schedulers.background import BackgroundScheduler
9 | from bitarray import bitarray
10 | import base64
11 | import json
12 | import time
13 | from datetime import datetime
14 | from contextlib import contextmanager
15 |
16 |
17 | MAX_LOGS_PER_DAY = 400_000_000
18 | TOTAL_CHECKBOXES = 1_000_000
19 | REACT_BUILD_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), 'dist'))
20 | # I found this by portscanning my own VPC because the DNS record wouldn't work lmfao
21 | REDIS_REPLICA_IP="10.108.0.13"
22 |
23 | app = Flask(__name__, static_folder=REACT_BUILD_DIRECTORY)
24 | CORS(app)
25 | socketio = SocketIO(app, cors_allowed_origins="*")
26 | scheduler = BackgroundScheduler()
27 |
28 | # Configuration
29 | USE_REDIS = os.environ.get('USE_REDIS', 'false').lower() == 'true'
30 |
31 | class RedisRateLimiter:
32 | def __init__(self, pool, limit, window):
33 | self.pool = pool
34 | self.limit = limit
35 | self.window = window
36 |
37 | def is_allowed(self, key: str) -> bool:
38 | with get_redis_connection(self.pool) as redis_client:
39 | pipe = redis_client.pipeline()
40 | now = int(time.time())
41 | key = f'rate_limit:{key}:{self.window}' # Include window in the key
42 |
43 | pipe.zadd(key, {now: now})
44 | pipe.zremrangebyscore(key, 0, now - self.window)
45 | pipe.zcard(key)
46 | pipe.expire(key, self.window)
47 |
48 | _, _, count, _ = pipe.execute()
49 |
50 | return count <= self.limit
51 |
52 | if USE_REDIS:
53 | import redis
54 | from redis import ConnectionPool
55 | # Create a connection pool
56 | pool = ConnectionPool(
57 | host=os.environ.get('REDIS_HOST', 'localhost'),
58 | port=int(os.environ.get('REDIS_PORT', 6379)),
59 | username=os.environ.get('REDIS_USERNAME', 'default'),
60 | password=os.environ.get('REDIS_PASSWORD', ''),
61 | db=0,
62 | connection_class=redis.SSLConnection,
63 | max_connections=425 # Adjust this number based on your needs
64 | )
65 |
66 | replica_pool = ConnectionPool(
67 | host=REDIS_REPLICA_IP,
68 | port=int(os.environ.get('REDIS_PORT', 6379)),
69 | username=os.environ.get('REDIS_USERNAME', 'default'),
70 | password=os.environ.get('REDIS_PASSWORD', ''),
71 | db=0,
72 | connection_class=redis.SSLConnection,
73 | max_connections=425 # Adjust this number based on your needs
74 | )
75 |
76 | @contextmanager
77 | def get_redis_connection(pool):
78 | connection = redis.Redis(connection_pool=pool)
79 | try:
80 | yield connection
81 | finally:
82 | connection.close()
83 |
84 | # redis_client = redis.Redis(connection_pool=pool)
85 | # print("connected to redis")
86 | # replica_client = redis.Redis(connection_pool=replica_pool)
87 | # print("connected to replica")
88 |
89 |
90 | def initialize_redis():
91 | with get_redis_connection(pool) as redis_client:
92 | if not redis_client.exists('truncated_bitset'):
93 | redis_client.set('truncated_bitset', b'\x00' * (TOTAL_CHECKBOXES // 8))
94 | if not redis_client.exists('count'):
95 | redis_client.set('count', '0')
96 |
97 | initialize_redis()
98 |
99 | # pubsub = replica_client.pubsub(ignore_subscribe_messages=True)
100 | pubsub = redis.Redis(connection_pool=replica_pool).pubsub(ignore_subscribe_messages=True)
101 | pubsub.subscribe('bit_toggle_channel')
102 |
103 | # Lua script for atomic bit setting and count update
104 | set_bit_script = """
105 | local key = KEYS[1]
106 | local index = tonumber(ARGV[1])
107 | local value = tonumber(ARGV[2])
108 | local current = redis.call('getbit', key, index)
109 | local diff = value - current
110 | redis.call('setbit', key, index, value)
111 | redis.call('incrby', 'count', diff)
112 | return diff"""
113 |
114 | new_set_bit_script="""
115 | local key = KEYS[1]
116 | local count_key = KEYS[2]
117 | local index = tonumber(ARGV[1])
118 | local max_count = tonumber(ARGV[2])
119 |
120 | local current_count = tonumber(redis.call('get', count_key) or "0")
121 | if current_count >= max_count then
122 | return {redis.call('getbit', key, index), 0} -- Return current count, current bit value, and 0 to indicate no change
123 | end
124 |
125 | local current_bit = redis.call('getbit', key, index)
126 | local new_bit = 1 - current_bit -- Toggle the bit
127 | local diff = new_bit - current_bit
128 |
129 | if diff > 0 and current_count + diff > max_count then
130 | return { current_bit, 0} -- Return current count, current bit value, and 0 to indicate no change
131 | end
132 |
133 | redis.call('setbit', key, index, new_bit)
134 | local new_count = current_count + diff
135 | redis.call('set', count_key, new_count)
136 |
137 | return {new_bit, diff} -- new bit value, and the change (1, 0, or -1)"""
138 |
139 | # set_bit_sha = redis_client.script_load(set_bit_script)
140 | # new_set_bit_sha = redis_client.script_load(new_set_bit_script)
141 | with get_redis_connection(pool) as redis_client:
142 | new_set_bit_sha = redis_client.script_load(new_set_bit_script)
143 |
144 | def get_bit(index):
145 | with get_redis_connection(pool) as redis_client:
146 | return bool(redis_client.getbit('truncated_bitset', index))
147 |
148 | def set_bit(index, value):
149 | with get_redis_connection(pool) as redis_client:
150 | [_count, diff] = redis_client.evalsha(new_set_bit_sha, 1, 'truncated_bitset', index, int(value))
151 | return diff != 0
152 |
153 | def _toggle_internal(index):
154 | with get_redis_connection(pool) as redis_client:
155 | result = redis_client.evalsha(
156 | new_set_bit_sha,
157 | 2, # number of keys
158 | 'truncated_bitset', # key for bitset
159 | 'count', # key for count
160 | index, # index to toggle
161 | TOTAL_CHECKBOXES # max count
162 | )
163 | new_bit_value, diff = result
164 | if diff == 0:
165 | return [False, None]
166 | return [True, new_bit_value]
167 |
168 | def get_full_state():
169 | with get_redis_connection(replica_pool) as replica_client:
170 | raw_data = replica_client.get("truncated_bitset")
171 | return base64.b64encode(raw_data).decode('utf-8')
172 |
173 | def get_count():
174 | with get_redis_connection(replica_pool) as replica_client:
175 | return int(replica_client.get('count') or 0)
176 |
177 | def emit_toggle(index, new_value, timestamp):
178 | with get_redis_connection(pool) as redis_client:
179 | redis_client.publish('bit_toggle_channel', json.dumps([index, new_value, timestamp]))
180 |
181 | one_second_limiter = RedisRateLimiter(pool, limit=7, window=1)
182 | fifteen_second_limiter = RedisRateLimiter(pool, limit=80, window=15)
183 | sixty_second_limiter = RedisRateLimiter(pool, limit=240, window=60)
184 |
185 | connection_limiter = RedisRateLimiter(pool, limit=20, window=15)
186 |
187 | limiters = [one_second_limiter, fifteen_second_limiter, sixty_second_limiter]
188 |
189 | def allow_toggle(key):
190 | return all(limiter.is_allowed(key) for limiter in limiters)
191 |
192 | def allow_connection(key):
193 | return connection_limiter.is_allowed(key)
194 |
195 | def log_checkbox_toggle(remote_ip, checkbox_index, checked_state):
196 | timestamp = datetime.now().isoformat()
197 | log_entry = f"{timestamp}|{remote_ip}|{checkbox_index}|{checked_state}"
198 |
199 | # Use the current date as part of the key
200 | key = f"checkbox_logs:{datetime.now().strftime('%Y-%m-%d')}"
201 | with get_redis_connection(pool) as redis_client:
202 | pipeline = redis_client.pipeline()
203 | pipeline.rpush(key, log_entry)
204 | pipeline.ltrim(key, 0, MAX_LOGS_PER_DAY - 1)
205 | pipeline.execute()
206 |
207 | else:
208 | # In-memory storage
209 | in_memory_storage = {'bitset': bitarray('0' * TOTAL_CHECKBOXES), 'count': 0}
210 |
211 | def get_bit(index):
212 | return bool(in_memory_storage['bitset'][index])
213 |
214 | def set_bit(index, value):
215 | current = in_memory_storage['bitset'][index]
216 | count = in_memory_storage['count']
217 | if count >= TOTAL_CHECKBOXES:
218 | return False
219 |
220 | in_memory_storage['bitset'][index] = value
221 | in_memory_storage['count'] += value - current
222 | return True
223 |
224 | def _toggle_internal(index):
225 | print("here")
226 | current = in_memory_storage['bitset'][index]
227 | count = in_memory_storage['count']
228 | if count >= TOTAL_CHECKBOXES:
229 | return [False, None]
230 |
231 | new_value = not current
232 | in_memory_storage['bitset'][index] = new_value
233 | in_memory_storage['count'] += 1 if new_value else -1
234 | return [True, new_value]
235 |
236 | def get_full_state():
237 | return base64.b64encode(in_memory_storage['bitset'].tobytes()).decode('utf-8')
238 |
239 | def get_count():
240 | return in_memory_storage['count']
241 |
242 | def emit_toggle(index, new_value, timestamp):
243 | update = [[index], [], timestamp] if new_value else [[], [index], timestamp]
244 | socketio.emit('batched_bit_toggles', update)
245 |
246 | limiters = []
247 |
248 | def allow_toggle(key):
249 | return True
250 |
251 | def allow_connection(key):
252 | return True
253 |
254 | def log_checkbox_toggle(remote_ip, checkbox_index, checked_state):
255 | pass
256 |
257 | def state_snapshot():
258 | full_state = get_full_state()
259 | count = get_count()
260 | timestamp = int(time.time() * 1000) # Current time in milliseconds
261 | return {'full_state': full_state, 'count': count, "timestamp": timestamp}
262 |
263 | @app.route('/api/initial-state')
264 | def get_initial_state():
265 | return jsonify(state_snapshot())
266 |
267 | def emit_full_state():
268 | print("Emitting full state")
269 | socketio.emit('full_state', state_snapshot())
270 |
271 |
272 | @socketio.on('toggle_bit')
273 | def handle_toggle(data):
274 | if not allow_toggle(request.sid):
275 | print(f"Rate limiting toggle request for {request.sid}")
276 | return False
277 |
278 | try:
279 | index = int(data['index'])
280 | except:
281 | return False
282 |
283 | if index >= TOTAL_CHECKBOXES:
284 | return False
285 |
286 | did_toggle, new_value = _toggle_internal(index)
287 | timestamp = int(time.time() * 1000) # Current time in milliseconds
288 |
289 | if did_toggle != 0:
290 | forwarded_for = request.headers.get('X-Forwarded-For') or "UNKNOWN_IP"
291 | log_checkbox_toggle(forwarded_for, index, new_value)
292 | emit_toggle(index, new_value, timestamp)
293 |
294 | @app.route('/', defaults={'path': ''})
295 | @app.route('/')
296 | def serve(path):
297 | if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
298 | return send_from_directory(app.static_folder, path)
299 | else:
300 | return send_file(os.path.join(app.static_folder, 'index.html'))
301 |
302 |
303 | #@socketio.on('connect')
304 | #def handle_connect():
305 | #forwarded_for = request.headers.get('X-Forwarded-For') or request.remote_addr
306 | #if forwarded_for and allow_connection(forwarded_for):
307 | #return True
308 | #else:
309 | #print(f"Rate limiting connection for {forwarded_for}")
310 | #return False
311 |
312 | def emit_state_updates():
313 | scheduler.add_job(emit_full_state, 'interval', seconds=45)
314 | scheduler.start()
315 |
316 | emit_state_updates()
317 |
318 | def handle_redis_messages():
319 | if USE_REDIS:
320 | message_count = 0
321 | updates = []
322 | while True:
323 | message = pubsub.get_message(timeout=0.01)
324 | if message is None:
325 | # No more messages available
326 | break
327 |
328 | if message['type'] == 'message':
329 | try:
330 | data = json.loads(message['data'])
331 | updates.append(data)
332 | message_count += 1
333 | except json.JSONDecodeError:
334 | print(f"Failed to decode message: {message['data']}")
335 |
336 | if message_count >= 600:
337 | break
338 |
339 | if message_count > 0:
340 | true_updates = []
341 | false_updates = []
342 | max_timestamp = 0
343 | for update in updates:
344 | if len(update) != 3: # backwards compatibility
345 | continue
346 | else:
347 | index, value, timestamp = update
348 | max_timestamp = max(max_timestamp, timestamp)
349 | if value:
350 | true_updates.append(index)
351 | else:
352 | false_updates.append(index)
353 | to_broadcast = [true_updates, false_updates, max_timestamp]
354 |
355 | socketio.emit('batched_bit_toggles', to_broadcast)
356 | # print(f"Processed {message_count} messages")
357 |
358 | def setup_redis_listener():
359 | if USE_REDIS:
360 | print("Redis listener job added to scheduler")
361 | scheduler.add_job(handle_redis_messages, 'interval', seconds=0.2)
362 |
363 | setup_redis_listener()
364 |
365 | if __name__ == '__main__':
366 | set_bit(0, True)
367 | set_bit(1, True)
368 | set_bit(100, True)
369 | set_bit(101, True)
370 |
371 | socketio.run(app, host="0.0.0.0", port=5001)
372 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
2 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
3 | github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
4 | github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
5 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
6 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
7 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
8 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
9 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
10 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
11 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
12 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
13 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
14 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
15 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
16 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
17 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
18 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
19 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
20 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
26 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
27 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
28 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
29 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
30 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
31 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
32 | github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
33 | github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
34 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
35 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
36 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
37 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
38 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
39 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
40 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
41 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
42 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
43 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
44 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
45 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
46 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
47 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
48 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
49 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
50 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
51 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
52 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
53 | github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
54 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
55 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
56 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
57 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ=
58 | github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
59 | github.com/googollee/go-socket.io v1.7.0 h1:ODcQSAvVIPvKozXtUGuJDV3pLwdpBLDs1Uoq/QHIlY8=
60 | github.com/googollee/go-socket.io v1.7.0/go.mod h1:0vGP8/dXR9SZUMMD4+xxaGo/lohOw3YWMh2WRiWeKxg=
61 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
62 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
63 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
64 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
65 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
66 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
67 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
68 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
69 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
70 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
71 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
72 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
73 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
75 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
76 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
77 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
78 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
79 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
80 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
81 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
82 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
83 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
84 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
86 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
87 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
88 | github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
89 | github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
90 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
91 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
92 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
93 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
94 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
95 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
96 | github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
97 | github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
98 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
99 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
100 | github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
101 | github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
102 | github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg=
103 | github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM=
104 | github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
105 | github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
106 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
107 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
108 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
109 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
110 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
111 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
112 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
113 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
114 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
115 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
116 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
117 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
118 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
119 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
120 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
121 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
122 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
123 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
124 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
125 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
126 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
127 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
128 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
129 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
130 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
131 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
132 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
133 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
134 | github.com/zishang520/engine.io-go-parser v1.2.5 h1:Disf4rvNQzDsgoC+3yuwuFx5A7JNWlPp+QLUW32WDtc=
135 | github.com/zishang520/engine.io-go-parser v1.2.5/go.mod h1:G1DciRIGH4/S7x01DIdZQaXrk09ZeRgEw5e/Z9ms4Is=
136 | github.com/zishang520/engine.io/v2 v2.1.1 h1:PZYi3/XW6jJh6yzVoF19JifgmMVggbLskSDNpXtaOFk=
137 | github.com/zishang520/engine.io/v2 v2.1.1/go.mod h1:FnXtT+k/6g2uOb9MpqY71DhV7COwlCH5DCbczn6Q3K8=
138 | github.com/zishang520/socket.io-go-parser/v2 v2.1.0 h1:YaTul861UxdTtq/v7XKmF52gWmDOqwugKBlFyiifKCE=
139 | github.com/zishang520/socket.io-go-parser/v2 v2.1.0/go.mod h1:zmToGML+lCjSjyGZMuVtnvgnFOnDuAxJZKwfDDDHiqI=
140 | github.com/zishang520/socket.io/v2 v2.2.0 h1:zm+6JFkI+ZmBCNjHXHHPRescqB+qaiFEYY4WvEgH60Q=
141 | github.com/zishang520/socket.io/v2 v2.2.0/go.mod h1:sYHxrHgsK83ylNDEoezfbq1lbpM2mnajc4e2IKAqyvY=
142 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
143 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
144 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
145 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
146 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
147 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
148 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
149 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
150 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
151 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
152 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
153 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
154 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
155 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
156 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
157 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
160 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
161 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
162 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
163 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
164 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
165 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
166 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
167 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
168 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
172 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
173 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
174 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
175 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
176 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
177 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
178 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
179 |
--------------------------------------------------------------------------------
/src/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useRef, useEffect } from "react";
2 | import { FixedSizeGrid as Grid } from "react-window";
3 | import { useWindowSize } from "react-use";
4 | import styled, { keyframes } from "styled-components";
5 | import BitSet from "../../bitset";
6 | import INDICES from "../../randomizedColors";
7 | import { abbrNum } from "../../utils";
8 | import useClickTracker from "../../hooks/use-click-tracker";
9 |
10 | const TOTAL_CHECKBOXES = 1000000;
11 | const CHECKBOX_SIZE = 35;
12 | const OVERSCAN_COUNT = 5;
13 | const ONE_SECOND_THRESHOLD = 7;
14 | const FIFTEEN_SECOND_THRESHOLD = 80;
15 | const SIXTY_SECOND_THRESHOLD = 240;
16 |
17 | const useForceUpdate = ({ bitSetRef, setCheckCount }) => {
18 | const [, setTick] = useState(0);
19 | return useCallback(() => {
20 | setTick((tick) => tick + 1);
21 | setCheckCount(bitSetRef?.current?.count() || 0);
22 | }, [bitSetRef, setCheckCount]);
23 | };
24 |
25 | const Checkbox = React.memo(
26 | ({ index, style, isChecked, handleChange, disabled }) => {
27 | let backgroundColor = null;
28 | if (INDICES[index]) {
29 | backgroundColor = `var(--${INDICES[index]}`;
30 | }
31 |
32 | return (
33 |
34 |
41 |
44 |
45 | );
46 | }
47 | );
48 |
49 | const isDesktopSafari = () => {
50 | const ua = navigator.userAgent;
51 | return /^((?!chrome|android).)*safari/i.test(ua) && !/mobile/i.test(ua);
52 | };
53 |
54 | const StyledCheckbox = styled.input`
55 | margin: 0;
56 | padding: 0;
57 | width: 25px;
58 | height: 25px;
59 | box-shadow: none;
60 | /* transform: translate(10px, 10px); */
61 |
62 | transform: ${isDesktopSafari() ? "translate(3px, 0px)" : "none"};
63 | `;
64 |
65 | const MaybeColoredDiv = styled.div`
66 | position: absolute;
67 | pointer-events: none;
68 | border: 5px solid var(--background-color);
69 | height: 29px;
70 | width: 29px;
71 | border-radius: 2px;
72 | `;
73 |
74 | const fadeIn = keyframes`
75 | from {
76 | opacity: 0;
77 | }
78 |
79 | to {
80 | opacity: 1;
81 | }
82 | `;
83 |
84 | const CheckboxWrapper = styled.div`
85 | display: flex;
86 | align-items: center;
87 | justify-content: center;
88 | width: ${CHECKBOX_SIZE}px;
89 | height: ${CHECKBOX_SIZE}px;
90 | opacity: var(--opacity);
91 | transition: opacity 0.5s;
92 | animation: ${fadeIn} 0.4s;
93 | `;
94 |
95 | const initialSelfCheckboxState = () => ({
96 | total: 0,
97 | totalGold: 0,
98 | totalRed: 0,
99 | totalGreen: 0,
100 | totalPurple: 0,
101 | totalOrange: 0,
102 | recentlyChecked: false,
103 | });
104 |
105 | const scoreString = ({ selfCheckboxState, allChecked }) => {
106 | const colors = ["gold", "red", "green", "purple"];
107 | const colorsToInclude = colors
108 | .map((color) => {
109 | const count =
110 | selfCheckboxState[
111 | `total${color.charAt(0).toUpperCase()}${color.slice(1)}`
112 | ];
113 | if (count !== 0) {
114 | return [color, count];
115 | }
116 | return null;
117 | })
118 | .filter((el) => el !== null);
119 |
120 | return (
121 |
122 | You {allChecked ? "" : "have "}checked {selfCheckboxState.total}{" "}
123 | {colorsToInclude.length > 0 ? "(" : ""}
124 | {colorsToInclude.map(([color, count]) => {
125 | return (
126 |
127 | {abbrNum(count, 2)}
128 |
129 | );
130 | })}
131 | {colorsToInclude.length > 0 ? ") " : " "}
132 | boxes
133 |
134 | );
135 | };
136 |
137 | const MailIcon = () => (
138 |
152 | );
153 |
154 | const DollarIcon = () => (
155 |
169 | );
170 |
171 | const App = () => {
172 | const { width, height } = useWindowSize();
173 | const gridRef = useRef();
174 | const [jumpToIndex, setJumpToIndex] = useState("");
175 |
176 | const gridWidth = Math.floor(width * 0.95);
177 | const columnCount = Math.floor(gridWidth / CHECKBOX_SIZE);
178 | const rowCount = Math.ceil(TOTAL_CHECKBOXES / columnCount);
179 | const bitSetRef = useRef(null);
180 | const frozenBitsetRef = useRef(null);
181 | const [checkCount, setCheckCount] = React.useState(0);
182 | const forceUpdate = useForceUpdate({ bitSetRef, setCheckCount });
183 | const [isLoading, setIsLoading] = useState(true);
184 | const recentlyCheckedClientSide = useRef({});
185 | const [clickCounts, trackClick] = useClickTracker();
186 | const [disabled, setDisabled] = useState(false);
187 | const [allChecked, setAllChecked] = useState(false);
188 | const clickTimeout = React.useRef();
189 | const [singlePlayerMode, setSinglePlayerMode] = useState(false);
190 | const doAlert = React.useCallback((s) => {
191 | window.alert(s);
192 | }, []);
193 |
194 | const [selfCheckboxState, setSelfCheckboxState] = useState(() => {
195 | const fromLocal = localStorage.getItem("selfCheckboxState");
196 | try {
197 | return fromLocal ? JSON.parse(fromLocal) : initialSelfCheckboxState();
198 | } catch (error) {
199 | console.error(
200 | "Failed to parse selfCheckboxState from localStorage:",
201 | error
202 | );
203 | const initial = initialSelfCheckboxState();
204 | localStorage.setItem("selfCheckboxState", JSON.stringify(initial));
205 | return initial;
206 | }
207 | });
208 |
209 | React.useEffect(() => {
210 | console.log(JSON.stringify(selfCheckboxState));
211 | localStorage.setItem(
212 | "selfCheckboxState",
213 | JSON.stringify(selfCheckboxState)
214 | );
215 | }, [selfCheckboxState]);
216 |
217 | useEffect(() => {
218 | const fetchInitialState = async () => {
219 | try {
220 | const bitset = BitSet.makeFull();
221 | const frozenBitset = BitSet.makeFull();
222 | setDisabled(true);
223 | setAllChecked(true);
224 | bitSetRef.current = bitset;
225 | frozenBitsetRef.current = frozenBitset;
226 | forceUpdate();
227 | setIsLoading(false);
228 | } catch (error) {
229 | console.error("Failed to create initial state:", error);
230 | setIsLoading(false);
231 | }
232 | };
233 |
234 | fetchInitialState();
235 | }, [forceUpdate]);
236 |
237 | const toggleBit = useCallback(
238 | async (index) => {
239 | trackClick();
240 | if (
241 | clickCounts.current.oneSecond > ONE_SECOND_THRESHOLD ||
242 | clickCounts.current.fifteenSeconds > FIFTEEN_SECOND_THRESHOLD ||
243 | clickCounts.current.sixtySeconds > SIXTY_SECOND_THRESHOLD
244 | ) {
245 | doAlert("CHILL LOL");
246 | setDisabled(true);
247 | clickTimeout.current && clearTimeout(clickTimeout.current);
248 | clickTimeout.current = setTimeout(() => {
249 | setDisabled(false);
250 | }, 2500);
251 | } else {
252 | try {
253 | bitSetRef.current?.toggle(index);
254 | const count = bitSetRef.current.count();
255 | setCheckCount(count);
256 | if (count >= 1000000) {
257 | setDisabled(true);
258 | setAllChecked(true);
259 | }
260 | forceUpdate();
261 | const isChecked = bitSetRef.current?.get(index);
262 | setSelfCheckboxState((prev) => {
263 | const newState = { ...prev };
264 | newState.total += isChecked ? 1 : -1;
265 | if (INDICES[index]) {
266 | const color = INDICES[index];
267 | newState[
268 | `total${color.charAt(0).toUpperCase()}${color.slice(1)}`
269 | ] += isChecked ? 1 : -1;
270 | }
271 | return newState;
272 | });
273 | if (singlePlayerMode) {
274 | localStorage.setItem(
275 | "localBitset",
276 | JSON.stringify(bitSetRef.current)
277 | );
278 | } else {
279 | // socketRef.current?.emit("toggle_bit", { index });
280 | }
281 | } catch (error) {
282 | console.error("Failed to toggle bit:", error);
283 | } finally {
284 | }
285 | }
286 | },
287 | [trackClick, clickCounts, doAlert, forceUpdate, singlePlayerMode]
288 | );
289 |
290 | const Cell = React.useCallback(
291 | ({ columnIndex, rowIndex, style, isLoading }) => {
292 | const index = rowIndex * columnCount + columnIndex;
293 | if (index >= TOTAL_CHECKBOXES) return null;
294 |
295 | let isFrozen = false;
296 | let isChecked = false;
297 | if (singlePlayerMode) {
298 | isChecked = bitSetRef.current?.get(index);
299 | } else {
300 | isFrozen = frozenBitsetRef.current.get(index);
301 | isChecked = isFrozen || bitSetRef.current?.get(index);
302 | }
303 |
304 | const handleChange = () => {
305 | toggleBit(index);
306 |
307 | const timeout = setTimeout(() => {
308 | delete recentlyCheckedClientSide.current[index];
309 | }, 1000);
310 |
311 | if (recentlyCheckedClientSide.current[index]) {
312 | clearTimeout(recentlyCheckedClientSide.current[index].timeout);
313 | }
314 |
315 | recentlyCheckedClientSide.current[index] = {
316 | value: !isChecked,
317 | timeout,
318 | };
319 | };
320 |
321 | return (
322 |
330 | );
331 | },
332 | [columnCount, disabled, toggleBit, singlePlayerMode]
333 | );
334 |
335 | const handleJumpToCheckbox = (e) => {
336 | e.preventDefault();
337 | const index = parseInt(jumpToIndex, 10) - 1; // Subtract 1 because array is 0-indexed
338 | if (index >= 0 && index < TOTAL_CHECKBOXES) {
339 | const rowIndex = Math.floor(index / columnCount);
340 | const columnIndex = index % columnCount;
341 | gridRef.current.scrollTo({
342 | scrollTop: rowIndex * CHECKBOX_SIZE,
343 | scrollLeft: columnIndex * CHECKBOX_SIZE,
344 | });
345 | }
346 | setJumpToIndex("");
347 | };
348 |
349 | const youHaveChecked = scoreString({ selfCheckboxState, allChecked });
350 | const cappedCheckCount = Math.min(1000000, checkCount).toLocaleString();
351 |
352 | const enableSinglePlayer = React.useCallback(
353 | (e) => {
354 | console.log("enabling single player");
355 | e.preventDefault();
356 | setSinglePlayerMode(true);
357 | const localJson = localStorage.getItem("localBitset");
358 | setDisabled(false);
359 |
360 | if (localJson) {
361 | try {
362 | const parsed = JSON.parse(localJson);
363 | bitSetRef.current = new BitSet(parsed);
364 | setCheckCount(bitSetRef.current.count());
365 | console.log(
366 | `Loaded single player state, count: ${bitSetRef.current.count()}`
367 | );
368 | } catch (error) {
369 | console.error("Failed to load local bitset:", error);
370 | const empty = BitSet.makeEmpty();
371 | bitSetRef.current = empty;
372 | setCheckCount(0);
373 | localStorage.setItem("localBitset", JSON.stringify(empty));
374 | forceUpdate();
375 | }
376 | forceUpdate();
377 | } else {
378 | const empty = BitSet.makeEmpty();
379 | bitSetRef.current = empty;
380 | setCheckCount(0);
381 | localStorage.setItem("localBitset", JSON.stringify(empty));
382 | forceUpdate();
383 | }
384 | },
385 | [forceUpdate]
386 | );
387 |
388 | return (
389 |
390 |
391 |
394 |
395 | a website by eieio{" "}
396 |
397 |
398 |
399 | {MailIcon()}
400 |
401 |
402 | {DollarIcon()}
403 |
404 |
405 |
406 | One Million Checkboxes
407 |
414 | {cappedCheckCount} boxes are ✅
415 |
416 |
417 |
420 |
421 | a website by eieio{" "}
422 |
423 |
424 |
425 | {MailIcon()}
426 |
427 |
428 | {DollarIcon()}
429 |
430 |
431 |
432 |
437 | {cappedCheckCount} boxes are ✅
438 |
439 |
440 |
441 | {allChecked ? (
442 |
443 | 🎉 we checked every box! 🎉
444 | {singlePlayerMode ? (
445 | you're playing alone now
446 | ) : (
447 |
448 | but you can still{" "}
449 | play alone{" "}
450 | if you'd like
451 |
452 | )}
453 |
454 | ) : (
455 |
456 | checking a box checks it for everyone!
457 | boxes freeze if they've been checked for a while
458 |
459 | )}
460 | {youHaveChecked}
461 |
462 |
482 | {isLoading ? (
483 | Loading...
484 | ) : (
485 |
503 | {Cell}
504 |
505 | )}
506 |
507 | );
508 | };
509 |
510 | const Heading = styled.div`
511 | display: grid;
512 | justify-content: space-between;
513 |
514 | align-items: baseline;
515 | width: var(--width);
516 | margin: -4px auto 0;
517 | grid-template-columns: 1fr auto 1fr;
518 | grid-template-rows: auto auto auto;
519 | grid-template-areas:
520 | "site title count"
521 | ". sub ."
522 | "you you you";
523 |
524 | padding-bottom: 10px;
525 | border-bottom: 2px solid var(--dark);
526 |
527 | @media (max-width: 850px) {
528 | grid-template-columns: 1fr auto auto 1fr;
529 | grid-template-rows: auto auto auto auto;
530 | grid-template-areas:
531 | "title title title title"
532 | ". sub sub ."
533 | "sitecount sitecount sitecount sitecount"
534 | "you you you you";
535 | }
536 | `;
537 |
538 | const DonoLinks = styled.span`
539 | @media (max-width: 550px) {
540 | margin-top: -6px;
541 | }
542 | `;
543 |
544 | const MailIconLink = styled.a`
545 | display: inline-flex;
546 | vertical-align: middle;
547 | color: var(--blue);
548 | text-decoration: none;
549 | border-radius: 5px;
550 | transition: background-color 0.3s ease;
551 |
552 | &:hover {
553 | color: var(--dark);
554 | }
555 | `;
556 |
557 | const DollarIconLink = styled.a`
558 | display: inline-flex;
559 | vertical-align: middle;
560 | color: var(--green) !important;
561 | text-decoration: none;
562 | border-radius: 5px;
563 | transition: background-color 0.3s ease;
564 | margin-left: 2px;
565 |
566 | &:hover {
567 | color: var(--dark) !important;
568 | }
569 | `;
570 |
571 | const JumpInput = styled.input`
572 | margin: 0;
573 | padding: 8px;
574 | height: 40px;
575 | font-size: 1rem;
576 | width: 160px;
577 | border: 2px solid var(--blue);
578 | border-radius: 0;
579 | `;
580 |
581 | const JumpButton = styled.button`
582 | margin: 0;
583 | padding: 8px;
584 | height: 40px;
585 | font-size: 1rem;
586 | background-color: var(--blue);
587 | border: none;
588 | color: white;
589 | cursor: pointer;
590 | transition: background-color 0.3s;
591 | `;
592 |
593 | const Title = styled.h1`
594 | margin: 0;
595 | padding: 8px 0 0 0;
596 | font-size: clamp(1.75rem, 2vw + 1rem, 3.5rem);
597 | font-family: "Sunset Demi", serif;
598 | text-align: center;
599 | grid-area: title;
600 | `;
601 |
602 | const SubHead = styled.h2`
603 | margin: 0;
604 | padding: 4px 0 0 0;
605 | flex: 1;
606 | font-size: clamp(1rem, 0.15vw + 1rem, 2.5rem);
607 | font-family: "Apercu Regular Pro", sans-serif;
608 |
609 | & a {
610 | color: var(--blue);
611 | text-decoration: underline;
612 | // dotted underline
613 | text-decoration-style: dashed;
614 | // move underline a little further down
615 | text-underline-offset: 0.12em;
616 | transition: color 0.3s;
617 | }
618 |
619 | & a:hover {
620 | color: var(--dark);
621 | }
622 | `;
623 |
624 | const Explanation = styled.p`
625 | font-size: 1rem;
626 | text-align: center;
627 | grid-area: sub;
628 | font-family: "Apercu Italic Pro", sans-serif;
629 | // italicize
630 | font-style: italic;
631 | margin-top: -10px;
632 | `;
633 |
634 | const PlayAlone = styled.button`
635 | margin: 0;
636 | padding: 0;
637 | border: none;
638 | outline: none;
639 | display: inline;
640 | color: var(--blue);
641 | text-decoration: underline;
642 | text-decoration-style: dashed;
643 | // move underline a little further down
644 | text-underline-offset: 0.12em;
645 | transition: color 0.3s;
646 | cursor: pointer;
647 |
648 | &:hover {
649 | color: var(--dark);
650 | }
651 | `;
652 |
653 | const YouHaveChecked = styled.div`
654 | font-size: 1rem;
655 | font-family: "Apercu Bold Pro", sans-serif;
656 | text-align: right;
657 | grid-area: you;
658 | margin-top: 10px;
659 | display: flex;
660 | flex-direction: column;
661 | `;
662 |
663 | const SiteHead = styled(SubHead)`
664 | text-align: left;
665 | grid-area: site;
666 | /* display: flex; */
667 | gap: 6px;
668 | align-items: baseline;
669 |
670 | display: var(--desktop-display);
671 | @media (max-width: 850px) {
672 | display: var(--mobile-display);
673 | }
674 | @media (max-width: 550px) {
675 | flex-direction: column;
676 | gap: 0px;
677 | width: fit-content;
678 | flex-grow: 0;
679 | flex-basis: fit-content;
680 |
681 | /* flex: 0; */
682 | }
683 | `;
684 |
685 | const CountHead = styled(SubHead)`
686 | text-align: right;
687 | grid-area: count;
688 | opacity: var(--opacity);
689 | transition: opacity 0.5s;
690 | display: var(--desktop-display);
691 | @media (max-width: 850px) {
692 | display: var(--mobile-display);
693 | flex-grow: 1;
694 | }
695 | `;
696 |
697 | const SiteCountMobile = styled.div`
698 | grid-area: sitecount;
699 | display: flex;
700 | justify-content: space-between;
701 | display: none;
702 | @media (max-width: 850px) {
703 | display: flex;
704 | }
705 | `;
706 |
707 | const Wrapper = styled.div`
708 | display: flex;
709 | align-items: center;
710 | flex-direction: column;
711 | height: 100dvh;
712 | `;
713 |
714 | const ColorSpan = styled.span`
715 | color: var(--color);
716 |
717 | &:not(:last-of-type):after {
718 | content: " ";
719 | }
720 | `;
721 |
722 | export default App;
723 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "embed"
7 | "encoding/base64"
8 | "encoding/json"
9 | "flag"
10 | "fmt"
11 | "log"
12 | "log/slog"
13 | "math"
14 | "math/rand"
15 | "net"
16 | "net/http"
17 | "os"
18 | "runtime/debug"
19 | "strconv"
20 | "sync/atomic"
21 | "time"
22 |
23 | "github.com/gin-contrib/static"
24 | "github.com/redis/go-redis/v9"
25 | "github.com/zishang520/socket.io/v2/socket"
26 |
27 | "github.com/alicebob/miniredis/v2"
28 | "github.com/gin-gonic/gin"
29 | "github.com/puzpuzpuz/xsync"
30 | )
31 |
32 | var (
33 | background = context.Background()
34 | mini *miniredis.Miniredis
35 | primaryRedisClient *redis.Client
36 | secondaryRedisClient *redis.Client
37 | port = flag.Int("port", 5001, "http port to listen to")
38 | logChannel = make(chan *toggleLogEntry, 20)
39 | activeConns atomic.Int64
40 | REDIS_SECONDARY_IP = flag.String(
41 | "redis-secondary",
42 | "10.108.0.15",
43 | "",
44 | )
45 | forceStateSnapshot = flag.Duration(
46 | "force-snapshot-interval",
47 | time.Second*57,
48 | "",
49 | )
50 | maxLogInterval = flag.Duration("max-log-interval", time.Second*5, "")
51 | maxLogBatchSize = flag.Int("max-log-batch", 200, "")
52 | mercyRatio = flag.Int64(
53 | "mercy-ratio",
54 | 2,
55 | "how quickly we should forgive the bad guys",
56 | )
57 | )
58 |
59 | //go:embed dist
60 | var distFolder embed.FS
61 |
62 | const (
63 | MAX_LOGS_PER_DAY = 400_000_000
64 | TOTAL_CHECKBOXES = 1_000_000
65 | )
66 |
67 | func initializeCustomBitset() []byte {
68 | // Calculate the sizes
69 | emptyFrontBytes := 2
70 | fullBytes := 125000 - 3
71 | emptyEndBytes := 1
72 |
73 | // Create the byte slice
74 | bitset := make([]byte, emptyFrontBytes+fullBytes+emptyEndBytes)
75 |
76 | // Fill the middle part with 1s (0xFF is a byte with all bits set to 1)
77 | for i := emptyFrontBytes; i < emptyFrontBytes+fullBytes; i++ {
78 | bitset[i] = 0xFF
79 | }
80 |
81 | return bitset
82 | }
83 |
84 | func initRedis() {
85 | log := slog.With("scope", "redis")
86 | if os.Getenv("REDIS_HOST") == "" {
87 | primaryRedisClient = miniClient()
88 | secondaryRedisClient = primaryRedisClient
89 | } else {
90 | p, err := primaryRedis()
91 | if err != nil {
92 | log.Error("Unable to talk to primary redis", "err", err)
93 | }
94 | s, err := replicaRedis()
95 | if err != nil {
96 | log.Error("Unable to talk to secondary redis", "err", err)
97 | }
98 | primaryRedisClient = p
99 | secondaryRedisClient = s
100 | }
101 | err := newSetBitScript.Load(background, primaryRedisClient).Err()
102 | if err != nil {
103 | log.Error("Unable to load scripts into primary redis, %s", err)
104 | }
105 | err = newSetBitScript.Load(background, secondaryRedisClient).Err()
106 | if err != nil {
107 | log.Error("Unable to load scripts into secondary redis, %s", err)
108 | }
109 | // if err := primaryRedisClient.SetNX(
110 | // background,
111 | // "truncated_bitset",
112 | // string(make([]byte, TOTAL_CHECKBOXES)),
113 | // 0,
114 | // ).Err(); err != nil {
115 | // log.Error("Unable to initialize bitset %s", err)
116 | // }
117 | if err := primaryRedisClient.SetNX(
118 | background,
119 | "sunset_bitset",
120 | // string(initializeCustomBitset()),
121 | string(make([]byte, TOTAL_CHECKBOXES / 8)),
122 | 0,
123 | ).Err(); err != nil {
124 | log.Error("Unable to initialize sunset bitset %s", err)
125 | }
126 | if err := primaryRedisClient.SetNX(
127 | background,
128 | "sunset_count",
129 | // "999976",
130 | "0",
131 | 0,
132 | ).Err(); err != nil {
133 | log.Error("Unable to initialize sunset count %s", err)
134 | }
135 | if err := primaryRedisClient.SetNX(
136 | background,
137 | "frozen_bitset",
138 | string(make([]byte, TOTAL_CHECKBOXES / 8)),
139 | 0,
140 | ).Err(); err != nil {
141 | log.Error("Unable to initialize frozen bitset %s", err)
142 | }
143 | if err := primaryRedisClient.SetNX(
144 | background,
145 | "freeze_time_ms",
146 | "22015",
147 | // "1000",
148 | 0,
149 | ).Err(); err != nil {
150 | log.Error("Unable to initialize freeze time %s", err)
151 | }
152 | if err := primaryRedisClient.SetNX(
153 | background,
154 | "frozen_count",
155 | "0",
156 | 0,
157 | ).Err(); err != nil {
158 | log.Error("Unable to initialize frozen count %s", err)
159 | }
160 | }
161 |
162 | type stateSnapshot struct {
163 | FullState string `json:"full_state"`
164 | FrozenState string `json:"frozen_state"`
165 | Count int `json:"count"`
166 | FrozenCount int `json:"frozen_count"`
167 | Timestamp int `json:"timestamp"`
168 | }
169 |
170 | func JSON(v any) string {
171 | buff, err := json.Marshal(v)
172 | if err != nil {
173 | slog.Error("Bad json ", "err", err)
174 | return "{}"
175 | }
176 | return string(buff)
177 | }
178 |
179 | func getStateSnapshot() *stateSnapshot {
180 | count, _ := secondaryRedisClient.Get(background, "sunset_count").Int()
181 | frozenCount, _ := secondaryRedisClient.Get(background, "frozen_count").Int()
182 | return &stateSnapshot{
183 | FullState: getFullState(),
184 | FrozenState: getFrozenState(),
185 | Count: count,
186 | FrozenCount: frozenCount,
187 | Timestamp: int(time.Now().UnixMilli()),
188 | }
189 | }
190 |
191 | type toggleLogEntry struct {
192 | ip string
193 | index int
194 | state bool
195 | }
196 |
197 | func logToggles(logs []*toggleLogEntry) {
198 | now := time.Now()
199 | key := fmt.Sprintf(
200 | "checkbox_logs:%s",
201 | now.Format(time.DateOnly),
202 | )
203 | pipeline := primaryRedisClient.Pipeline()
204 | t := "True"
205 | f := "False"
206 | for _, l := range logs {
207 | state := t
208 | if !l.state {
209 | state = f
210 | }
211 |
212 | entry := fmt.Sprintf(
213 | "%s|%s|%d|%s|s",
214 | now.Format(time.DateTime),
215 | l.ip,
216 | l.index,
217 | state,
218 | )
219 | pipeline.RPush(background, key, entry)
220 | }
221 |
222 | pipeline.LTrim(background, key, 0, int64(MAX_LOGS_PER_DAY-1))
223 | _, err := pipeline.Exec(background)
224 | if err != nil {
225 | slog.Error("error during redis log rotation", "err", err)
226 | }
227 | }
228 | func catch(f func(), cleanup ...func()) {
229 | defer func() {
230 | if msg := recover(); msg != nil {
231 | slog.Error("Recovered from panic %v", msg)
232 | debug.PrintStack()
233 | for _, c := range cleanup {
234 | catch(c)
235 | }
236 | }
237 | }()
238 | f()
239 | }
240 |
241 | func tryForever(f func()) {
242 |
243 | for {
244 | catch(f)
245 | }
246 | }
247 |
248 | func try(f func(args ...any)) func(...any) {
249 | return func(args ...any) {
250 | catch(func() {
251 | f(args...)
252 | })
253 | }
254 | }
255 |
256 | func handleLogs() {
257 | tryForever(func() {
258 | t := time.NewTicker(*maxLogInterval)
259 | var buff []*toggleLogEntry
260 | for {
261 | select {
262 | case <-t.C:
263 | if len(buff) == 0 {
264 | continue
265 | }
266 | case msg := <-logChannel:
267 | buff = append(buff, msg)
268 | if len(buff) < *maxLogBatchSize {
269 | continue
270 | }
271 | }
272 | abuseCount := 0
273 | activeClientCount := 0
274 | abuseMap.Range(func(key string, value *atomic.Int64) bool {
275 | abuseCount += int(value.Load())
276 | activeClientCount += 1
277 | return true
278 | })
279 | slog.Info(
280 | "submitting logs",
281 | "count",
282 | len(buff),
283 | "clients",
284 | activeClientCount,
285 | "totalAbuse",
286 | abuseCount,
287 | )
288 | logToggles(buff)
289 | buff = buff[:0]
290 | }
291 | })
292 | }
293 |
294 | var (
295 | abuseMap = xsync.NewMapOf[*atomic.Int64]()
296 | )
297 |
298 | var (
299 | maxAbuseRequests = flag.Int64(
300 | "max-abuse-requests",
301 | 400,
302 | "maximum nubmer of requests a client can make before we consider it abuse",
303 | )
304 | abuseResetInterval = flag.Duration(
305 | "abuse-reset",
306 | time.Minute,
307 | "reset the abuse pentaly after this time",
308 | )
309 | )
310 |
311 | func groupIPv6(ip string) (string, bool) {
312 |
313 | parsedIP := net.ParseIP(ip)
314 | if parsedIP == nil || parsedIP.To4() != nil {
315 | return ip, false // Return as-is if it's not a valid IPv6 address
316 | }
317 |
318 | ipv6Addr := parsedIP.To16()
319 | if ipv6Addr == nil {
320 | return ip, false // Shouldn't happen, but just in case
321 | }
322 |
323 | // Keep the first 48 bits (6 bytes) and zero out the rest
324 | // maybe 64?
325 | for i := 6; i < 16; i++ {
326 | ipv6Addr[i] = 0
327 | }
328 |
329 | return ipv6Addr.String(), true
330 | }
331 |
332 | func socketIP(s *socket.Socket) (string, bool) {
333 | // Check Cloudflare-specific header first
334 | log := slog.With("socketIP")
335 | NOLEN_IP := s.Request().Request().Header.Get("NOLEN-IP")
336 |
337 | cfIP := s.Request().Request().Header.Get("CF-Connecting-IP")
338 |
339 | if NOLEN_IP != "" {
340 | // check if it begins with "10."
341 | if len(NOLEN_IP) < 3 || NOLEN_IP[:3] == "10." {
342 | log.Info("SKIP NOLEN IP ITS PRIVATE")
343 | } else {
344 | log.Info("Using NOLEN IP", "ip", NOLEN_IP)
345 | return groupIPv6(NOLEN_IP)
346 | }
347 | }
348 |
349 | if cfIP != "" {
350 | log.Info("Using Cloudflare IP", "ip", cfIP)
351 | return groupIPv6(cfIP)
352 | }
353 | forwarded := s.Request().Request().Header.Get("X-Forwarded-For")
354 | if forwarded != "" {
355 | log.Info("Using forwarded IP", "ip", forwarded)
356 | return groupIPv6(forwarded)
357 |
358 | }
359 |
360 | addr, _ := net.ResolveTCPAddr("tcp", s.Conn().RemoteAddress())
361 | z := addr.IP.String()
362 | log.Info("Using remote IP", "ip", z)
363 | return groupIPv6(z)
364 | }
365 |
366 | func resetAbuseCounters() {
367 | tryForever(func() {
368 | t := time.NewTicker(*abuseResetInterval)
369 | zeros := []string{}
370 | for range t.C {
371 | abuseMap.Range(func(key string, value *atomic.Int64) bool {
372 | tmp := value.Load()
373 | tmp -= (*maxAbuseRequests * *mercyRatio)
374 | if tmp < 0 {
375 | zeros = append(zeros, key)
376 | value.Store(0)
377 | return true
378 | }
379 | value.Store(tmp)
380 | return true
381 | })
382 | }
383 |
384 | for _, name := range zeros {
385 | abuseMap.Delete(name)
386 | }
387 | })
388 | }
389 |
390 | func detectAbuse(ip string, isIPV6 bool) bool {
391 | count, _ := abuseMap.LoadOrCompute(ip, func() *atomic.Int64 {
392 | v := new(atomic.Int64)
393 | v.Store(0)
394 | return v
395 | })
396 | if isIPV6 {
397 | count.Add(1) // at some point I added 10 instead here
398 | } else {
399 | count.Add(1)
400 | }
401 | if count.Load() < *maxAbuseRequests {
402 | return false
403 | }
404 | // reducing this a bit to trim load a little more
405 | thousands := float64(count.Load()) / 700
406 | chance := math.Pow(0.5, thousands)
407 | return chance > rand.Float64()
408 | }
409 |
410 | func dumpHashsetState(rdb *redis.Client, log *slog.Logger) {
411 | result, err := rdb.HGetAll(context.Background(), "last_checked").Result()
412 | if err != nil {
413 | log.Error("Failed to get hashset state", "error", err)
414 | return
415 | }
416 |
417 | state := make(map[string]int64)
418 | for k, v := range result {
419 | timestamp, err := strconv.ParseInt(v, 10, 64)
420 | if err != nil {
421 | log.Error("Failed to parse timestamp", "key", k, "value", v, "error", err)
422 | continue
423 | }
424 | state[k] = timestamp
425 | }
426 |
427 | stateJSON, err := json.MarshalIndent(state, "", " ")
428 | if err != nil {
429 | log.Error("Failed to marshal hashset state", "error", err)
430 | return
431 | }
432 |
433 | log.Info("Current hashset state", "state", string(stateJSON))
434 | }
435 |
436 | func main() {
437 | flag.Parse()
438 | initRedis()
439 | r := gin.Default()
440 | r.Group("/api").GET("/initial-state", func(ctx *gin.Context) {
441 | ctx.JSON(200,
442 | getStateSnapshot(),
443 | )
444 | })
445 | go resetAbuseCounters()
446 | defer primaryRedisClient.Close()
447 | defer secondaryRedisClient.Close()
448 | go handleLogs()
449 |
450 | ws := socket.NewServer(nil, nil)
451 | ws.On("connection", func(a ...any) {
452 | client := a[0].(*socket.Socket)
453 | catch(func() {
454 | ip, isIPV6 := socketIP(client)
455 | activeConns.Add(1)
456 | log := slog.With(
457 | "client",
458 | client.Id(),
459 | "ip",
460 | ip,
461 | "isIPV6",
462 | isIPV6,
463 | )
464 | if isIPV6 {
465 | // add socket to "ipv6" room
466 | client.Join("ipv6")
467 | }
468 | client.On("disconnect", try(func(a ...any) {
469 | activeConns.Add(-1)
470 | log.Debug("leaving")
471 | }))
472 | if detectAbuse(ip, isIPV6) {
473 | log.Info("rejecting connection from suspected abuse ip")
474 | client.Conn().Close(true)
475 | return
476 | }
477 |
478 | client.On("unsubscribe", try(func(a ...any) {
479 | log.Info("client unsubbed")
480 | client.Join("nomessage")
481 | client.Emit("unsubscribed", "unsubscribed")
482 | }))
483 |
484 | client.On("toggle_bit", try(func(a ...any) {
485 | if detectAbuse(ip, isIPV6) {
486 | client.Conn().Close(true)
487 | log.Info("rejecting toggle from suspected abuse ip")
488 | return
489 | }
490 | data := a[0].(map[string]any)
491 | index := int(data["index"].(float64))
492 | tlg := log.WithGroup("toggle_bit").With("index", index)
493 | if index >= TOTAL_CHECKBOXES || index < 0 {
494 | log.Error("attmepted to toggle bad index")
495 | return
496 | }
497 | res := frozenSetBitScript.Run(
498 | background,
499 | primaryRedisClient,
500 | []string{
501 | "sunset_bitset",
502 | "sunset_count",
503 | "frozen_bitset",
504 | "frozen_count",
505 | "freeze_time_ms",
506 | },
507 | int(index),
508 | TOTAL_CHECKBOXES,
509 | )
510 | if res.Err() != nil {
511 | tlg.Error("Unable to toggle bit", "err", res.Err())
512 | return
513 | }
514 | ts := time.Now().UnixMilli()
515 | nv, _ := res.Int64Slice()
516 | nbv, diff, newly_frozen := nv[0], nv[1], nv[2]
517 | if diff != 0 {
518 | tlg.Debug("toggled bit")
519 |
520 | logChannel <- &toggleLogEntry{
521 | ip: ip,
522 | index: index,
523 | state: nbv > 0,
524 | }
525 | primaryRedisClient.Publish(
526 | background,
527 | "bit_toggle_channel",
528 | JSON([]any{
529 | index, int(nbv), ts,
530 | }),
531 | )
532 | }
533 | if newly_frozen == 1 {
534 | tlg.Debug("frozen bit")
535 | primaryRedisClient.Publish(
536 | background,
537 | "frozen_bit_channel",
538 | JSON([]any{
539 | index,
540 | }),
541 | )
542 | }
543 | }))
544 | slog.Debug("New connection", "socket", a)
545 | }, func() {
546 | client.Disconnect(true)
547 | })
548 | })
549 |
550 | go func() {
551 | tryForever(func() {
552 | t := time.NewTicker(*forceStateSnapshot)
553 | log := slog.With("scope", "forceStateSnapshot")
554 | for range t.C {
555 | log.Debug("starting snapshot send")
556 | ws.Except("ipv6").Except("nomessage").Emit("full_state", getStateSnapshot())
557 | log.Debug("compete snapshot send")
558 | }
559 | })
560 | }()
561 |
562 | go func() {
563 | tryForever(func() {
564 | maxBatchSize := 400
565 | ticker := time.NewTicker(time.Second / 10)
566 | subscriber := secondaryRedisClient.Subscribe(
567 | background,
568 | "bit_toggle_channel",
569 | )
570 | defer subscriber.Close()
571 | log := slog.With("subscriber", subscriber)
572 |
573 | messages := subscriber.Channel()
574 | changed := make(map[int]bool, maxBatchSize)
575 | maxTs := 0
576 | tmp := make([]int, 3)
577 |
578 | emitAll := func() {
579 | on := make([]int, 0, len(changed)/2)
580 | off := make([]int, 0, len(changed)/2)
581 | for k, v := range changed {
582 | if v {
583 | on = append(on, k)
584 | } else {
585 | off = append(off, k)
586 | }
587 | }
588 | ws.Except("nomessage").Emit("batched_bit_toggles", []any{on, off, maxTs})
589 | log.Debug("emmitting", "on", on, "off", off)
590 | changed = make(map[int]bool, maxBatchSize)
591 | maxTs = 0
592 | }
593 | for {
594 | select {
595 | case msg := <-messages:
596 | json.Unmarshal([]byte(msg.Payload), &tmp)
597 | index, nbv, ts := tmp[0], tmp[1], tmp[2]
598 | changed[index] = nbv > 0
599 | maxTs = max(ts, maxTs)
600 | if len(changed) < maxBatchSize {
601 | continue
602 | }
603 | case <-ticker.C:
604 | if len(changed) == 0 {
605 | continue
606 | }
607 | }
608 |
609 | catch(emitAll)
610 | }
611 | })
612 | }()
613 |
614 | go func() {
615 | tryForever(func() {
616 | maxBatchSize := 400
617 | ticker := time.NewTicker(time.Second / 7)
618 | subscriber := secondaryRedisClient.Subscribe(
619 | background,
620 | "frozen_bit_channel",
621 | )
622 | defer subscriber.Close()
623 | log := slog.With("subscriber", subscriber)
624 |
625 | messages := subscriber.Channel()
626 | frozen := make([]int, 0, maxBatchSize)
627 | tmp := make([]int, 1)
628 | emitAll := func() {
629 | ws.Except().Emit("batched_frozen_bits", frozen)
630 | log.Debug("emmitting", "frozen", frozen)
631 | frozen = make([]int, 0, maxBatchSize)
632 | }
633 | for {
634 | select {
635 | case msg := <-messages:
636 | json.Unmarshal([]byte(msg.Payload), &tmp)
637 | index := tmp[0]
638 | frozen = append(frozen, index)
639 | if len(frozen) < maxBatchSize {
640 | continue
641 | }
642 | case <-ticker.C:
643 | if len(frozen) == 0 {
644 | continue
645 | }
646 | }
647 |
648 | catch(emitAll)
649 | }
650 | })
651 | }()
652 |
653 | wss := ws.ServeHandler(nil)
654 | gin.WrapF(func(w http.ResponseWriter, r *http.Request) {
655 | ip := ""
656 | NOLEN_IP := r.Header.Get("NOLEN-IP")
657 | cfIP := r.Header.Get("CF-Connecting-IP")
658 | forwarded := r.Header.Get("X-Forwarded-For")
659 | if NOLEN_IP != "" {
660 | ip = NOLEN_IP
661 | } else if cfIP != "" {
662 | ip = cfIP
663 | } else if forwarded != "" {
664 | ip = forwarded
665 | } else {
666 | ip = "10.0.0.1"
667 | }
668 | parsedIp, isIPV6 := groupIPv6(ip)
669 |
670 | if detectAbuse(parsedIp, isIPV6) {
671 | slog.With("ip", forwarded).
672 | Info("Rejecting http reqeust from suspected abuse ip")
673 | w.WriteHeader(400)
674 | return
675 | }
676 |
677 | wss.ServeHTTP(w, r)
678 | })
679 |
680 | h := gin.WrapH(ws.ServeHandler(nil))
681 | r.GET("/socket.io/", h)
682 | r.POST("/socket.io/", h)
683 | r.NoRoute(static.Serve("/", static.EmbedFolder(distFolder, "dist")))
684 | go r.Run(
685 | fmt.Sprintf(":%d", *port),
686 | )
687 | go r.Run(
688 | fmt.Sprintf(":%d", *port+1),
689 | )
690 | go r.Run(
691 | fmt.Sprintf(":%d", *port+2),
692 | )
693 | r.Run(
694 | fmt.Sprintf(":%d", *port+3),
695 | )
696 | }
697 |
698 | func envOr(name, def string) string {
699 | s := os.Getenv(name)
700 | if s == "" {
701 | return def
702 | }
703 | return s
704 | }
705 | func miniClient() *redis.Client {
706 | if mini == nil {
707 | mini = miniredis.NewMiniRedis()
708 | mini.Start()
709 | }
710 | client := redis.NewClient(&redis.Options{
711 | Addr: mini.Addr(),
712 | })
713 | l := slog.With("scope", "miniredis")
714 | l.Info("Using miniredis", "addr", mini.Addr(), "port", mini.Port())
715 | if ping := client.Ping(background); ping.Err() != nil {
716 | log.Fatalf("Unable to estable connection to miniredis %s", ping.Err())
717 | }
718 | return client
719 |
720 | }
721 | func primaryRedis() (*redis.Client, error) {
722 | return redisClient(
723 | envOr("REDIS_HOST", "localhost"),
724 | envOr("REDIS_PORT", "6379"),
725 | envOr("REDIS_USERNAME", "default"),
726 | envOr("REDIS_PASSWORD", ""),
727 | )
728 | }
729 | func replicaRedis() (*redis.Client, error) {
730 | return redisClient(
731 | *REDIS_SECONDARY_IP,
732 | envOr("REDIS_PORT", "6379"),
733 | envOr("REDIS_USERNAME", "default"),
734 | envOr("REDIS_PASSWORD", ""),
735 | )
736 | }
737 |
738 | func getFullState() string {
739 |
740 | buff, err := secondaryRedisClient.Get(background, "sunset_bitset").
741 | Bytes()
742 | if err != nil {
743 | log.Panicf("Unable to read bitset from redis", err)
744 | }
745 | return base64.RawStdEncoding.EncodeToString(buff)
746 | }
747 |
748 | func getFrozenState() string {
749 | buff, err := secondaryRedisClient.Get(background, "frozen_bitset").
750 | Bytes()
751 | if err != nil {
752 | log.Panicf("Unable to read frozen bitset from redis", err)
753 | }
754 | return base64.StdEncoding.EncodeToString(buff)
755 | }
756 |
757 | func redisClient(host, port, user, pass string) (*redis.Client, error) {
758 | rdb := redis.NewClient(&redis.Options{
759 | Addr: fmt.Sprintf(
760 | "%s:%s",
761 | host, port,
762 | ),
763 | Username: user,
764 | Password: pass, // no password set
765 | DB: 0, // use default DB
766 | MaxIdleConns: 20,
767 | MaxActiveConns: 40,
768 | DialTimeout: time.Second * 10,
769 | ContextTimeoutEnabled: true,
770 | // PoolTimeout: time.Second * 1,
771 | TLSConfig: &tls.Config{
772 | InsecureSkipVerify: true,
773 | },
774 | })
775 | return rdb, rdb.Ping(background).Err()
776 | }
777 |
778 | // redis scripts
779 | var (
780 | setBitScript = redis.NewScript(`
781 | local key = KEYS[1]
782 | local index = tonumber(ARGV[1])
783 | local value = tonumber(ARGV[2])
784 | local current = redis.call('getbit', key, index)
785 | local diff = value - current
786 | redis.call('setbit', key, index, value)
787 | redis.call('incrby', 'count', diff)
788 | return diff`)
789 |
790 | newSetBitScript = redis.NewScript(`
791 | local key = KEYS[1]
792 | local count_key = KEYS[2]
793 | local index = tonumber(ARGV[1])
794 | local max_count = tonumber(ARGV[2])
795 |
796 | local current_count = tonumber(redis.call('get', count_key) or "0")
797 | if current_count >= max_count then
798 | return {redis.call('getbit', key, index), 0} -- Return current count, current bit value, and 0 to indicate no change
799 | end
800 |
801 | local current_bit = redis.call('getbit', key, index)
802 | local new_bit = 1 - current_bit -- Toggle the bit
803 | local diff = new_bit - current_bit
804 |
805 | if diff > 0 and current_count + diff > max_count then
806 | return { current_bit, 0} -- Return current count, current bit value, and 0 to indicate no change
807 | end
808 |
809 | redis.call('setbit', key, index, new_bit)
810 | local new_count = current_count + diff
811 | redis.call('set', count_key, new_count)
812 |
813 | return {new_bit, diff} -- new bit value, and the change (1, 0, or -1)`)
814 |
815 | frozenSetBitScript = redis.NewScript(`
816 | local bitset_key = KEYS[1]
817 | local count_key = KEYS[2]
818 | local frozen_bitset_key = KEYS[3]
819 | local frozen_count_key = KEYS[4]
820 | local freeze_time_key = KEYS[5]
821 | local index = tonumber(ARGV[1])
822 | local max_count = tonumber(ARGV[2])
823 |
824 | -- Sentinel value for unchecked boxes (0 is a good choice as it's falsy in Lua)
825 | local UNCHECKED_SENTINEL = 0
826 |
827 | -- Get current Redis time in milliseconds
828 | local redis_time = redis.call('TIME')
829 | local current_time = tonumber(redis_time[1]) * 1000 + math.floor(tonumber(redis_time[2]) / 1000)
830 |
831 | -- Get freeze_time from Redis (in milliseconds)
832 | local freeze_time = tonumber(redis.call('get', freeze_time_key) or "0")
833 |
834 | -- Get current state
835 | local current_count = tonumber(redis.call('get', count_key) or "0")
836 | local current_bit = redis.call('getbit', bitset_key, index)
837 | local frozen_bit = redis.call('getbit', frozen_bitset_key, index)
838 |
839 | -- Check if the box is already frozen
840 | if frozen_bit == 1 then
841 | return {current_bit, 0, 0} -- Return current bit value, 0 for no change, and 0 to indicate not newly frozen
842 | end
843 |
844 | -- If we're at max count, no changes allowed
845 | if current_count >= max_count then
846 | return {current_bit, 0, 0}
847 | end
848 |
849 | -- Toggle the bit
850 | local new_bit = 1 - current_bit
851 | local diff = new_bit - current_bit
852 |
853 | -- If we're unchecking (new_bit == 0), check the freeze_time
854 | if new_bit == 0 then
855 | local last_checked = tonumber(redis.call('hget', 'last_checked', index) or UNCHECKED_SENTINEL)
856 | if last_checked ~= UNCHECKED_SENTINEL and current_time - last_checked >= freeze_time then
857 | -- Box is frozen, update frozen bitset and count
858 | redis.call('setbit', frozen_bitset_key, index, 1)
859 | redis.call('incr', frozen_count_key)
860 | return {1, 0, 1} -- Return 1 (checked), 0 for no change, and 1 to indicate newly frozen
861 | else
862 | -- Set the sentinel value instead of deleting
863 | redis.call('hset', 'last_checked', index, UNCHECKED_SENTINEL)
864 | end
865 | else
866 | -- We're checking the box, update last_checked time
867 | redis.call('hset', 'last_checked', index, current_time)
868 | end
869 |
870 | -- Proceed with the change
871 | redis.call('setbit', bitset_key, index, new_bit)
872 | local new_count = current_count + diff
873 | redis.call('set', count_key, new_count)
874 |
875 | return {new_bit, diff, 0} -- new bit value, the change (-1, 0, or 1), and 0 to indicate not frozen`)
876 | )
877 |
--------------------------------------------------------------------------------
/src/randomizedColors.js:
--------------------------------------------------------------------------------
1 | const INDICES = {
2 | 45: "gold",
3 | 192: "red",
4 | 339: "green",
5 | 782: "red",
6 | 1073: "gold",
7 | 1317: "green",
8 | 1744: "red",
9 | 2104: "red",
10 | 2473: "green",
11 | 2793: "green",
12 | 3094: "red",
13 | 3363: "purple",
14 | 3700: "green",
15 | 4012: "red",
16 | 4466: "gold",
17 | 4884: "red",
18 | 5279: "green",
19 | 5685: "gold",
20 | 5999: "green",
21 | 6378: "gold",
22 | 6846: "green",
23 | 7294: "red",
24 | 7597: "purple",
25 | 7945: "gold",
26 | 8335: "green",
27 | 8751: "gold",
28 | 9076: "green",
29 | 9343: "red",
30 | 9586: "green",
31 | 9932: "purple",
32 | 10225: "gold",
33 | 10510: "purple",
34 | 10766: "green",
35 | 11040: "red",
36 | 11368: "green",
37 | 11635: "green",
38 | 11972: "green",
39 | 12232: "green",
40 | 12633: "green",
41 | 13065: "gold",
42 | 13332: "green",
43 | 13794: "red",
44 | 14132: "gold",
45 | 14532: "purple",
46 | 14988: "green",
47 | 15279: "red",
48 | 15596: "purple",
49 | 16069: "green",
50 | 16498: "green",
51 | 16922: "purple",
52 | 17194: "gold",
53 | 17528: "gold",
54 | 17816: "red",
55 | 18060: "green",
56 | 18446: "green",
57 | 18857: "green",
58 | 19229: "red",
59 | 19649: "red",
60 | 20104: "purple",
61 | 20436: "green",
62 | 20681: "gold",
63 | 21103: "red",
64 | 21549: "green",
65 | 21806: "purple",
66 | 22047: "gold",
67 | 22457: "purple",
68 | 22876: "red",
69 | 23206: "gold",
70 | 23604: "purple",
71 | 23935: "gold",
72 | 24278: "red",
73 | 24563: "gold",
74 | 24965: "purple",
75 | 25328: "purple",
76 | 25622: "purple",
77 | 26086: "purple",
78 | 26356: "purple",
79 | 26691: "red",
80 | 27035: "purple",
81 | 27331: "gold",
82 | 27743: "red",
83 | 28105: "red",
84 | 28378: "gold",
85 | 28797: "gold",
86 | 29167: "red",
87 | 29631: "red",
88 | 29996: "purple",
89 | 30291: "red",
90 | 30656: "red",
91 | 30963: "red",
92 | 31328: "purple",
93 | 31734: "red",
94 | 32193: "green",
95 | 32602: "gold",
96 | 33022: "red",
97 | 33471: "purple",
98 | 33737: "red",
99 | 34072: "red",
100 | 34537: "green",
101 | 34969: "red",
102 | 35372: "gold",
103 | 35638: "gold",
104 | 36081: "purple",
105 | 36425: "gold",
106 | 36700: "green",
107 | 37147: "red",
108 | 37428: "gold",
109 | 37843: "red",
110 | 38223: "green",
111 | 38654: "purple",
112 | 39075: "purple",
113 | 39369: "green",
114 | 39686: "red",
115 | 40053: "red",
116 | 40378: "purple",
117 | 40651: "green",
118 | 41067: "red",
119 | 41399: "red",
120 | 41861: "red",
121 | 42163: "gold",
122 | 42409: "purple",
123 | 42704: "red",
124 | 42972: "green",
125 | 43229: "purple",
126 | 43682: "green",
127 | 44150: "purple",
128 | 44548: "purple",
129 | 45002: "red",
130 | 45444: "gold",
131 | 45780: "gold",
132 | 46127: "purple",
133 | 46531: "gold",
134 | 46994: "gold",
135 | 47234: "green",
136 | 47669: "red",
137 | 48101: "red",
138 | 48549: "purple",
139 | 48825: "green",
140 | 49295: "purple",
141 | 49738: "purple",
142 | 49977: "red",
143 | 50380: "red",
144 | 50692: "purple",
145 | 51108: "green",
146 | 51550: "green",
147 | 51958: "purple",
148 | 52433: "purple",
149 | 52791: "green",
150 | 53052: "red",
151 | 53524: "purple",
152 | 53993: "gold",
153 | 54274: "gold",
154 | 54527: "green",
155 | 54884: "purple",
156 | 55241: "red",
157 | 55714: "green",
158 | 56101: "green",
159 | 56417: "green",
160 | 56840: "red",
161 | 57094: "green",
162 | 57385: "green",
163 | 57763: "purple",
164 | 58016: "green",
165 | 58342: "green",
166 | 58726: "purple",
167 | 59010: "red",
168 | 59406: "purple",
169 | 59871: "gold",
170 | 60309: "red",
171 | 60672: "red",
172 | 60998: "red",
173 | 61347: "green",
174 | 61745: "purple",
175 | 62211: "red",
176 | 62536: "purple",
177 | 62791: "purple",
178 | 63119: "purple",
179 | 63489: "purple",
180 | 63894: "gold",
181 | 64294: "green",
182 | 64555: "red",
183 | 64871: "gold",
184 | 65276: "gold",
185 | 65734: "red",
186 | 66074: "purple",
187 | 66429: "gold",
188 | 66836: "green",
189 | 67097: "red",
190 | 67360: "gold",
191 | 67835: "purple",
192 | 68160: "red",
193 | 68492: "gold",
194 | 68871: "red",
195 | 69186: "purple",
196 | 69647: "red",
197 | 69987: "green",
198 | 70324: "gold",
199 | 70574: "red",
200 | 70995: "gold",
201 | 71250: "red",
202 | 71671: "purple",
203 | 72059: "gold",
204 | 72346: "purple",
205 | 72601: "gold",
206 | 73007: "green",
207 | 73354: "green",
208 | 73627: "green",
209 | 73980: "gold",
210 | 74351: "purple",
211 | 74602: "green",
212 | 74906: "gold",
213 | 75369: "green",
214 | 75735: "gold",
215 | 76047: "red",
216 | 76344: "red",
217 | 76585: "purple",
218 | 76948: "gold",
219 | 77198: "gold",
220 | 77636: "gold",
221 | 77873: "purple",
222 | 78120: "green",
223 | 78432: "purple",
224 | 78770: "green",
225 | 79219: "red",
226 | 79612: "gold",
227 | 79935: "green",
228 | 80225: "green",
229 | 80587: "red",
230 | 81040: "purple",
231 | 81500: "red",
232 | 81958: "purple",
233 | 82214: "purple",
234 | 82451: "green",
235 | 82713: "purple",
236 | 82959: "purple",
237 | 83378: "green",
238 | 83701: "red",
239 | 84046: "purple",
240 | 84383: "green",
241 | 84652: "gold",
242 | 85060: "gold",
243 | 85524: "gold",
244 | 85813: "purple",
245 | 86119: "gold",
246 | 86592: "red",
247 | 86881: "gold",
248 | 87139: "red",
249 | 87583: "gold",
250 | 88002: "gold",
251 | 88281: "purple",
252 | 88602: "green",
253 | 89041: "red",
254 | 89400: "purple",
255 | 89832: "red",
256 | 90279: "gold",
257 | 90712: "gold",
258 | 91088: "red",
259 | 91402: "red",
260 | 91730: "green",
261 | 92012: "red",
262 | 92311: "red",
263 | 92607: "gold",
264 | 92905: "purple",
265 | 93189: "red",
266 | 93630: "purple",
267 | 93873: "red",
268 | 94332: "purple",
269 | 94789: "green",
270 | 95207: "gold",
271 | 95583: "gold",
272 | 95995: "gold",
273 | 96377: "green",
274 | 96843: "purple",
275 | 97189: "red",
276 | 97593: "red",
277 | 97860: "green",
278 | 98100: "green",
279 | 98479: "gold",
280 | 98731: "purple",
281 | 98987: "red",
282 | 99391: "gold",
283 | 99658: "red",
284 | 100011: "green",
285 | 100304: "purple",
286 | 100724: "red",
287 | 101107: "green",
288 | 101467: "gold",
289 | 101893: "red",
290 | 102229: "green",
291 | 102622: "green",
292 | 102864: "gold",
293 | 103205: "green",
294 | 103587: "purple",
295 | 104000: "purple",
296 | 104386: "green",
297 | 104830: "purple",
298 | 105122: "red",
299 | 105484: "gold",
300 | 105959: "purple",
301 | 106428: "red",
302 | 106717: "green",
303 | 107173: "gold",
304 | 107629: "green",
305 | 108031: "purple",
306 | 108315: "red",
307 | 108756: "green",
308 | 109098: "purple",
309 | 109559: "purple",
310 | 110012: "purple",
311 | 110256: "gold",
312 | 110640: "gold",
313 | 110993: "purple",
314 | 111467: "gold",
315 | 111745: "gold",
316 | 112171: "gold",
317 | 112439: "green",
318 | 112727: "purple",
319 | 113018: "red",
320 | 113277: "green",
321 | 113556: "purple",
322 | 113866: "gold",
323 | 114254: "red",
324 | 114561: "purple",
325 | 114814: "gold",
326 | 115200: "red",
327 | 115589: "red",
328 | 115830: "green",
329 | 116084: "red",
330 | 116450: "gold",
331 | 116829: "gold",
332 | 117278: "purple",
333 | 117669: "green",
334 | 118126: "gold",
335 | 118524: "green",
336 | 118917: "purple",
337 | 119276: "purple",
338 | 119674: "gold",
339 | 120040: "red",
340 | 120514: "green",
341 | 120812: "purple",
342 | 121109: "green",
343 | 121387: "purple",
344 | 121760: "red",
345 | 122124: "green",
346 | 122479: "red",
347 | 122799: "gold",
348 | 123235: "gold",
349 | 123534: "purple",
350 | 123881: "purple",
351 | 124187: "purple",
352 | 124451: "red",
353 | 124872: "purple",
354 | 125316: "green",
355 | 125726: "red",
356 | 126008: "gold",
357 | 126345: "gold",
358 | 126658: "green",
359 | 127023: "gold",
360 | 127340: "gold",
361 | 127685: "gold",
362 | 128149: "green",
363 | 128620: "green",
364 | 129022: "gold",
365 | 129285: "red",
366 | 129532: "green",
367 | 129809: "purple",
368 | 130209: "gold",
369 | 130478: "purple",
370 | 130828: "green",
371 | 131151: "red",
372 | 131568: "green",
373 | 131945: "red",
374 | 132329: "red",
375 | 132769: "purple",
376 | 133169: "purple",
377 | 133498: "green",
378 | 133867: "purple",
379 | 134226: "green",
380 | 134487: "gold",
381 | 134897: "gold",
382 | 135149: "purple",
383 | 135542: "gold",
384 | 135819: "gold",
385 | 136165: "green",
386 | 136451: "green",
387 | 136721: "gold",
388 | 136963: "purple",
389 | 137222: "gold",
390 | 137665: "purple",
391 | 137962: "red",
392 | 138238: "red",
393 | 138591: "green",
394 | 138918: "gold",
395 | 139249: "gold",
396 | 139598: "green",
397 | 140013: "purple",
398 | 140455: "gold",
399 | 140707: "green",
400 | 140949: "green",
401 | 141294: "gold",
402 | 141642: "red",
403 | 142016: "green",
404 | 142350: "purple",
405 | 142652: "purple",
406 | 142917: "gold",
407 | 143289: "red",
408 | 143762: "green",
409 | 144198: "purple",
410 | 144554: "red",
411 | 144852: "gold",
412 | 145095: "green",
413 | 145465: "purple",
414 | 145860: "red",
415 | 146314: "gold",
416 | 146726: "purple",
417 | 147095: "purple",
418 | 147342: "green",
419 | 147769: "red",
420 | 148208: "green",
421 | 148657: "purple",
422 | 149102: "red",
423 | 149386: "red",
424 | 149648: "red",
425 | 149887: "gold",
426 | 150170: "purple",
427 | 150533: "gold",
428 | 150819: "purple",
429 | 151150: "purple",
430 | 151462: "red",
431 | 151821: "green",
432 | 152293: "purple",
433 | 152575: "purple",
434 | 153049: "gold",
435 | 153298: "purple",
436 | 153722: "green",
437 | 154022: "purple",
438 | 154366: "green",
439 | 154780: "gold",
440 | 155235: "red",
441 | 155657: "red",
442 | 156080: "gold",
443 | 156485: "green",
444 | 156749: "red",
445 | 157219: "green",
446 | 157627: "purple",
447 | 158041: "red",
448 | 158320: "gold",
449 | 158795: "red",
450 | 159174: "green",
451 | 159444: "gold",
452 | 159794: "green",
453 | 160085: "purple",
454 | 160512: "red",
455 | 160910: "green",
456 | 161291: "gold",
457 | 161761: "gold",
458 | 162109: "gold",
459 | 162574: "red",
460 | 162879: "red",
461 | 163122: "purple",
462 | 163489: "red",
463 | 163938: "purple",
464 | 164337: "purple",
465 | 164722: "green",
466 | 165061: "green",
467 | 165415: "purple",
468 | 165748: "purple",
469 | 166176: "red",
470 | 166521: "green",
471 | 166980: "green",
472 | 167316: "gold",
473 | 167601: "green",
474 | 167866: "gold",
475 | 168264: "gold",
476 | 168639: "red",
477 | 168882: "red",
478 | 169239: "green",
479 | 169698: "purple",
480 | 169988: "gold",
481 | 170382: "green",
482 | 170770: "red",
483 | 171240: "purple",
484 | 171599: "green",
485 | 171973: "purple",
486 | 172431: "gold",
487 | 172867: "purple",
488 | 173170: "gold",
489 | 173409: "gold",
490 | 173783: "gold",
491 | 174162: "red",
492 | 174447: "purple",
493 | 174686: "red",
494 | 174986: "red",
495 | 175445: "gold",
496 | 175711: "green",
497 | 176147: "red",
498 | 176451: "green",
499 | 176772: "purple",
500 | 177102: "gold",
501 | 177341: "green",
502 | 177654: "gold",
503 | 178063: "purple",
504 | 178369: "purple",
505 | 178835: "red",
506 | 179240: "gold",
507 | 179547: "gold",
508 | 179872: "purple",
509 | 180168: "green",
510 | 180526: "purple",
511 | 180782: "gold",
512 | 181192: "red",
513 | 181481: "gold",
514 | 181810: "red",
515 | 182204: "purple",
516 | 182677: "gold",
517 | 183081: "red",
518 | 183545: "purple",
519 | 183922: "purple",
520 | 184235: "purple",
521 | 184509: "purple",
522 | 184762: "purple",
523 | 185140: "purple",
524 | 185517: "green",
525 | 185866: "purple",
526 | 186202: "purple",
527 | 186513: "gold",
528 | 186809: "green",
529 | 187160: "red",
530 | 187463: "red",
531 | 187774: "gold",
532 | 188136: "gold",
533 | 188378: "green",
534 | 188772: "green",
535 | 189077: "purple",
536 | 189479: "gold",
537 | 189894: "red",
538 | 190286: "purple",
539 | 190612: "purple",
540 | 191001: "red",
541 | 191404: "gold",
542 | 191780: "green",
543 | 192089: "gold",
544 | 192409: "gold",
545 | 192760: "gold",
546 | 193016: "green",
547 | 193304: "red",
548 | 193600: "purple",
549 | 194049: "red",
550 | 194360: "red",
551 | 194732: "gold",
552 | 195060: "green",
553 | 195324: "purple",
554 | 195690: "green",
555 | 196038: "green",
556 | 196335: "red",
557 | 196634: "green",
558 | 197104: "red",
559 | 197556: "red",
560 | 198000: "red",
561 | 198402: "purple",
562 | 198855: "red",
563 | 199164: "gold",
564 | 199453: "green",
565 | 199898: "green",
566 | 200210: "red",
567 | 200476: "purple",
568 | 200923: "green",
569 | 201396: "red",
570 | 201693: "gold",
571 | 202098: "green",
572 | 202336: "green",
573 | 202763: "purple",
574 | 203230: "red",
575 | 203608: "gold",
576 | 203884: "red",
577 | 204265: "gold",
578 | 204601: "purple",
579 | 204968: "purple",
580 | 205227: "purple",
581 | 205509: "green",
582 | 205800: "gold",
583 | 206198: "green",
584 | 206472: "purple",
585 | 206862: "green",
586 | 207236: "red",
587 | 207698: "green",
588 | 207955: "purple",
589 | 208195: "gold",
590 | 208598: "purple",
591 | 208957: "gold",
592 | 209250: "gold",
593 | 209658: "red",
594 | 210007: "gold",
595 | 210257: "green",
596 | 210550: "red",
597 | 210841: "gold",
598 | 211169: "gold",
599 | 211505: "green",
600 | 211819: "red",
601 | 212238: "green",
602 | 212631: "red",
603 | 213079: "red",
604 | 213545: "gold",
605 | 213982: "green",
606 | 214231: "purple",
607 | 214557: "green",
608 | 214944: "red",
609 | 215293: "purple",
610 | 215615: "green",
611 | 215999: "gold",
612 | 216262: "gold",
613 | 216724: "purple",
614 | 217087: "purple",
615 | 217558: "red",
616 | 217828: "gold",
617 | 218143: "gold",
618 | 218569: "purple",
619 | 218897: "purple",
620 | 219298: "red",
621 | 219662: "green",
622 | 219961: "green",
623 | 220401: "gold",
624 | 220741: "gold",
625 | 221105: "gold",
626 | 221551: "red",
627 | 221990: "red",
628 | 222380: "red",
629 | 222730: "gold",
630 | 222974: "purple",
631 | 223232: "green",
632 | 223616: "purple",
633 | 223898: "purple",
634 | 224248: "green",
635 | 224528: "red",
636 | 224845: "gold",
637 | 225296: "purple",
638 | 225541: "gold",
639 | 226015: "green",
640 | 226435: "red",
641 | 226855: "red",
642 | 227298: "gold",
643 | 227612: "red",
644 | 228024: "purple",
645 | 228466: "purple",
646 | 228775: "red",
647 | 229163: "gold",
648 | 229487: "red",
649 | 229886: "gold",
650 | 230174: "gold",
651 | 230455: "gold",
652 | 230830: "red",
653 | 231229: "red",
654 | 231539: "red",
655 | 231955: "red",
656 | 232294: "gold",
657 | 232612: "gold",
658 | 233079: "red",
659 | 233398: "green",
660 | 233759: "green",
661 | 234009: "red",
662 | 234453: "green",
663 | 234920: "gold",
664 | 235277: "green",
665 | 235699: "gold",
666 | 236043: "red",
667 | 236498: "gold",
668 | 236780: "gold",
669 | 237098: "green",
670 | 237540: "red",
671 | 237994: "green",
672 | 238339: "red",
673 | 238691: "gold",
674 | 238931: "purple",
675 | 239184: "green",
676 | 239567: "gold",
677 | 239956: "purple",
678 | 240379: "green",
679 | 240741: "gold",
680 | 241087: "purple",
681 | 241460: "purple",
682 | 241733: "red",
683 | 242004: "green",
684 | 242446: "purple",
685 | 242896: "green",
686 | 243298: "green",
687 | 243676: "green",
688 | 244046: "green",
689 | 244452: "green",
690 | 244764: "green",
691 | 245028: "purple",
692 | 245276: "gold",
693 | 245632: "purple",
694 | 245933: "green",
695 | 246270: "gold",
696 | 246511: "green",
697 | 246972: "purple",
698 | 247410: "purple",
699 | 247764: "red",
700 | 248060: "gold",
701 | 248375: "gold",
702 | 248702: "gold",
703 | 249024: "gold",
704 | 249338: "gold",
705 | 249700: "red",
706 | 250084: "red",
707 | 250443: "purple",
708 | 250684: "gold",
709 | 251098: "green",
710 | 251473: "red",
711 | 251899: "green",
712 | 252331: "gold",
713 | 252671: "gold",
714 | 253098: "gold",
715 | 253353: "green",
716 | 253634: "green",
717 | 253970: "gold",
718 | 254230: "gold",
719 | 254646: "red",
720 | 255049: "gold",
721 | 255349: "red",
722 | 255664: "red",
723 | 256074: "green",
724 | 256477: "green",
725 | 256793: "purple",
726 | 257217: "red",
727 | 257681: "red",
728 | 258100: "red",
729 | 258441: "green",
730 | 258682: "gold",
731 | 259072: "purple",
732 | 259408: "red",
733 | 259653: "purple",
734 | 260004: "red",
735 | 260330: "red",
736 | 260672: "gold",
737 | 261028: "purple",
738 | 261295: "purple",
739 | 261658: "red",
740 | 262027: "gold",
741 | 262466: "purple",
742 | 262740: "gold",
743 | 263131: "purple",
744 | 263544: "green",
745 | 263969: "red",
746 | 264331: "purple",
747 | 264693: "purple",
748 | 265147: "green",
749 | 265559: "purple",
750 | 265935: "gold",
751 | 266262: "green",
752 | 266615: "red",
753 | 266895: "green",
754 | 267146: "red",
755 | 267418: "gold",
756 | 267831: "red",
757 | 268280: "purple",
758 | 268575: "purple",
759 | 268988: "red",
760 | 269403: "purple",
761 | 269717: "red",
762 | 270063: "gold",
763 | 270328: "gold",
764 | 270679: "green",
765 | 271049: "purple",
766 | 271466: "red",
767 | 271740: "green",
768 | 271993: "gold",
769 | 272274: "red",
770 | 272736: "gold",
771 | 273035: "purple",
772 | 273481: "gold",
773 | 273722: "gold",
774 | 274135: "green",
775 | 274379: "purple",
776 | 274797: "green",
777 | 275107: "red",
778 | 275387: "red",
779 | 275645: "gold",
780 | 276078: "green",
781 | 276326: "gold",
782 | 276716: "gold",
783 | 277074: "gold",
784 | 277523: "red",
785 | 277966: "red",
786 | 278344: "green",
787 | 278753: "gold",
788 | 279090: "green",
789 | 279460: "green",
790 | 279758: "purple",
791 | 280135: "gold",
792 | 280573: "purple",
793 | 280975: "red",
794 | 281406: "purple",
795 | 281756: "gold",
796 | 282138: "gold",
797 | 282403: "green",
798 | 282796: "green",
799 | 283076: "purple",
800 | 283463: "green",
801 | 283797: "green",
802 | 284205: "gold",
803 | 284495: "gold",
804 | 284775: "gold",
805 | 285178: "green",
806 | 285517: "gold",
807 | 285918: "red",
808 | 286201: "purple",
809 | 286453: "green",
810 | 286706: "purple",
811 | 287103: "gold",
812 | 287391: "purple",
813 | 287751: "purple",
814 | 287991: "purple",
815 | 288400: "red",
816 | 288753: "green",
817 | 289191: "gold",
818 | 289630: "green",
819 | 289988: "green",
820 | 290281: "red",
821 | 290657: "gold",
822 | 291099: "red",
823 | 291451: "red",
824 | 291900: "green",
825 | 292229: "purple",
826 | 292683: "green",
827 | 293114: "purple",
828 | 293382: "purple",
829 | 293634: "purple",
830 | 294027: "green",
831 | 294479: "purple",
832 | 294899: "purple",
833 | 295159: "purple",
834 | 295436: "gold",
835 | 295687: "purple",
836 | 295969: "green",
837 | 296356: "gold",
838 | 296663: "green",
839 | 297055: "red",
840 | 297383: "gold",
841 | 297713: "green",
842 | 298026: "purple",
843 | 298356: "red",
844 | 298758: "red",
845 | 299120: "red",
846 | 299464: "red",
847 | 299735: "gold",
848 | 300064: "gold",
849 | 300468: "purple",
850 | 300779: "red",
851 | 301143: "gold",
852 | 301612: "green",
853 | 302051: "purple",
854 | 302470: "purple",
855 | 302817: "green",
856 | 303184: "gold",
857 | 303535: "green",
858 | 303787: "red",
859 | 304259: "red",
860 | 304644: "gold",
861 | 304981: "gold",
862 | 305387: "purple",
863 | 305734: "green",
864 | 306178: "purple",
865 | 306487: "green",
866 | 306856: "green",
867 | 307182: "green",
868 | 307425: "purple",
869 | 307756: "purple",
870 | 308050: "gold",
871 | 308376: "gold",
872 | 308677: "gold",
873 | 308988: "green",
874 | 309372: "gold",
875 | 309810: "red",
876 | 310183: "green",
877 | 310575: "red",
878 | 310923: "red",
879 | 311294: "green",
880 | 311696: "purple",
881 | 311951: "red",
882 | 312224: "gold",
883 | 312699: "purple",
884 | 313017: "red",
885 | 313332: "gold",
886 | 313570: "purple",
887 | 314019: "green",
888 | 314471: "red",
889 | 314826: "gold",
890 | 315213: "purple",
891 | 315633: "red",
892 | 315998: "gold",
893 | 316362: "gold",
894 | 316696: "purple",
895 | 316984: "purple",
896 | 317295: "green",
897 | 317687: "purple",
898 | 318028: "purple",
899 | 318346: "red",
900 | 318711: "red",
901 | 319125: "gold",
902 | 319581: "gold",
903 | 320005: "purple",
904 | 320345: "purple",
905 | 320608: "purple",
906 | 320955: "red",
907 | 321192: "green",
908 | 321500: "green",
909 | 321961: "red",
910 | 322427: "gold",
911 | 322758: "red",
912 | 323174: "gold",
913 | 323463: "purple",
914 | 323834: "red",
915 | 324105: "purple",
916 | 324399: "gold",
917 | 324833: "green",
918 | 325259: "red",
919 | 325689: "gold",
920 | 326004: "green",
921 | 326393: "purple",
922 | 326865: "green",
923 | 327306: "gold",
924 | 327745: "gold",
925 | 328049: "gold",
926 | 328383: "gold",
927 | 328772: "green",
928 | 329137: "gold",
929 | 329447: "gold",
930 | 329817: "purple",
931 | 330094: "red",
932 | 330411: "green",
933 | 330864: "gold",
934 | 331314: "purple",
935 | 331617: "red",
936 | 331941: "green",
937 | 332337: "purple",
938 | 332675: "gold",
939 | 333098: "gold",
940 | 333406: "gold",
941 | 333703: "purple",
942 | 334040: "gold",
943 | 334417: "purple",
944 | 334654: "red",
945 | 335067: "gold",
946 | 335417: "purple",
947 | 335806: "gold",
948 | 336190: "green",
949 | 336507: "purple",
950 | 336849: "green",
951 | 337167: "green",
952 | 337549: "gold",
953 | 337937: "red",
954 | 338258: "gold",
955 | 338626: "red",
956 | 339039: "gold",
957 | 339286: "purple",
958 | 339689: "green",
959 | 340118: "purple",
960 | 340581: "green",
961 | 340994: "gold",
962 | 341294: "gold",
963 | 341551: "green",
964 | 341876: "purple",
965 | 342264: "purple",
966 | 342633: "green",
967 | 342961: "purple",
968 | 343298: "purple",
969 | 343556: "purple",
970 | 343820: "red",
971 | 344143: "red",
972 | 344576: "green",
973 | 344925: "purple",
974 | 345305: "green",
975 | 345589: "red",
976 | 346022: "red",
977 | 346312: "red",
978 | 346732: "red",
979 | 347018: "green",
980 | 347378: "green",
981 | 347647: "gold",
982 | 348006: "gold",
983 | 348414: "red",
984 | 348669: "purple",
985 | 348930: "gold",
986 | 349337: "gold",
987 | 349622: "green",
988 | 349970: "red",
989 | 350325: "red",
990 | 350619: "red",
991 | 350860: "purple",
992 | 351233: "gold",
993 | 351611: "purple",
994 | 352000: "gold",
995 | 352243: "red",
996 | 352666: "gold",
997 | 353136: "purple",
998 | 353505: "red",
999 | 353904: "gold",
1000 | 354364: "red",
1001 | 354673: "red",
1002 | 355131: "green",
1003 | 355548: "green",
1004 | 355916: "purple",
1005 | 356377: "green",
1006 | 356702: "red",
1007 | 357024: "purple",
1008 | 357429: "red",
1009 | 357871: "gold",
1010 | 358124: "purple",
1011 | 358533: "purple",
1012 | 358778: "gold",
1013 | 359096: "purple",
1014 | 359501: "purple",
1015 | 359892: "green",
1016 | 360194: "red",
1017 | 360611: "green",
1018 | 360895: "red",
1019 | 361200: "purple",
1020 | 361563: "green",
1021 | 361992: "green",
1022 | 362414: "gold",
1023 | 362702: "green",
1024 | 363083: "red",
1025 | 363505: "purple",
1026 | 363761: "green",
1027 | 364125: "green",
1028 | 364458: "purple",
1029 | 364881: "purple",
1030 | 365214: "green",
1031 | 365564: "gold",
1032 | 365992: "green",
1033 | 366338: "red",
1034 | 366794: "gold",
1035 | 367179: "green",
1036 | 367458: "green",
1037 | 367696: "gold",
1038 | 367970: "purple",
1039 | 368394: "purple",
1040 | 368816: "gold",
1041 | 369269: "green",
1042 | 369703: "green",
1043 | 370133: "red",
1044 | 370379: "gold",
1045 | 370723: "green",
1046 | 371043: "purple",
1047 | 371435: "green",
1048 | 371757: "green",
1049 | 372056: "red",
1050 | 372381: "red",
1051 | 372659: "green",
1052 | 373089: "purple",
1053 | 373463: "red",
1054 | 373767: "purple",
1055 | 374094: "green",
1056 | 374374: "gold",
1057 | 374838: "gold",
1058 | 375249: "green",
1059 | 375692: "red",
1060 | 376077: "red",
1061 | 376358: "red",
1062 | 376666: "green",
1063 | 376996: "green",
1064 | 377444: "red",
1065 | 377695: "red",
1066 | 377973: "purple",
1067 | 378335: "green",
1068 | 378748: "green",
1069 | 379170: "red",
1070 | 379570: "gold",
1071 | 380036: "gold",
1072 | 380385: "green",
1073 | 380719: "red",
1074 | 381023: "purple",
1075 | 381464: "gold",
1076 | 381781: "red",
1077 | 382247: "green",
1078 | 382612: "red",
1079 | 382896: "purple",
1080 | 383223: "purple",
1081 | 383535: "purple",
1082 | 383902: "purple",
1083 | 384284: "green",
1084 | 384741: "red",
1085 | 385082: "red",
1086 | 385350: "purple",
1087 | 385755: "red",
1088 | 386197: "purple",
1089 | 386651: "purple",
1090 | 386941: "gold",
1091 | 387365: "purple",
1092 | 387821: "gold",
1093 | 388084: "gold",
1094 | 388370: "gold",
1095 | 388710: "gold",
1096 | 389165: "green",
1097 | 389540: "red",
1098 | 390008: "gold",
1099 | 390475: "green",
1100 | 390857: "purple",
1101 | 391187: "red",
1102 | 391587: "purple",
1103 | 391831: "gold",
1104 | 392078: "red",
1105 | 392530: "gold",
1106 | 392936: "gold",
1107 | 393282: "purple",
1108 | 393571: "gold",
1109 | 393965: "green",
1110 | 394227: "purple",
1111 | 394628: "gold",
1112 | 394961: "purple",
1113 | 395259: "red",
1114 | 395677: "green",
1115 | 395948: "green",
1116 | 396406: "red",
1117 | 396799: "gold",
1118 | 397166: "green",
1119 | 397458: "gold",
1120 | 397883: "gold",
1121 | 398243: "gold",
1122 | 398687: "green",
1123 | 399108: "purple",
1124 | 399457: "gold",
1125 | 399765: "green",
1126 | 400051: "gold",
1127 | 400380: "red",
1128 | 400851: "purple",
1129 | 401173: "green",
1130 | 401619: "red",
1131 | 402014: "gold",
1132 | 402403: "gold",
1133 | 402667: "purple",
1134 | 403064: "purple",
1135 | 403454: "gold",
1136 | 403893: "green",
1137 | 404137: "green",
1138 | 404513: "red",
1139 | 404794: "red",
1140 | 405153: "red",
1141 | 405426: "gold",
1142 | 405773: "green",
1143 | 406081: "gold",
1144 | 406376: "green",
1145 | 406676: "green",
1146 | 407097: "purple",
1147 | 407523: "green",
1148 | 407915: "red",
1149 | 408253: "red",
1150 | 408572: "purple",
1151 | 409013: "red",
1152 | 409450: "green",
1153 | 409806: "purple",
1154 | 410261: "gold",
1155 | 410586: "purple",
1156 | 411038: "purple",
1157 | 411405: "red",
1158 | 411735: "red",
1159 | 412094: "red",
1160 | 412450: "gold",
1161 | 412874: "purple",
1162 | 413260: "purple",
1163 | 413582: "purple",
1164 | 413950: "purple",
1165 | 414331: "gold",
1166 | 414606: "green",
1167 | 414977: "purple",
1168 | 415376: "gold",
1169 | 415838: "green",
1170 | 416183: "red",
1171 | 416514: "green",
1172 | 416982: "gold",
1173 | 417339: "gold",
1174 | 417782: "red",
1175 | 418104: "green",
1176 | 418442: "gold",
1177 | 418724: "green",
1178 | 419161: "red",
1179 | 419611: "red",
1180 | 419904: "green",
1181 | 420233: "red",
1182 | 420490: "gold",
1183 | 420731: "purple",
1184 | 420994: "purple",
1185 | 421358: "purple",
1186 | 421758: "gold",
1187 | 422023: "red",
1188 | 422387: "red",
1189 | 422661: "green",
1190 | 423008: "gold",
1191 | 423291: "green",
1192 | 423561: "gold",
1193 | 423994: "purple",
1194 | 424250: "green",
1195 | 424559: "gold",
1196 | 424860: "red",
1197 | 425290: "red",
1198 | 425635: "gold",
1199 | 426041: "purple",
1200 | 426459: "red",
1201 | 426757: "gold",
1202 | 427087: "red",
1203 | 427537: "purple",
1204 | 427958: "gold",
1205 | 428322: "green",
1206 | 428727: "green",
1207 | 429049: "green",
1208 | 429389: "green",
1209 | 429861: "green",
1210 | 430258: "red",
1211 | 430592: "red",
1212 | 430888: "green",
1213 | 431231: "gold",
1214 | 431579: "gold",
1215 | 431909: "purple",
1216 | 432332: "purple",
1217 | 432623: "gold",
1218 | 432887: "gold",
1219 | 433301: "green",
1220 | 433647: "purple",
1221 | 433971: "purple",
1222 | 434256: "green",
1223 | 434716: "purple",
1224 | 434978: "green",
1225 | 435258: "gold",
1226 | 435613: "green",
1227 | 435998: "purple",
1228 | 436463: "purple",
1229 | 436830: "green",
1230 | 437212: "gold",
1231 | 437647: "purple",
1232 | 438022: "green",
1233 | 438321: "red",
1234 | 438740: "purple",
1235 | 439181: "purple",
1236 | 439476: "purple",
1237 | 439893: "gold",
1238 | 440311: "gold",
1239 | 440626: "red",
1240 | 440909: "red",
1241 | 441245: "green",
1242 | 441544: "red",
1243 | 441839: "purple",
1244 | 442094: "green",
1245 | 442420: "purple",
1246 | 442669: "red",
1247 | 442978: "red",
1248 | 443309: "red",
1249 | 443553: "gold",
1250 | 443864: "purple",
1251 | 444144: "green",
1252 | 444520: "red",
1253 | 444783: "purple",
1254 | 445208: "gold",
1255 | 445527: "red",
1256 | 445970: "purple",
1257 | 446333: "gold",
1258 | 446683: "green",
1259 | 446934: "purple",
1260 | 447235: "purple",
1261 | 447509: "purple",
1262 | 447815: "gold",
1263 | 448221: "purple",
1264 | 448535: "red",
1265 | 448859: "purple",
1266 | 449189: "purple",
1267 | 449505: "gold",
1268 | 449776: "purple",
1269 | 450237: "green",
1270 | 450582: "gold",
1271 | 450828: "red",
1272 | 451198: "red",
1273 | 451477: "purple",
1274 | 451809: "gold",
1275 | 452153: "green",
1276 | 452610: "gold",
1277 | 452942: "purple",
1278 | 453393: "purple",
1279 | 453740: "purple",
1280 | 454135: "red",
1281 | 454463: "red",
1282 | 454737: "purple",
1283 | 455069: "purple",
1284 | 455533: "purple",
1285 | 455964: "green",
1286 | 456238: "green",
1287 | 456544: "green",
1288 | 456852: "red",
1289 | 457286: "purple",
1290 | 457591: "gold",
1291 | 457934: "gold",
1292 | 458200: "green",
1293 | 458483: "green",
1294 | 458860: "purple",
1295 | 459155: "gold",
1296 | 459445: "purple",
1297 | 459790: "gold",
1298 | 460095: "gold",
1299 | 460556: "gold",
1300 | 460803: "purple",
1301 | 461138: "red",
1302 | 461567: "red",
1303 | 461969: "purple",
1304 | 462358: "green",
1305 | 462602: "purple",
1306 | 462902: "red",
1307 | 463227: "gold",
1308 | 463658: "gold",
1309 | 463928: "gold",
1310 | 464238: "gold",
1311 | 464661: "green",
1312 | 465039: "gold",
1313 | 465380: "purple",
1314 | 465639: "purple",
1315 | 465918: "gold",
1316 | 466207: "gold",
1317 | 466561: "purple",
1318 | 466984: "purple",
1319 | 467379: "green",
1320 | 467674: "purple",
1321 | 468140: "gold",
1322 | 468553: "green",
1323 | 468919: "purple",
1324 | 469196: "gold",
1325 | 469535: "green",
1326 | 469902: "purple",
1327 | 470249: "green",
1328 | 470641: "gold",
1329 | 471032: "purple",
1330 | 471317: "red",
1331 | 471681: "red",
1332 | 471940: "gold",
1333 | 472275: "purple",
1334 | 472572: "gold",
1335 | 472858: "gold",
1336 | 473247: "purple",
1337 | 473546: "red",
1338 | 473973: "purple",
1339 | 474397: "purple",
1340 | 474698: "purple",
1341 | 474993: "purple",
1342 | 475274: "red",
1343 | 475647: "gold",
1344 | 476049: "gold",
1345 | 476404: "gold",
1346 | 476861: "purple",
1347 | 477242: "green",
1348 | 477640: "purple",
1349 | 478005: "purple",
1350 | 478428: "purple",
1351 | 478765: "red",
1352 | 479136: "gold",
1353 | 479526: "purple",
1354 | 479855: "purple",
1355 | 480245: "green",
1356 | 480558: "purple",
1357 | 481015: "purple",
1358 | 481343: "red",
1359 | 481657: "purple",
1360 | 481971: "red",
1361 | 482394: "green",
1362 | 482773: "green",
1363 | 483139: "red",
1364 | 483607: "gold",
1365 | 483884: "red",
1366 | 484158: "green",
1367 | 484574: "gold",
1368 | 485030: "purple",
1369 | 485378: "red",
1370 | 485849: "red",
1371 | 486086: "gold",
1372 | 486446: "gold",
1373 | 486758: "purple",
1374 | 487031: "red",
1375 | 487384: "red",
1376 | 487782: "purple",
1377 | 488101: "purple",
1378 | 488481: "green",
1379 | 488730: "gold",
1380 | 489165: "gold",
1381 | 489525: "gold",
1382 | 489811: "gold",
1383 | 490270: "purple",
1384 | 490538: "green",
1385 | 490907: "purple",
1386 | 491324: "gold",
1387 | 491721: "green",
1388 | 492188: "gold",
1389 | 492580: "purple",
1390 | 492879: "gold",
1391 | 493282: "purple",
1392 | 493686: "purple",
1393 | 494120: "purple",
1394 | 494366: "purple",
1395 | 494733: "red",
1396 | 495091: "purple",
1397 | 495459: "red",
1398 | 495858: "gold",
1399 | 496116: "purple",
1400 | 496500: "red",
1401 | 496902: "red",
1402 | 497311: "green",
1403 | 497632: "gold",
1404 | 498056: "red",
1405 | 498443: "purple",
1406 | 498833: "red",
1407 | 499122: "green",
1408 | 499378: "purple",
1409 | 499755: "green",
1410 | 500065: "green",
1411 | 500303: "green",
1412 | 500596: "gold",
1413 | 500919: "red",
1414 | 501304: "red",
1415 | 501741: "purple",
1416 | 501991: "red",
1417 | 502409: "purple",
1418 | 502835: "red",
1419 | 503262: "gold",
1420 | 503559: "green",
1421 | 504026: "green",
1422 | 504354: "green",
1423 | 504688: "gold",
1424 | 505147: "gold",
1425 | 505440: "gold",
1426 | 505720: "purple",
1427 | 505974: "green",
1428 | 506230: "gold",
1429 | 506563: "gold",
1430 | 506837: "green",
1431 | 507120: "purple",
1432 | 507438: "red",
1433 | 507719: "green",
1434 | 508036: "red",
1435 | 508281: "gold",
1436 | 508537: "gold",
1437 | 508859: "red",
1438 | 509176: "gold",
1439 | 509619: "gold",
1440 | 509898: "red",
1441 | 510302: "red",
1442 | 510760: "green",
1443 | 511114: "green",
1444 | 511426: "purple",
1445 | 511761: "red",
1446 | 512112: "gold",
1447 | 512501: "red",
1448 | 512925: "gold",
1449 | 513332: "green",
1450 | 513586: "green",
1451 | 513866: "green",
1452 | 514299: "green",
1453 | 514672: "purple",
1454 | 514923: "red",
1455 | 515272: "purple",
1456 | 515548: "gold",
1457 | 515862: "red",
1458 | 516208: "gold",
1459 | 516616: "red",
1460 | 517002: "purple",
1461 | 517404: "red",
1462 | 517753: "green",
1463 | 518203: "red",
1464 | 518674: "purple",
1465 | 518938: "gold",
1466 | 519375: "red",
1467 | 519763: "green",
1468 | 520020: "gold",
1469 | 520347: "red",
1470 | 520718: "green",
1471 | 521034: "red",
1472 | 521442: "purple",
1473 | 521895: "red",
1474 | 522252: "gold",
1475 | 522674: "purple",
1476 | 522936: "purple",
1477 | 523342: "red",
1478 | 523817: "red",
1479 | 524105: "gold",
1480 | 524570: "purple",
1481 | 524973: "red",
1482 | 525282: "green",
1483 | 525648: "green",
1484 | 525989: "gold",
1485 | 526255: "red",
1486 | 526715: "green",
1487 | 527018: "gold",
1488 | 527314: "purple",
1489 | 527586: "green",
1490 | 527866: "green",
1491 | 528124: "purple",
1492 | 528406: "green",
1493 | 528859: "purple",
1494 | 529134: "green",
1495 | 529527: "gold",
1496 | 529810: "purple",
1497 | 530213: "red",
1498 | 530548: "purple",
1499 | 530843: "gold",
1500 | 531123: "purple",
1501 | 531473: "purple",
1502 | 531915: "gold",
1503 | 532356: "green",
1504 | 532829: "green",
1505 | 533170: "purple",
1506 | 533477: "red",
1507 | 533778: "gold",
1508 | 534240: "green",
1509 | 534647: "purple",
1510 | 534937: "gold",
1511 | 535392: "red",
1512 | 535743: "green",
1513 | 536207: "purple",
1514 | 536537: "green",
1515 | 536810: "green",
1516 | 537171: "green",
1517 | 537451: "red",
1518 | 537743: "green",
1519 | 538053: "red",
1520 | 538439: "red",
1521 | 538756: "gold",
1522 | 539023: "red",
1523 | 539270: "purple",
1524 | 539554: "red",
1525 | 539914: "gold",
1526 | 540220: "purple",
1527 | 540528: "red",
1528 | 540792: "red",
1529 | 541202: "green",
1530 | 541578: "gold",
1531 | 541879: "red",
1532 | 542314: "gold",
1533 | 542607: "purple",
1534 | 542875: "green",
1535 | 543257: "gold",
1536 | 543715: "green",
1537 | 543983: "red",
1538 | 544360: "red",
1539 | 544697: "red",
1540 | 544973: "green",
1541 | 545358: "gold",
1542 | 545714: "purple",
1543 | 545979: "green",
1544 | 546244: "green",
1545 | 546571: "purple",
1546 | 547001: "purple",
1547 | 547473: "green",
1548 | 547751: "green",
1549 | 548165: "purple",
1550 | 548500: "green",
1551 | 548909: "red",
1552 | 549205: "gold",
1553 | 549566: "gold",
1554 | 550020: "red",
1555 | 550371: "purple",
1556 | 550728: "purple",
1557 | 551029: "green",
1558 | 551408: "red",
1559 | 551805: "green",
1560 | 552257: "red",
1561 | 552705: "red",
1562 | 552969: "gold",
1563 | 553280: "red",
1564 | 553602: "green",
1565 | 553912: "purple",
1566 | 554329: "green",
1567 | 554687: "gold",
1568 | 555011: "green",
1569 | 555368: "green",
1570 | 555834: "gold",
1571 | 556104: "gold",
1572 | 556385: "gold",
1573 | 556827: "purple",
1574 | 557235: "red",
1575 | 557662: "green",
1576 | 557903: "gold",
1577 | 558295: "gold",
1578 | 558589: "purple",
1579 | 559019: "gold",
1580 | 559489: "red",
1581 | 559933: "red",
1582 | 560316: "red",
1583 | 560717: "green",
1584 | 560969: "green",
1585 | 561341: "green",
1586 | 561680: "gold",
1587 | 562033: "gold",
1588 | 562502: "purple",
1589 | 562789: "red",
1590 | 563106: "purple",
1591 | 563464: "red",
1592 | 563764: "green",
1593 | 564217: "green",
1594 | 564512: "red",
1595 | 564918: "red",
1596 | 565157: "gold",
1597 | 565436: "purple",
1598 | 565805: "green",
1599 | 566280: "green",
1600 | 566520: "green",
1601 | 566923: "gold",
1602 | 567324: "gold",
1603 | 567621: "red",
1604 | 567938: "green",
1605 | 568347: "purple",
1606 | 568677: "green",
1607 | 569114: "purple",
1608 | 569544: "gold",
1609 | 569871: "purple",
1610 | 570334: "purple",
1611 | 570635: "green",
1612 | 570952: "gold",
1613 | 571375: "green",
1614 | 571777: "red",
1615 | 572051: "gold",
1616 | 572347: "purple",
1617 | 572597: "green",
1618 | 572835: "purple",
1619 | 573258: "purple",
1620 | 573586: "red",
1621 | 574001: "gold",
1622 | 574452: "purple",
1623 | 574857: "red",
1624 | 575166: "gold",
1625 | 575445: "green",
1626 | 575828: "red",
1627 | 576205: "green",
1628 | 576517: "purple",
1629 | 576879: "gold",
1630 | 577347: "purple",
1631 | 577810: "green",
1632 | 578188: "gold",
1633 | 578634: "purple",
1634 | 579011: "red",
1635 | 579485: "purple",
1636 | 579759: "green",
1637 | 580199: "purple",
1638 | 580566: "purple",
1639 | 580907: "purple",
1640 | 581293: "green",
1641 | 581635: "purple",
1642 | 582026: "purple",
1643 | 582495: "gold",
1644 | 582770: "red",
1645 | 583041: "purple",
1646 | 583442: "purple",
1647 | 583801: "green",
1648 | 584112: "red",
1649 | 584390: "purple",
1650 | 584701: "gold",
1651 | 584949: "purple",
1652 | 585309: "green",
1653 | 585592: "gold",
1654 | 585895: "gold",
1655 | 586358: "purple",
1656 | 586645: "gold",
1657 | 586906: "red",
1658 | 587145: "green",
1659 | 587504: "purple",
1660 | 587920: "purple",
1661 | 588290: "green",
1662 | 588696: "green",
1663 | 588997: "purple",
1664 | 589238: "purple",
1665 | 589608: "green",
1666 | 589918: "purple",
1667 | 590349: "gold",
1668 | 590658: "red",
1669 | 591060: "purple",
1670 | 591376: "gold",
1671 | 591686: "red",
1672 | 591978: "red",
1673 | 592334: "purple",
1674 | 592629: "purple",
1675 | 592882: "gold",
1676 | 593153: "red",
1677 | 593449: "green",
1678 | 593740: "green",
1679 | 594142: "green",
1680 | 594486: "red",
1681 | 594842: "purple",
1682 | 595219: "red",
1683 | 595560: "green",
1684 | 595936: "purple",
1685 | 596256: "purple",
1686 | 596621: "purple",
1687 | 596938: "green",
1688 | 597348: "purple",
1689 | 597808: "purple",
1690 | 598142: "gold",
1691 | 598473: "gold",
1692 | 598787: "green",
1693 | 599064: "green",
1694 | 599503: "purple",
1695 | 599830: "purple",
1696 | 600072: "green",
1697 | 600547: "green",
1698 | 600896: "red",
1699 | 601324: "green",
1700 | 601773: "green",
1701 | 602016: "gold",
1702 | 602292: "gold",
1703 | 602748: "gold",
1704 | 603036: "red",
1705 | 603352: "gold",
1706 | 603613: "purple",
1707 | 603912: "gold",
1708 | 604269: "purple",
1709 | 604686: "gold",
1710 | 605157: "gold",
1711 | 605442: "red",
1712 | 605805: "red",
1713 | 606243: "gold",
1714 | 606693: "green",
1715 | 606953: "red",
1716 | 607306: "gold",
1717 | 607768: "green",
1718 | 608167: "purple",
1719 | 608553: "purple",
1720 | 608792: "purple",
1721 | 609077: "red",
1722 | 609339: "red",
1723 | 609756: "green",
1724 | 610096: "green",
1725 | 610564: "gold",
1726 | 610946: "gold",
1727 | 611376: "red",
1728 | 611638: "purple",
1729 | 611942: "red",
1730 | 612183: "green",
1731 | 612658: "green",
1732 | 612929: "purple",
1733 | 613232: "green",
1734 | 613590: "green",
1735 | 613956: "purple",
1736 | 614223: "red",
1737 | 614464: "green",
1738 | 614814: "green",
1739 | 615193: "purple",
1740 | 615515: "red",
1741 | 615941: "gold",
1742 | 616268: "gold",
1743 | 616632: "red",
1744 | 616934: "gold",
1745 | 617288: "purple",
1746 | 617760: "purple",
1747 | 618130: "green",
1748 | 618545: "purple",
1749 | 618808: "purple",
1750 | 619197: "purple",
1751 | 619543: "red",
1752 | 619965: "green",
1753 | 620222: "gold",
1754 | 620587: "green",
1755 | 620936: "purple",
1756 | 621262: "red",
1757 | 621502: "gold",
1758 | 621820: "red",
1759 | 622270: "purple",
1760 | 622530: "red",
1761 | 622882: "purple",
1762 | 623191: "purple",
1763 | 623520: "green",
1764 | 623815: "purple",
1765 | 624176: "gold",
1766 | 624565: "red",
1767 | 624992: "red",
1768 | 625373: "green",
1769 | 625658: "purple",
1770 | 626050: "green",
1771 | 626510: "red",
1772 | 626843: "purple",
1773 | 627262: "red",
1774 | 627501: "red",
1775 | 627896: "gold",
1776 | 628176: "red",
1777 | 628529: "green",
1778 | 628951: "purple",
1779 | 629262: "red",
1780 | 629545: "red",
1781 | 629878: "green",
1782 | 630192: "gold",
1783 | 630525: "purple",
1784 | 630909: "green",
1785 | 631349: "green",
1786 | 631620: "gold",
1787 | 632085: "purple",
1788 | 632380: "purple",
1789 | 632624: "purple",
1790 | 633044: "gold",
1791 | 633488: "red",
1792 | 633907: "green",
1793 | 634153: "purple",
1794 | 634570: "red",
1795 | 634911: "red",
1796 | 635372: "gold",
1797 | 635687: "green",
1798 | 636149: "purple",
1799 | 636449: "green",
1800 | 636728: "purple",
1801 | 637099: "red",
1802 | 637486: "red",
1803 | 637771: "red",
1804 | 638196: "gold",
1805 | 638479: "red",
1806 | 638848: "green",
1807 | 639125: "red",
1808 | 639376: "red",
1809 | 639716: "purple",
1810 | 640097: "purple",
1811 | 640339: "green",
1812 | 640693: "gold",
1813 | 640987: "gold",
1814 | 641416: "green",
1815 | 641757: "purple",
1816 | 642015: "red",
1817 | 642294: "red",
1818 | 642570: "green",
1819 | 642896: "green",
1820 | 643337: "purple",
1821 | 643688: "red",
1822 | 644056: "purple",
1823 | 644448: "green",
1824 | 644739: "purple",
1825 | 645185: "red",
1826 | 645548: "red",
1827 | 645991: "gold",
1828 | 646342: "red",
1829 | 646725: "green",
1830 | 647165: "green",
1831 | 647557: "gold",
1832 | 647903: "red",
1833 | 648201: "green",
1834 | 648468: "purple",
1835 | 648816: "red",
1836 | 649285: "gold",
1837 | 649549: "red",
1838 | 649900: "purple",
1839 | 650353: "purple",
1840 | 650710: "red",
1841 | 651164: "red",
1842 | 651558: "green",
1843 | 651841: "purple",
1844 | 652286: "purple",
1845 | 652673: "green",
1846 | 653059: "green",
1847 | 653301: "green",
1848 | 653614: "gold",
1849 | 653857: "green",
1850 | 654209: "purple",
1851 | 654671: "purple",
1852 | 655104: "red",
1853 | 655479: "purple",
1854 | 655717: "green",
1855 | 656009: "red",
1856 | 656268: "red",
1857 | 656620: "green",
1858 | 657049: "red",
1859 | 657397: "red",
1860 | 657708: "gold",
1861 | 658023: "purple",
1862 | 658479: "green",
1863 | 658824: "purple",
1864 | 659069: "green",
1865 | 659518: "gold",
1866 | 659895: "green",
1867 | 660227: "red",
1868 | 660678: "green",
1869 | 660942: "gold",
1870 | 661359: "red",
1871 | 661779: "red",
1872 | 662169: "red",
1873 | 662641: "gold",
1874 | 663064: "red",
1875 | 663311: "green",
1876 | 663625: "red",
1877 | 663879: "purple",
1878 | 664131: "green",
1879 | 664408: "red",
1880 | 664742: "red",
1881 | 664981: "gold",
1882 | 665260: "purple",
1883 | 665690: "green",
1884 | 666049: "gold",
1885 | 666339: "gold",
1886 | 666773: "red",
1887 | 667168: "green",
1888 | 667642: "red",
1889 | 668058: "gold",
1890 | 668502: "purple",
1891 | 668811: "purple",
1892 | 669229: "gold",
1893 | 669703: "purple",
1894 | 669985: "red",
1895 | 670309: "green",
1896 | 670692: "green",
1897 | 670999: "green",
1898 | 671436: "purple",
1899 | 671860: "gold",
1900 | 672198: "green",
1901 | 672602: "purple",
1902 | 672911: "gold",
1903 | 673328: "green",
1904 | 673594: "gold",
1905 | 673835: "green",
1906 | 674246: "red",
1907 | 674703: "green",
1908 | 675013: "gold",
1909 | 675330: "purple",
1910 | 675632: "green",
1911 | 675983: "purple",
1912 | 676324: "purple",
1913 | 676700: "red",
1914 | 676961: "gold",
1915 | 677206: "red",
1916 | 677654: "gold",
1917 | 678025: "purple",
1918 | 678337: "green",
1919 | 678585: "red",
1920 | 678991: "purple",
1921 | 679263: "gold",
1922 | 679715: "green",
1923 | 680081: "gold",
1924 | 680490: "red",
1925 | 680739: "red",
1926 | 681167: "purple",
1927 | 681619: "green",
1928 | 682044: "purple",
1929 | 682341: "gold",
1930 | 682653: "green",
1931 | 683128: "gold",
1932 | 683537: "purple",
1933 | 683978: "green",
1934 | 684222: "purple",
1935 | 684592: "red",
1936 | 684935: "purple",
1937 | 685365: "red",
1938 | 685767: "green",
1939 | 686027: "purple",
1940 | 686438: "red",
1941 | 686717: "red",
1942 | 687133: "gold",
1943 | 687446: "gold",
1944 | 687879: "purple",
1945 | 688346: "green",
1946 | 688618: "gold",
1947 | 689049: "gold",
1948 | 689462: "gold",
1949 | 689914: "gold",
1950 | 690195: "red",
1951 | 690470: "purple",
1952 | 690806: "gold",
1953 | 691066: "purple",
1954 | 691317: "purple",
1955 | 691569: "red",
1956 | 691893: "green",
1957 | 692355: "green",
1958 | 692678: "green",
1959 | 693091: "green",
1960 | 693501: "red",
1961 | 693791: "gold",
1962 | 694123: "purple",
1963 | 694534: "red",
1964 | 694794: "red",
1965 | 695221: "green",
1966 | 695458: "purple",
1967 | 695721: "purple",
1968 | 696026: "green",
1969 | 696441: "purple",
1970 | 696848: "purple",
1971 | 697316: "green",
1972 | 697712: "gold",
1973 | 697988: "red",
1974 | 698227: "purple",
1975 | 698680: "red",
1976 | 699113: "gold",
1977 | 699565: "purple",
1978 | 699904: "purple",
1979 | 700193: "green",
1980 | 700599: "purple",
1981 | 700963: "gold",
1982 | 701203: "red",
1983 | 701644: "purple",
1984 | 702082: "gold",
1985 | 702343: "green",
1986 | 702803: "red",
1987 | 703241: "gold",
1988 | 703697: "red",
1989 | 704139: "red",
1990 | 704450: "red",
1991 | 704845: "purple",
1992 | 705289: "gold",
1993 | 705608: "green",
1994 | 705918: "red",
1995 | 706319: "purple",
1996 | 706560: "gold",
1997 | 706923: "purple",
1998 | 707248: "purple",
1999 | 707722: "purple",
2000 | 708050: "gold",
2001 | 708459: "purple",
2002 | 708761: "red",
2003 | 709175: "gold",
2004 | 709603: "gold",
2005 | 709918: "gold",
2006 | 710245: "gold",
2007 | 710641: "green",
2008 | 711023: "gold",
2009 | 711437: "red",
2010 | 711848: "gold",
2011 | 712092: "red",
2012 | 712341: "gold",
2013 | 712645: "red",
2014 | 713101: "red",
2015 | 713528: "gold",
2016 | 713974: "purple",
2017 | 714367: "red",
2018 | 714781: "green",
2019 | 715171: "gold",
2020 | 715585: "purple",
2021 | 715971: "purple",
2022 | 716438: "purple",
2023 | 716697: "red",
2024 | 717064: "purple",
2025 | 717439: "green",
2026 | 717789: "red",
2027 | 718218: "gold",
2028 | 718692: "gold",
2029 | 719124: "purple",
2030 | 719550: "red",
2031 | 719947: "green",
2032 | 720319: "gold",
2033 | 720581: "red",
2034 | 721005: "green",
2035 | 721247: "gold",
2036 | 721621: "purple",
2037 | 721914: "red",
2038 | 722337: "gold",
2039 | 722698: "green",
2040 | 723055: "purple",
2041 | 723474: "purple",
2042 | 723882: "red",
2043 | 724251: "red",
2044 | 724625: "purple",
2045 | 724928: "green",
2046 | 725247: "gold",
2047 | 725611: "red",
2048 | 726059: "gold",
2049 | 726301: "green",
2050 | 726678: "gold",
2051 | 726964: "red",
2052 | 727205: "gold",
2053 | 727454: "gold",
2054 | 727915: "gold",
2055 | 728330: "red",
2056 | 728646: "gold",
2057 | 729036: "purple",
2058 | 729294: "green",
2059 | 729588: "red",
2060 | 730050: "gold",
2061 | 730290: "gold",
2062 | 730644: "green",
2063 | 730940: "green",
2064 | 731319: "green",
2065 | 731556: "red",
2066 | 731941: "green",
2067 | 732199: "gold",
2068 | 732554: "green",
2069 | 733021: "red",
2070 | 733286: "red",
2071 | 733600: "red",
2072 | 733928: "gold",
2073 | 734294: "purple",
2074 | 734769: "purple",
2075 | 735103: "purple",
2076 | 735530: "gold",
2077 | 735849: "red",
2078 | 736212: "gold",
2079 | 736452: "purple",
2080 | 736744: "purple",
2081 | 736993: "purple",
2082 | 737367: "purple",
2083 | 737777: "purple",
2084 | 738189: "green",
2085 | 738463: "green",
2086 | 738906: "purple",
2087 | 739347: "red",
2088 | 739705: "purple",
2089 | 740159: "gold",
2090 | 740619: "purple",
2091 | 740935: "green",
2092 | 741398: "red",
2093 | 741642: "gold",
2094 | 741927: "green",
2095 | 742222: "gold",
2096 | 742689: "purple",
2097 | 742948: "green",
2098 | 743313: "purple",
2099 | 743742: "red",
2100 | 744049: "purple",
2101 | 744368: "green",
2102 | 744662: "green",
2103 | 744904: "green",
2104 | 745159: "red",
2105 | 745597: "red",
2106 | 745871: "gold",
2107 | 746277: "green",
2108 | 746645: "purple",
2109 | 746924: "gold",
2110 | 747226: "red",
2111 | 747478: "gold",
2112 | 747763: "purple",
2113 | 748157: "red",
2114 | 748500: "purple",
2115 | 748851: "gold",
2116 | 749276: "red",
2117 | 749662: "red",
2118 | 749972: "purple",
2119 | 750227: "green",
2120 | 750555: "red",
2121 | 750992: "green",
2122 | 751423: "purple",
2123 | 751821: "gold",
2124 | 752181: "green",
2125 | 752607: "green",
2126 | 752889: "green",
2127 | 753312: "gold",
2128 | 753760: "green",
2129 | 754072: "red",
2130 | 754481: "gold",
2131 | 754731: "red",
2132 | 755063: "red",
2133 | 755303: "red",
2134 | 755669: "purple",
2135 | 755911: "red",
2136 | 756299: "purple",
2137 | 756714: "red",
2138 | 757066: "gold",
2139 | 757510: "red",
2140 | 757975: "green",
2141 | 758273: "purple",
2142 | 758577: "purple",
2143 | 758873: "gold",
2144 | 759306: "green",
2145 | 759762: "red",
2146 | 760170: "red",
2147 | 760510: "purple",
2148 | 760939: "red",
2149 | 761385: "green",
2150 | 761681: "red",
2151 | 762112: "red",
2152 | 762547: "green",
2153 | 763001: "green",
2154 | 763374: "gold",
2155 | 763792: "green",
2156 | 764109: "gold",
2157 | 764555: "gold",
2158 | 764983: "gold",
2159 | 765371: "red",
2160 | 765677: "purple",
2161 | 766022: "red",
2162 | 766379: "gold",
2163 | 766735: "purple",
2164 | 766995: "green",
2165 | 767465: "green",
2166 | 767869: "gold",
2167 | 768342: "purple",
2168 | 768739: "red",
2169 | 768977: "gold",
2170 | 769252: "gold",
2171 | 769531: "gold",
2172 | 769931: "red",
2173 | 770322: "red",
2174 | 770598: "green",
2175 | 770950: "green",
2176 | 771187: "green",
2177 | 771651: "red",
2178 | 772080: "purple",
2179 | 772331: "purple",
2180 | 772700: "gold",
2181 | 773081: "gold",
2182 | 773358: "gold",
2183 | 773683: "green",
2184 | 774102: "purple",
2185 | 774486: "purple",
2186 | 774934: "green",
2187 | 775372: "red",
2188 | 775784: "gold",
2189 | 776164: "red",
2190 | 776408: "purple",
2191 | 776716: "gold",
2192 | 777016: "red",
2193 | 777459: "green",
2194 | 777734: "purple",
2195 | 778126: "purple",
2196 | 778531: "gold",
2197 | 779004: "green",
2198 | 779416: "purple",
2199 | 779869: "gold",
2200 | 780244: "red",
2201 | 780534: "green",
2202 | 780968: "gold",
2203 | 781338: "purple",
2204 | 781768: "green",
2205 | 782098: "green",
2206 | 782531: "purple",
2207 | 782890: "green",
2208 | 783351: "gold",
2209 | 783780: "purple",
2210 | 784240: "green",
2211 | 784524: "green",
2212 | 784932: "green",
2213 | 785212: "purple",
2214 | 785552: "red",
2215 | 785874: "red",
2216 | 786338: "green",
2217 | 786660: "purple",
2218 | 786957: "red",
2219 | 787324: "red",
2220 | 787732: "gold",
2221 | 788015: "green",
2222 | 788417: "green",
2223 | 788663: "gold",
2224 | 789059: "purple",
2225 | 789513: "gold",
2226 | 789760: "red",
2227 | 790191: "purple",
2228 | 790501: "green",
2229 | 790964: "purple",
2230 | 791210: "red",
2231 | 791607: "red",
2232 | 791932: "purple",
2233 | 792291: "gold",
2234 | 792564: "green",
2235 | 792820: "red",
2236 | 793268: "red",
2237 | 793633: "red",
2238 | 793933: "gold",
2239 | 794183: "purple",
2240 | 794511: "purple",
2241 | 794932: "purple",
2242 | 795353: "gold",
2243 | 795725: "gold",
2244 | 796009: "gold",
2245 | 796456: "purple",
2246 | 796843: "purple",
2247 | 797129: "green",
2248 | 797443: "green",
2249 | 797722: "green",
2250 | 797999: "green",
2251 | 798302: "green",
2252 | 798721: "red",
2253 | 799119: "red",
2254 | 799501: "purple",
2255 | 799844: "gold",
2256 | 800214: "red",
2257 | 800629: "red",
2258 | 800958: "purple",
2259 | 801238: "red",
2260 | 801590: "green",
2261 | 801984: "green",
2262 | 802451: "red",
2263 | 802802: "red",
2264 | 803195: "purple",
2265 | 803528: "green",
2266 | 803983: "purple",
2267 | 804221: "green",
2268 | 804479: "gold",
2269 | 804852: "red",
2270 | 805157: "purple",
2271 | 805488: "purple",
2272 | 805901: "green",
2273 | 806141: "red",
2274 | 806420: "purple",
2275 | 806703: "green",
2276 | 806952: "gold",
2277 | 807263: "purple",
2278 | 807589: "red",
2279 | 807952: "gold",
2280 | 808274: "gold",
2281 | 808527: "green",
2282 | 808771: "green",
2283 | 809091: "green",
2284 | 809563: "green",
2285 | 809845: "green",
2286 | 810299: "gold",
2287 | 810669: "red",
2288 | 810943: "gold",
2289 | 811186: "green",
2290 | 811530: "gold",
2291 | 811829: "gold",
2292 | 812097: "red",
2293 | 812443: "gold",
2294 | 812867: "red",
2295 | 813266: "green",
2296 | 813677: "gold",
2297 | 813967: "green",
2298 | 814215: "green",
2299 | 814551: "green",
2300 | 814807: "green",
2301 | 815274: "green",
2302 | 815539: "gold",
2303 | 815860: "green",
2304 | 816248: "green",
2305 | 816561: "red",
2306 | 817018: "red",
2307 | 817308: "green",
2308 | 817617: "purple",
2309 | 817868: "green",
2310 | 818212: "gold",
2311 | 818683: "green",
2312 | 819092: "gold",
2313 | 819394: "purple",
2314 | 819796: "red",
2315 | 820233: "gold",
2316 | 820628: "gold",
2317 | 821048: "green",
2318 | 821335: "green",
2319 | 821640: "green",
2320 | 821891: "gold",
2321 | 822215: "red",
2322 | 822514: "purple",
2323 | 822929: "purple",
2324 | 823295: "red",
2325 | 823754: "green",
2326 | 824014: "purple",
2327 | 824424: "gold",
2328 | 824711: "purple",
2329 | 825130: "gold",
2330 | 825559: "purple",
2331 | 825962: "gold",
2332 | 826219: "gold",
2333 | 826552: "purple",
2334 | 826973: "green",
2335 | 827246: "red",
2336 | 827675: "green",
2337 | 827967: "green",
2338 | 828247: "green",
2339 | 828501: "red",
2340 | 828962: "gold",
2341 | 829359: "purple",
2342 | 829607: "gold",
2343 | 829974: "gold",
2344 | 830442: "purple",
2345 | 830877: "purple",
2346 | 831283: "red",
2347 | 831627: "red",
2348 | 832070: "red",
2349 | 832527: "green",
2350 | 832924: "green",
2351 | 833379: "gold",
2352 | 833760: "purple",
2353 | 834122: "purple",
2354 | 834406: "green",
2355 | 834670: "red",
2356 | 835014: "gold",
2357 | 835399: "purple",
2358 | 835661: "red",
2359 | 836068: "red",
2360 | 836466: "green",
2361 | 836911: "gold",
2362 | 837381: "green",
2363 | 837748: "gold",
2364 | 838176: "purple",
2365 | 838545: "gold",
2366 | 838878: "green",
2367 | 839192: "red",
2368 | 839452: "gold",
2369 | 839887: "purple",
2370 | 840268: "red",
2371 | 840735: "green",
2372 | 840996: "red",
2373 | 841471: "red",
2374 | 841719: "purple",
2375 | 842188: "gold",
2376 | 842434: "purple",
2377 | 842690: "gold",
2378 | 842996: "purple",
2379 | 843373: "gold",
2380 | 843670: "red",
2381 | 844131: "purple",
2382 | 844572: "gold",
2383 | 844826: "red",
2384 | 845277: "purple",
2385 | 845681: "gold",
2386 | 846125: "red",
2387 | 846378: "green",
2388 | 846773: "gold",
2389 | 847205: "purple",
2390 | 847669: "red",
2391 | 848130: "green",
2392 | 848498: "green",
2393 | 848761: "red",
2394 | 849128: "red",
2395 | 849456: "red",
2396 | 849697: "red",
2397 | 849955: "green",
2398 | 850345: "green",
2399 | 850584: "purple",
2400 | 850963: "red",
2401 | 851236: "red",
2402 | 851509: "red",
2403 | 851917: "purple",
2404 | 852384: "purple",
2405 | 852804: "red",
2406 | 853256: "red",
2407 | 853575: "green",
2408 | 853877: "red",
2409 | 854338: "green",
2410 | 854618: "red",
2411 | 855092: "gold",
2412 | 855333: "green",
2413 | 855799: "gold",
2414 | 856191: "purple",
2415 | 856665: "gold",
2416 | 856960: "red",
2417 | 857411: "green",
2418 | 857751: "green",
2419 | 858129: "gold",
2420 | 858543: "red",
2421 | 858818: "green",
2422 | 859074: "gold",
2423 | 859434: "red",
2424 | 859708: "red",
2425 | 860176: "red",
2426 | 860578: "gold",
2427 | 860888: "green",
2428 | 861339: "green",
2429 | 861737: "purple",
2430 | 862176: "purple",
2431 | 862622: "purple",
2432 | 863058: "red",
2433 | 863414: "green",
2434 | 863880: "red",
2435 | 864193: "purple",
2436 | 864449: "green",
2437 | 864859: "red",
2438 | 865170: "gold",
2439 | 865429: "gold",
2440 | 865852: "purple",
2441 | 866322: "green",
2442 | 866560: "purple",
2443 | 866916: "gold",
2444 | 867381: "red",
2445 | 867833: "green",
2446 | 868242: "gold",
2447 | 868663: "red",
2448 | 868991: "red",
2449 | 869365: "green",
2450 | 869723: "purple",
2451 | 870017: "purple",
2452 | 870295: "gold",
2453 | 870651: "red",
2454 | 870895: "gold",
2455 | 871261: "purple",
2456 | 871562: "gold",
2457 | 871967: "purple",
2458 | 872395: "purple",
2459 | 872763: "green",
2460 | 873060: "purple",
2461 | 873399: "purple",
2462 | 873656: "purple",
2463 | 873959: "gold",
2464 | 874258: "gold",
2465 | 874660: "red",
2466 | 875022: "green",
2467 | 875359: "purple",
2468 | 875665: "green",
2469 | 876099: "purple",
2470 | 876339: "gold",
2471 | 876793: "gold",
2472 | 877170: "red",
2473 | 877519: "green",
2474 | 877968: "red",
2475 | 878209: "gold",
2476 | 878606: "red",
2477 | 878873: "gold",
2478 | 879319: "red",
2479 | 879592: "purple",
2480 | 879838: "purple",
2481 | 880232: "red",
2482 | 880471: "red",
2483 | 880878: "purple",
2484 | 881273: "gold",
2485 | 881730: "green",
2486 | 882113: "gold",
2487 | 882362: "red",
2488 | 882815: "green",
2489 | 883279: "green",
2490 | 883524: "purple",
2491 | 883780: "red",
2492 | 884039: "gold",
2493 | 884509: "red",
2494 | 884877: "purple",
2495 | 885238: "red",
2496 | 885661: "gold",
2497 | 886042: "green",
2498 | 886406: "red",
2499 | 886805: "green",
2500 | 887067: "green",
2501 | 887378: "purple",
2502 | 887827: "purple",
2503 | 888201: "red",
2504 | 888620: "green",
2505 | 888867: "red",
2506 | 889148: "gold",
2507 | 889416: "gold",
2508 | 889775: "purple",
2509 | 890146: "purple",
2510 | 890505: "green",
2511 | 890766: "purple",
2512 | 891122: "purple",
2513 | 891393: "red",
2514 | 891710: "green",
2515 | 892040: "gold",
2516 | 892501: "red",
2517 | 892769: "red",
2518 | 893048: "red",
2519 | 893358: "green",
2520 | 893665: "green",
2521 | 894099: "gold",
2522 | 894433: "purple",
2523 | 894884: "green",
2524 | 895287: "purple",
2525 | 895691: "red",
2526 | 896081: "green",
2527 | 896464: "purple",
2528 | 896934: "red",
2529 | 897189: "green",
2530 | 897548: "green",
2531 | 897838: "purple",
2532 | 898295: "green",
2533 | 898573: "purple",
2534 | 898929: "gold",
2535 | 899375: "red",
2536 | 899847: "gold",
2537 | 900101: "gold",
2538 | 900364: "green",
2539 | 900624: "red",
2540 | 901046: "red",
2541 | 901318: "gold",
2542 | 901774: "purple",
2543 | 902121: "gold",
2544 | 902398: "green",
2545 | 902753: "green",
2546 | 903082: "gold",
2547 | 903486: "gold",
2548 | 903783: "green",
2549 | 904193: "red",
2550 | 904487: "green",
2551 | 904727: "purple",
2552 | 905190: "gold",
2553 | 905485: "purple",
2554 | 905907: "green",
2555 | 906219: "green",
2556 | 906633: "purple",
2557 | 906971: "gold",
2558 | 907366: "red",
2559 | 907781: "red",
2560 | 908160: "red",
2561 | 908448: "gold",
2562 | 908882: "gold",
2563 | 909343: "red",
2564 | 909674: "gold",
2565 | 910125: "green",
2566 | 910529: "green",
2567 | 910823: "purple",
2568 | 911183: "purple",
2569 | 911619: "gold",
2570 | 912080: "green",
2571 | 912449: "gold",
2572 | 912898: "gold",
2573 | 913250: "green",
2574 | 913557: "red",
2575 | 914024: "purple",
2576 | 914417: "purple",
2577 | 914726: "green",
2578 | 915098: "purple",
2579 | 915500: "gold",
2580 | 915752: "purple",
2581 | 916151: "purple",
2582 | 916432: "red",
2583 | 916803: "gold",
2584 | 917081: "gold",
2585 | 917483: "purple",
2586 | 917834: "green",
2587 | 918181: "gold",
2588 | 918598: "purple",
2589 | 918908: "green",
2590 | 919266: "red",
2591 | 919708: "green",
2592 | 919958: "purple",
2593 | 920389: "green",
2594 | 920699: "green",
2595 | 921109: "gold",
2596 | 921519: "red",
2597 | 921926: "green",
2598 | 922282: "green",
2599 | 922623: "gold",
2600 | 923020: "purple",
2601 | 923485: "purple",
2602 | 923908: "red",
2603 | 924370: "red",
2604 | 924729: "gold",
2605 | 925033: "gold",
2606 | 925309: "green",
2607 | 925729: "red",
2608 | 926111: "purple",
2609 | 926573: "green",
2610 | 926934: "green",
2611 | 927193: "gold",
2612 | 927576: "gold",
2613 | 928006: "red",
2614 | 928300: "green",
2615 | 928566: "gold",
2616 | 928958: "green",
2617 | 929241: "gold",
2618 | 929562: "gold",
2619 | 929836: "purple",
2620 | 930230: "red",
2621 | 930702: "purple",
2622 | 931033: "gold",
2623 | 931387: "purple",
2624 | 931771: "green",
2625 | 932078: "green",
2626 | 932535: "gold",
2627 | 932915: "gold",
2628 | 933264: "red",
2629 | 933579: "purple",
2630 | 934031: "gold",
2631 | 934339: "green",
2632 | 934692: "gold",
2633 | 934966: "green",
2634 | 935248: "red",
2635 | 935656: "gold",
2636 | 935917: "red",
2637 | 936184: "purple",
2638 | 936505: "gold",
2639 | 936925: "gold",
2640 | 937281: "red",
2641 | 937678: "gold",
2642 | 938089: "purple",
2643 | 938380: "red",
2644 | 938702: "green",
2645 | 939034: "red",
2646 | 939437: "green",
2647 | 939779: "purple",
2648 | 940248: "purple",
2649 | 940507: "gold",
2650 | 940944: "purple",
2651 | 941347: "red",
2652 | 941641: "red",
2653 | 941881: "gold",
2654 | 942130: "red",
2655 | 942558: "gold",
2656 | 942934: "green",
2657 | 943215: "green",
2658 | 943645: "gold",
2659 | 944033: "green",
2660 | 944382: "gold",
2661 | 944767: "green",
2662 | 945055: "purple",
2663 | 945408: "red",
2664 | 945655: "purple",
2665 | 946041: "gold",
2666 | 946491: "purple",
2667 | 946827: "green",
2668 | 947190: "green",
2669 | 947611: "green",
2670 | 947942: "green",
2671 | 948366: "red",
2672 | 948732: "red",
2673 | 949000: "green",
2674 | 949448: "purple",
2675 | 949781: "red",
2676 | 950130: "green",
2677 | 950394: "green",
2678 | 950818: "gold",
2679 | 951236: "gold",
2680 | 951488: "red",
2681 | 951921: "red",
2682 | 952174: "green",
2683 | 952603: "purple",
2684 | 952989: "gold",
2685 | 953294: "red",
2686 | 953734: "gold",
2687 | 954146: "red",
2688 | 954501: "purple",
2689 | 954923: "gold",
2690 | 955228: "red",
2691 | 955544: "purple",
2692 | 955811: "green",
2693 | 956258: "red",
2694 | 956686: "gold",
2695 | 957125: "gold",
2696 | 957592: "red",
2697 | 958039: "green",
2698 | 958435: "purple",
2699 | 958823: "purple",
2700 | 959231: "gold",
2701 | 959477: "gold",
2702 | 959914: "purple",
2703 | 960165: "green",
2704 | 960575: "gold",
2705 | 960966: "purple",
2706 | 961215: "gold",
2707 | 961512: "green",
2708 | 961985: "gold",
2709 | 962289: "green",
2710 | 962616: "red",
2711 | 963081: "gold",
2712 | 963329: "red",
2713 | 963804: "gold",
2714 | 964255: "purple",
2715 | 964492: "gold",
2716 | 964906: "green",
2717 | 965240: "red",
2718 | 965605: "red",
2719 | 966025: "red",
2720 | 966401: "green",
2721 | 966698: "purple",
2722 | 967033: "gold",
2723 | 967479: "gold",
2724 | 967731: "green",
2725 | 968191: "purple",
2726 | 968546: "red",
2727 | 968972: "red",
2728 | 969396: "gold",
2729 | 969640: "green",
2730 | 969998: "gold",
2731 | 970276: "gold",
2732 | 970742: "green",
2733 | 971112: "gold",
2734 | 971449: "gold",
2735 | 971917: "purple",
2736 | 972154: "purple",
2737 | 972544: "green",
2738 | 972925: "gold",
2739 | 973201: "red",
2740 | 973486: "gold",
2741 | 973806: "green",
2742 | 974144: "green",
2743 | 974576: "green",
2744 | 974831: "purple",
2745 | 975133: "green",
2746 | 975452: "green",
2747 | 975771: "red",
2748 | 976233: "purple",
2749 | 976675: "green",
2750 | 976998: "purple",
2751 | 977262: "purple",
2752 | 977512: "gold",
2753 | 977868: "purple",
2754 | 978263: "gold",
2755 | 978542: "red",
2756 | 978889: "red",
2757 | 979194: "gold",
2758 | 979469: "purple",
2759 | 979822: "gold",
2760 | 980209: "purple",
2761 | 980545: "purple",
2762 | 980799: "purple",
2763 | 981107: "gold",
2764 | 981485: "gold",
2765 | 981801: "green",
2766 | 982142: "green",
2767 | 982395: "green",
2768 | 982829: "gold",
2769 | 983170: "purple",
2770 | 983558: "purple",
2771 | 983839: "purple",
2772 | 984292: "purple",
2773 | 984539: "red",
2774 | 984830: "green",
2775 | 985095: "gold",
2776 | 985461: "green",
2777 | 985925: "green",
2778 | 986379: "red",
2779 | 986712: "purple",
2780 | 987079: "gold",
2781 | 987388: "purple",
2782 | 987808: "gold",
2783 | 988144: "red",
2784 | 988475: "purple",
2785 | 988936: "red",
2786 | 989327: "gold",
2787 | 989575: "green",
2788 | 989860: "green",
2789 | 990219: "green",
2790 | 990685: "gold",
2791 | 990945: "red",
2792 | 991229: "purple",
2793 | 991670: "red",
2794 | 992127: "red",
2795 | 992538: "purple",
2796 | 992931: "red",
2797 | 993334: "red",
2798 | 993782: "gold",
2799 | 994179: "gold",
2800 | 994428: "purple",
2801 | 994900: "gold",
2802 | 995302: "red",
2803 | 995752: "red",
2804 | 996164: "red",
2805 | 996514: "purple",
2806 | 996961: "green",
2807 | 997353: "purple",
2808 | 997748: "purple",
2809 | 998174: "green",
2810 | 998444: "red",
2811 | 998732: "green",
2812 | 999066: "gold",
2813 | 999444: "green",
2814 | 999748: "gold",
2815 | };
2816 |
2817 | export default INDICES;
2818 |
--------------------------------------------------------------------------------