Compare commits
	
		
			8 Commits
		
	
	
		
			163502505a
			...
			4b3863e0d1
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						4b3863e0d1 | |
| 
							
							
								
									
								
								 | 
						99b24d0eac | |
| 
							
							
								
									
								
								 | 
						5f5893a408 | |
| 
							
							
								
									
								
								 | 
						4a592915fd | |
| 
							
							
								
									
								
								 | 
						51ef13f5d6 | |
| 
							
							
								
									
								
								 | 
						aba5685898 | |
| 
							
							
								
									
								
								 | 
						47a0b22503 | |
| 
							
							
								
									
								
								 | 
						958e04c47b | 
| 
						 | 
					@ -7,3 +7,10 @@ coverage/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Environment configuration
 | 
					# Environment configuration
 | 
				
			||||||
.env
 | 
					.env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# NodeJS files
 | 
				
			||||||
 | 
					node_modules/
 | 
				
			||||||
 | 
					pnpm-lock.yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Default web build output directory
 | 
				
			||||||
 | 
					public/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -139,6 +139,54 @@ version = "0.7.2"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 | 
					checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "askama"
 | 
				
			||||||
 | 
					version = "0.11.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "askama_derive",
 | 
				
			||||||
 | 
					 "askama_escape",
 | 
				
			||||||
 | 
					 "askama_shared",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "askama_derive"
 | 
				
			||||||
 | 
					version = "0.11.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "askama_shared",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "askama_escape"
 | 
				
			||||||
 | 
					version = "0.10.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "askama_shared"
 | 
				
			||||||
 | 
					version = "0.12.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "askama_escape",
 | 
				
			||||||
 | 
					 "humansize",
 | 
				
			||||||
 | 
					 "mime",
 | 
				
			||||||
 | 
					 "mime_guess",
 | 
				
			||||||
 | 
					 "nom 7.1.1",
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					 "percent-encoding",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					 "toml",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "async-attributes"
 | 
					name = "async-attributes"
 | 
				
			||||||
version = "1.1.2"
 | 
					version = "1.1.2"
 | 
				
			||||||
| 
						 | 
					@ -420,6 +468,12 @@ version = "0.13.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 | 
					checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "beef"
 | 
				
			||||||
 | 
					version = "0.5.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "bitflags"
 | 
					name = "bitflags"
 | 
				
			||||||
version = "1.3.2"
 | 
					version = "1.3.2"
 | 
				
			||||||
| 
						 | 
					@ -519,6 +573,21 @@ dependencies = [
 | 
				
			||||||
 "generic-array",
 | 
					 "generic-array",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "clap"
 | 
				
			||||||
 | 
					version = "2.34.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "ansi_term",
 | 
				
			||||||
 | 
					 "atty",
 | 
				
			||||||
 | 
					 "bitflags",
 | 
				
			||||||
 | 
					 "strsim 0.8.0",
 | 
				
			||||||
 | 
					 "textwrap 0.11.0",
 | 
				
			||||||
 | 
					 "unicode-width",
 | 
				
			||||||
 | 
					 "vec_map",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "clap"
 | 
					name = "clap"
 | 
				
			||||||
version = "3.2.22"
 | 
					version = "3.2.22"
 | 
				
			||||||
| 
						 | 
					@ -531,9 +600,9 @@ dependencies = [
 | 
				
			||||||
 "clap_lex 0.2.4",
 | 
					 "clap_lex 0.2.4",
 | 
				
			||||||
 "indexmap",
 | 
					 "indexmap",
 | 
				
			||||||
 "once_cell",
 | 
					 "once_cell",
 | 
				
			||||||
 "strsim",
 | 
					 "strsim 0.10.0",
 | 
				
			||||||
 "termcolor",
 | 
					 "termcolor",
 | 
				
			||||||
 "textwrap",
 | 
					 "textwrap 0.15.1",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -547,7 +616,7 @@ dependencies = [
 | 
				
			||||||
 "clap_derive 4.0.10",
 | 
					 "clap_derive 4.0.10",
 | 
				
			||||||
 "clap_lex 0.3.0",
 | 
					 "clap_lex 0.3.0",
 | 
				
			||||||
 "once_cell",
 | 
					 "once_cell",
 | 
				
			||||||
 "strsim",
 | 
					 "strsim 0.10.0",
 | 
				
			||||||
 "termcolor",
 | 
					 "termcolor",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -595,6 +664,12 @@ dependencies = [
 | 
				
			||||||
 "os_str_bytes",
 | 
					 "os_str_bytes",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "codemap"
 | 
				
			||||||
 | 
					version = "0.1.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "color-eyre"
 | 
					name = "color-eyre"
 | 
				
			||||||
version = "0.6.2"
 | 
					version = "0.6.2"
 | 
				
			||||||
| 
						 | 
					@ -799,7 +874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
 | 
					checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "cfg-if",
 | 
					 "cfg-if",
 | 
				
			||||||
 "hashbrown",
 | 
					 "hashbrown 0.12.3",
 | 
				
			||||||
 "lock_api",
 | 
					 "lock_api",
 | 
				
			||||||
 "once_cell",
 | 
					 "once_cell",
 | 
				
			||||||
 "parking_lot_core 0.9.3",
 | 
					 "parking_lot_core 0.9.3",
 | 
				
			||||||
| 
						 | 
					@ -1183,6 +1258,34 @@ dependencies = [
 | 
				
			||||||
 "wasm-bindgen",
 | 
					 "wasm-bindgen",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "grass"
 | 
				
			||||||
 | 
					version = "0.11.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "bc5bedc3dbd71dcdd41900e1f58e4d431fa69dd67c04ae1f86ae1a0339edd849"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "beef",
 | 
				
			||||||
 | 
					 "clap 2.34.0",
 | 
				
			||||||
 | 
					 "codemap",
 | 
				
			||||||
 | 
					 "indexmap",
 | 
				
			||||||
 | 
					 "lasso",
 | 
				
			||||||
 | 
					 "num-bigint",
 | 
				
			||||||
 | 
					 "num-rational",
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					 "once_cell",
 | 
				
			||||||
 | 
					 "phf 0.9.0",
 | 
				
			||||||
 | 
					 "rand 0.8.5",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "hashbrown"
 | 
				
			||||||
 | 
					version = "0.11.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "ahash",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hashbrown"
 | 
					name = "hashbrown"
 | 
				
			||||||
version = "0.12.3"
 | 
					version = "0.12.3"
 | 
				
			||||||
| 
						 | 
					@ -1198,7 +1301,7 @@ version = "0.8.1"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
 | 
					checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "hashbrown",
 | 
					 "hashbrown 0.12.3",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -1333,6 +1436,12 @@ version = "1.8.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 | 
					checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "humansize"
 | 
				
			||||||
 | 
					version = "1.1.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "iana-time-zone"
 | 
					name = "iana-time-zone"
 | 
				
			||||||
version = "0.1.50"
 | 
					version = "0.1.50"
 | 
				
			||||||
| 
						 | 
					@ -1369,7 +1478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
 | 
					checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "autocfg",
 | 
					 "autocfg",
 | 
				
			||||||
 "hashbrown",
 | 
					 "hashbrown 0.12.3",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -1426,6 +1535,15 @@ dependencies = [
 | 
				
			||||||
 "log",
 | 
					 "log",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "lasso"
 | 
				
			||||||
 | 
					version = "0.5.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e8647c8a01e5f7878eacb2c323c4c949fdb63773110f0686c7810769874b7e0a"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "hashbrown 0.11.2",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "lazy_static"
 | 
					name = "lazy_static"
 | 
				
			||||||
version = "1.4.0"
 | 
					version = "1.4.0"
 | 
				
			||||||
| 
						 | 
					@ -1606,6 +1724,18 @@ dependencies = [
 | 
				
			||||||
 "num-traits",
 | 
					 "num-traits",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "num-rational"
 | 
				
			||||||
 | 
					version = "0.4.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "autocfg",
 | 
				
			||||||
 | 
					 "num-bigint",
 | 
				
			||||||
 | 
					 "num-integer",
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "num-traits"
 | 
					name = "num-traits"
 | 
				
			||||||
version = "0.2.15"
 | 
					version = "0.2.15"
 | 
				
			||||||
| 
						 | 
					@ -1762,11 +1892,22 @@ version = "0.8.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
 | 
					checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "phf_macros",
 | 
					 "phf_macros 0.8.0",
 | 
				
			||||||
 "phf_shared 0.8.0",
 | 
					 "phf_shared 0.8.0",
 | 
				
			||||||
 "proc-macro-hack",
 | 
					 "proc-macro-hack",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_macros 0.9.0",
 | 
				
			||||||
 | 
					 "phf_shared 0.9.0",
 | 
				
			||||||
 | 
					 "proc-macro-hack",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "phf"
 | 
					name = "phf"
 | 
				
			||||||
version = "0.10.1"
 | 
					version = "0.10.1"
 | 
				
			||||||
| 
						 | 
					@ -1806,6 +1947,16 @@ dependencies = [
 | 
				
			||||||
 "rand 0.7.3",
 | 
					 "rand 0.7.3",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_generator"
 | 
				
			||||||
 | 
					version = "0.9.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_shared 0.9.0",
 | 
				
			||||||
 | 
					 "rand 0.8.5",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "phf_generator"
 | 
					name = "phf_generator"
 | 
				
			||||||
version = "0.10.0"
 | 
					version = "0.10.0"
 | 
				
			||||||
| 
						 | 
					@ -1830,6 +1981,20 @@ dependencies = [
 | 
				
			||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_macros"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "b706f5936eb50ed880ae3009395b43ed19db5bff2ebd459c95e7bf013a89ab86"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_generator 0.9.1",
 | 
				
			||||||
 | 
					 "phf_shared 0.9.0",
 | 
				
			||||||
 | 
					 "proc-macro-hack",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "phf_shared"
 | 
					name = "phf_shared"
 | 
				
			||||||
version = "0.8.0"
 | 
					version = "0.8.0"
 | 
				
			||||||
| 
						 | 
					@ -1839,6 +2004,15 @@ dependencies = [
 | 
				
			||||||
 "siphasher",
 | 
					 "siphasher",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_shared"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "siphasher",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "phf_shared"
 | 
					name = "phf_shared"
 | 
				
			||||||
version = "0.10.0"
 | 
					version = "0.10.0"
 | 
				
			||||||
| 
						 | 
					@ -1880,6 +2054,34 @@ version = "0.1.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
					checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "plotters"
 | 
				
			||||||
 | 
					version = "0.3.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					 "plotters-backend",
 | 
				
			||||||
 | 
					 "plotters-svg",
 | 
				
			||||||
 | 
					 "wasm-bindgen",
 | 
				
			||||||
 | 
					 "web-sys",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "plotters-backend"
 | 
				
			||||||
 | 
					version = "0.3.4"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "plotters-svg"
 | 
				
			||||||
 | 
					version = "0.3.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "plotters-backend",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "polling"
 | 
					name = "polling"
 | 
				
			||||||
version = "2.3.0"
 | 
					version = "2.3.0"
 | 
				
			||||||
| 
						 | 
					@ -2865,6 +3067,12 @@ dependencies = [
 | 
				
			||||||
 "unicode-normalization",
 | 
					 "unicode-normalization",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "strsim"
 | 
				
			||||||
 | 
					version = "0.8.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "strsim"
 | 
					name = "strsim"
 | 
				
			||||||
version = "0.10.0"
 | 
					version = "0.10.0"
 | 
				
			||||||
| 
						 | 
					@ -2932,6 +3140,15 @@ dependencies = [
 | 
				
			||||||
 "winapi-util",
 | 
					 "winapi-util",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "textwrap"
 | 
				
			||||||
 | 
					version = "0.11.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unicode-width",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "textwrap"
 | 
					name = "textwrap"
 | 
				
			||||||
version = "0.15.1"
 | 
					version = "0.15.1"
 | 
				
			||||||
| 
						 | 
					@ -2988,11 +3205,14 @@ dependencies = [
 | 
				
			||||||
name = "tildes-statistics"
 | 
					name = "tildes-statistics"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "askama",
 | 
				
			||||||
 "async-std",
 | 
					 "async-std",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "clap 4.0.10",
 | 
					 "clap 4.0.10",
 | 
				
			||||||
 "color-eyre",
 | 
					 "color-eyre",
 | 
				
			||||||
 "dotenvy",
 | 
					 "dotenvy",
 | 
				
			||||||
 | 
					 "grass",
 | 
				
			||||||
 | 
					 "plotters",
 | 
				
			||||||
 "sea-orm",
 | 
					 "sea-orm",
 | 
				
			||||||
 "sea-orm-migration",
 | 
					 "sea-orm-migration",
 | 
				
			||||||
 "surf",
 | 
					 "surf",
 | 
				
			||||||
| 
						 | 
					@ -3094,6 +3314,15 @@ dependencies = [
 | 
				
			||||||
 "pin-project-lite",
 | 
					 "pin-project-lite",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "toml"
 | 
				
			||||||
 | 
					version = "0.5.9"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tracing"
 | 
					name = "tracing"
 | 
				
			||||||
version = "0.1.36"
 | 
					version = "0.1.36"
 | 
				
			||||||
| 
						 | 
					@ -3281,6 +3510,12 @@ dependencies = [
 | 
				
			||||||
 "version_check",
 | 
					 "version_check",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "vec_map"
 | 
				
			||||||
 | 
					version = "0.8.2"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "version_check"
 | 
					name = "version_check"
 | 
				
			||||||
version = "0.9.4"
 | 
					version = "0.9.4"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,10 +12,12 @@ name = "tildes-statistics"
 | 
				
			||||||
path = "source/main.rs"
 | 
					path = "source/main.rs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					askama = "0.11.1"
 | 
				
			||||||
async-std = "1.12.0"
 | 
					async-std = "1.12.0"
 | 
				
			||||||
chrono = "0.4.22"
 | 
					chrono = "0.4.22"
 | 
				
			||||||
color-eyre = "0.6.2"
 | 
					color-eyre = "0.6.2"
 | 
				
			||||||
dotenvy = "0.15.5"
 | 
					dotenvy = "0.15.5"
 | 
				
			||||||
 | 
					grass = "0.11.2"
 | 
				
			||||||
sea-orm-migration = "0.9.3"
 | 
					sea-orm-migration = "0.9.3"
 | 
				
			||||||
tracing = "0.1.36"
 | 
					tracing = "0.1.36"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +25,11 @@ tracing = "0.1.36"
 | 
				
			||||||
features = ["derive"]
 | 
					features = ["derive"]
 | 
				
			||||||
version = "4.0.10"
 | 
					version = "4.0.10"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies.plotters]
 | 
				
			||||||
 | 
					default-features = false
 | 
				
			||||||
 | 
					features = ["line_series", "point_series", "svg_backend"]
 | 
				
			||||||
 | 
					version = "0.3.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.sea-orm]
 | 
					[dependencies.sea-orm]
 | 
				
			||||||
features = ["macros", "mock", "runtime-async-std-rustls", "sqlx-postgres"]
 | 
					features = ["macros", "mock", "runtime-async-std-rustls", "sqlx-postgres"]
 | 
				
			||||||
version = "0.9.3"
 | 
					version = "0.9.3"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,8 +5,8 @@ RUN USER=root cargo new --bin tildes-statistics
 | 
				
			||||||
WORKDIR /tildes-statistics
 | 
					WORKDIR /tildes-statistics
 | 
				
			||||||
RUN mv src source
 | 
					RUN mv src source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Copy the Cargo files and build in release, caching the dependencies.
 | 
					# Copy the configuration files and build in release, caching the dependencies.
 | 
				
			||||||
COPY Cargo.* .
 | 
					COPY Cargo.lock Cargo.toml askama.toml .
 | 
				
			||||||
RUN cargo build --release
 | 
					RUN cargo build --release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Then copy our code. This way when only the source code changes, the
 | 
					# Then copy our code. This way when only the source code changes, the
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					[general]
 | 
				
			||||||
 | 
					dirs = ["source/templates"]
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "private": "true",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "test": "stylelint 'source/**/*.scss'"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "modern-normalize": "^1.1.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "stylelint": "^14.12.1",
 | 
				
			||||||
 | 
					    "stylelint-config-standard-scss": "^5.0.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "stylelint": {
 | 
				
			||||||
 | 
					    "extends": [
 | 
				
			||||||
 | 
					      "stylelint-config-standard-scss"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "rules": {
 | 
				
			||||||
 | 
					      "string-quotes": "single"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,116 @@
 | 
				
			||||||
 | 
					//! All code for drawing the [`plotters`] charts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use {
 | 
				
			||||||
 | 
					  async_std::{fs::create_dir_all, path::PathBuf},
 | 
				
			||||||
 | 
					  color_eyre::Result,
 | 
				
			||||||
 | 
					  plotters::prelude::*,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::group_data::GroupDataModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BACKGROUND_1: RGBColor = RGBColor(17, 17, 17);
 | 
				
			||||||
 | 
					const BACKGROUND_2: RGBColor = RGBColor(0, 0, 0);
 | 
				
			||||||
 | 
					const FOREGROUND: RGBColor = RGBColor(255, 255, 255);
 | 
				
			||||||
 | 
					const ACCENT_1: RGBColor = RGBColor(255, 0, 255);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The chart for the user count.
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct UserCountChart {
 | 
				
			||||||
 | 
					  /// The groups to use for user counts.
 | 
				
			||||||
 | 
					  pub groups: Vec<GroupDataModel>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UserCountChart {
 | 
				
			||||||
 | 
					  /// Render the chart and write it to file.
 | 
				
			||||||
 | 
					  pub async fn render(&self, parent: &PathBuf, group_name: &str) -> Result<()> {
 | 
				
			||||||
 | 
					    let parent = parent.join("charts");
 | 
				
			||||||
 | 
					    create_dir_all(&parent).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (mut datapoints, mut min_count, mut max_count) = (vec![], i64::MAX, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (index, group) in self.groups.iter().enumerate() {
 | 
				
			||||||
 | 
					      datapoints.push(((index + 1) as isize, group.subscribers));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if group.subscribers > max_count {
 | 
				
			||||||
 | 
					        max_count = group.subscribers;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if group.subscribers < min_count {
 | 
				
			||||||
 | 
					        min_count = group.subscribers;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let datapoints_len = datapoints.len() as isize;
 | 
				
			||||||
 | 
					    let min_count = min_count - 10;
 | 
				
			||||||
 | 
					    let max_count = max_count + 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let path = parent.join("user-count.svg");
 | 
				
			||||||
 | 
					    let chart_root = SVGBackend::new(&path, (1280, 720)).into_drawing_area();
 | 
				
			||||||
 | 
					    chart_root.fill(&BACKGROUND_1)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let text_style =
 | 
				
			||||||
 | 
					      |font_size: i32| ("sans-serif", font_size).into_font().color(&FOREGROUND);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let chart_root = chart_root
 | 
				
			||||||
 | 
					      .margin(20, 20, 20, 20)
 | 
				
			||||||
 | 
					      .titled("Tildes User Count", text_style(30))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chart_root.fill(&BACKGROUND_1)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut chart = ChartBuilder::on(&chart_root)
 | 
				
			||||||
 | 
					      .caption(
 | 
				
			||||||
 | 
					        format!("Using the {group_name} subscriber count."),
 | 
				
			||||||
 | 
					        text_style(20),
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .x_label_area_size(40)
 | 
				
			||||||
 | 
					      .y_label_area_size(40)
 | 
				
			||||||
 | 
					      .margin(10)
 | 
				
			||||||
 | 
					      .build_cartesian_2d(0..(datapoints_len + 1), min_count..max_count)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chart
 | 
				
			||||||
 | 
					      .configure_mesh()
 | 
				
			||||||
 | 
					      .x_labels(datapoints.len() + 2)
 | 
				
			||||||
 | 
					      .x_label_formatter(&|x| format!("{:0}", datapoints_len - x))
 | 
				
			||||||
 | 
					      .x_desc("N days ago")
 | 
				
			||||||
 | 
					      .y_labels(5)
 | 
				
			||||||
 | 
					      .y_label_formatter(&|y| format!("{y:0}"))
 | 
				
			||||||
 | 
					      .label_style(text_style(20))
 | 
				
			||||||
 | 
					      .axis_style(&BACKGROUND_2)
 | 
				
			||||||
 | 
					      .light_line_style(&BACKGROUND_2)
 | 
				
			||||||
 | 
					      .bold_line_style(&BACKGROUND_1)
 | 
				
			||||||
 | 
					      .draw()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chart
 | 
				
			||||||
 | 
					      .draw_series(LineSeries::new(
 | 
				
			||||||
 | 
					        datapoints.clone(),
 | 
				
			||||||
 | 
					        ACCENT_1.stroke_width(2),
 | 
				
			||||||
 | 
					      ))?
 | 
				
			||||||
 | 
					      .label("User Count")
 | 
				
			||||||
 | 
					      .legend(|(x, y)| {
 | 
				
			||||||
 | 
					        PathElement::new(vec![(x, y), (x + 20, y)], ACCENT_1.stroke_width(4))
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    chart.draw_series(PointSeries::of_element(
 | 
				
			||||||
 | 
					      datapoints,
 | 
				
			||||||
 | 
					      5,
 | 
				
			||||||
 | 
					      &ACCENT_1,
 | 
				
			||||||
 | 
					      &|(x, y), size, style| {
 | 
				
			||||||
 | 
					        EmptyElement::at((x, y))
 | 
				
			||||||
 | 
					          + Circle::new((0, 0), size, style.filled())
 | 
				
			||||||
 | 
					          + Text::new(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              if (x - 1) % 2 != 0 {
 | 
				
			||||||
 | 
					                String::new()
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                format!("{:0}", y)
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            (-10, 15),
 | 
				
			||||||
 | 
					            text_style(20),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
//! All CLI-related code.
 | 
					//! All CLI-related code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use {
 | 
					use {
 | 
				
			||||||
 | 
					  async_std::path::PathBuf,
 | 
				
			||||||
  chrono::NaiveDate,
 | 
					  chrono::NaiveDate,
 | 
				
			||||||
  clap::{Parser, Subcommand},
 | 
					  clap::{Parser, Subcommand},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -43,6 +44,13 @@ pub enum MainSubcommands {
 | 
				
			||||||
    #[command(subcommand)]
 | 
					    #[command(subcommand)]
 | 
				
			||||||
    command: SnapshotSubcommands,
 | 
					    command: SnapshotSubcommands,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Website management.
 | 
				
			||||||
 | 
					  Web {
 | 
				
			||||||
 | 
					    /// Website management.
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    command: WebSubcommands,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Migrate subcommands.
 | 
					/// Migrate subcommands.
 | 
				
			||||||
| 
						 | 
					@ -86,3 +94,14 @@ pub enum SnapshotSubcommands {
 | 
				
			||||||
    date: Option<NaiveDate>,
 | 
					    date: Option<NaiveDate>,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Website subcommands.
 | 
				
			||||||
 | 
					#[derive(Debug, Subcommand)]
 | 
				
			||||||
 | 
					pub enum WebSubcommands {
 | 
				
			||||||
 | 
					  /// Build the website.
 | 
				
			||||||
 | 
					  Build {
 | 
				
			||||||
 | 
					    /// The output directory for the website files.
 | 
				
			||||||
 | 
					    #[clap(short, long, default_value = "public")]
 | 
				
			||||||
 | 
					    output: PathBuf,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,21 @@
 | 
				
			||||||
//! All logic for running the CLI.
 | 
					//! All logic for running the CLI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use {
 | 
					use {
 | 
				
			||||||
  clap::Parser, color_eyre::Result, sea_orm_migration::MigratorTrait,
 | 
					  async_std::fs::create_dir_all, clap::Parser, color_eyre::Result,
 | 
				
			||||||
  tracing::info,
 | 
					  sea_orm_migration::MigratorTrait, tracing::info,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
  cli::{Cli, MainSubcommands, MigrateSubcommands, SnapshotSubcommands},
 | 
					  charts::UserCountChart,
 | 
				
			||||||
  group_data::get_all_by_snapshot,
 | 
					  cli::{
 | 
				
			||||||
 | 
					    Cli, MainSubcommands, MigrateSubcommands, SnapshotSubcommands,
 | 
				
			||||||
 | 
					    WebSubcommands,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  group_data::GroupDataModel,
 | 
				
			||||||
  migrations::Migrator,
 | 
					  migrations::Migrator,
 | 
				
			||||||
  snapshots::{self, get_by_date},
 | 
					  scss::generate_css,
 | 
				
			||||||
 | 
					  snapshots::SnapshotModel,
 | 
				
			||||||
 | 
					  templates::HomeTemplate,
 | 
				
			||||||
  utilities::{create_db, today},
 | 
					  utilities::{create_db, today},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,18 +49,20 @@ pub async fn run() -> Result<()> {
 | 
				
			||||||
      command: snapshot_command,
 | 
					      command: snapshot_command,
 | 
				
			||||||
    } => match snapshot_command {
 | 
					    } => match snapshot_command {
 | 
				
			||||||
      SnapshotSubcommands::Create { force } => {
 | 
					      SnapshotSubcommands::Create { force } => {
 | 
				
			||||||
        snapshots::create(&db, force).await?;
 | 
					        SnapshotModel::create(&db, force).await?;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      SnapshotSubcommands::List {} => {
 | 
					      SnapshotSubcommands::List {} => {
 | 
				
			||||||
        for snapshot in snapshots::get_all(&db).await? {
 | 
					        for snapshot in SnapshotModel::get_all(&db).await? {
 | 
				
			||||||
          info!("Snapshot {snapshot:?}")
 | 
					          info!("Snapshot {snapshot:?}")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      SnapshotSubcommands::Show { date } => {
 | 
					      SnapshotSubcommands::Show { date } => {
 | 
				
			||||||
        let date = date.unwrap_or_else(today);
 | 
					        let date = date.unwrap_or_else(today);
 | 
				
			||||||
        let snapshot = if let Some(snapshot) = get_by_date(&db, date).await? {
 | 
					        let snapshot = if let Some(snapshot) =
 | 
				
			||||||
 | 
					          SnapshotModel::get_by_date(&db, date).await?
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
          info!("Snapshot {snapshot:?}");
 | 
					          info!("Snapshot {snapshot:?}");
 | 
				
			||||||
          snapshot
 | 
					          snapshot
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
| 
						 | 
					@ -62,8 +70,8 @@ pub async fn run() -> Result<()> {
 | 
				
			||||||
          return Ok(());
 | 
					          return Ok(());
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let groups = get_all_by_snapshot(&db, &snapshot).await?;
 | 
					        for group in GroupDataModel::get_all_by_snapshot(&db, &snapshot).await?
 | 
				
			||||||
        for group in groups {
 | 
					        {
 | 
				
			||||||
          info!(
 | 
					          info!(
 | 
				
			||||||
            id = group.id,
 | 
					            id = group.id,
 | 
				
			||||||
            name = group.name,
 | 
					            name = group.name,
 | 
				
			||||||
| 
						 | 
					@ -72,6 +80,35 @@ pub async fn run() -> Result<()> {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MainSubcommands::Web {
 | 
				
			||||||
 | 
					      command: web_command,
 | 
				
			||||||
 | 
					    } => match web_command {
 | 
				
			||||||
 | 
					      WebSubcommands::Build { output } => {
 | 
				
			||||||
 | 
					        let user_count_group =
 | 
				
			||||||
 | 
					          if let Some(snapshot) = SnapshotModel::get_most_recent(&db).await? {
 | 
				
			||||||
 | 
					            GroupDataModel::get_highest_subscribers(&db, &snapshot).await?
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        create_dir_all(&output).await?;
 | 
				
			||||||
 | 
					        HomeTemplate::new(
 | 
				
			||||||
 | 
					          user_count_group.as_ref().map(|group| group.subscribers),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .render_to_file(&output)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					        generate_css(&output).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(group) = user_count_group {
 | 
				
			||||||
 | 
					          let groups =
 | 
				
			||||||
 | 
					            GroupDataModel::get_n_most_recent(&db, 30, &group.name).await?;
 | 
				
			||||||
 | 
					          UserCountChart { groups }
 | 
				
			||||||
 | 
					            .render(&output, &group.name)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Ok(())
 | 
					  Ok(())
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,55 @@
 | 
				
			||||||
//! All logic for group datas.
 | 
					//! All logic for group datas.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use {color_eyre::Result, sea_orm::prelude::*};
 | 
					use {
 | 
				
			||||||
 | 
					  color_eyre::Result,
 | 
				
			||||||
 | 
					  sea_orm::{prelude::*, QueryOrder, QuerySelect},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::entities::{group_data, snapshot};
 | 
					pub use crate::{
 | 
				
			||||||
 | 
					  entities::group_data::{
 | 
				
			||||||
 | 
					    ActiveModel as GroupDataActiveModel, Column as GroupDataColumn,
 | 
				
			||||||
 | 
					    Entity as GroupDataEntity, Model as GroupDataModel,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  snapshots::SnapshotModel,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get all group datas from a given snapshot.
 | 
					impl GroupDataModel {
 | 
				
			||||||
pub async fn get_all_by_snapshot(
 | 
					  /// Get all group datas from a given snapshot.
 | 
				
			||||||
  db: &DatabaseConnection,
 | 
					  pub async fn get_all_by_snapshot(
 | 
				
			||||||
  snapshot: &snapshot::Model,
 | 
					    db: &DatabaseConnection,
 | 
				
			||||||
) -> Result<Vec<group_data::Model>> {
 | 
					    snapshot: &SnapshotModel,
 | 
				
			||||||
  let groups = snapshot.find_related(group_data::Entity).all(db).await?;
 | 
					  ) -> Result<Vec<Self>> {
 | 
				
			||||||
  Ok(groups)
 | 
					    let groups = snapshot.find_related(GroupDataEntity).all(db).await?;
 | 
				
			||||||
 | 
					    Ok(groups)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get the group with the highest subscriber count from a given snapshot.
 | 
				
			||||||
 | 
					  pub async fn get_highest_subscribers(
 | 
				
			||||||
 | 
					    db: &DatabaseConnection,
 | 
				
			||||||
 | 
					    snapshot: &SnapshotModel,
 | 
				
			||||||
 | 
					  ) -> Result<Option<Self>> {
 | 
				
			||||||
 | 
					    let group = snapshot
 | 
				
			||||||
 | 
					      .find_related(GroupDataEntity)
 | 
				
			||||||
 | 
					      .order_by_desc(GroupDataColumn::Subscribers)
 | 
				
			||||||
 | 
					      .one(db)
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(group)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get the N most recently saved group datas from a given group name.
 | 
				
			||||||
 | 
					  pub async fn get_n_most_recent(
 | 
				
			||||||
 | 
					    db: &DatabaseConnection,
 | 
				
			||||||
 | 
					    amount: u64,
 | 
				
			||||||
 | 
					    name: &str,
 | 
				
			||||||
 | 
					  ) -> Result<Vec<Self>> {
 | 
				
			||||||
 | 
					    let groups = GroupDataEntity::find()
 | 
				
			||||||
 | 
					      .order_by_desc(GroupDataColumn::SnapshotId)
 | 
				
			||||||
 | 
					      .filter(GroupDataColumn::Name.eq(name))
 | 
				
			||||||
 | 
					      .limit(amount)
 | 
				
			||||||
 | 
					      .all(db)
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(groups)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,10 +11,13 @@ use {
 | 
				
			||||||
  tracing_subscriber::filter::{EnvFilter, LevelFilter},
 | 
					  tracing_subscriber::filter::{EnvFilter, LevelFilter},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod charts;
 | 
				
			||||||
pub mod cli;
 | 
					pub mod cli;
 | 
				
			||||||
pub mod group_data;
 | 
					pub mod group_data;
 | 
				
			||||||
pub mod migrations;
 | 
					pub mod migrations;
 | 
				
			||||||
 | 
					pub mod scss;
 | 
				
			||||||
pub mod snapshots;
 | 
					pub mod snapshots;
 | 
				
			||||||
 | 
					pub mod templates;
 | 
				
			||||||
pub mod utilities;
 | 
					pub mod utilities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// The entities code is auto-generated using `sea-orm-cli`. With a database
 | 
					/// The entities code is auto-generated using `sea-orm-cli`. With a database
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					html {
 | 
				
			||||||
 | 
					  font-size: 62.5%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  --small-spacing: 4px;
 | 
				
			||||||
 | 
					  --medium-spacing: 8px;
 | 
				
			||||||
 | 
					  --large-spacing: 16px;
 | 
				
			||||||
 | 
					  --background-1: #222;
 | 
				
			||||||
 | 
					  --background-2: #111;
 | 
				
			||||||
 | 
					  --foreground-1: #fff;
 | 
				
			||||||
 | 
					  --anchor-1: #f0f;
 | 
				
			||||||
 | 
					  --anchor-2: #000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  background-color: var(--background-1);
 | 
				
			||||||
 | 
					  color: var(--foreground-1);
 | 
				
			||||||
 | 
					  font-size: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a,
 | 
				
			||||||
 | 
					a:visited {
 | 
				
			||||||
 | 
					  color: var(--anchor-1);
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    background-color: var(--anchor-1);
 | 
				
			||||||
 | 
					    color: var(--anchor-2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1,
 | 
				
			||||||
 | 
					h2,
 | 
				
			||||||
 | 
					h3,
 | 
				
			||||||
 | 
					p,
 | 
				
			||||||
 | 
					ol,
 | 
				
			||||||
 | 
					li {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bold {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.underline {
 | 
				
			||||||
 | 
					  text-decoration: underline;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					@mixin responsive-container($breakpoint) {
 | 
				
			||||||
 | 
					  margin-left: auto;
 | 
				
			||||||
 | 
					  margin-right: auto;
 | 
				
			||||||
 | 
					  width: $breakpoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: $breakpoint) {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-header,
 | 
				
			||||||
 | 
					.page-main,
 | 
				
			||||||
 | 
					.page-footer {
 | 
				
			||||||
 | 
					  @include responsive-container(1200px);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  margin-bottom: var(--large-spacing);
 | 
				
			||||||
 | 
					  padding: var(--large-spacing);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-header {
 | 
				
			||||||
 | 
					  border-bottom: 4px solid var(--background-2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-main {
 | 
				
			||||||
 | 
					  h2 {
 | 
				
			||||||
 | 
					    margin-bottom: var(--medium-spacing);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.page-footer {
 | 
				
			||||||
 | 
					  background-color: var(--background-2);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: var(--large-spacing);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					//! All SCSS files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use {
 | 
				
			||||||
 | 
					  async_std::{
 | 
				
			||||||
 | 
					    fs::{create_dir_all, write},
 | 
				
			||||||
 | 
					    path::PathBuf,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  color_eyre::{eyre::Context, Result},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MODERN_NORMALIZE_CSS: &str =
 | 
				
			||||||
 | 
					  include_str!("../../node_modules/modern-normalize/modern-normalize.css");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generate the CSS files and write them.
 | 
				
			||||||
 | 
					pub async fn generate_css(parent: &PathBuf) -> Result<()> {
 | 
				
			||||||
 | 
					  let parent = parent.join("css");
 | 
				
			||||||
 | 
					  create_dir_all(&parent).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let render = |scss: &str| -> Result<String> {
 | 
				
			||||||
 | 
					    grass::from_string(scss.to_string(), &grass::Options::default())
 | 
				
			||||||
 | 
					      .wrap_err("Failed SCSS render")
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let css_to_create = vec![
 | 
				
			||||||
 | 
					    ("modern-normalize.css", MODERN_NORMALIZE_CSS, false),
 | 
				
			||||||
 | 
					    ("common.css", include_str!("common.scss"), true),
 | 
				
			||||||
 | 
					    ("index.css", include_str!("index.scss"), true),
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (file, css, is_scss) in css_to_create {
 | 
				
			||||||
 | 
					    let path = parent.join(file);
 | 
				
			||||||
 | 
					    if is_scss {
 | 
				
			||||||
 | 
					      write(path, render(css)?).await?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      write(path, css).await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -8,69 +8,71 @@ use {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
  entities::{group_data, snapshot},
 | 
					  group_data::{GroupDataActiveModel, GroupDataEntity},
 | 
				
			||||||
  snapshots::get_by_date,
 | 
					  snapshots::{SnapshotActiveModel, SnapshotModel},
 | 
				
			||||||
  utilities::{create_http_client, download_html, today},
 | 
					  utilities::{create_http_client, download_html, today},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a snapshot for today.
 | 
					impl SnapshotModel {
 | 
				
			||||||
pub async fn create(db: &DatabaseConnection, force: bool) -> Result<()> {
 | 
					  /// Create a snapshot for today.
 | 
				
			||||||
  let snapshot_date = today();
 | 
					  pub async fn create(db: &DatabaseConnection, force: bool) -> Result<()> {
 | 
				
			||||||
  match (force, get_by_date(db, snapshot_date).await?) {
 | 
					    let snapshot_date = today();
 | 
				
			||||||
    (true, Some(existing)) => {
 | 
					    match (force, Self::get_by_date(db, snapshot_date).await?) {
 | 
				
			||||||
      info!("Removing existing snapshot {:?}", existing);
 | 
					      (true, Some(existing)) => {
 | 
				
			||||||
      existing.delete(db).await?;
 | 
					        info!("Removing existing snapshot {:?}", existing);
 | 
				
			||||||
    }
 | 
					        existing.delete(db).await?;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    (false, Some(existing)) => {
 | 
					      (false, Some(existing)) => {
 | 
				
			||||||
      info!("Snapshot for today already exists");
 | 
					        info!("Snapshot for today already exists");
 | 
				
			||||||
      info!("Use --force to override snapshot {:?}", existing);
 | 
					        info!("Use --force to override snapshot {:?}", existing);
 | 
				
			||||||
      return Ok(());
 | 
					        return Ok(());
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    (_, None) => (),
 | 
					      (_, None) => (),
 | 
				
			||||||
  };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let transaction = db.begin().await?;
 | 
					    let transaction = db.begin().await?;
 | 
				
			||||||
  let snapshot = snapshot::ActiveModel {
 | 
					    let snapshot = SnapshotActiveModel {
 | 
				
			||||||
    date: Set(snapshot_date),
 | 
					      date: Set(snapshot_date),
 | 
				
			||||||
    ..Default::default()
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .insert(&transaction)
 | 
					 | 
				
			||||||
  .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  info!("Scraping data for snapshot {:?}", snapshot);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let http = create_http_client()?;
 | 
					 | 
				
			||||||
  let group_list = GroupList::from_html(
 | 
					 | 
				
			||||||
    &download_html(&http, "https://tildes.net/groups").await?,
 | 
					 | 
				
			||||||
  )?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let mut groups_to_insert = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  for summary in group_list.summaries {
 | 
					 | 
				
			||||||
    debug!(summary = ?summary);
 | 
					 | 
				
			||||||
    let group = Group::from_html(
 | 
					 | 
				
			||||||
      &download_html(&http, format!("https://tildes.net/{}", summary.name))
 | 
					 | 
				
			||||||
        .await?,
 | 
					 | 
				
			||||||
    )?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    debug!(group = ?group);
 | 
					 | 
				
			||||||
    groups_to_insert.push(group_data::ActiveModel {
 | 
					 | 
				
			||||||
      description: Set(group.description),
 | 
					 | 
				
			||||||
      name: Set(group.name),
 | 
					 | 
				
			||||||
      snapshot_id: Set(snapshot.id),
 | 
					 | 
				
			||||||
      subscribers: Set(group.subscribers.into()),
 | 
					 | 
				
			||||||
      ..Default::default()
 | 
					      ..Default::default()
 | 
				
			||||||
    });
 | 
					    }
 | 
				
			||||||
  }
 | 
					    .insert(&transaction)
 | 
				
			||||||
 | 
					 | 
				
			||||||
  info!("Inserting {} groups", groups_to_insert.len());
 | 
					 | 
				
			||||||
  group_data::Entity::insert_many(groups_to_insert)
 | 
					 | 
				
			||||||
    .exec(&transaction)
 | 
					 | 
				
			||||||
    .await?;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  transaction.commit().await?;
 | 
					    info!("Scraping data for snapshot {:?}", snapshot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Ok(())
 | 
					    let http = create_http_client()?;
 | 
				
			||||||
 | 
					    let group_list = GroupList::from_html(
 | 
				
			||||||
 | 
					      &download_html(&http, "https://tildes.net/groups").await?,
 | 
				
			||||||
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut groups_to_insert = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for summary in group_list.summaries {
 | 
				
			||||||
 | 
					      debug!(summary = ?summary);
 | 
				
			||||||
 | 
					      let group = Group::from_html(
 | 
				
			||||||
 | 
					        &download_html(&http, format!("https://tildes.net/{}", summary.name))
 | 
				
			||||||
 | 
					          .await?,
 | 
				
			||||||
 | 
					      )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      debug!(group = ?group);
 | 
				
			||||||
 | 
					      groups_to_insert.push(GroupDataActiveModel {
 | 
				
			||||||
 | 
					        description: Set(group.description),
 | 
				
			||||||
 | 
					        name: Set(group.name),
 | 
				
			||||||
 | 
					        snapshot_id: Set(snapshot.id),
 | 
				
			||||||
 | 
					        subscribers: Set(group.subscribers.into()),
 | 
				
			||||||
 | 
					        ..Default::default()
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info!("Inserting {} groups", groups_to_insert.len());
 | 
				
			||||||
 | 
					    GroupDataEntity::insert_many(groups_to_insert)
 | 
				
			||||||
 | 
					      .exec(&transaction)
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,29 +5,44 @@ use {
 | 
				
			||||||
  sea_orm::{prelude::*, QueryOrder},
 | 
					  sea_orm::{prelude::*, QueryOrder},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::entities::snapshot;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod create;
 | 
					mod create;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use create::create;
 | 
					pub use crate::entities::snapshot::{
 | 
				
			||||||
 | 
					  ActiveModel as SnapshotActiveModel, Column as SnapshotColumn,
 | 
				
			||||||
 | 
					  Entity as SnapshotEntity, Model as SnapshotModel,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get a snapshot for a given date.
 | 
					impl SnapshotModel {
 | 
				
			||||||
pub async fn get_by_date(
 | 
					  /// Get a snapshot for a given date.
 | 
				
			||||||
  db: &DatabaseConnection,
 | 
					  pub async fn get_by_date(
 | 
				
			||||||
  date: ChronoDate,
 | 
					    db: &DatabaseConnection,
 | 
				
			||||||
) -> Result<Option<snapshot::Model>> {
 | 
					    date: ChronoDate,
 | 
				
			||||||
  let existing = snapshot::Entity::find()
 | 
					  ) -> Result<Option<Self>> {
 | 
				
			||||||
    .filter(snapshot::Column::Date.eq(date))
 | 
					    let existing = SnapshotEntity::find()
 | 
				
			||||||
    .order_by_desc(snapshot::Column::Date)
 | 
					      .filter(SnapshotColumn::Date.eq(date))
 | 
				
			||||||
    .one(db)
 | 
					      .order_by_desc(SnapshotColumn::Date)
 | 
				
			||||||
    .await?;
 | 
					      .one(db)
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Ok(existing)
 | 
					    Ok(existing)
 | 
				
			||||||
}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get all snapshots.
 | 
					  /// Get all snapshots.
 | 
				
			||||||
pub async fn get_all(db: &DatabaseConnection) -> Result<Vec<snapshot::Model>> {
 | 
					  pub async fn get_all(db: &DatabaseConnection) -> Result<Vec<Self>> {
 | 
				
			||||||
  let snapshots = snapshot::Entity::find().all(db).await?;
 | 
					    let snapshots = SnapshotEntity::find().all(db).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Ok(snapshots)
 | 
					    Ok(snapshots)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get the most recent snapshot.
 | 
				
			||||||
 | 
					  pub async fn get_most_recent(
 | 
				
			||||||
 | 
					    db: &DatabaseConnection,
 | 
				
			||||||
 | 
					  ) -> Result<Option<Self>> {
 | 
				
			||||||
 | 
					    let snapshot = SnapshotEntity::find()
 | 
				
			||||||
 | 
					      .order_by_desc(SnapshotColumn::Date)
 | 
				
			||||||
 | 
					      .one(db)
 | 
				
			||||||
 | 
					      .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(snapshot)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					  <title>{{ page_title }}</title>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="/css/modern-normalize.css">
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="/css/common.css">
 | 
				
			||||||
 | 
					  {% block head %}{% endblock %}
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  {% block body %}{% endblock %}
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block head %}
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="/css/index.css">
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block body %}
 | 
				
			||||||
 | 
					<header class="page-header">
 | 
				
			||||||
 | 
					  <h1>Tildes Statistics</h1>
 | 
				
			||||||
 | 
					</header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<main class="page-main">
 | 
				
			||||||
 | 
					  <h2>General</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <p>
 | 
				
			||||||
 | 
					    There are currently an
 | 
				
			||||||
 | 
					    <abbr title="Based on the group with the highest subscriber count.">estimated</abbr>
 | 
				
			||||||
 | 
					    <span class="underline">{{ user_count }}</span>
 | 
				
			||||||
 | 
					    registered users on Tildes.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <img src="/charts/user-count.svg" alt="User Count Chart">
 | 
				
			||||||
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<footer class="page-footer">
 | 
				
			||||||
 | 
					  <p>
 | 
				
			||||||
 | 
					    Last generated on
 | 
				
			||||||
 | 
					    <time class="underline" datetime="{{ today }}">{{- today -}}</time>.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <p>
 | 
				
			||||||
 | 
					    © Code
 | 
				
			||||||
 | 
					    <a href="https://git.bauke.xyz/Bauke/tildes-statistics">AGPL-3.0-or-later</a>,
 | 
				
			||||||
 | 
					    charts & data
 | 
				
			||||||
 | 
					    <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a>.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <p class="bold">
 | 
				
			||||||
 | 
					    Consider joining <a href="https://tildes.net">Tildes</a>, a non-profit
 | 
				
			||||||
 | 
					    community site driven by its users' interests.
 | 
				
			||||||
 | 
					  </p>
 | 
				
			||||||
 | 
					</footer>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					//! All HTML templates.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use {
 | 
				
			||||||
 | 
					  askama::Template,
 | 
				
			||||||
 | 
					  async_std::{fs::write, path::PathBuf},
 | 
				
			||||||
 | 
					  chrono::NaiveDate,
 | 
				
			||||||
 | 
					  color_eyre::Result,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::utilities::today;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The template for the home page.
 | 
				
			||||||
 | 
					#[derive(Template)]
 | 
				
			||||||
 | 
					#[template(path = "index.html")]
 | 
				
			||||||
 | 
					pub struct HomeTemplate {
 | 
				
			||||||
 | 
					  /// The string for the `<title>` element.
 | 
				
			||||||
 | 
					  pub page_title: String,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The date of today's snapshot.
 | 
				
			||||||
 | 
					  pub today: NaiveDate,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The user count from the group with the most subscribers.
 | 
				
			||||||
 | 
					  pub user_count: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl HomeTemplate {
 | 
				
			||||||
 | 
					  /// Create a new [`HomeTemplate`].
 | 
				
			||||||
 | 
					  pub fn new(user_count: Option<i64>) -> Self {
 | 
				
			||||||
 | 
					    Self {
 | 
				
			||||||
 | 
					      page_title: "Tildes Statistics".to_string(),
 | 
				
			||||||
 | 
					      today: today(),
 | 
				
			||||||
 | 
					      user_count: user_count
 | 
				
			||||||
 | 
					        .map(|n| n.to_string())
 | 
				
			||||||
 | 
					        .unwrap_or_else(|| "unknown".to_string()),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Render the template and write it to file.
 | 
				
			||||||
 | 
					  pub async fn render_to_file(&self, parent: &PathBuf) -> Result<()> {
 | 
				
			||||||
 | 
					    write(parent.join("index.html"), self.render()?).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue