mirror of
https://github.com/PreMiD/PreMiD.git
synced 2026-04-06 04:41:58 +02:00
Compare commits
1699 Commits
v0.1
...
api-worker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9e6639492 | ||
|
|
3258179040 | ||
|
|
086d476af2 | ||
|
|
146bf9e270 | ||
|
|
a02f25ba29 | ||
|
|
416b65f0d4 | ||
|
|
f8e9fc832d | ||
|
|
86b0f07216 | ||
|
|
9eb5c03877 | ||
|
|
e63e1270aa | ||
|
|
f730e71bbf | ||
|
|
8b68bf85c8 | ||
|
|
e4c794a9ad | ||
|
|
6e8258d76f | ||
|
|
56b796c621 | ||
|
|
0de59c48b4 | ||
|
|
60056e069d | ||
|
|
b6bad90919 | ||
|
|
ee21bb9dec | ||
|
|
6efac4fef1 | ||
|
|
93424793bd | ||
|
|
affcb6a0cf | ||
|
|
bb56949dfb | ||
|
|
c06fe04b65 | ||
|
|
ef976341ba | ||
|
|
38893891af | ||
|
|
63eeeefda7 | ||
|
|
056db21cb0 | ||
|
|
d8dc08c6c3 | ||
|
|
634391b6e3 | ||
|
|
c46cf6975a | ||
|
|
68c6b4fcdc | ||
|
|
55fa07d5b5 | ||
|
|
903c238b33 | ||
|
|
acd9afb2b1 | ||
|
|
4bd42390eb | ||
|
|
c014504464 | ||
|
|
24fe349b60 | ||
|
|
ee5428ce08 | ||
|
|
e4b1010160 | ||
|
|
34c42d59ed | ||
|
|
d9267361aa | ||
|
|
0d5382fd50 | ||
|
|
e9015b1204 | ||
|
|
cea36426ab | ||
|
|
48c141094e | ||
|
|
e67fb97e14 | ||
|
|
0bd0d759f6 | ||
|
|
60b7f63409 | ||
|
|
78b482be4f | ||
|
|
9db9e931b6 | ||
|
|
665263e9b5 | ||
|
|
60257dbe53 | ||
|
|
411a70f567 | ||
|
|
4d1b092ee5 | ||
|
|
aa41f1cdae | ||
|
|
04b5d54697 | ||
|
|
fb096bc4be | ||
|
|
6e9e4ae1b6 | ||
|
|
d8f73202b9 | ||
|
|
b175a08dce | ||
|
|
2284ee94ad | ||
|
|
78a3311342 | ||
|
|
a1fabd3fd6 | ||
|
|
93c62cc38f | ||
|
|
8553613593 | ||
|
|
bf83dc4452 | ||
|
|
91bf2237c2 | ||
|
|
ae9b579e84 | ||
|
|
2488d98ede | ||
|
|
3eedb8ba81 | ||
|
|
bc5cc3054b | ||
|
|
5340b6ec2e | ||
|
|
0a28b2181b | ||
|
|
389ee87757 | ||
|
|
f3976f5aaf | ||
|
|
e85015725d | ||
|
|
a624b7a1cb | ||
|
|
6c85d66eab | ||
|
|
175f5ec430 | ||
|
|
18dc9fd594 | ||
|
|
6dd3c5ffcb | ||
|
|
dad3fbf44d | ||
|
|
c42fbfe8b0 | ||
|
|
3a30457944 | ||
|
|
2eae287e18 | ||
|
|
457882c515 | ||
|
|
224925c772 | ||
|
|
b5fca1c943 | ||
|
|
265a4ffffd | ||
|
|
ec5ff30ef3 | ||
|
|
947b3b91a9 | ||
|
|
71eefae4b4 | ||
|
|
6800649154 | ||
|
|
2100e4420b | ||
|
|
d4fc4c97ce | ||
|
|
ad658b1433 | ||
|
|
40c94fa8e5 | ||
|
|
902b1e4887 | ||
|
|
2e98a5a24e | ||
|
|
fbb9c815df | ||
|
|
0ed9f5b264 | ||
|
|
692fb2a8a5 | ||
|
|
151f7df388 | ||
|
|
9281ce0984 | ||
|
|
a82d435e22 | ||
|
|
6deda1ec8f | ||
|
|
258585efa0 | ||
|
|
2d1eaafbf9 | ||
|
|
5837c5be5b | ||
|
|
2106300635 | ||
|
|
82e7a260fb | ||
|
|
64f5cd6c12 | ||
|
|
8e56c12b29 | ||
|
|
febb0e1927 | ||
|
|
4ea97b6b42 | ||
|
|
8322b09221 | ||
|
|
854cb4145c | ||
|
|
c36f60d960 | ||
|
|
6bf1d58daa | ||
|
|
1c4cb32a6e | ||
|
|
009384a20d | ||
|
|
6e17a2d382 | ||
|
|
0e75dab8ff | ||
|
|
517119365e | ||
|
|
3e15ddf988 | ||
|
|
fb279a418a | ||
|
|
3e86393b25 | ||
|
|
14c476101b | ||
|
|
10cdf6ff47 | ||
|
|
975c354617 | ||
|
|
744a252469 | ||
|
|
e0305773c7 | ||
|
|
af25311e36 | ||
|
|
0877e706d2 | ||
|
|
ec516f2d02 | ||
|
|
311b1d96fa | ||
|
|
be181718e3 | ||
|
|
85ec19bda3 | ||
|
|
6314acbf81 | ||
|
|
c8136da505 | ||
|
|
3a41060d91 | ||
|
|
9e12a9f84f | ||
|
|
664e5c3e73 | ||
|
|
34bf4daa6a | ||
|
|
b2f4051e11 | ||
|
|
03cc674601 | ||
|
|
da3d007a35 | ||
|
|
80d9888b37 | ||
|
|
3ae265d6fe | ||
|
|
3c9e7708f8 | ||
|
|
7663eacb58 | ||
|
|
85d5fa14fa | ||
|
|
2b55d34c19 | ||
|
|
9d1c2167be | ||
|
|
ce52ce9a97 | ||
|
|
d73db7e5a9 | ||
|
|
98ce2ff939 | ||
|
|
f98d615e7b | ||
|
|
9c461c83ff | ||
|
|
dc8dacd5f5 | ||
|
|
db47ae9043 | ||
|
|
a0f4e141b5 | ||
|
|
5bc4c0e5c0 | ||
|
|
c4765b8eeb | ||
|
|
ebc953b549 | ||
|
|
b407003450 | ||
|
|
3857702040 | ||
|
|
e997e946a0 | ||
|
|
2bcb87ec9c | ||
|
|
1d5af93390 | ||
|
|
8a0e10eab0 | ||
|
|
13e413329c | ||
|
|
d0fac8c495 | ||
|
|
74525de586 | ||
|
|
115923b935 | ||
|
|
b7af6bb80e | ||
|
|
b053e6395c | ||
|
|
698ea34848 | ||
|
|
d689324e62 | ||
|
|
fee8133965 | ||
|
|
b93db93236 | ||
|
|
1efa4b9bb0 | ||
|
|
4cf00f5ca9 | ||
|
|
94303a9bca | ||
|
|
fa611923b4 | ||
|
|
c57d4db5a3 | ||
|
|
5d7760caa3 | ||
|
|
0d5fe8e5ab | ||
|
|
1ac552f3cd | ||
|
|
4cf4e01286 | ||
|
|
e96e2c0cb6 | ||
|
|
8fb93a09f9 | ||
|
|
4d20ba95b1 | ||
|
|
85f428a295 | ||
|
|
a3018f528d | ||
|
|
3d8431c4d5 | ||
|
|
074d55b6d4 | ||
|
|
d22d88b3a6 | ||
|
|
f55a83977b | ||
|
|
4ccc02f16c | ||
|
|
759b2abef9 | ||
|
|
f7f04213de | ||
|
|
76cb9026f9 | ||
|
|
501b632828 | ||
|
|
39840a34a3 | ||
|
|
de336dda25 | ||
|
|
21cb3041b5 | ||
|
|
b340cd9b5d | ||
|
|
37d0516679 | ||
|
|
46618ca6eb | ||
|
|
a7b4fb1615 | ||
|
|
731024523b | ||
|
|
545d9315d2 | ||
|
|
8e7ae26081 | ||
|
|
cd45426657 | ||
|
|
fe26213210 | ||
|
|
e4e10669ed | ||
|
|
d35f4bced6 | ||
|
|
048cb8a828 | ||
|
|
9f1d9892c8 | ||
|
|
7ce6d935dd | ||
|
|
0f3af215e2 | ||
|
|
16bd93f566 | ||
|
|
9fb52a94b9 | ||
|
|
58a597526a | ||
|
|
fd42f07989 | ||
|
|
6ba5219225 | ||
|
|
16718e8bd4 | ||
|
|
46b13cba7c | ||
|
|
f010eba40e | ||
|
|
fea6d8bbc9 | ||
|
|
66f9abcbcd | ||
|
|
4610218121 | ||
|
|
12321cb0d6 | ||
|
|
feb74e0fb9 | ||
|
|
b10eb97a57 | ||
|
|
bc049b9b15 | ||
|
|
36019acbd3 | ||
|
|
c0d19f02e7 | ||
|
|
18f7e95665 | ||
|
|
06b5255ccc | ||
|
|
0ab4821c74 | ||
|
|
3e09ddf024 | ||
|
|
e142f7750d | ||
|
|
ddec0a7957 | ||
|
|
dfc90c143f | ||
|
|
1f887642bf | ||
|
|
1a28153fcf | ||
|
|
349b9cf837 | ||
|
|
32f53c98e0 | ||
|
|
80ef747e84 | ||
|
|
cc425b5a42 | ||
|
|
5db7f80e28 | ||
|
|
b27b04b3ea | ||
|
|
713fa607e0 | ||
|
|
8b492bc468 | ||
|
|
c20c7d4c0e | ||
|
|
5aab0ba965 | ||
|
|
aa1ba3c6a3 | ||
|
|
6d54b6e518 | ||
|
|
fee54828fc | ||
|
|
cbe200cdf3 | ||
|
|
09d296a875 | ||
|
|
53e1a92138 | ||
|
|
65509641b0 | ||
|
|
badd6dc45d | ||
|
|
dd40dbe43b | ||
|
|
07926d5a80 | ||
|
|
f155b90672 | ||
|
|
3f820c6527 | ||
|
|
8e41051dd7 | ||
|
|
17bda0dc95 | ||
|
|
cf281971cf | ||
|
|
5f1d6e6da3 | ||
|
|
6e7cb8e3c2 | ||
|
|
69c3fc5f67 | ||
|
|
d5e3ba8709 | ||
|
|
531c6405f3 | ||
|
|
b94f7b41ac | ||
|
|
a3a23065c7 | ||
|
|
07acf57e0d | ||
|
|
16010543f5 | ||
|
|
b0ec1f63de | ||
|
|
386a47390e | ||
|
|
9b662a4333 | ||
|
|
e119b4bf0e | ||
|
|
7e58c83777 | ||
|
|
896c7d426f | ||
|
|
e3b179fa9d | ||
|
|
f5efb5664d | ||
|
|
2ae23cac2c | ||
|
|
94b24fb079 | ||
|
|
c50e3e922a | ||
|
|
9adc3a72e7 | ||
|
|
70255ef9a8 | ||
|
|
2af5cffeef | ||
|
|
0dca8768b8 | ||
|
|
5942c4ac08 | ||
|
|
3c34c6571c | ||
|
|
f5d23cc7c1 | ||
|
|
9081b440d5 | ||
|
|
db7a638729 | ||
|
|
eff85171e6 | ||
|
|
508137efde | ||
|
|
991c651082 | ||
|
|
8d91013608 | ||
|
|
9ab00bace5 | ||
|
|
88e50540e8 | ||
|
|
015a788901 | ||
|
|
c3bb662e7b | ||
|
|
638d0eec34 | ||
|
|
c5274421a5 | ||
|
|
fdafdf5dce | ||
|
|
743cf8b2af | ||
|
|
b23bd7ceb3 | ||
|
|
570c6ef730 | ||
|
|
bb7d7edad5 | ||
|
|
9a14d38e63 | ||
|
|
f65bce0de5 | ||
|
|
8005b9b86b | ||
|
|
1b4ed842a6 | ||
|
|
7cd372cc59 | ||
|
|
3b2b35d760 | ||
|
|
464e39f0d1 | ||
|
|
f66a883272 | ||
|
|
7e2172533c | ||
|
|
4237c89589 | ||
|
|
45542ce1a8 | ||
|
|
5cb286efa9 | ||
|
|
ab8284163c | ||
|
|
1ce819d324 | ||
|
|
e557afb7b2 | ||
|
|
7eb28067d8 | ||
|
|
e08a1072a7 | ||
|
|
1031206441 | ||
|
|
5a6c14b9b9 | ||
|
|
2487053341 | ||
|
|
e53ed627d6 | ||
|
|
4e66c1795c | ||
|
|
b668366d8a | ||
|
|
6caa35889a | ||
|
|
6c0f66cdd5 | ||
|
|
6d74ae4723 | ||
|
|
606e62ec3f | ||
|
|
a916982107 | ||
|
|
9f43b3d9e1 | ||
|
|
313b228e14 | ||
|
|
a180d7f7ee | ||
|
|
7bf2c309d0 | ||
|
|
bfc1fb6db5 | ||
|
|
5373630030 | ||
|
|
f82e6fc29b | ||
|
|
3390c7ad30 | ||
|
|
40401ed8ac | ||
|
|
9cdc0d343a | ||
|
|
11bb4de08b | ||
|
|
37fdfebc74 | ||
|
|
29aaed7f4b | ||
|
|
3c4e08f6a4 | ||
|
|
6718c49854 | ||
|
|
c1c34b7be3 | ||
|
|
256a9baa57 | ||
|
|
e28e8571ef | ||
|
|
ba79a7e1ff | ||
|
|
44a7cc9b2f | ||
|
|
8fb4ef7ce0 | ||
|
|
86a1d05a35 | ||
|
|
d7a50e9984 | ||
|
|
1a27221563 | ||
|
|
780d61d84a | ||
|
|
f73e5f4c51 | ||
|
|
388b1a6196 | ||
|
|
327e347ebe | ||
|
|
0e9790a721 | ||
|
|
ea81cd0e16 | ||
|
|
6b2d494f6e | ||
|
|
3866716326 | ||
|
|
8de108548c | ||
|
|
3595dae0f1 | ||
|
|
2d266508f8 | ||
|
|
833bacc65d | ||
|
|
283434aa09 | ||
|
|
87b44a6f40 | ||
|
|
f5817f4c2f | ||
|
|
8a43fd7894 | ||
|
|
a2f57095d7 | ||
|
|
1827e0da5b | ||
|
|
6993cd792d | ||
|
|
4c9454cae8 | ||
|
|
1e86025c3d | ||
|
|
af44b45ea6 | ||
|
|
4ac7384b2f | ||
|
|
83146c71c0 | ||
|
|
993d747a64 | ||
|
|
6ecc95bbb0 | ||
|
|
177e4d081f | ||
|
|
b3ed1566ca | ||
|
|
0333dcdd20 | ||
|
|
f52cd72f02 | ||
|
|
b96e383e6a | ||
|
|
feca8660f3 | ||
|
|
3c561c642a | ||
|
|
068f41f8e5 | ||
|
|
e534e65d2d | ||
|
|
9c95de2f14 | ||
|
|
0e380b72b4 | ||
|
|
4b3489f8d9 | ||
|
|
3f4bb10562 | ||
|
|
f3524bf564 | ||
|
|
8805e62f22 | ||
|
|
e825207787 | ||
|
|
e1a6dfee5d | ||
|
|
e05c7e7dd1 | ||
|
|
4743bd13e8 | ||
|
|
d8ba83e7f1 | ||
|
|
98f1cc4277 | ||
|
|
6d30154fce | ||
|
|
fc0dac1981 | ||
|
|
1cac465520 | ||
|
|
d01504cc73 | ||
|
|
6b27336c5c | ||
|
|
cc2f9342b3 | ||
|
|
198defa47b | ||
|
|
b6e4127183 | ||
|
|
abe67a2694 | ||
|
|
5a08428129 | ||
|
|
809c9e31f9 | ||
|
|
07d3e2753a | ||
|
|
f82005c573 | ||
|
|
9a2a6afadb | ||
|
|
bad410fd78 | ||
|
|
bb67f12a89 | ||
|
|
977d1da3b0 | ||
|
|
3e80669e23 | ||
|
|
a1f23efb42 | ||
|
|
4f065454c0 | ||
|
|
c31128939e | ||
|
|
a27c8dc44b | ||
|
|
3d67b0e07d | ||
|
|
d01de7337f | ||
|
|
6e1ca7987e | ||
|
|
717cf66b90 | ||
|
|
c84cc4655f | ||
|
|
28ff813c49 | ||
|
|
7cef8a3854 | ||
|
|
73a18ed957 | ||
|
|
69a7938955 | ||
|
|
808aaea818 | ||
|
|
57d8928966 | ||
|
|
e46655654b | ||
|
|
0c1508f761 | ||
|
|
a2ee25b559 | ||
|
|
a3968bf9f7 | ||
|
|
799a626005 | ||
|
|
154d398724 | ||
|
|
a7005b91da | ||
|
|
6b508d472f | ||
|
|
9266b43c7a | ||
|
|
5721a7eecb | ||
|
|
7f2f09137c | ||
|
|
3b80fe6cdf | ||
|
|
b995c8c564 | ||
|
|
f13517d75b | ||
|
|
06945b7a3c | ||
|
|
8beaf37f5a | ||
|
|
8cb9f006de | ||
|
|
0f2c818cb2 | ||
|
|
fcfedce514 | ||
|
|
c2541eb2ac | ||
|
|
8262784152 | ||
|
|
13bfba8f22 | ||
|
|
e708a779c7 | ||
|
|
f93cca8043 | ||
|
|
e3133749f7 | ||
|
|
0e870dbb1b | ||
|
|
8911232658 | ||
|
|
b8abcd1848 | ||
|
|
ea69588488 | ||
|
|
f32243a402 | ||
|
|
80a5d4eea5 | ||
|
|
40a663ad21 | ||
|
|
6aa42d4ee0 | ||
|
|
f57289cc46 | ||
|
|
f84e74f230 | ||
|
|
55ed97bfeb | ||
|
|
e4320d9a16 | ||
|
|
01e43a9223 | ||
|
|
f204afdc6d | ||
|
|
5b9ffe320a | ||
|
|
e291c110fe | ||
|
|
3b83839f6c | ||
|
|
26f6e97ad8 | ||
|
|
5381789bfd | ||
|
|
50da27c698 | ||
|
|
7d7a6f83a1 | ||
|
|
1bd865dd56 | ||
|
|
ffe43387e3 | ||
|
|
2359da9ed5 | ||
|
|
41cd3c0add | ||
|
|
494c0625a7 | ||
|
|
0a2e215846 | ||
|
|
a94ff3e660 | ||
|
|
c98bf61041 | ||
|
|
e639a988fd | ||
|
|
f86c283f9e | ||
|
|
23f7074913 | ||
|
|
19913013b8 | ||
|
|
e50ef300e4 | ||
|
|
4298eb0ee2 | ||
|
|
b2e0012d6e | ||
|
|
c4700a6f77 | ||
|
|
6755117bff | ||
|
|
9d8c5dadfc | ||
|
|
ee914305f7 | ||
|
|
883b85db9f | ||
|
|
977f0bf8e0 | ||
|
|
c4c05ffc0b | ||
|
|
07ef916183 | ||
|
|
7e240dee16 | ||
|
|
45e454c467 | ||
|
|
42f0d942d0 | ||
|
|
d525e3c87c | ||
|
|
74b0f5cf2b | ||
|
|
ac49523bfa | ||
|
|
eb702a29b5 | ||
|
|
59f739a60f | ||
|
|
7c8cc19c0e | ||
|
|
c0bf16f5f8 | ||
|
|
f9038a69ed | ||
|
|
dceaeadcdd | ||
|
|
23e71ddcb3 | ||
|
|
080417fc64 | ||
|
|
24d61c6dd0 | ||
|
|
dfceb60f4d | ||
|
|
369e810c48 | ||
|
|
cf56d681b3 | ||
|
|
7d9604a067 | ||
|
|
00725b6f9c | ||
|
|
ff3341c506 | ||
|
|
2a5dfcc617 | ||
|
|
a57e9245a7 | ||
|
|
48cfd57476 | ||
|
|
16afffad83 | ||
|
|
914aaa0bce | ||
|
|
237f751824 | ||
|
|
d76a58732e | ||
|
|
c58188f52c | ||
|
|
f61120230c | ||
|
|
1c7bd5fcb6 | ||
|
|
d39d6051aa | ||
|
|
16d1d0af57 | ||
|
|
64871f5e72 | ||
|
|
27aeadb946 | ||
|
|
21b5b58951 | ||
|
|
cf13342286 | ||
|
|
42565a61bb | ||
|
|
0c8b337acf | ||
|
|
baa9efb97e | ||
|
|
f8df39a62a | ||
|
|
e4258e1a2a | ||
|
|
8e77d5cdcc | ||
|
|
3aff551a85 | ||
|
|
2eaac6ce00 | ||
|
|
8d3c5b2925 | ||
|
|
d6e80c2689 | ||
|
|
c7210b89cb | ||
|
|
adf34a4c9a | ||
|
|
41223b02e3 | ||
|
|
7e3e6f82f4 | ||
|
|
f29728d915 | ||
|
|
96f2cf8468 | ||
|
|
87dfc30866 | ||
|
|
a4508e0d88 | ||
|
|
398dedb2ae | ||
|
|
b9db443fe4 | ||
|
|
4da95c4c15 | ||
|
|
75844f00b4 | ||
|
|
e5ab4cf3b9 | ||
|
|
484f49346c | ||
|
|
7cd02f2c9a | ||
|
|
b965eb49c9 | ||
|
|
b21281678b | ||
|
|
d5046d9173 | ||
|
|
315bd4ea96 | ||
|
|
9708d78ff2 | ||
|
|
6dd8749cc4 | ||
|
|
bb222f4858 | ||
|
|
5bfb150b89 | ||
|
|
86846c6631 | ||
|
|
5c0fd897ac | ||
|
|
61ebf7207e | ||
|
|
57fcfc7377 | ||
|
|
6e35f9393d | ||
|
|
57e83f8d46 | ||
|
|
7e20dba636 | ||
|
|
ce4c61671e | ||
|
|
06a330d25e | ||
|
|
eb2a614094 | ||
|
|
a27cf3a765 | ||
|
|
26a77642a1 | ||
|
|
3191c9176e | ||
|
|
8866af8c30 | ||
|
|
a06c120b28 | ||
|
|
d5361c085d | ||
|
|
7d7573f58f | ||
|
|
55bb2f377e | ||
|
|
738a25fcc4 | ||
|
|
538888df75 | ||
|
|
a2c767181d | ||
|
|
63f954d8e7 | ||
|
|
fea67deaa3 | ||
|
|
e969d5471a | ||
|
|
b8987b279e | ||
|
|
890e0c5361 | ||
|
|
8fb9364c3a | ||
|
|
c6019e181f | ||
|
|
58a00d7eb6 | ||
|
|
c05cb798f2 | ||
|
|
fed5e453a8 | ||
|
|
68a3478301 | ||
|
|
e3704dd8e7 | ||
|
|
fb76dd7b6a | ||
|
|
9fddc7ff34 | ||
|
|
605b496a01 | ||
|
|
d1c6b957f7 | ||
|
|
5953a74e8d | ||
|
|
85b16aa6b9 | ||
|
|
e1253cadae | ||
|
|
acef51a73e | ||
|
|
82649cbfc2 | ||
|
|
c7a2c5347e | ||
|
|
eca57f9276 | ||
|
|
1ca392fb40 | ||
|
|
0cfec775b1 | ||
|
|
0d3d9472c7 | ||
|
|
9360f17c23 | ||
|
|
c0369461fc | ||
|
|
3bef8009de | ||
|
|
7c0d78ee3b | ||
|
|
9439055ebb | ||
|
|
74e3896af8 | ||
|
|
688cc2924c | ||
|
|
7049d8f0b9 | ||
|
|
ff47789095 | ||
|
|
f58079b62b | ||
|
|
08efebc87c | ||
|
|
d953ab89f4 | ||
|
|
d171c61bcc | ||
|
|
cce63f02b5 | ||
|
|
655c7babdb | ||
|
|
22703796d6 | ||
|
|
f8aeb97d6c | ||
|
|
8739d2ae42 | ||
|
|
f27fe16d8f | ||
|
|
8453db60fb | ||
|
|
18267465df | ||
|
|
a8fa434e23 | ||
|
|
11f4c3defb | ||
|
|
9e7db72b70 | ||
|
|
0c856bad5a | ||
|
|
e24b8963e6 | ||
|
|
c4ea44d025 | ||
|
|
c80b248ad4 | ||
|
|
c44f8a7ab1 | ||
|
|
02f0458e09 | ||
|
|
ff7fd622a8 | ||
|
|
1d066654b8 | ||
|
|
0b9167413e | ||
|
|
0f02624dbc | ||
|
|
f9728f1847 | ||
|
|
63488d82eb | ||
|
|
819d2e6ea0 | ||
|
|
2878b9df60 | ||
|
|
f08c695702 | ||
|
|
5d8d5b0c27 | ||
|
|
686a95514e | ||
|
|
a3eea0d968 | ||
|
|
4be969d189 | ||
|
|
5123dc58c8 | ||
|
|
12f2ea38b0 | ||
|
|
baf4a663df | ||
|
|
8fce5cd523 | ||
|
|
7eb97c0103 | ||
|
|
33e83626bf | ||
|
|
38d6b07318 | ||
|
|
e5b95f10a2 | ||
|
|
44ce8e8d09 | ||
|
|
008422434b | ||
|
|
3e3dfd5636 | ||
|
|
5838a98d2f | ||
|
|
f6be32d492 | ||
|
|
8c7fee9f6d | ||
|
|
e9a245b144 | ||
|
|
2eb2514e74 | ||
|
|
7e69bf8ac3 | ||
|
|
53dbe582ac | ||
|
|
30e94a8964 | ||
|
|
c89bfcd4f9 | ||
|
|
ed3bbc5032 | ||
|
|
855618d926 | ||
|
|
26fcdc4b6e | ||
|
|
7e9b5582c0 | ||
|
|
a850aba829 | ||
|
|
e672acac43 | ||
|
|
0e39061f1b | ||
|
|
c0ed81298d | ||
|
|
dffed32515 | ||
|
|
a679908c6b | ||
|
|
63fac649d9 | ||
|
|
78a78340a0 | ||
|
|
1f1f3efb43 | ||
|
|
4bc8ea068e | ||
|
|
a85b73b935 | ||
|
|
e4b01de2b5 | ||
|
|
b21be7d2c9 | ||
|
|
d410c4699b | ||
|
|
e7b259f764 | ||
|
|
0405098dcc | ||
|
|
6aa7b2afe7 | ||
|
|
255edefa03 | ||
|
|
32e8538e88 | ||
|
|
5bb697bb85 | ||
|
|
46d8e1ecf5 | ||
|
|
53e4615a7a | ||
|
|
7b9eae280d | ||
|
|
e5934f9e85 | ||
|
|
26fae1dd06 | ||
|
|
c0aaba73de | ||
|
|
e849d230e7 | ||
|
|
2c23822ace | ||
|
|
342e44a20f | ||
|
|
121891f825 | ||
|
|
1da64adca1 | ||
|
|
96c8fecb9d | ||
|
|
178858d67c | ||
|
|
18b2899b25 | ||
|
|
8f6c324267 | ||
|
|
9fad4423f4 | ||
|
|
f82f03a06f | ||
|
|
73b82b12a7 | ||
|
|
f49c9ea60d | ||
|
|
f0c53c54b7 | ||
|
|
ab2547cfa0 | ||
|
|
496512f84c | ||
|
|
b954427f1a | ||
|
|
aaebe5e65f | ||
|
|
7fc6cefba1 | ||
|
|
a30ed090c4 | ||
|
|
d60d22d0ec | ||
|
|
890cf02a5e | ||
|
|
af68656310 | ||
|
|
21203685ba | ||
|
|
9caca125d0 | ||
|
|
cb182ac1b2 | ||
|
|
58bebf1b33 | ||
|
|
0ee1d67122 | ||
|
|
e44da5e088 | ||
|
|
18e2801bf8 | ||
|
|
c0dc7fec56 | ||
|
|
2ceba8a949 | ||
|
|
a4b9a1f415 | ||
|
|
3bb7e12a1c | ||
|
|
1704c31a85 | ||
|
|
65cf34b3e0 | ||
|
|
c17e9238a7 | ||
|
|
e56d281631 | ||
|
|
0361dbe153 | ||
|
|
25f0ed2b29 | ||
|
|
ddae09fe85 | ||
|
|
ac9b1369da | ||
|
|
7422c27c7d | ||
|
|
ccc133046c | ||
|
|
bf0e4abece | ||
|
|
e7abd2bebe | ||
|
|
a18ec8a20f | ||
|
|
4b58daac14 | ||
|
|
e76c1ecaf1 | ||
|
|
df4a4b1dce | ||
|
|
028f155268 | ||
|
|
0ccf272a1a | ||
|
|
b2442b3702 | ||
|
|
47d50d0500 | ||
|
|
7c69006e60 | ||
|
|
38bd00212e | ||
|
|
f3a1c5f569 | ||
|
|
6961907eec | ||
|
|
5b66cdd8d4 | ||
|
|
06d5a08dd0 | ||
|
|
6cf14a099b | ||
|
|
93cee61bfb | ||
|
|
82c8262a2c | ||
|
|
b455c5aa2c | ||
|
|
df352a53bc | ||
|
|
2546d38fce | ||
|
|
68984fa817 | ||
|
|
804ff2d2c7 | ||
|
|
da5db665eb | ||
|
|
a457a69c21 | ||
|
|
0d4fb97292 | ||
|
|
b39fa254b9 | ||
|
|
8e7996424c | ||
|
|
2b10984921 | ||
|
|
ba6cb75501 | ||
|
|
d792a73dcf | ||
|
|
dc3477b4cd | ||
|
|
45655ed3cc | ||
|
|
e0a0048d39 | ||
|
|
39c842c026 | ||
|
|
d4e3b4d40f | ||
|
|
7aac2586ef | ||
|
|
fe56d63258 | ||
|
|
79d05914bd | ||
|
|
cb336f47fe | ||
|
|
c95d54c2e9 | ||
|
|
0b47b11601 | ||
|
|
eed3895f1b | ||
|
|
fd6c4d0bf6 | ||
|
|
e50d5a4a88 | ||
|
|
7770fb81d1 | ||
|
|
bd4fcaf2f9 | ||
|
|
297e174a84 | ||
|
|
71c704588d | ||
|
|
26b2ac1830 | ||
|
|
266988327f | ||
|
|
58b55bf2ba | ||
|
|
df0f9e01bc | ||
|
|
d6cc83a8ff | ||
|
|
44f23d804d | ||
|
|
cc06e52488 | ||
|
|
a6fe077d88 | ||
|
|
df39ea19e7 | ||
|
|
2f455d03f5 | ||
|
|
98c1de387d | ||
|
|
53d185bf9c | ||
|
|
0ef3987956 | ||
|
|
a7ce1d1468 | ||
|
|
9cc2942a49 | ||
|
|
4ffe34c538 | ||
|
|
22b12073f9 | ||
|
|
83489fff84 | ||
|
|
2511dd5b4e | ||
|
|
767bd08f0c | ||
|
|
d2ba2ff544 | ||
|
|
f6ad1226f5 | ||
|
|
785b0d7395 | ||
|
|
ea91a55bd6 | ||
|
|
c73f13baa8 | ||
|
|
a44c68ef46 | ||
|
|
218627e62d | ||
|
|
5d6e775b02 | ||
|
|
2b86cff3f3 | ||
|
|
0bc70dbc9e | ||
|
|
d1f7baad2b | ||
|
|
6d57bc2c5a | ||
|
|
8e3cf93011 | ||
|
|
51f6d17fbb | ||
|
|
b2e5229c78 | ||
|
|
87ec87e09d | ||
|
|
25bf7ac915 | ||
|
|
bcadad5564 | ||
|
|
bacda4624f | ||
|
|
9c5434e32c | ||
|
|
058421b9aa | ||
|
|
f947f50213 | ||
|
|
54688e6a71 | ||
|
|
d789bd7710 | ||
|
|
b3fa69f7c7 | ||
|
|
3ec47b2310 | ||
|
|
9bc37d8ecd | ||
|
|
dd21c46331 | ||
|
|
3c69cf7d5a | ||
|
|
97f8a0faf6 | ||
|
|
ce8b6dc995 | ||
|
|
d78eebdb0e | ||
|
|
01f78e0595 | ||
|
|
fc8e710a07 | ||
|
|
929131f5e6 | ||
|
|
4fbb0e454e | ||
|
|
3832a8811d | ||
|
|
8f6461e80f | ||
|
|
e21e12820b | ||
|
|
1edff1048f | ||
|
|
8e71e6b0ea | ||
|
|
1efd3dbea9 | ||
|
|
d02ad5a5b6 | ||
|
|
8758047ff0 | ||
|
|
c17df3c2b6 | ||
|
|
fe54d6537f | ||
|
|
dcc3516587 | ||
|
|
7039296c64 | ||
|
|
cdba36628f | ||
|
|
b2465f9bdc | ||
|
|
4b4a398c99 | ||
|
|
d292bd033f | ||
|
|
faa5683b26 | ||
|
|
ce0ae1bf04 | ||
|
|
b8f49b9312 | ||
|
|
7b9bcdfd10 | ||
|
|
72d65a18ef | ||
|
|
160a008a0b | ||
|
|
030f5ed315 | ||
|
|
018051572c | ||
|
|
fc2d1ffaca | ||
|
|
418eb81e1c | ||
|
|
5c2c576cb4 | ||
|
|
9cf11bf2d9 | ||
|
|
a050d220d0 | ||
|
|
5e64d55ac0 | ||
|
|
d32a657d62 | ||
|
|
78e5e298b3 | ||
|
|
44ad0f3a51 | ||
|
|
e783cc64f5 | ||
|
|
c1f10a1c46 | ||
|
|
a0231ed598 | ||
|
|
900ba2f3a2 | ||
|
|
519e224914 | ||
|
|
2e067f98ff | ||
|
|
63ca296037 | ||
|
|
2221053c66 | ||
|
|
5ce6354bd3 | ||
|
|
44ae334492 | ||
|
|
4020de976f | ||
|
|
9929a5f578 | ||
|
|
3dd1e916c1 | ||
|
|
5b810e8cf5 | ||
|
|
a9356fbd4a | ||
|
|
6aad9e6130 | ||
|
|
258b4113db | ||
|
|
66d5b7a6f4 | ||
|
|
4c4df5011e | ||
|
|
2b99b888bc | ||
|
|
618e1f64df | ||
|
|
fca00e7084 | ||
|
|
7d92df6626 | ||
|
|
f00b552305 | ||
|
|
d7b5f5f25b | ||
|
|
a4c3088e02 | ||
|
|
0e44df8cfa | ||
|
|
eddc2214ed | ||
|
|
db3f87d219 | ||
|
|
d962124da2 | ||
|
|
e1c8afd5ba | ||
|
|
01614667a4 | ||
|
|
43bd4ff468 | ||
|
|
d5bdb78de6 | ||
|
|
acd5e547fd | ||
|
|
201eb38a35 | ||
|
|
9b13c2554a | ||
|
|
21918cadd9 | ||
|
|
55191a6e9b | ||
|
|
e93a30d2e1 | ||
|
|
443fce9fd1 | ||
|
|
02f4a730c7 | ||
|
|
f02626a118 | ||
|
|
7632219a83 | ||
|
|
4e563b3c05 | ||
|
|
92a60bd03e | ||
|
|
1238ef62fa | ||
|
|
0a101e3b69 | ||
|
|
ecd2f0e0f2 | ||
|
|
9be3cc1f13 | ||
|
|
c64c503ba9 | ||
|
|
d704b2ecac | ||
|
|
66539aa422 | ||
|
|
f8c20e55a4 | ||
|
|
3b5448a334 | ||
|
|
4b1614e247 | ||
|
|
bb4f843a33 | ||
|
|
9008039a2a | ||
|
|
fb1ee381fe | ||
|
|
8ad0ce8911 | ||
|
|
59c4b6db86 | ||
|
|
f4e15215ec | ||
|
|
b57cc559d9 | ||
|
|
4a1d6047ac | ||
|
|
3d411826f2 | ||
|
|
898c0a2f7c | ||
|
|
2a9bc2278d | ||
|
|
989209758a | ||
|
|
1d4386e9e9 | ||
|
|
d8e45814bb | ||
|
|
ce01f8bce0 | ||
|
|
76384e38c8 | ||
|
|
ac64512ff7 | ||
|
|
7d9261dace | ||
|
|
6187b849a7 | ||
|
|
724d0b2567 | ||
|
|
2d24e2e0eb | ||
|
|
d4d65044b3 | ||
|
|
717f1e7eec | ||
|
|
30b58d213e | ||
|
|
0404b5f1e4 | ||
|
|
f2a946790d | ||
|
|
37ea2260b8 | ||
|
|
938ad5f8b9 | ||
|
|
4e881415d2 | ||
|
|
96af470708 | ||
|
|
3275abafb0 | ||
|
|
2538a41fcb | ||
|
|
d93b76a664 | ||
|
|
ea5703d99d | ||
|
|
22c3c797ff | ||
|
|
0cc0ec6c55 | ||
|
|
d1e5fdc1e0 | ||
|
|
ed9c5a0dc7 | ||
|
|
e1fae2e219 | ||
|
|
e5cf24e1cc | ||
|
|
3d1f970262 | ||
|
|
a002655b3d | ||
|
|
9fe2384b47 | ||
|
|
a6781414a5 | ||
|
|
cbdbac4e01 | ||
|
|
c867e62502 | ||
|
|
341fe5d46e | ||
|
|
e70eefdbbf | ||
|
|
67c2daf4ef | ||
|
|
178cfd4a6b | ||
|
|
6d868e21da | ||
|
|
691a6d0956 | ||
|
|
6eda60d314 | ||
|
|
41b0896c2e | ||
|
|
e8bdeed556 | ||
|
|
c29dd321e3 | ||
|
|
6062534d91 | ||
|
|
d6bb61c428 | ||
|
|
65a56684ec | ||
|
|
e1dfe30661 | ||
|
|
87cb1cc4f7 | ||
|
|
0be75ca38e | ||
|
|
54b2561da1 | ||
|
|
fbba7a30d0 | ||
|
|
42d361b7ad | ||
|
|
6d41e6b962 | ||
|
|
ed32aad64a | ||
|
|
e4896fe935 | ||
|
|
2f81690783 | ||
|
|
dc3ec24cd7 | ||
|
|
797171fc3a | ||
|
|
16e12a8a7b | ||
|
|
0e07124cbc | ||
|
|
5db66e3fcb | ||
|
|
01770d25f8 | ||
|
|
e4366c8dfa | ||
|
|
f19446eda9 | ||
|
|
0d7f1657de | ||
|
|
e905f01305 | ||
|
|
e5e7fa3f8a | ||
|
|
69575622a2 | ||
|
|
ee229e9ed8 | ||
|
|
21a9481459 | ||
|
|
f28732904d | ||
|
|
06828e2cfd | ||
|
|
e89c58e2e9 | ||
|
|
cb1203a2b8 | ||
|
|
be64aba265 | ||
|
|
316281d42b | ||
|
|
91520753b0 | ||
|
|
94bb94e8f4 | ||
|
|
fcf020658a | ||
|
|
4d8cd8a926 | ||
|
|
ec871c1a41 | ||
|
|
acf7fe3117 | ||
|
|
55f99980cc | ||
|
|
7079cc0d20 | ||
|
|
dfb68c512b | ||
|
|
85ff73c055 | ||
|
|
96bb31de5f | ||
|
|
b06f13e32a | ||
|
|
512282c446 | ||
|
|
0986583365 | ||
|
|
7e5c07c6e5 | ||
|
|
99d14139d2 | ||
|
|
b5419ee747 | ||
|
|
22e9f3308b | ||
|
|
d1161207da | ||
|
|
f8aad34bb1 | ||
|
|
8b9686edba | ||
|
|
e0e8a66d48 | ||
|
|
6d029505ed | ||
|
|
e5c7c4b02f | ||
|
|
a17bbc4d15 | ||
|
|
bc5c9df40d | ||
|
|
59c5dfe703 | ||
|
|
953e2e6a73 | ||
|
|
f16b3cb2dd | ||
|
|
b8d5978668 | ||
|
|
6dc2d5092d | ||
|
|
45227c6628 | ||
|
|
76f88ffd66 | ||
|
|
5a6864f26e | ||
|
|
8eca4c8054 | ||
|
|
1a12ce8992 | ||
|
|
db8d6ce4ab | ||
|
|
9874c5f417 | ||
|
|
9f2e4277bb | ||
|
|
2d933416f8 | ||
|
|
6886f3081f | ||
|
|
3edbbda7fa | ||
|
|
7e7160e0bf | ||
|
|
a9f92dc9f3 | ||
|
|
57d6da52bb | ||
|
|
72dc85d90d | ||
|
|
ff5ff54e88 | ||
|
|
6ab0fa3fbd | ||
|
|
4d4c02ebb3 | ||
|
|
a34542171c | ||
|
|
fce6138031 | ||
|
|
38c8197989 | ||
|
|
b7f4578c34 | ||
|
|
e5ea72e334 | ||
|
|
73a0f610c3 | ||
|
|
ee2d884434 | ||
|
|
a9ac568dc0 | ||
|
|
365dac5c2b | ||
|
|
511069b641 | ||
|
|
342d3bbd49 | ||
|
|
28da67c0c5 | ||
|
|
47c9de2e22 | ||
|
|
9e6f8c16a5 | ||
|
|
1f8d3aa7f4 | ||
|
|
209c16eff8 | ||
|
|
49b0135798 | ||
|
|
1d55dad4b1 | ||
|
|
f4e9434294 | ||
|
|
97cf766466 | ||
|
|
debcd3dc0d | ||
|
|
495d7ba9aa | ||
|
|
aac405a99d | ||
|
|
22fed1dfba | ||
|
|
ce79a638f5 | ||
|
|
ae420910e9 | ||
|
|
8068d4e591 | ||
|
|
cd3a7375e6 | ||
|
|
be9f60abf3 | ||
|
|
6431afec31 | ||
|
|
43c744df59 | ||
|
|
116d7b4365 | ||
|
|
a4fd51445d | ||
|
|
66393224ba | ||
|
|
8faceb1376 | ||
|
|
3ce0379c99 | ||
|
|
24a8dfdfdb | ||
|
|
3952044a82 | ||
|
|
a11128ae9f | ||
|
|
ca802a7254 | ||
|
|
ae7d4770fa | ||
|
|
430c00c689 | ||
|
|
302f7018d4 | ||
|
|
804445637d | ||
|
|
e9d1718793 | ||
|
|
d9953ff0ac | ||
|
|
5433bf013d | ||
|
|
244cbb39ca | ||
|
|
a0eac8e0dc | ||
|
|
f8c473d039 | ||
|
|
fc259f5c20 | ||
|
|
a3a6ec4838 | ||
|
|
c32a8c1614 | ||
|
|
50f4e623f7 | ||
|
|
b7de63c5f5 | ||
|
|
4ed41cf06a | ||
|
|
d613ceb3eb | ||
|
|
91ee57997b | ||
|
|
50a7a94dac | ||
|
|
61c5d7e637 | ||
|
|
816ba32bb0 | ||
|
|
3c6937ea94 | ||
|
|
bf897eb02a | ||
|
|
254bb9e0a9 | ||
|
|
2cdd9a7fc0 | ||
|
|
774abcc612 | ||
|
|
bbea1fba25 | ||
|
|
1c68b02343 | ||
|
|
1f2054beb7 | ||
|
|
2385942f7c | ||
|
|
36bef805ea | ||
|
|
ed1f88db9d | ||
|
|
c4390a0bb0 | ||
|
|
301e761897 | ||
|
|
72173298e3 | ||
|
|
c4265edaf4 | ||
|
|
93f9bde307 | ||
|
|
0bf8f978f9 | ||
|
|
a6522bbcf2 | ||
|
|
44aeddcdbd | ||
|
|
c6a630875f | ||
|
|
b18fa8fa76 | ||
|
|
689808a9db | ||
|
|
8bb45d6f43 | ||
|
|
cd1ff1699b | ||
|
|
4ca13ca884 | ||
|
|
b05138630a | ||
|
|
9394ac47f4 | ||
|
|
c43716b0d9 | ||
|
|
e8b54ddc1f | ||
|
|
37d756b1b0 | ||
|
|
e3140aaab8 | ||
|
|
238aa2e509 | ||
|
|
54c6d4fb96 | ||
|
|
969b921fae | ||
|
|
40add5ca44 | ||
|
|
06db4610ca | ||
|
|
e4334f256a | ||
|
|
4286baec0f | ||
|
|
cd1df94235 | ||
|
|
649b73f0e1 | ||
|
|
e967d4b344 | ||
|
|
e5a71b8044 | ||
|
|
9886957034 | ||
|
|
3e7d155002 | ||
|
|
b96add529c | ||
|
|
e91db6aeec | ||
|
|
1b25283284 | ||
|
|
2bc61baac3 | ||
|
|
f862f1a2c4 | ||
|
|
3074169446 | ||
|
|
afee575f10 | ||
|
|
94a53a278c | ||
|
|
4a3ac7176f | ||
|
|
305c5bad68 | ||
|
|
4570da2e26 | ||
|
|
7ec0487e37 | ||
|
|
3aa3dccda7 | ||
|
|
21efa7f639 | ||
|
|
614fd1a85d | ||
|
|
05bd6a5d48 | ||
|
|
208fa85e8f | ||
|
|
2b703cac3a | ||
|
|
ce59872a66 | ||
|
|
c402c416e5 | ||
|
|
466b4314c5 | ||
|
|
40f8aac655 | ||
|
|
2afd1ab938 | ||
|
|
722fcaa2e1 | ||
|
|
5d3830d3de | ||
|
|
da244fe836 | ||
|
|
c00ff163f1 | ||
|
|
6bb18f18fd | ||
|
|
b8ea8f8a97 | ||
|
|
f9c01d03d3 | ||
|
|
f05dcf99ff | ||
|
|
a8edec2184 | ||
|
|
de3db81222 | ||
|
|
e0a49bc71e | ||
|
|
031e65efa7 | ||
|
|
3a1978e2fe | ||
|
|
edffa28d89 | ||
|
|
2c60fadb99 | ||
|
|
adfd9f63fa | ||
|
|
22f3f245b5 | ||
|
|
53e832fbf0 | ||
|
|
884c180e43 | ||
|
|
a1d5c0252d | ||
|
|
94a40f4b79 | ||
|
|
523467f47e | ||
|
|
eea48c257f | ||
|
|
d9e6718217 | ||
|
|
84afda69c0 | ||
|
|
820a1c1604 | ||
|
|
941d79410b | ||
|
|
78d2b8a68b | ||
|
|
0d147c7585 | ||
|
|
2f23d6a67d | ||
|
|
df6c5fee9c | ||
|
|
41f6bc588c | ||
|
|
660eedb917 | ||
|
|
37cc205dfa | ||
|
|
78beaf0b45 | ||
|
|
8c5cb09e70 | ||
|
|
3163e53502 | ||
|
|
1a58cdb08f | ||
|
|
183ebfad96 | ||
|
|
49e7e14422 | ||
|
|
6576099748 | ||
|
|
5d6bc5bc55 | ||
|
|
edf0c62ce3 | ||
|
|
0a9f3efecc | ||
|
|
a7bc98d1e3 | ||
|
|
ac03bc50b7 | ||
|
|
61a8d07912 | ||
|
|
2d744f1080 | ||
|
|
2582fe20db | ||
|
|
6a322ef9d5 | ||
|
|
bbcbd55e5a | ||
|
|
99f6e1b346 | ||
|
|
a7c564ee20 | ||
|
|
bf9b6f634c | ||
|
|
1698c9311b | ||
|
|
6b7c2c6f01 | ||
|
|
54e52758e5 | ||
|
|
4f9a85b3a7 | ||
|
|
7fe63a2cfe | ||
|
|
a7c418ac79 | ||
|
|
3a51ce523c | ||
|
|
ff0f8bd86e | ||
|
|
12d4b1448d | ||
|
|
3e552f19b5 | ||
|
|
13e2ce88d5 | ||
|
|
276814f2d8 | ||
|
|
4263c59117 | ||
|
|
786e1441d7 | ||
|
|
4100673705 | ||
|
|
759d4475fc | ||
|
|
2e986a4bdd | ||
|
|
005afae9aa | ||
|
|
739cdea934 | ||
|
|
894e774523 | ||
|
|
397a3f5d80 | ||
|
|
c30c6a4cfb | ||
|
|
01134c94b8 | ||
|
|
0cd963a9cf | ||
|
|
1db452829d | ||
|
|
9e7d0d7429 | ||
|
|
de891f783f | ||
|
|
dd4922ea77 | ||
|
|
e0ac7022fc | ||
|
|
0809c80965 | ||
|
|
1e5e8ef5fc | ||
|
|
bc44ee346a | ||
|
|
2ed9a55c9b | ||
|
|
88196afc14 | ||
|
|
60605eb347 | ||
|
|
d361621dc3 | ||
|
|
1e99b7ffa2 | ||
|
|
143e02d16c | ||
|
|
92cb18389e | ||
|
|
ce4383505c | ||
|
|
6e0ab7f24d | ||
|
|
64461d649b | ||
|
|
19acd42336 | ||
|
|
5d050348ea | ||
|
|
6d34e8e9bb | ||
|
|
5a80658ec5 | ||
|
|
be3ba2219a | ||
|
|
00026ee452 | ||
|
|
706e381f02 | ||
|
|
d06655316b | ||
|
|
17308694d8 | ||
|
|
ef7dfa7709 | ||
|
|
871ece2514 | ||
|
|
106ea10d93 | ||
|
|
5398b98195 | ||
|
|
4caef40690 | ||
|
|
a7ab6f11fb | ||
|
|
41b1d68de6 | ||
|
|
c322c8818b | ||
|
|
6a2de3a9a7 | ||
|
|
f40c52d317 | ||
|
|
c2774fc562 | ||
|
|
bad7fd1234 | ||
|
|
3bc2227f70 | ||
|
|
8c2c25ad52 | ||
|
|
0dc4511f0f | ||
|
|
3cdf057be5 | ||
|
|
727e11996c | ||
|
|
7259154e0c | ||
|
|
8e5212b37a | ||
|
|
b09b4929ea | ||
|
|
5629bb63c0 | ||
|
|
2c100466c4 | ||
|
|
a963567d95 | ||
|
|
6f9b1586bf | ||
|
|
3076ed37dd | ||
|
|
38fe1c60f6 | ||
|
|
fc551a7acd | ||
|
|
1e974807e5 | ||
|
|
0dc258ebde | ||
|
|
b26e92ecb7 | ||
|
|
6488a62c45 | ||
|
|
cf3825be83 | ||
|
|
a4bb14d1a0 | ||
|
|
a5e431e31c | ||
|
|
4005f2c6de | ||
|
|
b0b9be552b | ||
|
|
acacdc870a | ||
|
|
66a89572ca | ||
|
|
a12c817821 | ||
|
|
d19912ff5b | ||
|
|
111ede487d | ||
|
|
6f7ca3cb5e | ||
|
|
e6b63095ad | ||
|
|
a40e8e3c57 | ||
|
|
0d129be808 | ||
|
|
d70ab1beab | ||
|
|
5666cbd9c0 | ||
|
|
b0d4eb7c3f | ||
|
|
ed5773883b | ||
|
|
fc668cb395 | ||
|
|
d7746c48f2 | ||
|
|
b5cb340a43 | ||
|
|
44089e28f7 | ||
|
|
1c6fb7f1ac | ||
|
|
4198b31cd7 | ||
|
|
638364d3d2 | ||
|
|
e7195e40b0 | ||
|
|
1e084234fd | ||
|
|
833813098d | ||
|
|
df0e27e56a | ||
|
|
5b8fbf8ea6 | ||
|
|
3286d38fc8 | ||
|
|
8838185cbf | ||
|
|
0be5bfd050 | ||
|
|
154b265e1f | ||
|
|
7a9e965cc2 | ||
|
|
e05a770f51 | ||
|
|
fd5e5ff7d4 | ||
|
|
53e73cc42a | ||
|
|
c419eef405 | ||
|
|
0c24ac38be | ||
|
|
67535d8691 | ||
|
|
1205d5d113 | ||
|
|
7be63d8e99 | ||
|
|
695cf18449 | ||
|
|
a117d42f67 | ||
|
|
a98dd3382c | ||
|
|
34e0755b78 | ||
|
|
975f8d81ce | ||
|
|
769b6465d1 | ||
|
|
12d3a781ff | ||
|
|
0bab251f3d | ||
|
|
8eb0d43287 | ||
|
|
44e69429ad | ||
|
|
d787de821a | ||
|
|
7c616718fd | ||
|
|
2edfab25d7 | ||
|
|
584fc6088e | ||
|
|
99254b3315 | ||
|
|
e5f3d86b9f | ||
|
|
0322aeada2 | ||
|
|
6d9ca8a820 | ||
|
|
c57f647ddd | ||
|
|
1f5669532f | ||
|
|
055d6a6790 | ||
|
|
9eb03f2c16 | ||
|
|
f6569f5091 | ||
|
|
cb0a632944 | ||
|
|
576c3cc7d2 | ||
|
|
8490e93b99 | ||
|
|
31d318b7d2 | ||
|
|
cd50872acb | ||
|
|
ff7ff8ee58 | ||
|
|
14d29dc167 | ||
|
|
99aa8579cd | ||
|
|
5df7be8fef | ||
|
|
4fbd56cf08 | ||
|
|
01f9ccdb4d | ||
|
|
c6749105e0 | ||
|
|
7a582a6576 | ||
|
|
dfab4af72c | ||
|
|
a200246843 | ||
|
|
89a6c467d4 | ||
|
|
a4bc108cdc | ||
|
|
d0f5b803fa | ||
|
|
10f1acc0ac | ||
|
|
10da4bc4e6 | ||
|
|
cb6d5865a4 | ||
|
|
fe19187dad | ||
|
|
024e9b247e | ||
|
|
d058940728 | ||
|
|
5ca16e8f1c | ||
|
|
d52ddfed36 | ||
|
|
b1ba8e4d6e | ||
|
|
2e9c5d3bac | ||
|
|
ac79d8482f | ||
|
|
597594b5bc | ||
|
|
7736259f70 | ||
|
|
f3558f4aa7 | ||
|
|
e6fa3dd823 | ||
|
|
c4b1502385 | ||
|
|
ccb75f4166 | ||
|
|
bc3932d008 | ||
|
|
9e29398b11 | ||
|
|
a0646862fc | ||
|
|
2085b0340f | ||
|
|
1550997d55 | ||
|
|
18f6c24809 | ||
|
|
7c387140d4 | ||
|
|
91765df50d | ||
|
|
ac4cd8f5c6 | ||
|
|
27ef9efb38 | ||
|
|
874dd9ff62 | ||
|
|
a3cce7edcd | ||
|
|
1ac98acefd | ||
|
|
9370a9331b | ||
|
|
cfe2522699 | ||
|
|
ee9bd8dd1a | ||
|
|
532ac4f8a7 | ||
|
|
1859fce603 | ||
|
|
452c1da6b8 | ||
|
|
75569abbe2 | ||
|
|
0ce772a6d6 | ||
|
|
92c28368f6 | ||
|
|
216228d9e4 | ||
|
|
c13145971a | ||
|
|
c78e90588a | ||
|
|
8e3b7b0418 | ||
|
|
f3eac47dc7 | ||
|
|
bf7304df3b | ||
|
|
c5ddf1a1c7 | ||
|
|
de951e3ea0 | ||
|
|
44b7e34d7f | ||
|
|
e025496bbc | ||
|
|
db747ea246 | ||
|
|
84c1110f18 | ||
|
|
73253cda17 | ||
|
|
34f6ac59f6 | ||
|
|
da814b0a5a | ||
|
|
99421e7617 | ||
|
|
c30eb02d91 | ||
|
|
59185b6112 | ||
|
|
79c070ec0d | ||
|
|
ce4b1c8014 | ||
|
|
3d3d44440a | ||
|
|
a91f7126e1 | ||
|
|
d9358b6134 | ||
|
|
fd4cdcaba6 | ||
|
|
13fd612b50 | ||
|
|
905872232d | ||
|
|
409fa79f59 | ||
|
|
82304058b2 | ||
|
|
ec457a0e44 | ||
|
|
f176938831 | ||
|
|
33c5a40e9f | ||
|
|
57874362c3 | ||
|
|
c1581f3845 | ||
|
|
72d510a6c1 | ||
|
|
723233be84 | ||
|
|
ad9a0b2f50 | ||
|
|
b881ea1e03 | ||
|
|
e2fb3802d7 | ||
|
|
eb7674cf04 | ||
|
|
3ee169ba14 | ||
|
|
4a903053de | ||
|
|
961ad02e8a | ||
|
|
472a063979 | ||
|
|
4fd61c97df | ||
|
|
5b81c31ba2 | ||
|
|
ef9507a282 | ||
|
|
4efe137e82 | ||
|
|
cea5a282c2 | ||
|
|
2ad69bf1e9 | ||
|
|
397cfaf23a | ||
|
|
8223c073ef | ||
|
|
b564b5d0f6 | ||
|
|
c744f1ff93 | ||
|
|
39656bec03 | ||
|
|
5b1f7db2c5 | ||
|
|
f914026c14 | ||
|
|
156150bd36 | ||
|
|
99b7e83324 | ||
|
|
4222d98e49 | ||
|
|
cd00881afb | ||
|
|
d1cf1e4243 | ||
|
|
cf5227a81f | ||
|
|
2110fbee8d | ||
|
|
680116082b | ||
|
|
94092eb6af | ||
|
|
1286fb165c | ||
|
|
36a22bd5eb | ||
|
|
15fa4b80b2 | ||
|
|
2c5281a41c | ||
|
|
2e433d73db | ||
|
|
1f915c6c5e | ||
|
|
bec69b3abf | ||
|
|
530e82cc6d | ||
|
|
29765230b1 | ||
|
|
29a556c7d1 | ||
|
|
7f3d4ba8aa | ||
|
|
9c714c9c4e | ||
|
|
91ef9955b4 | ||
|
|
97f0f3784d | ||
|
|
42d903ac16 | ||
|
|
b9111348d3 | ||
|
|
69b7fa79de | ||
|
|
4c67233e83 | ||
|
|
c9d14992b9 | ||
|
|
f06411e018 | ||
|
|
73b9c419ff | ||
|
|
e763d98134 | ||
|
|
0533256b87 | ||
|
|
4d0c874a95 | ||
|
|
ae1608e0bb | ||
|
|
88ae7c8994 | ||
|
|
d697ce8c81 | ||
|
|
a684cf1294 | ||
|
|
b9d4028074 | ||
|
|
916cb7e76f | ||
|
|
6dd61af5dd | ||
|
|
5bee7c0791 | ||
|
|
a4a731f04a | ||
|
|
b15d3c5375 | ||
|
|
99ba73f194 | ||
|
|
1456c15f91 | ||
|
|
8a26580453 | ||
|
|
af393ad0ca | ||
|
|
8a46f3a7d2 | ||
|
|
0aff71496c | ||
|
|
f3a5642bcd | ||
|
|
b97e6e0870 | ||
|
|
3e704c3d22 | ||
|
|
ae02fa80ce | ||
|
|
ec5c1038b3 | ||
|
|
e5e6295a83 | ||
|
|
19560fe60f | ||
|
|
1f1b469a4b | ||
|
|
eb095a7497 | ||
|
|
6415b095ab | ||
|
|
d93c959da8 | ||
|
|
09893e1409 | ||
|
|
93ec6384d0 | ||
|
|
a221bf33b2 | ||
|
|
c7ec3d2197 | ||
|
|
18d04099fd | ||
|
|
fec52ee19f | ||
|
|
d87a22da23 | ||
|
|
4bda273947 | ||
|
|
7c8a2e32b4 | ||
|
|
45e8d0c2b6 | ||
|
|
3fe0b93567 | ||
|
|
06147dd5cc | ||
|
|
44dae3832e | ||
|
|
4f06d804e9 | ||
|
|
fb87b201da | ||
|
|
a58090b334 | ||
|
|
0050bfe36f | ||
|
|
d40233cb1e | ||
|
|
3125f8e7d1 | ||
|
|
0646fb77f2 | ||
|
|
c5a3a17a94 | ||
|
|
e4b1880025 | ||
|
|
c0b74c1605 | ||
|
|
96aed66af2 | ||
|
|
65e23a90a1 | ||
|
|
f3405b5724 | ||
|
|
5bef1e5663 | ||
|
|
5120cee05a | ||
|
|
895e593d02 | ||
|
|
32d8070119 | ||
|
|
46273ecbe5 | ||
|
|
4268099a06 | ||
|
|
921db2f6cb | ||
|
|
f640c45464 | ||
|
|
d4dc7a32a4 | ||
|
|
33e5171742 | ||
|
|
ebb4857782 | ||
|
|
c47d253967 | ||
|
|
c58e7d2ccf | ||
|
|
f3cad86f6f | ||
|
|
a23655de42 | ||
|
|
b7b899ea40 | ||
|
|
89029b1bc3 | ||
|
|
9b4212e00c | ||
|
|
6b158e1e23 | ||
|
|
6cae350ef2 | ||
|
|
3b83738208 | ||
|
|
6b894eadf7 | ||
|
|
13128b6a0f | ||
|
|
4b84238fb4 | ||
|
|
7345ecde7b | ||
|
|
8272cf0ade | ||
|
|
cae3d41af0 | ||
|
|
54061018ea | ||
|
|
0281b3c9f9 | ||
|
|
70c8d3f837 | ||
|
|
19dcd072c7 | ||
|
|
4c5474cb0f | ||
|
|
2c5baf6b87 | ||
|
|
508c2fe5b6 | ||
|
|
200bd1be74 | ||
|
|
29617de259 | ||
|
|
96f1a1153b | ||
|
|
c931fd1737 | ||
|
|
8f8e9a538d | ||
|
|
8dbec207cb | ||
|
|
4ca0fff9d9 | ||
|
|
97378addf5 | ||
|
|
1888243853 | ||
|
|
e0e61f7e7d | ||
|
|
d2b7054d80 | ||
|
|
afbd0a3301 | ||
|
|
5a8c10ba82 | ||
|
|
c1b0aaec2d | ||
|
|
3ac0b252ff | ||
|
|
f199c3051b | ||
|
|
aa575ee267 | ||
|
|
20c9120b3f | ||
|
|
8e17cff31c | ||
|
|
69641c0b93 | ||
|
|
0a39d8622c | ||
|
|
2a8f5c519b | ||
|
|
b53a898f82 | ||
|
|
45582751be | ||
|
|
819d238294 | ||
|
|
76dd0516cd | ||
|
|
1535cbb20d | ||
|
|
69c6ac9fdd | ||
|
|
2f9bc9a2bc | ||
|
|
6125523988 |
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1 @@
|
||||
FROM mcr.microsoft.com/devcontainers/base:bullseye
|
||||
24
.devcontainer/devcontainer.json
Normal file
24
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "PreMiD",
|
||||
"dockerComposeFile": ["docker-compose.yml"],
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "lts",
|
||||
"nvmVersion": "latest"
|
||||
},
|
||||
"ghcr.io/joshuanianji/devcontainer-features/mount-pnpm-store:1": {},
|
||||
"ghcr.io/dhoeric/features/act:1": {}
|
||||
},
|
||||
"overrideFeatureInstallOrder": ["ghcr.io/devcontainers/features/node:1", "ghcr.io/joshuanianji/devcontainer-features/mount-pnpm-store:1"],
|
||||
"postCreateCommand": "pnpm i --frozen-lockfile",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["Gruntfuggly.todo-tree", "YoavBls.pretty-ts-errors", "EditorConfig.EditorConfig", "DeepScan.vscode-deepscan", "esbenp.prettier-vscode"]
|
||||
}
|
||||
},
|
||||
"shutdownAction": "stopCompose"
|
||||
}
|
||||
32
.devcontainer/docker-compose.yml
Normal file
32
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||
# array). The sample below assumes your primary file is in the root of your project.
|
||||
#
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
- ..:/workspaces:cached
|
||||
|
||||
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||
# cap_add:
|
||||
# - SYS_PTRACE
|
||||
# security_opt:
|
||||
# - seccomp:unconfined
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.vscode
|
||||
.DS_Store
|
||||
.Trashes
|
||||
.nuxt
|
||||
.output
|
||||
dist
|
||||
node_modules
|
||||
.env
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
generated
|
||||
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
* text eol=lf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.ttf binary
|
||||
*.woff binary
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: [PreMiD, Timeraa]
|
||||
patreon: Timeraa
|
||||
ko_fi: Timeraa
|
||||
36
.github/actions/build-and-push-docker/action.yaml
vendored
Normal file
36
.github/actions/build-and-push-docker/action.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Build and Push Docker Image
|
||||
description: Builds a Docker image and pushes it to GitHub Container Registry
|
||||
inputs:
|
||||
app:
|
||||
description: Name of the app
|
||||
required: true
|
||||
token:
|
||||
description: GitHub token
|
||||
required: true
|
||||
outputs:
|
||||
version:
|
||||
description: Version of the app
|
||||
value: ${{ steps.get_version.outputs.version }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get package.json version
|
||||
id: get_version
|
||||
run: echo ::set-output name=version::$(node -p "require('./apps/${{ inputs.app }}/package.json').version")
|
||||
shell: bash
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repo
|
||||
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
||||
shell: bash
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: premid/premid/.github/actions/build-docker@monorepo
|
||||
with:
|
||||
dockerfile: ./apps/${{ inputs.app }}/Dockerfile
|
||||
push: true
|
||||
token: ${{ inputs.token }}
|
||||
tags: ghcr.io/${{ steps.repo.outputs.lowercase }}/${{ inputs.app }}:${{ steps.get_version.outputs.version }},ghcr.io/${{ steps.repo.outputs.lowercase }}/${{ inputs.app }}:latest
|
||||
46
.github/actions/build-docker/action.yaml
vendored
Normal file
46
.github/actions/build-docker/action.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build Docker Image
|
||||
description: Builds a Docker image using Docker Buildx
|
||||
inputs:
|
||||
dockerfile:
|
||||
description: Path to the Dockerfile
|
||||
required: true
|
||||
tags:
|
||||
description: Comma-separated list of tags for the Docker image
|
||||
required: true
|
||||
push:
|
||||
description: Whether to push the Docker image to the registry
|
||||
required: false
|
||||
default: "false"
|
||||
token:
|
||||
description: GitHub Token
|
||||
required: false
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.push == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ inputs.token }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ inputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
5
.github/renovate.json
vendored
Normal file
5
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>Recodive/Recodive:renovate-config"],
|
||||
"automerge": false
|
||||
}
|
||||
68
.github/workflows/cd.yaml
vendored
Normal file
68
.github/workflows/cd.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: CD
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- monorepo
|
||||
tags:
|
||||
- "*"
|
||||
permissions:
|
||||
packages: write
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- pd
|
||||
- schema-server
|
||||
- website
|
||||
- api-worker
|
||||
- api-master
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push website
|
||||
uses: docker/build-push-action@v6
|
||||
if: matrix.target == 'website'
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: SERVICE=${{ matrix.target }}
|
||||
target: website
|
||||
tags: ghcr.io/premid/${{ matrix.target }}:beta-${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Get package.json version
|
||||
if: matrix.target != 'website'
|
||||
id: get_version
|
||||
run: echo ::set-output name=version::$(node -p "require('./apps/${{ matrix.target }}/package.json').version")
|
||||
shell: bash
|
||||
|
||||
- name: Build and push other images
|
||||
uses: docker/build-push-action@v6
|
||||
if: matrix.target != 'website' && startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
target: prod
|
||||
build-args: SERVICE=${{ matrix.target }}
|
||||
tags: ghcr.io/premid/${{ matrix.target }}:latest,ghcr.io/premid/${{ matrix.target }}:${{ steps.get_version.outputs.version }}
|
||||
71
.github/workflows/ci.yaml
vendored
Normal file
71
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Build, Lint and Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- monorepo
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: Build, Lint and Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: pnpm
|
||||
node-version-file: package.json
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Codegen
|
||||
run: pnpm -r codegen
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
build-docker:
|
||||
name: Build Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- pd
|
||||
- schema-server
|
||||
- api-worker
|
||||
- api-master
|
||||
- website
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get Target
|
||||
id: get_target
|
||||
run: echo "target=$([[ ${{ matrix.target }} == 'website' ]] && echo 'website' || echo 'prod')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
target: ${{ steps.get_target.outputs.target }}
|
||||
build-args: SERVICE=${{ matrix.target }}
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,2 +1,25 @@
|
||||
node_modules
|
||||
out
|
||||
dist
|
||||
tmp
|
||||
lib
|
||||
|
||||
.vscode
|
||||
.env
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
|
||||
src/package-lock.json
|
||||
src/package.json
|
||||
src/update.ini
|
||||
|
||||
*.exe
|
||||
*.app
|
||||
*.xml.backup
|
||||
*.js
|
||||
!eslint.config.js
|
||||
|
||||
coverage
|
||||
*.tsbuildinfo
|
||||
2
.husky/commit-msg
Normal file
2
.husky/commit-msg
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
pnpm exec commitlint --edit $1
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*.js
|
||||
*.ts
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @Timeraa
|
||||
39
CODE_OF_CONDUCT.md
Normal file
39
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@premid.app or by contacting a staff member on our [Discord server](https://discord.premid.app). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
FROM gplane/pnpm:node20-alpine AS base
|
||||
RUN corepack enable
|
||||
ARG SERVICE
|
||||
|
||||
FROM base AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN pnpm i --frozen-lockfile
|
||||
|
||||
RUN if [ "$SERVICE" != "website" ]; then pnpm run -r codegen; fi
|
||||
RUN if [ "$SERVICE" != "website" ]; then pnpm run build; fi
|
||||
RUN if [ "$SERVICE" == "website" ]; then pnpm --filter @premid/website run build; fi
|
||||
RUN if [ "$SERVICE" != "website" ]; then pnpm --filter @premid/${SERVICE} deploy --prod /prod/${SERVICE}; fi
|
||||
|
||||
FROM node:20-alpine AS prod
|
||||
ARG SERVICE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /prod/${SERVICE} ./
|
||||
ENV PORT=80
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
FROM node:20-alpine AS website
|
||||
WORKDIR /app
|
||||
ENV PORT=80
|
||||
|
||||
COPY --from=build /app/apps/website/.output /app
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["node", "server/index.mjs"]
|
||||
373
LICENSE
Normal file
373
LICENSE
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
54
README.md
Normal file
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
<img src="https://cdn.rcd.gg/PreMiD.png" width="150px" />
|
||||
|
||||
# PreMiD
|
||||
|
||||
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/PreMiD/PreMiD)
|
||||
[](https://crowdin.com/project/premid)
|
||||
|
||||
This is the monorepo for PreMiD. PreMiD is a simple, configurable utility that allows you to show what you're watching/listening to on your Discord profile.
|
||||
|
||||
## Getting Started
|
||||
|
||||
**If you are a user looking to install PreMiD, please visit the [official website](https://premid.app).**
|
||||
|
||||
If you are a developer looking to contribute to PreMiD, read along.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Packages](#packages)
|
||||
- [License](#license)
|
||||
|
||||
## Packages
|
||||
|
||||
This monorepo is split into multiple packages / projects. Here's a list of them:
|
||||
|
||||
- [apps/api](apps/api) - The API for PreMiD.
|
||||
- [apps/website](apps/website) - The website for PreMiD.
|
||||
- [apps/docs](apps/docs) - The official documentation for PreMiD.
|
||||
- [apps/pd](apps/pd/README.md) - A simple url shortener service to shorten urls longer than 256 characters.
|
||||
- [apps/schema-server](apps/schema-server) - Simple Schema server for the Presence manifest.
|
||||
- [packages/db](packages/db) - Database schema for PreMiD.
|
||||
|
||||
## Development
|
||||
|
||||
### Release
|
||||
|
||||
To release a new version of a package, run the following command:
|
||||
|
||||
```bash
|
||||
cd apps/<app>
|
||||
pnpm bumpp -y -t <app>-v
|
||||
```
|
||||
|
||||
Replace `<app>` with the name of the package you want to release. For example, to release a new version of the `schema-server` package, you would run:
|
||||
|
||||
```bash
|
||||
cd apps/schema-server
|
||||
pnpm bumpp -y -t schema-server-v
|
||||
```
|
||||
|
||||
This will use bumpp to bump the version of the package in the `package.json` file, create a tag for the new version, and push the changes to the remote repository.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MPL-2.0 License](LICENSE).
|
||||
31
apps/api-master/package.json
Normal file
31
apps/api-master/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@premid/api-master",
|
||||
"type": "module",
|
||||
"version": "0.0.24",
|
||||
"private": true,
|
||||
"description": "PreMiD's api master",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps .",
|
||||
"dev": "node --watch --env-file .env --enable-source-maps ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@envelop/sentry": "^9.0.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.52.1",
|
||||
"@opentelemetry/node": "^0.24.0",
|
||||
"@sentry/node": "^8.17.0",
|
||||
"cron": "^3.1.7",
|
||||
"debug": "^4.3.6",
|
||||
"ioredis": "^5.3.2",
|
||||
"ky": "^1.7.2",
|
||||
"p-limit": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12"
|
||||
}
|
||||
}
|
||||
105
apps/api-master/src/functions/clearOldSessions.ts
Normal file
105
apps/api-master/src/functions/clearOldSessions.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import pLimit from "p-limit";
|
||||
import ky, { HTTPError, TimeoutError } from "ky";
|
||||
import { mainLog, redis } from "../index.js";
|
||||
|
||||
let inProgress = false;
|
||||
export async function clearOldSessions() {
|
||||
if (inProgress) {
|
||||
mainLog("Session cleanup already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
inProgress = true;
|
||||
const now = Date.now();
|
||||
let cursor = "0";
|
||||
let totalSessions = 0;
|
||||
let cleared = 0;
|
||||
const batchSize = 100;
|
||||
let keysToDelete: string[] = [];
|
||||
|
||||
mainLog("Starting session cleanup");
|
||||
|
||||
const limit = pLimit(100); // Create a limit of 100 concurrent operations
|
||||
|
||||
do {
|
||||
const [nextCursor, result] = await redis.hscan("pmd-api.sessions", cursor, "COUNT", batchSize);
|
||||
cursor = nextCursor;
|
||||
totalSessions += result.length / 2;
|
||||
|
||||
const deletePromises = [];
|
||||
|
||||
for (let i = 0; i < result.length; i += 2) {
|
||||
const key = result[i];
|
||||
const value = result[i + 1];
|
||||
|
||||
if (!key || !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const session = JSON.parse(value) as {
|
||||
token: string;
|
||||
session: string;
|
||||
lastUpdated: number;
|
||||
};
|
||||
|
||||
if (now - session.lastUpdated < 30000)
|
||||
continue;
|
||||
|
||||
deletePromises.push(limit(() => deleteSession(session, key)));
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(deletePromises);
|
||||
results.forEach((result) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
keysToDelete.push(result.value);
|
||||
cleared++;
|
||||
}
|
||||
});
|
||||
|
||||
if (keysToDelete.length >= batchSize) {
|
||||
await redis.hdel("pmd-api.sessions", ...keysToDelete);
|
||||
keysToDelete = [];
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
if (keysToDelete.length > 0) {
|
||||
await redis.hdel("pmd-api.sessions", ...keysToDelete);
|
||||
}
|
||||
|
||||
if (totalSessions === 0) {
|
||||
mainLog("No sessions to clear");
|
||||
}
|
||||
else {
|
||||
mainLog(`Checked ${totalSessions} sessions, cleared ${cleared}`);
|
||||
}
|
||||
|
||||
inProgress = false;
|
||||
}
|
||||
|
||||
async function deleteSession(session: { token: string; session: string }, key: string): Promise<string> {
|
||||
try {
|
||||
await ky.post("https://discord.com/api/v10/users/@me/headless-sessions/delete", {
|
||||
json: {
|
||||
token: session.session,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.token}`,
|
||||
},
|
||||
retry: 3,
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof TimeoutError) {
|
||||
mainLog(`Session deletion aborted due to timeout for key ${key}`);
|
||||
}
|
||||
else if (error instanceof HTTPError) {
|
||||
mainLog(`Failed to delete session for key ${key}: [${error.name}] ${error.message} ${JSON.stringify(await error.response.json())}`);
|
||||
}
|
||||
else {
|
||||
mainLog(`Failed to delete session for key ${key}: Unknown error`);
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
30
apps/api-master/src/functions/createRedis.ts
Normal file
30
apps/api-master/src/functions/createRedis.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { hostname } from "node:os";
|
||||
import process from "node:process";
|
||||
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
/* c8 ignore start */
|
||||
export default function createRedis(): Redis {
|
||||
const redis = new Redis({
|
||||
connectionName: `api-master-${hostname()}-${process.pid.toString()}`,
|
||||
lazyConnect: true,
|
||||
name: "mymaster",
|
||||
sentinels: process.env.REDIS_SENTINELS?.split(",").map(s => ({
|
||||
host: s,
|
||||
port: 26_379,
|
||||
})),
|
||||
});
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
redis.on("error", (error) => {
|
||||
console.error("Redis error", error);
|
||||
});
|
||||
|
||||
/* c8 ignore next 4 */
|
||||
redis.on("connect", () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Redis connected");
|
||||
});
|
||||
|
||||
return redis;
|
||||
}
|
||||
13
apps/api-master/src/functions/setCounter.ts
Normal file
13
apps/api-master/src/functions/setCounter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { redis } from "../index.js";
|
||||
import { counter } from "../tracing.js";
|
||||
|
||||
let activeActivities = 0;
|
||||
counter.add(0);
|
||||
export async function setCounter() {
|
||||
const length = await redis.hlen("pmd-api.sessions");
|
||||
if (length === activeActivities)
|
||||
return;
|
||||
const diff = length - activeActivities;
|
||||
activeActivities = length;
|
||||
counter.add(diff);
|
||||
}
|
||||
39
apps/api-master/src/functions/updateActivePresenceGauge.ts
Normal file
39
apps/api-master/src/functions/updateActivePresenceGauge.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { redis } from "../index.js";
|
||||
import { activePresenceGauge } from "../tracing.js";
|
||||
|
||||
//* Track previously recorded services
|
||||
const previousServices = new Set<string>();
|
||||
|
||||
//* Function to update the gauge with per-service counts
|
||||
export async function updateActivePresenceGauge() {
|
||||
const pattern = "pmd-api.heartbeatUpdates.*";
|
||||
let cursor: string = "0";
|
||||
const serviceCounts = new Map<string, number>();
|
||||
|
||||
do {
|
||||
const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); //* Use SCAN with COUNT for memory efficiency
|
||||
cursor = newCursor;
|
||||
for (const key of keys) {
|
||||
const hash = await redis.hgetall(key);
|
||||
const service = hash.service;
|
||||
const version = hash.version; //* Get version from hash
|
||||
serviceCounts.set(`${service}:${version}`, (serviceCounts.get(`${service}:${version}`) || 0) + 1);
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
// Set current counts and remove from previousServices
|
||||
serviceCounts.forEach((count, serviceVersion) => {
|
||||
const [presence_name, version] = serviceVersion.split(":");
|
||||
activePresenceGauge.record(count, { presence_name, version });
|
||||
previousServices.delete(serviceVersion);
|
||||
});
|
||||
|
||||
// Set gauge to 0 for services that are no longer active
|
||||
previousServices.forEach((serviceVersion) => {
|
||||
const [presence_name, version] = serviceVersion.split(":");
|
||||
activePresenceGauge.record(0, { presence_name, version });
|
||||
});
|
||||
|
||||
// Update the set of previous services
|
||||
serviceCounts.forEach((_, serviceVersion) => previousServices.add(serviceVersion));
|
||||
}
|
||||
44
apps/api-master/src/index.ts
Normal file
44
apps/api-master/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { CronJob } from "cron";
|
||||
|
||||
import debug from "debug";
|
||||
import { clearOldSessions } from "./functions/clearOldSessions.js";
|
||||
import createRedis from "./functions/createRedis.js";
|
||||
import { setCounter } from "./functions/setCounter.js";
|
||||
import "./tracing.js";
|
||||
import { updateActivePresenceGauge } from "./functions/updateActivePresenceGauge.js"; //* Added import
|
||||
|
||||
export const redis = createRedis();
|
||||
|
||||
export const mainLog = debug("api-master");
|
||||
|
||||
debug("Starting cron jobs");
|
||||
|
||||
void new CronJob(
|
||||
// Every 5 seconds
|
||||
"*/5 * * * * *",
|
||||
() => {
|
||||
clearOldSessions();
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
void new CronJob(
|
||||
// Every second
|
||||
"* * * * * *",
|
||||
() => {
|
||||
setCounter();
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
void new CronJob(
|
||||
// Every 5 seconds
|
||||
"*/5 * * * * *",
|
||||
() => {
|
||||
updateActivePresenceGauge();
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
24
apps/api-master/src/tracing.ts
Normal file
24
apps/api-master/src/tracing.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ValueType } from "@opentelemetry/api";
|
||||
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const prometheusExporter = new PrometheusExporter();
|
||||
|
||||
const provider = new MeterProvider({
|
||||
readers: [prometheusExporter],
|
||||
});
|
||||
|
||||
const meter = provider.getMeter("nice");
|
||||
|
||||
export const counter = meter.createUpDownCounter("active_activites", {
|
||||
description: "Number of active activities",
|
||||
valueType: ValueType.INT,
|
||||
});
|
||||
|
||||
// * Replace Observable Gauge with regular Gauge
|
||||
export const activePresenceGauge = meter.createGauge("active_presence_names", {
|
||||
description: "Number of active presence names per service",
|
||||
valueType: ValueType.INT,
|
||||
});
|
||||
|
||||
prometheusExporter.startServer();
|
||||
9
apps/api-master/tsconfig.app.json
Normal file
9
apps/api-master/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
8
apps/api-master/tsconfig.json
Normal file
8
apps/api-master/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["environment.d.ts", "src", "codegen.ts"]
|
||||
}
|
||||
1
apps/api-worker/.gitignore
vendored
Normal file
1
apps/api-worker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
generated
|
||||
26
apps/api-worker/codegen.ts
Normal file
26
apps/api-worker/codegen.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
generates: {
|
||||
"dist/generated/schema-v5.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
schema: "src/graphql/schema/v5/**/*.gql",
|
||||
},
|
||||
"src/generated/graphql-v5.ts": {
|
||||
config: {
|
||||
scalars: {
|
||||
StringOrStringArray: "string | string[]",
|
||||
},
|
||||
},
|
||||
plugins: ["typescript", "typescript-resolvers"],
|
||||
schema: "src/graphql/schema/v5/**/*.gql",
|
||||
},
|
||||
"src/generated/schema-v5.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
schema: "src/graphql/schema/v5/**/*.gql",
|
||||
},
|
||||
},
|
||||
overwrite: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
7
apps/api-worker/environment.d.ts
vendored
Normal file
7
apps/api-worker/environment.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
NODE_ENV?: "development" | "production" | "test";
|
||||
DATABASE_URL?: string;
|
||||
SESSION_KEEP_ALIVE_INTERVAL?: string;
|
||||
}
|
||||
}
|
||||
49
apps/api-worker/package.json
Normal file
49
apps/api-worker/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@premid/api-worker",
|
||||
"type": "module",
|
||||
"version": "0.0.13",
|
||||
"private": true,
|
||||
"description": "PreMiD's api",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps .",
|
||||
"dev": "node --watch --env-file .env --enable-source-maps .",
|
||||
"build": "pnpm codegen",
|
||||
"codegen": "graphql-codegen --config codegen.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "^2.3.0",
|
||||
"@envelop/sentry": "^9.0.0",
|
||||
"@escape.tech/graphql-armor-max-aliases": "^2.5.0",
|
||||
"@escape.tech/graphql-armor-max-depth": "^2.3.0",
|
||||
"@escape.tech/graphql-armor-max-directives": "^2.2.0",
|
||||
"@escape.tech/graphql-armor-max-tokens": "^2.4.0",
|
||||
"@fastify/websocket": "^10.0.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.52.1",
|
||||
"@opentelemetry/node": "^0.24.0",
|
||||
"@premid/db": "workspace:*",
|
||||
"@sentry/node": "^8.17.0",
|
||||
"arktype": "2.0.0-rc.6",
|
||||
"defu": "^6.1.4",
|
||||
"discord-api-types": "^0.37.92",
|
||||
"fastify": "^4.28.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-parse-resolve-info": "^4.13.0",
|
||||
"graphql-yoga": "^5.6.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"mongoose": "^8.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.2",
|
||||
"@graphql-codegen/schema-ast": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "4.0.9",
|
||||
"@graphql-codegen/typescript-resolvers": "4.2.1",
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"@types/ws": "^8.5.12"
|
||||
}
|
||||
}
|
||||
110
apps/api-worker/src/classes/Socket.ts
Normal file
110
apps/api-worker/src/classes/Socket.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { REST } from "@discordjs/rest";
|
||||
import { scope, type } from "arktype";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import WebSocket from "ws";
|
||||
import type { FastifyRequest } from "fastify";
|
||||
import type { RawData } from "ws";
|
||||
import { redis } from "../functions/createServer.js";
|
||||
import { counter } from "../tracing.js";
|
||||
|
||||
const schema = scope({
|
||||
token: {
|
||||
"+": "delete",
|
||||
"type": "'token'",
|
||||
"token": "string.trim",
|
||||
"expires": "number.epoch",
|
||||
},
|
||||
session: {
|
||||
"+": "delete",
|
||||
"type": "'session'",
|
||||
"token": "string.trim",
|
||||
},
|
||||
validMessages: "token | session",
|
||||
}).export();
|
||||
|
||||
export class Socket {
|
||||
currentToken: typeof schema.token.infer | undefined;
|
||||
currentSession: typeof schema.session.infer | undefined;
|
||||
discord = new REST({ version: "10", authPrefix: "Bearer" });
|
||||
|
||||
constructor(
|
||||
public readonly socket: WebSocket.WebSocket,
|
||||
public readonly request: FastifyRequest,
|
||||
) {
|
||||
counter.add(1);
|
||||
socket.on("message", this.onMessage.bind(this));
|
||||
socket.on("close", () => this.onClose());
|
||||
}
|
||||
|
||||
async onMessage(message: RawData) {
|
||||
try {
|
||||
const out = schema.validMessages(JSON.parse(message.toString()));
|
||||
|
||||
if (out instanceof type.errors) {
|
||||
return this.close(1003, out.summary);
|
||||
}
|
||||
|
||||
switch (out.type) {
|
||||
case "token": {
|
||||
this.discord.setToken(out.token);
|
||||
if (!await this.isTokenValid(out)) {
|
||||
return this.close(1003, "Invalid token");
|
||||
}
|
||||
this.currentToken = out;
|
||||
break;
|
||||
}
|
||||
case "session": {
|
||||
await redis.hdel("pmd-api.sessions", out.token);
|
||||
this.currentSession = out;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
this.close(1011, "Internal Error");
|
||||
}
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
counter.add(-1);
|
||||
|
||||
if (!this.currentToken || !this.currentSession)
|
||||
return;
|
||||
|
||||
await redis.hset(
|
||||
"pmd-api.sessions",
|
||||
this.currentSession.token,
|
||||
JSON.stringify({
|
||||
session: this.currentSession.token,
|
||||
token: this.currentToken.token,
|
||||
lastUpdated: Date.now(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async isTokenValid(token: typeof schema.token.infer) {
|
||||
// ? Check the expiration date of the token
|
||||
if (token.expires < Date.now())
|
||||
return false;
|
||||
|
||||
// ? See if we can get the user's information
|
||||
try {
|
||||
await this.discord.get(Routes.user());
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
send(data: any) {
|
||||
this.socket.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
close(code: number = 1000, message?: string) {
|
||||
if (this.socket.readyState === WebSocket.CLOSED)
|
||||
return;
|
||||
this.socket.close(code, message);
|
||||
}
|
||||
}
|
||||
10
apps/api-worker/src/constants.ts
Normal file
10
apps/api-worker/src/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import process from "node:process";
|
||||
import { defu } from "defu";
|
||||
|
||||
const disabledFlags = process.env.DISABLED_FEATURE_FLAGS?.split(",") ?? [];
|
||||
const flags = Object.fromEntries(disabledFlags.map(flag => [flag, false]));
|
||||
|
||||
export const featureFlags = defu(flags, {
|
||||
WebSocketManager: true,
|
||||
SessionKeepAlive: true,
|
||||
});
|
||||
30
apps/api-worker/src/functions/createRedis.ts
Normal file
30
apps/api-worker/src/functions/createRedis.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { hostname } from "node:os";
|
||||
import process from "node:process";
|
||||
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
/* c8 ignore start */
|
||||
export default function createRedis(): Redis {
|
||||
const redis = new Redis({
|
||||
connectionName: `api-${hostname()}-${process.pid.toString()}`,
|
||||
lazyConnect: true,
|
||||
name: "mymaster",
|
||||
sentinels: process.env.REDIS_SENTINELS?.split(",").map(s => ({
|
||||
host: s,
|
||||
port: 26_379,
|
||||
})),
|
||||
});
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
redis.on("error", (error) => {
|
||||
console.error("Redis error", error);
|
||||
});
|
||||
|
||||
/* c8 ignore next 4 */
|
||||
redis.on("connect", () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Redis connected");
|
||||
});
|
||||
|
||||
return redis;
|
||||
}
|
||||
25
apps/api-worker/src/functions/createServer.test.ts
Normal file
25
apps/api-worker/src/functions/createServer.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe.concurrent("createServer", () => {
|
||||
it("should create a server", async () => {
|
||||
const createServer = await import("./createServer.js");
|
||||
const server = await createServer.default();
|
||||
expect(server).toBeDefined();
|
||||
expect(server).toHaveProperty("listen");
|
||||
});
|
||||
|
||||
it("should handle graphql requests", async () => {
|
||||
const createServer = await import("./createServer.js");
|
||||
const server = await createServer.default();
|
||||
expect(server).toBeDefined();
|
||||
expect(server).toHaveProperty("listen");
|
||||
|
||||
const response = await server.inject({
|
||||
method: "GET",
|
||||
url: "/v5/graphql",
|
||||
});
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
97
apps/api-worker/src/functions/createServer.ts
Normal file
97
apps/api-worker/src/functions/createServer.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { useSentry } from "@envelop/sentry";
|
||||
import { maxAliasesPlugin } from "@escape.tech/graphql-armor-max-aliases";
|
||||
import { maxDepthPlugin } from "@escape.tech/graphql-armor-max-depth";
|
||||
import { maxDirectivesPlugin } from "@escape.tech/graphql-armor-max-directives";
|
||||
import { maxTokensPlugin } from "@escape.tech/graphql-armor-max-tokens";
|
||||
import fastifyWebsocket from "@fastify/websocket";
|
||||
import fastify from "fastify";
|
||||
|
||||
import { createSchema, createYoga } from "graphql-yoga";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { Socket } from "../classes/Socket.js";
|
||||
import { resolvers } from "../graphql/resolvers/v5/index.js";
|
||||
import { sessionKeepAlive } from "../routes/sessionKeepAlive.js";
|
||||
import { featureFlags } from "../constants.js";
|
||||
import createRedis from "./createRedis.js";
|
||||
|
||||
export interface FastifyContext {
|
||||
request: FastifyRequest;
|
||||
reply: FastifyReply;
|
||||
}
|
||||
|
||||
const __dirname = new URL(".", import.meta.url).pathname;
|
||||
|
||||
export default async function createServer() {
|
||||
const app = fastify({ logger: true });
|
||||
const yoga = createYoga<FastifyContext>({
|
||||
graphqlEndpoint: "/v5/graphql",
|
||||
logging: {
|
||||
/* c8 ignore next 12 */
|
||||
debug: (...arguments_) => {
|
||||
for (const argument of arguments_) app.log.debug(argument);
|
||||
},
|
||||
error: (...arguments_) => {
|
||||
for (const argument of arguments_) app.log.error(argument);
|
||||
},
|
||||
info: (...arguments_) => {
|
||||
for (const argument of arguments_) app.log.info(argument);
|
||||
},
|
||||
warn: (...arguments_) => {
|
||||
for (const argument of arguments_) app.log.warn(argument);
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
maxAliasesPlugin(),
|
||||
maxDepthPlugin(),
|
||||
maxDirectivesPlugin(),
|
||||
maxTokensPlugin(),
|
||||
/* useSentry(), */
|
||||
],
|
||||
schema: createSchema<FastifyContext>({
|
||||
resolvers,
|
||||
typeDefs: await readFile(
|
||||
resolve(__dirname, "../generated/schema-v5.graphql"),
|
||||
"utf8",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
app.route({
|
||||
handler: async (request, reply) => {
|
||||
const response = await yoga.handleNodeRequest(request, {
|
||||
reply,
|
||||
request,
|
||||
});
|
||||
for (const [key, value] of response.headers.entries())
|
||||
void reply.header(key, value);
|
||||
|
||||
void reply.status(response.status);
|
||||
|
||||
void reply.send(response.body);
|
||||
|
||||
return reply;
|
||||
},
|
||||
method: ["GET", "POST", "OPTIONS"],
|
||||
url: "/v5/graphql",
|
||||
});
|
||||
|
||||
app.register(fastifyWebsocket);
|
||||
|
||||
app.register(async (app) => {
|
||||
app.get("/v5/ws", { websocket: true }, (websocket, request) => {
|
||||
void new Socket(websocket, request);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/v5/feature-flags", async (request, reply) => {
|
||||
void reply.send(featureFlags);
|
||||
});
|
||||
|
||||
app.post("/v5/session-keep-alive", sessionKeepAlive);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export const redis = createRedis();
|
||||
@@ -0,0 +1,32 @@
|
||||
import { type } from "arktype";
|
||||
import { redis } from "../../../../functions/createServer.js";
|
||||
import type { MutationResolvers } from "../../../../generated/graphql-v5.js";
|
||||
|
||||
const addScienceSchema = type({
|
||||
identifier: "string.uuid & string.lower",
|
||||
presences: "string.trim[]",
|
||||
platform: {
|
||||
arch: "string.trim",
|
||||
os: "string.trim",
|
||||
},
|
||||
});
|
||||
|
||||
const mutation: MutationResolvers["addScience"] = async (_parent, input) => {
|
||||
const out = addScienceSchema(input);
|
||||
|
||||
if (out instanceof type.errors)
|
||||
throw new Error(out.summary);
|
||||
|
||||
await redis.hset(
|
||||
"pmd-api.scienceUpdates",
|
||||
out.identifier,
|
||||
JSON.stringify(out),
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: "AddScienceResult",
|
||||
...out,
|
||||
};
|
||||
};
|
||||
|
||||
export default mutation;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { type } from "arktype";
|
||||
import type { MutationResolvers } from "../../../../generated/graphql-v5.js";
|
||||
import { redis } from "../../../../functions/createServer.js";
|
||||
|
||||
const heartbeatSchema = type({
|
||||
identifier: "string.uuid & string.lower",
|
||||
presence: {
|
||||
service: "string.trim",
|
||||
version: "string.semver",
|
||||
language: "string.trim",
|
||||
since: "number.epoch",
|
||||
},
|
||||
extension: {
|
||||
"version": "string.semver",
|
||||
"language": "string.trim",
|
||||
"connected?": {
|
||||
app: "number.integer",
|
||||
discord: "boolean",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mutation: MutationResolvers["heartbeat"] = async (_parent, input) => {
|
||||
const out = heartbeatSchema(input);
|
||||
|
||||
if (out instanceof type.errors)
|
||||
throw new Error(out.summary);
|
||||
|
||||
// * Use Redis Hash with 'service' in the key to store heartbeat data
|
||||
const redisKey = `pmd-api.heartbeatUpdates.${out.identifier}`;
|
||||
await redis.hset(redisKey, {
|
||||
service: out.presence.service,
|
||||
version: out.presence.version,
|
||||
language: out.presence.language,
|
||||
since: out.presence.since.toString(),
|
||||
extension_version: out.extension.version,
|
||||
extension_language: out.extension.language,
|
||||
extension_connected_app: out.extension.connected?.app?.toString(),
|
||||
extension_connected_discord: out.extension.connected?.discord?.toString(),
|
||||
});
|
||||
await redis.expire(redisKey, 300);
|
||||
|
||||
return {
|
||||
__typename: "HeartbeatResult",
|
||||
...out,
|
||||
};
|
||||
};
|
||||
|
||||
export default mutation;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { MutationResolvers } from "../../../../generated/graphql-v5.js";
|
||||
import addScience from "./addScience.js";
|
||||
import heartbeat from "./heartbeat.js";
|
||||
|
||||
export const Mutation: MutationResolvers = {
|
||||
addScience,
|
||||
heartbeat,
|
||||
};
|
||||
6
apps/api-worker/src/graphql/resolvers/v5/Query/index.ts
Normal file
6
apps/api-worker/src/graphql/resolvers/v5/Query/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { QueryResolvers } from "../../../../generated/graphql-v5.js";
|
||||
import presences from "./presences.js";
|
||||
|
||||
export const Query: QueryResolvers = {
|
||||
presences,
|
||||
};
|
||||
58
apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts
Normal file
58
apps/api-worker/src/graphql/resolvers/v5/Query/presences.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Presence } from "@premid/db";
|
||||
import { parseResolveInfo } from "graphql-parse-resolve-info";
|
||||
import type { PresenceSchema } from "@premid/db/Presence.js";
|
||||
import type { FilterQuery } from "mongoose";
|
||||
|
||||
import type { QueryResolvers } from "../../../../generated/graphql-v5.js";
|
||||
|
||||
const resolver: QueryResolvers["presences"] = async (
|
||||
_parent,
|
||||
{ author, contributor, limit, query, service, start, tag },
|
||||
_context,
|
||||
info,
|
||||
) => {
|
||||
const authorFilter: FilterQuery<PresenceSchema> = author
|
||||
? { "metadata.author.name": author }
|
||||
: {};
|
||||
const contributorFilter: FilterQuery<PresenceSchema> = contributor
|
||||
? { "metadata.contributors.name": contributor }
|
||||
: {};
|
||||
const serviceFilter: FilterQuery<PresenceSchema> = service
|
||||
? Array.isArray(service)
|
||||
? { "metadata.service": { $in: service } }
|
||||
: { "metadata.service": service }
|
||||
: {};
|
||||
const queryFilter: FilterQuery<PresenceSchema> = query
|
||||
? { "metadata.service": { $options: "i", $regex: query } }
|
||||
: {};
|
||||
const tagFilter: FilterQuery<PresenceSchema> = tag
|
||||
? { "metadata.tags": tag }
|
||||
: {};
|
||||
|
||||
const presences = await Presence.find(
|
||||
{
|
||||
...authorFilter,
|
||||
...contributorFilter,
|
||||
...serviceFilter,
|
||||
...queryFilter,
|
||||
...tagFilter,
|
||||
},
|
||||
Object.assign(
|
||||
{},
|
||||
...Object.keys(parseResolveInfo(info)!.fieldsByTypeName.Presence!).map(
|
||||
fieldName => ({ [fieldName]: true }),
|
||||
),
|
||||
) as Record<string, boolean>,
|
||||
{ ...(limit ? { limit } : {}), ...(start ? { skip: start } : {}) },
|
||||
);
|
||||
|
||||
return presences.map(presence => ({
|
||||
iframeJs: presence.iframeJs,
|
||||
metadata: presence.metadata,
|
||||
presenceJs: presence.presenceJs,
|
||||
url: presence.url,
|
||||
users: 0,
|
||||
}));
|
||||
};
|
||||
|
||||
export default resolver;
|
||||
8
apps/api-worker/src/graphql/resolvers/v5/index.ts
Normal file
8
apps/api-worker/src/graphql/resolvers/v5/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Resolvers } from "../../../generated/graphql-v5.js";
|
||||
import { Mutation } from "./Mutation/index.js";
|
||||
import { Query } from "./Query/index.js";
|
||||
|
||||
export const resolvers: Resolvers = {
|
||||
Query,
|
||||
Mutation,
|
||||
};
|
||||
19
apps/api-worker/src/graphql/schema/v5/addScience.gql
Normal file
19
apps/api-worker/src/graphql/schema/v5/addScience.gql
Normal file
@@ -0,0 +1,19 @@
|
||||
type Mutation {
|
||||
addScience(identifier: String!, presences: [String!]!, platform: PlatformInput!): AddScienceResult
|
||||
}
|
||||
|
||||
input PlatformInput {
|
||||
arch: String!
|
||||
os: String!
|
||||
}
|
||||
|
||||
type AddScienceResult {
|
||||
identifier: String!
|
||||
presences: [String!]!
|
||||
platform: Platform!
|
||||
}
|
||||
|
||||
type Platform {
|
||||
arch: String!
|
||||
os: String!
|
||||
}
|
||||
21
apps/api-worker/src/graphql/schema/v5/availableLanguages.gql
Normal file
21
apps/api-worker/src/graphql/schema/v5/availableLanguages.gql
Normal file
@@ -0,0 +1,21 @@
|
||||
type Query {
|
||||
"""
|
||||
Get the available languages
|
||||
"""
|
||||
availableLanguages: [Language!]!
|
||||
}
|
||||
|
||||
type Language {
|
||||
"""
|
||||
Language code
|
||||
"""
|
||||
lang: String!
|
||||
"""
|
||||
Native name of the language, eg. 'English', 'Deutsch', 'Español', etc.
|
||||
"""
|
||||
nativeName: String!
|
||||
"""
|
||||
'ltr' or 'rtl'
|
||||
"""
|
||||
direction: String!
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
type Query {
|
||||
"""
|
||||
Get the available presence languages for a specific presence
|
||||
"""
|
||||
availablePresenceLanguages(
|
||||
"""
|
||||
Presence, e.g. 'Netflix'
|
||||
"""
|
||||
presence: StringOrStringArray
|
||||
): [PresenceLanguage!]!
|
||||
}
|
||||
|
||||
type PresenceLanguage {
|
||||
"""
|
||||
Presence, e.g. 'Netflix'
|
||||
"""
|
||||
presence: String!
|
||||
"""
|
||||
The available languages for the presence
|
||||
"""
|
||||
languages: [Language!]!
|
||||
}
|
||||
49
apps/api-worker/src/graphql/schema/v5/heartbeat.gql
Normal file
49
apps/api-worker/src/graphql/schema/v5/heartbeat.gql
Normal file
@@ -0,0 +1,49 @@
|
||||
type Mutation {
|
||||
heartbeat(
|
||||
identifier: String!
|
||||
presence: HeartbeatPresenceInput
|
||||
extension: HeartbeatExtensionInput!
|
||||
): HeartbeatResult!
|
||||
}
|
||||
|
||||
input HeartbeatPresenceInput {
|
||||
service: String!
|
||||
version: String!
|
||||
language: String!
|
||||
since: Float!
|
||||
}
|
||||
|
||||
input HeartbeatExtensionInput {
|
||||
version: String!
|
||||
language: String!
|
||||
connected: HeartbeatConnectedInput
|
||||
}
|
||||
|
||||
input HeartbeatConnectedInput {
|
||||
app: Int!
|
||||
discord: Boolean!
|
||||
}
|
||||
|
||||
type HeartbeatResult {
|
||||
identifier: String!
|
||||
presence: HeartbeatPresence
|
||||
extension: HeartbeatExtension!
|
||||
}
|
||||
|
||||
type HeartbeatPresence {
|
||||
service: String!
|
||||
version: String!
|
||||
language: String!
|
||||
since: Float!
|
||||
}
|
||||
|
||||
type HeartbeatExtension {
|
||||
version: String!
|
||||
language: String!
|
||||
connected: HeartbeatConnected
|
||||
}
|
||||
|
||||
type HeartbeatConnected {
|
||||
app: Int!
|
||||
discord: Boolean!
|
||||
}
|
||||
62
apps/api-worker/src/graphql/schema/v5/presences.gql
Normal file
62
apps/api-worker/src/graphql/schema/v5/presences.gql
Normal file
@@ -0,0 +1,62 @@
|
||||
type Query {
|
||||
presences(
|
||||
service: StringOrStringArray
|
||||
author: String
|
||||
contributor: String
|
||||
start: Int
|
||||
limit: Int
|
||||
query: String
|
||||
tag: String
|
||||
): [Presence!]!
|
||||
}
|
||||
|
||||
type Presence {
|
||||
url: String!
|
||||
metadata: PresenceMetadata!
|
||||
presenceJs: String!
|
||||
iframeJs: String
|
||||
users: Int!
|
||||
}
|
||||
|
||||
type PresenceMetadata {
|
||||
author: PresenceMetadataUser!
|
||||
contributors: [PresenceMetadataUser!]
|
||||
altnames: [String!]
|
||||
service: String!
|
||||
description: Scalar! # serialize
|
||||
url: Scalar! # serialize
|
||||
version: String!
|
||||
logo: String!
|
||||
thumbnail: String!
|
||||
color: String!
|
||||
tags: [String!]!
|
||||
category: String!
|
||||
iframe: Boolean
|
||||
regExp: String
|
||||
iFrameRegExp: String
|
||||
readLogs: Boolean
|
||||
button: Boolean
|
||||
warning: Boolean
|
||||
settings: [PresenceMetadataSettings!]
|
||||
}
|
||||
|
||||
type PresenceMetadataUser {
|
||||
id: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type PresenceMetadataSettings {
|
||||
id: String!
|
||||
title: String
|
||||
icon: String
|
||||
if: PresenceMetadataSettingsIf # serialize
|
||||
placeholder: String
|
||||
value: Scalar # serialize
|
||||
values: Scalar # serialize
|
||||
multiLanguage: Scalar # serialize
|
||||
}
|
||||
|
||||
type PresenceMetadataSettingsIf {
|
||||
propertyNames: String
|
||||
patternProperties: Scalar
|
||||
}
|
||||
1
apps/api-worker/src/graphql/schema/v5/scalar/Scalar.gql
Normal file
1
apps/api-worker/src/graphql/schema/v5/scalar/Scalar.gql
Normal file
@@ -0,0 +1 @@
|
||||
scalar Scalar
|
||||
@@ -0,0 +1 @@
|
||||
scalar StringOrStringArray
|
||||
27
apps/api-worker/src/index.ts
Normal file
27
apps/api-worker/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable no-console */
|
||||
import process from "node:process";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { connect } from "mongoose";
|
||||
import "./tracing.js";
|
||||
import createServer from "./functions/createServer.js";
|
||||
|
||||
// TODO SETUP SENTRY
|
||||
Sentry.init({
|
||||
integrations: [
|
||||
Sentry.graphqlIntegration(),
|
||||
Sentry.mongooseIntegration(),
|
||||
],
|
||||
});
|
||||
|
||||
if (!process.env.DATABASE_URL)
|
||||
throw new Error("DATABASE_URL is not set");
|
||||
|
||||
await connect(process.env.DATABASE_URL, { appName: "PreMiD API" });
|
||||
|
||||
const server = await createServer();
|
||||
const url = await server.listen({
|
||||
port: Number.parseInt(process.env.PORT ?? "3001"),
|
||||
host: process.env.HOST ?? "0.0.0.0",
|
||||
});
|
||||
|
||||
console.log(`Server listening at ${url}`);
|
||||
64
apps/api-worker/src/routes/sessionKeepAlive.ts
Normal file
64
apps/api-worker/src/routes/sessionKeepAlive.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import process from "node:process";
|
||||
import { REST } from "@discordjs/rest";
|
||||
import { type } from "arktype";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { redis } from "../functions/createServer.js";
|
||||
import { featureFlags } from "../constants.js";
|
||||
|
||||
const schema = type({
|
||||
token: "string.trim",
|
||||
session: "string.trim",
|
||||
version: "string.semver & string.trim",
|
||||
scienceId: "string.trim",
|
||||
});
|
||||
|
||||
export async function sessionKeepAlive(request: FastifyRequest, reply: FastifyReply) {
|
||||
if (!featureFlags.SessionKeepAlive)
|
||||
return reply.status(202).send();
|
||||
|
||||
//* Get the headers
|
||||
const out = schema({
|
||||
token: request.headers["x-token"],
|
||||
session: request.headers["x-session"],
|
||||
version: request.headers["x-version"] ?? "2.6.8",
|
||||
scienceId: request.headers["x-science-id"] ?? request.headers["x-token"],
|
||||
});
|
||||
|
||||
if (out instanceof type.errors)
|
||||
return reply.status(400).send({ code: "MISSING_HEADERS", message: out.message });
|
||||
|
||||
if (!await isTokenValid(out.token))
|
||||
return reply.status(400).send({ code: "INVALID_TOKEN", message: "The token is invalid" });
|
||||
|
||||
await redis.hset(
|
||||
"pmd-api.sessions",
|
||||
out.scienceId,
|
||||
JSON.stringify({
|
||||
session: out.session,
|
||||
token: out.token,
|
||||
lastUpdated: Date.now(),
|
||||
}),
|
||||
);
|
||||
|
||||
const interval = Number.parseInt(process.env.SESSION_KEEP_ALIVE_INTERVAL ?? "5000");
|
||||
|
||||
return reply.status(200).send({
|
||||
code: "OK",
|
||||
message: "Session updated",
|
||||
nextUpdate: interval,
|
||||
});
|
||||
}
|
||||
|
||||
async function isTokenValid(token: string) {
|
||||
const discord = new REST({ version: "10", authPrefix: "Bearer" });
|
||||
|
||||
discord.setToken(token);
|
||||
try {
|
||||
await discord.get(Routes.user());
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
18
apps/api-worker/src/tracing.ts
Normal file
18
apps/api-worker/src/tracing.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ValueType } from "@opentelemetry/api";
|
||||
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const prometheusExporter = new PrometheusExporter();
|
||||
|
||||
const provider = new MeterProvider({
|
||||
readers: [prometheusExporter],
|
||||
});
|
||||
|
||||
const meter = provider.getMeter("nice");
|
||||
|
||||
export const counter = meter.createUpDownCounter("active_activites", {
|
||||
description: "Number of active activities",
|
||||
valueType: ValueType.INT,
|
||||
});
|
||||
|
||||
prometheusExporter.startServer();
|
||||
10
apps/api-worker/tsconfig.app.json
Normal file
10
apps/api-worker/tsconfig.app.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"types": ["@ark/schema"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
8
apps/api-worker/tsconfig.json
Normal file
8
apps/api-worker/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["environment.d.ts", "src", "codegen.ts"]
|
||||
}
|
||||
1
apps/docs/.vitepress/.gitignore
vendored
Normal file
1
apps/docs/.vitepress/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
cache
|
||||
129
apps/docs/.vitepress/config.mts
Normal file
129
apps/docs/.vitepress/config.mts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Documentation",
|
||||
description: "Official Documentation",
|
||||
locales: {
|
||||
root: {
|
||||
label: "English",
|
||||
lang: "en-US",
|
||||
},
|
||||
de: {
|
||||
label: "Deutsch",
|
||||
lang: "de-DE",
|
||||
},
|
||||
},
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{
|
||||
text: "Presence Development",
|
||||
link: "/dev/getting-started",
|
||||
},
|
||||
{
|
||||
text: "Reference",
|
||||
link: "/reference/presence",
|
||||
},
|
||||
],
|
||||
sidebar: {
|
||||
"/default": {
|
||||
base: "/",
|
||||
items: [
|
||||
{
|
||||
text: "Getting Started",
|
||||
link: "/",
|
||||
items: [
|
||||
{
|
||||
text: "Introduction",
|
||||
link: "/introduction/",
|
||||
},
|
||||
{
|
||||
text: "Installation",
|
||||
link: "/installation/",
|
||||
},
|
||||
{
|
||||
text: "Setup",
|
||||
link: "/setup/",
|
||||
},
|
||||
{
|
||||
text: "FAQ",
|
||||
link: "/faq/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Development",
|
||||
link: "/development/",
|
||||
items: [
|
||||
{
|
||||
text: "Presence Development",
|
||||
link: "/presence-development/",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Getting Started",
|
||||
link: "/presence-development/getting-started/",
|
||||
},
|
||||
{
|
||||
text: "Creating a Presence",
|
||||
link: "/presence-development/creating-a-presence/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Contribute",
|
||||
link: "/contribute/",
|
||||
items: [
|
||||
{
|
||||
text: "Report a Bug",
|
||||
link: "https://github.com/PreMiD",
|
||||
},
|
||||
{
|
||||
text: "Submit a Feature",
|
||||
link: "https://discord.premid.app",
|
||||
},
|
||||
{
|
||||
text: "Donate",
|
||||
link: "https://patreon.com/Timeraa",
|
||||
},
|
||||
{
|
||||
text: "Translate",
|
||||
link: "https://crowdin.com/project/premid",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"/reference/": {
|
||||
base: "/reference",
|
||||
items: [
|
||||
{
|
||||
text: "Reference",
|
||||
link: "/presence",
|
||||
items: [
|
||||
{
|
||||
text: "Presence",
|
||||
link: "/presence",
|
||||
},
|
||||
{
|
||||
text: "Iframe",
|
||||
link: "/iframe",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/PreMiD" },
|
||||
{ icon: "discord", link: "https://discord.premid.app" },
|
||||
{ icon: "x", link: "https://x.com/PreMiDapp" },
|
||||
],
|
||||
i18nRouting: true,
|
||||
logo: "/logo.svg",
|
||||
search: { provider: "local" },
|
||||
},
|
||||
lastUpdated: true,
|
||||
});
|
||||
77
apps/docs/dev/creating-a-presence.md
Normal file
77
apps/docs/dev/creating-a-presence.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Creating a Presence
|
||||
|
||||
## Introduction
|
||||
|
||||
PreMiD Presences are the core of the PreMiD application. They allow you to add support for your favorite websites and services, or create your own custom Presences. This guide will walk you through the process of creating a Presence for PreMiD.
|
||||
|
||||
Please go through the [Getting Started](./getting-started) guide before proceeding with this guide. It will help you set up your development environment and install the necessary tools.
|
||||
|
||||
To make the process of creating a Presence easier, we have provided a command-line interface (CLI) tool. This tool will help you generate a new Presence project with all the necessary files and configurations, so you can start coding your Presence right away.
|
||||
|
||||
The CLI will also help you build and test your Presence before submitting it to the PreMiD Store. Testing is required to ensure your Presence works as expected and meets the quality standards. Proof that your Presence works is required for it to be approved. This is usually done by providing a video or a screenshot of your Presence in action.
|
||||
|
||||
## Creating a New Presence
|
||||
|
||||
To create a new Presence, we will run some scripts. This will generate a new Presence project with all the necessary files and configurations. To create a new Presence, follow these steps:
|
||||
|
||||
1. Open your terminal and run the following command:
|
||||
|
||||
```sh
|
||||
pnpm create
|
||||
```
|
||||
|
||||
2. Follow the on-screen instructions to create a new Presence project.
|
||||
|
||||
### Coding your Presence
|
||||
|
||||
Once you have created a new Presence project, you can start coding your Presence. First of all, you need to understand the structure of a Presence project.
|
||||
|
||||
#### Presence Structure
|
||||
|
||||
A Presence project consists of the following files and directories:
|
||||
|
||||
- `metadata.json`: This file contains the metadata for your Presence, such as the name, description, and version.
|
||||
- `presence.ts`: This file contains the code for your Presence. This is where you will write the logic to detect the presence of your website or service.
|
||||
- `iframe.ts` (optional): This file contains the code for the Presence's iframe. This is where we will be able to interact with embedded iframes on the website.
|
||||
|
||||
#### Development Server
|
||||
|
||||
Let's start a Development Server to build and test your Presence. Follow these steps:
|
||||
|
||||
1. Start a development server to be able to build and test your Presence:
|
||||
|
||||
```sh
|
||||
pnpm dev "Your Presence Name"
|
||||
```
|
||||
|
||||
2. Open your browser and go to the PreMiD Extension settings page, then enable Developer Mode.
|
||||
3. You should now see your new Presence in the list of Presences.
|
||||
|
||||
#### Editing the `presence.ts` File
|
||||
|
||||
In order to fetch the data from the website, you need to write the logic in the `presence.ts` file. We will use native JavaScript functions to fetch the data from the website. Let's see an example:
|
||||
|
||||
```ts
|
||||
const presence = new Presence({
|
||||
clientId: "Your Client ID",
|
||||
});
|
||||
|
||||
const enum Asset {
|
||||
Logo = "https://cdn.rcd.gg/PreMiD.png",
|
||||
}
|
||||
|
||||
presence.on("UpdateData", async () => {
|
||||
const title = document.querySelector("title");
|
||||
const description = document.querySelector("meta[name=\"description\"]");
|
||||
|
||||
const data: PresenceData = {
|
||||
details: title.textContent,
|
||||
state: description.getAttribute("content"),
|
||||
largeImageKey: Asset.Logo,
|
||||
};
|
||||
|
||||
presence.setActivity(data);
|
||||
});
|
||||
```
|
||||
|
||||
In this example, we are fetching the title and description of the website and setting them as the Presence details and state, respectively. We are also setting a custom logo as the large image key.
|
||||
50
apps/docs/dev/getting-started.md
Normal file
50
apps/docs/dev/getting-started.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Git](https://git-scm.com/).
|
||||
- [Node.js](https://nodejs.org/) version 18 or higher, includes [Corepack](https://github.com/nodejs/corepack) by default.
|
||||
- Terminal for accessing PreMiD's Developer Tools via its command-line interface (CLI).
|
||||
- Text Editor with [TypeScript](https://www.typescriptlang.org/) syntax highlighting support.
|
||||
- [Visual Studio Code](https://code.visualstudio.com/) is recommended, as it includes TypeScript support out-of-the-box.
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
- Open your terminal and run the following command:
|
||||
```sh
|
||||
git clone https://github.com/PreMiD/Presences.git
|
||||
```
|
||||
- Change your working directory to the repository:
|
||||
```sh
|
||||
cd Presences
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
- Ensure you have Node.js installed by running:
|
||||
```sh
|
||||
node -v
|
||||
```
|
||||
If you see a version number, Node.js is installed. Make sure you have Node.js version 18 or higher.
|
||||
- Enable Corepack by running:
|
||||
```sh
|
||||
corepack enable
|
||||
```
|
||||
- Install the project dependencies:
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Coding your Presence
|
||||
|
||||
Follow the [Creating a Presence](./creating-a-presence) guide to get started with coding your own Presence.
|
||||
|
||||
## Submitting your Presence
|
||||
|
||||
Once you've finished coding your Presence, you can submit it to the PreMiD Store for others to use. Follow the [Submitting a Presence](./submitting-a-presence) guide to learn how to submit your Presence.
|
||||
|
||||
A member of the PreMiD Team will review your submission and if it meets the guidelines, it will be added to the PreMiD Store for everyone to use. If your submission is rejected, you will receive feedback on how to improve it.
|
||||
|
||||
Please note that all submissions are subject to review and approval by the PreMiD Team. We reserve the right to reject any submission that does not meet our guidelines or quality standards. Reviews may take up to 7 days to complete.
|
||||
0
apps/docs/dev/submitting-a-presence.md
Normal file
0
apps/docs/dev/submitting-a-presence.md
Normal file
5
apps/docs/getting-started.md
Normal file
5
apps/docs/getting-started.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Getting Started
|
||||
|
||||
Welcome to the official documentation for PreMiD! This guide will help you get started with PreMiD.
|
||||
|
||||
## Installation
|
||||
51
apps/docs/index.md
Normal file
51
apps/docs/index.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "PreMiD"
|
||||
text: "Documentation"
|
||||
tagline: "The official documentation for PreMiD."
|
||||
image:
|
||||
src: /logo.svg
|
||||
alt: PreMiD Logo
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /getting-started
|
||||
- theme: alt
|
||||
text: Presence Development
|
||||
link: /dev/getting-started
|
||||
features:
|
||||
- icon: 🛠️
|
||||
title: Extensible
|
||||
details: Add Presences for your favorite websites and services. Or create your own!
|
||||
- icon: 🌐
|
||||
title: Cross-Platform
|
||||
details: PreMiD is available for all major browsers and platforms.
|
||||
- icon: 🚀
|
||||
title: Lightweight
|
||||
details: PreMiD is designed to be as lightweight as possible, so it won't slow down your system.
|
||||
---
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, rgb(209, 122, 254) 30%, rgb(89, 195, 246));
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(-45deg, rgb(209, 122, 254) 50%, rgb(89, 195, 246) 50%);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
apps/docs/package.json
Normal file
14
apps/docs/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@premid/docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Documentation for Premid",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"preview": "vitepress preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "1.3.1"
|
||||
}
|
||||
}
|
||||
1
apps/docs/public/logo.svg
Normal file
1
apps/docs/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 288 KiB |
68
apps/docs/reference/presence.md
Normal file
68
apps/docs/reference/presence.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Presence Class
|
||||
|
||||
The `Presence` class is the main class used to create a Presence.
|
||||
|
||||
## Overview
|
||||
|
||||
The `Presence` class is the main class used to create a Presence. It is used to interact with the PreMiD Extension.
|
||||
|
||||
### Example
|
||||
|
||||
```javascript
|
||||
const presence = new Presence({
|
||||
clientId: "<Your Client ID>",
|
||||
});
|
||||
|
||||
presence.on("UpdateData", () => {
|
||||
// Logic to update the presence data
|
||||
|
||||
presence.setActivity({
|
||||
details: "Example Presence",
|
||||
state: "Example State",
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Constructor
|
||||
|
||||
### `new Presence(options: PresenceOptions)`
|
||||
|
||||
Creates a new `Presence` instance.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `options` (`PresenceOptions`): The options for the Presence.
|
||||
|
||||
#### Returns
|
||||
|
||||
- `Presence`: The new `Presence` instance.
|
||||
|
||||
## Properties
|
||||
|
||||
### `clientId: string`
|
||||
|
||||
The Client ID of the Presence.
|
||||
|
||||
## Methods
|
||||
|
||||
### `setActivity(activity: PresenceActivity)`
|
||||
|
||||
Sets the activity of the Presence.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `activity` (`PresenceActivity`): The activity to set.
|
||||
|
||||
### `clearActivity()`
|
||||
|
||||
Clears the activity of the Presence.
|
||||
|
||||
### `on(event: string, listener: Function)`
|
||||
|
||||
Adds a listener to an event.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `event` (`string`): The event to listen to.
|
||||
|
||||
- `listener` (`Function`): The listener to add.
|
||||
3
apps/pd/README.md
Normal file
3
apps/pd/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @premid/pd
|
||||
|
||||
A simple url shortener service to shorten urls longer than 256 characters.
|
||||
12
apps/pd/environment.d.ts
vendored
Normal file
12
apps/pd/environment.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
NODE_ENV?: "development" | "production" | "test";
|
||||
REDIS_URL?: string;
|
||||
MAX_FILE_SIZE?: string;
|
||||
PORT?: string;
|
||||
HOST?: string;
|
||||
RATELIMIT_MAX?: string;
|
||||
RATELIMIT_WINDOW?: string;
|
||||
BASE_URL?: string;
|
||||
}
|
||||
}
|
||||
BIN
apps/pd/fixtures/1x1.png
Normal file
BIN
apps/pd/fixtures/1x1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
BIN
apps/pd/fixtures/test.mp4
Normal file
BIN
apps/pd/fixtures/test.mp4
Normal file
Binary file not shown.
34
apps/pd/package.json
Normal file
34
apps/pd/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@premid/pd",
|
||||
"type": "module",
|
||||
"version": "1.1.9",
|
||||
"private": true,
|
||||
"description": "A small service to shorten image urls to get around Discord's 256 character limit",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps .",
|
||||
"dev": "node --watch --enable-source-maps ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/multipart": "^8.1.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@keyv/redis": "^2.8.4",
|
||||
"fastify": "^4.26.0",
|
||||
"file-type": "^19.0.0",
|
||||
"got": "^14.2.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"ipaddr.js": "^2.1.0",
|
||||
"keyv": "^4.5.4",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
}
|
||||
9
apps/pd/src/functions/createKeyv.test.ts
Normal file
9
apps/pd/src/functions/createKeyv.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import createKeyv from "./createKeyv.js";
|
||||
|
||||
it("should return keyv instance", () => {
|
||||
const keyv = createKeyv();
|
||||
|
||||
expect(keyv).toStrictEqual(expect.any(Object));
|
||||
});
|
||||
28
apps/pd/src/functions/createKeyv.ts
Normal file
28
apps/pd/src/functions/createKeyv.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import process from "node:process";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import Keyv from "keyv";
|
||||
|
||||
import redis from "../redis.js";
|
||||
|
||||
export default function createKeyv() {
|
||||
let options: Keyv.Options<string> | undefined;
|
||||
|
||||
/* c8 ignore next 8 */
|
||||
if (process.env.REDIS_SENTINELS) {
|
||||
options = {
|
||||
namespace: "pd",
|
||||
store: new KeyvRedis(redis),
|
||||
};
|
||||
}
|
||||
|
||||
const keyv = new Keyv<string>(
|
||||
options,
|
||||
);
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
keyv.on("error", (error) => {
|
||||
console.error("Keyv connection error:", error);
|
||||
});
|
||||
|
||||
return keyv;
|
||||
}
|
||||
30
apps/pd/src/functions/createRedis.ts
Normal file
30
apps/pd/src/functions/createRedis.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable no-console */
|
||||
import { hostname } from "node:os";
|
||||
|
||||
import process from "node:process";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
/* c8 ignore start */
|
||||
export default function createRedis(): Redis {
|
||||
const redis = new Redis({
|
||||
connectionName: `pd-${hostname()}-${process.pid.toString()}`,
|
||||
lazyConnect: true,
|
||||
name: "mymaster",
|
||||
sentinels: process.env.REDIS_SENTINELS?.split(",").map(s => ({
|
||||
host: s,
|
||||
port: 26_379,
|
||||
})),
|
||||
});
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
redis.on("error", (error) => {
|
||||
console.error("Redis error", error);
|
||||
});
|
||||
|
||||
/* c8 ignore next 3 */
|
||||
redis.on("connect", () => {
|
||||
console.log("Redis connected");
|
||||
});
|
||||
|
||||
return redis;
|
||||
}
|
||||
10
apps/pd/src/functions/createServer.test.ts
Normal file
10
apps/pd/src/functions/createServer.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createServer } from "../functions/createServer.js";
|
||||
|
||||
describe("createServer", () => {
|
||||
it("should return a fastify instance", async () => {
|
||||
const server = await createServer();
|
||||
expect(server).toBeDefined();
|
||||
});
|
||||
});
|
||||
62
apps/pd/src/functions/createServer.ts
Normal file
62
apps/pd/src/functions/createServer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import process from "node:process";
|
||||
import cors from "@fastify/cors";
|
||||
import fastifyMultipart from "@fastify/multipart";
|
||||
import ratelimit from "@fastify/rate-limit";
|
||||
import fastify from "fastify";
|
||||
import type { Redis } from "ioredis";
|
||||
|
||||
import createFromBase64 from "../routes/createFromBase64.js";
|
||||
import createFromImage from "../routes/createFromImage.js";
|
||||
import createShortenedLink from "../routes/createShortenedLink.js";
|
||||
import getFullLink from "../routes/getFullLink.js";
|
||||
|
||||
export async function createServer(redis?: Redis) {
|
||||
const server = fastify({
|
||||
trustProxy: true,
|
||||
});
|
||||
|
||||
await server.register(cors, {
|
||||
methods: ["GET"],
|
||||
origin: "*",
|
||||
});
|
||||
|
||||
await server.register(ratelimit, {
|
||||
max: Number.parseInt(process.env.RATELIMIT_MAX ?? "25"),
|
||||
nameSpace: "pd-ratelimit-",
|
||||
redis,
|
||||
timeWindow: process.env.RATELIMIT_WINDOW ?? "1 minute",
|
||||
});
|
||||
|
||||
await server.register(fastifyMultipart, {
|
||||
limits: {
|
||||
fileSize: Number.parseInt(process.env.MAX_FILE_SIZE ?? (5 * 1024 * 1024).toString()),
|
||||
files: 1,
|
||||
},
|
||||
});
|
||||
|
||||
server.post("/create/image", createFromImage);
|
||||
server.post("/create/base64", createFromBase64);
|
||||
server.get("/create/*", createShortenedLink);
|
||||
|
||||
server.get(
|
||||
"/*",
|
||||
{
|
||||
config: {
|
||||
rateLimit: false,
|
||||
},
|
||||
},
|
||||
getFullLink,
|
||||
);
|
||||
|
||||
server.get(
|
||||
"/health",
|
||||
{
|
||||
config: {
|
||||
rateLimit: false,
|
||||
},
|
||||
},
|
||||
(_, reply) => reply.status(204).send(),
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
30
apps/pd/src/functions/getGoogleAddresses.test.ts
Normal file
30
apps/pd/src/functions/getGoogleAddresses.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import got from "got";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe.concurrent("getGoogleAddresses", () => {
|
||||
it("should return an array of CIDR objects", async () => {
|
||||
const { default: getGoogleAddresses } = await import("./getGoogleAddresses.js");
|
||||
|
||||
vi.spyOn(got, "get").mockResolvedValue({
|
||||
body: JSON.stringify({
|
||||
prefixes: [
|
||||
{ ipv4Prefix: "0.0.0.0" },
|
||||
{ ipv6Prefix: "0000:0000:0000::/16" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await getGoogleAddresses();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"ipv4Prefix": "0.0.0.0",
|
||||
},
|
||||
{
|
||||
"ipv6Prefix": "0000:0000:0000::/16",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
24
apps/pd/src/functions/getGoogleAddresses.ts
Normal file
24
apps/pd/src/functions/getGoogleAddresses.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import got from "got";
|
||||
|
||||
import type { CIDR } from "./isInCidRange.js";
|
||||
|
||||
export default async function getGoogleAddresses(): Promise<CIDR> {
|
||||
const { body } = await got.get("https://www.gstatic.com/ipranges/cloud.json");
|
||||
const result = JSON.parse(body) as GoogleResult;
|
||||
return result.prefixes.map(({ ipv4Prefix, ipv6Prefix }) => {
|
||||
return ipv6Prefix ? { ipv6Prefix } : { ipv4Prefix };
|
||||
});
|
||||
}
|
||||
|
||||
interface GoogleResult {
|
||||
syncToken: string;
|
||||
creationTime: string;
|
||||
prefixes: GoogleIP[];
|
||||
}
|
||||
|
||||
interface GoogleIP {
|
||||
ipv6Prefix: string;
|
||||
ipv4Prefix: string;
|
||||
service: string;
|
||||
scope: string;
|
||||
}
|
||||
31
apps/pd/src/functions/isInCidRange.test.ts
Normal file
31
apps/pd/src/functions/isInCidRange.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import isInCIDRRange from "./isInCidRange.js";
|
||||
|
||||
it("isInCIDRRange - IPv4 - in range", () => {
|
||||
const CIDRs = [{ ipv4Prefix: "192.0.2.0/24" }];
|
||||
const ip = "192.0.2.123";
|
||||
const result = isInCIDRRange(CIDRs, ip);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("isInCIDRRange - IPv4 - not in range", () => {
|
||||
const CIDRs = [{ ipv4Prefix: "192.0.2.0/24" }];
|
||||
const ip = "192.0.3.123";
|
||||
const result = isInCIDRRange(CIDRs, ip);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("isInCIDRRange - IPv6 - in range", () => {
|
||||
const CIDRs = [{ ipv6Prefix: "2001:db8::/32" }];
|
||||
const ip = "2001:db8::1234";
|
||||
const result = isInCIDRRange(CIDRs, ip);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("isInCIDRRange - IPv6 - not in range", () => {
|
||||
const CIDRs = [{ ipv6Prefix: "2001:db8::/32" }];
|
||||
const ip = "2001:db9::1234";
|
||||
const result = isInCIDRRange(CIDRs, ip);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
29
apps/pd/src/functions/isInCidRange.ts
Normal file
29
apps/pd/src/functions/isInCidRange.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
export default function isInCIDRRange(CIDRs: CIDR, ip: string) {
|
||||
const parsed = ipaddr.parse(ip);
|
||||
|
||||
for (const CIDR of CIDRs.filter((c) => {
|
||||
if (parsed.kind() === "ipv4" && "ipv4Prefix" in c)
|
||||
return true;
|
||||
else if (parsed.kind() === "ipv6" && "ipv6Prefix" in c)
|
||||
return true;
|
||||
else return false;
|
||||
})) {
|
||||
const check = parsed.match(ipaddr.parseCIDR("ipv4Prefix" in CIDR ? CIDR.ipv4Prefix : CIDR.ipv6Prefix));
|
||||
|
||||
if (check)
|
||||
return check;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CIDR = (
|
||||
| {
|
||||
ipv4Prefix: string;
|
||||
}
|
||||
| {
|
||||
ipv6Prefix: string;
|
||||
}
|
||||
)[];
|
||||
3
apps/pd/src/googleCIDRs.ts
Normal file
3
apps/pd/src/googleCIDRs.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import getGoogleAddresses from "./functions/getGoogleAddresses.js";
|
||||
|
||||
export default await getGoogleAddresses();
|
||||
14
apps/pd/src/index.test.ts
Normal file
14
apps/pd/src/index.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createServer } from "./functions/createServer.js";
|
||||
|
||||
it("/health", async () => {
|
||||
const server = await createServer();
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: "/health",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(204);
|
||||
expect(result.body).toBe("");
|
||||
});
|
||||
20
apps/pd/src/index.ts
Normal file
20
apps/pd/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable no-console */
|
||||
/* c8 ignore start */
|
||||
import process from "node:process";
|
||||
import { createServer } from "./functions/createServer.js";
|
||||
import redis from "./redis.js";
|
||||
|
||||
if (!process.env.REDIS_SENTINELS)
|
||||
console.log("WARNING: No REDIS_SENTINELS environment variable set");
|
||||
if (process.env.NODE_ENV === "production" && !process.env.BASE_URL)
|
||||
throw new Error("BASE_URL environment variable is required in production");
|
||||
|
||||
export const server = await createServer(redis);
|
||||
|
||||
const url = await server.listen({
|
||||
host: process.env.HOST ?? "0.0.0.0",
|
||||
port: Number.parseInt(process.env.PORT ?? "80"),
|
||||
});
|
||||
|
||||
console.log(`Server listening at ${url}`);
|
||||
// TODO Make proper error codes & json responses
|
||||
3
apps/pd/src/keyv.ts
Normal file
3
apps/pd/src/keyv.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createKeyv from "./functions/createKeyv.js";
|
||||
|
||||
export default createKeyv();
|
||||
3
apps/pd/src/redis.ts
Normal file
3
apps/pd/src/redis.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createRedis from "./functions/createRedis.js";
|
||||
|
||||
export default createRedis();
|
||||
94
apps/pd/src/routes/createFromBase64.test.ts
Normal file
94
apps/pd/src/routes/createFromBase64.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { createServer } from "../functions/createServer.js";
|
||||
|
||||
describe.concurrent("createFromBase64", async () => {
|
||||
const server = await createServer();
|
||||
|
||||
it("should return a 400 when the body is not present", async ({ expect }) => {
|
||||
const result = await server.inject({
|
||||
method: "POST",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid body\"");
|
||||
});
|
||||
|
||||
it("should return a 400 when the body is not a string", async ({ expect }) => {
|
||||
const result = await server.inject({
|
||||
method: "POST",
|
||||
payload: new Blob([]),
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid body\"");
|
||||
});
|
||||
|
||||
it("should return a 400 when the body is not a valid base64 string", async ({ expect }) => {
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: "data:image/png;base64t",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid base64 string\"");
|
||||
});
|
||||
|
||||
it("should return a 400 when the base64 string is not a valid image", async ({ expect }) => {
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: "data:image/sv;base64,a",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid base64 string\"");
|
||||
|
||||
const result2 = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: "data:image/svg+xml;base64,s",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result2.statusCode).toBe(400);
|
||||
expect(result2.body).toMatchInlineSnapshot("\"Supported types: png, jpeg, jpg, gif, webp\"");
|
||||
});
|
||||
|
||||
it("should return a 200 when the base64 string is valid", async ({ expect }) => {
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: "data:image/png;base64,s",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
|
||||
const result2 = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: "data:image/png;base64,s",
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(result2.statusCode).toBe(200);
|
||||
expect(result2.body).toStrictEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
49
apps/pd/src/routes/createFromBase64.ts
Normal file
49
apps/pd/src/routes/createFromBase64.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import process from "node:process";
|
||||
import mime from "mime-types";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { RouteHandlerMethod } from "fastify";
|
||||
|
||||
import keyv from "../keyv.js";
|
||||
|
||||
const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
const { body } = request;
|
||||
|
||||
if (!body)
|
||||
return reply.status(400).send("Invalid body");
|
||||
|
||||
if (typeof body !== "string")
|
||||
return reply.status(400).send("Invalid body");
|
||||
|
||||
const matches = body.match(/^data:(.+);base64,(.+)/);
|
||||
|
||||
if (!matches || matches.length === 0)
|
||||
return reply.status(400).send("Invalid base64 string");
|
||||
|
||||
const type = mime.extension(matches.at(1)!);
|
||||
|
||||
if (!type)
|
||||
return reply.status(400).send("Invalid base64 string");
|
||||
|
||||
if (!["png", "jpeg", "jpg", "gif", "webp"].includes(type))
|
||||
return reply.status(400).send("Supported types: png, jpeg, jpg, gif, webp");
|
||||
|
||||
const hash = crypto.createHash("sha256").update(body).digest("hex");
|
||||
const existingUrl = await keyv.get(hash);
|
||||
|
||||
if (existingUrl) {
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + existingUrl);
|
||||
}
|
||||
|
||||
const uniqueId = `${nanoid(10)}.${type}`;
|
||||
|
||||
await keyv.set(hash, uniqueId, 30 * 60 * 1000);
|
||||
await keyv.set(uniqueId, body, 30 * 60 * 1000);
|
||||
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + uniqueId);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
106
apps/pd/src/routes/createFromImage.test.ts
Normal file
106
apps/pd/src/routes/createFromImage.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import type { RequestOptions } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { afterAll, beforeAll, describe, it } from "vitest";
|
||||
|
||||
import { createServer } from "../functions/createServer.js";
|
||||
|
||||
describe.concurrent("createFromImage", async () => {
|
||||
const server = await createServer();
|
||||
const form = new FormData();
|
||||
const defaultRequestOptions: RequestOptions = {
|
||||
hostname: "localhost",
|
||||
method: "POST",
|
||||
path: "/create/image",
|
||||
protocol: "http:",
|
||||
};
|
||||
|
||||
let url: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
url = await server.listen();
|
||||
defaultRequestOptions.port = (server.server.address() as AddressInfo).port;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
void server.close();
|
||||
});
|
||||
|
||||
it("should return a 400 when request is not multipart", async ({ expect }) => {
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(await result.text()).toMatchInlineSnapshot("\"Request is not multipart\"");
|
||||
});
|
||||
|
||||
it("should return a 400 status code when no file is provided", async ({ expect }) => {
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(await result.text()).toMatchInlineSnapshot("\"Invalid file\"");
|
||||
});
|
||||
it("should return a 400 status code when the file is invalid", async ({ expect }) => {
|
||||
form.set("file", Buffer.alloc(1024 * 1024 * 2));
|
||||
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(await result.text()).toMatchInlineSnapshot("\"Invalid file\"");
|
||||
|
||||
form.set("file", new Blob([new Uint8Array(1024 * 1024 * 2)]));
|
||||
|
||||
const result2 = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result2.status).toBe(400);
|
||||
expect(await result2.text()).toMatchInlineSnapshot("\"Invalid file\"");
|
||||
});
|
||||
|
||||
it("should return a 400 status code when the file is not an image", async ({ expect }) => {
|
||||
form.set("file", new Blob([await readFile(new URL("../../fixtures/test.mp4", import.meta.url))]));
|
||||
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(400);
|
||||
expect(await result.text()).toMatchInlineSnapshot("\"Only png, jpeg, jpg, gif and webp are supported\"");
|
||||
});
|
||||
|
||||
it("should return a 200 status code when the file is valid", async ({ expect }) => {
|
||||
form.set("file", new Blob([await readFile(new URL("../../fixtures/1x1.png", import.meta.url))]));
|
||||
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(await result.text()).toStrictEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("should return a 200 status code when the file is valid and the same file is uploaded again", async ({ expect }) => {
|
||||
form.set("file", new Blob([await readFile(new URL("../../fixtures/1x1.png", import.meta.url))]));
|
||||
|
||||
const result = await fetch(`${url}/create/image`, {
|
||||
body: form,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(await result.text()).toStrictEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
55
apps/pd/src/routes/createFromImage.ts
Normal file
55
apps/pd/src/routes/createFromImage.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import process from "node:process";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { RouteHandlerMethod } from "fastify";
|
||||
|
||||
import keyv from "../keyv.js";
|
||||
|
||||
const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
if (!request.isMultipart())
|
||||
return reply.status(400).send("Request is not multipart");
|
||||
|
||||
const file = await request.file();
|
||||
|
||||
if (!file)
|
||||
return reply.status(400).send("Invalid file");
|
||||
|
||||
const type = await fileTypeFromBuffer(await file.toBuffer());
|
||||
|
||||
if (!type)
|
||||
return reply.status(400).send("Invalid file");
|
||||
|
||||
if (![
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
].includes(type.mime)) {
|
||||
return reply.status(400).send("Only png, jpeg, jpg, gif and webp are supported");
|
||||
}
|
||||
|
||||
const buffer = await file.toBuffer();
|
||||
const body = `data:${type.mime};base64,${buffer.toString("base64")}`;
|
||||
const hash = crypto.createHash("sha256").update(body).digest("hex");
|
||||
const existingUrl = await keyv.get(hash);
|
||||
|
||||
if (existingUrl) {
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + existingUrl);
|
||||
}
|
||||
|
||||
const uniqueId = `${nanoid(10)}.${type.ext}`;
|
||||
|
||||
await Promise.all([
|
||||
keyv.set(hash, uniqueId, 30 * 60 * 1000),
|
||||
keyv.set(uniqueId, body, 30 * 60 * 1000),
|
||||
]);
|
||||
|
||||
void reply.header("Cache-control", `public, max-age=${(30 * 60).toString()}`);
|
||||
return reply.send(process.env.BASE_URL! + uniqueId);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
64
apps/pd/src/routes/createShortenedLink.test.ts
Normal file
64
apps/pd/src/routes/createShortenedLink.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createServer } from "../functions/createServer.js";
|
||||
|
||||
describe.concurrent("/create", async () => {
|
||||
const server = await createServer();
|
||||
|
||||
it("should return a 400 status code when no URL is provided", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: "/create/",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid URL\"");
|
||||
});
|
||||
|
||||
it("should return a 400 status code when the URL is too short", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: "/create/https://www.google.com",
|
||||
});
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"URL is too short\"");
|
||||
});
|
||||
|
||||
it("should return a 400 status code when the URL is invalid", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/file://www.googl${"e".repeat(256)}`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.body).toMatchInlineSnapshot("\"Invalid URL\"");
|
||||
});
|
||||
|
||||
it("should return a 200 status code when the URL is valid", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.googl${"e".repeat(256)}.com`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("should return a 200 status code when the URL is valid and already exists", async () => {
|
||||
const result = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.googl${"d".repeat(256)}.com`,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(200);
|
||||
expect(result.body).toStrictEqual(expect.any(String));
|
||||
const { body } = result;
|
||||
const result2 = await server.inject({
|
||||
method: "GET",
|
||||
url: `/create/https://www.googl${"d".repeat(256)}.com`,
|
||||
});
|
||||
|
||||
expect(result2.statusCode).toBe(200);
|
||||
expect(result2.body).toStrictEqual(body);
|
||||
});
|
||||
});
|
||||
39
apps/pd/src/routes/createShortenedLink.ts
Normal file
39
apps/pd/src/routes/createShortenedLink.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import process from "node:process";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { RouteHandlerMethod } from "fastify";
|
||||
|
||||
import keyv from "../keyv.js";
|
||||
|
||||
const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
const url = request.url.replace("/create/", "").trim();
|
||||
|
||||
if (url.length === 0)
|
||||
return reply.status(400).send("Invalid URL");
|
||||
|
||||
if (url.length < 256)
|
||||
return reply.status(400).send("URL is too short");
|
||||
|
||||
const urlObject = new URL(url);
|
||||
if (!["http:", "https:"].includes(urlObject.protocol))
|
||||
return reply.status(400).send("Invalid URL");
|
||||
|
||||
const hash = crypto.createHash("sha256").update(url).digest("hex");
|
||||
const existingShortenedUrl = await keyv.get(hash);
|
||||
|
||||
void reply.header("Cache-control", "public, max-age=1800");
|
||||
|
||||
if (existingShortenedUrl) {
|
||||
await Promise.all([keyv.set(hash, existingShortenedUrl, 1800), keyv.set(existingShortenedUrl, url, 1800)]);
|
||||
return reply.send(process.env.BASE_URL! + existingShortenedUrl);
|
||||
}
|
||||
|
||||
const uniqueId = nanoid(10);
|
||||
|
||||
await Promise.all([keyv.set(hash, uniqueId, 1800), keyv.set(uniqueId, url, 1800)]);
|
||||
|
||||
return reply.send(process.env.BASE_URL! + uniqueId);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
101
apps/pd/src/routes/getFullLink.test.ts
Normal file
101
apps/pd/src/routes/getFullLink.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createServer } from "../functions/createServer.js";
|
||||
import * as isInCIDRRange from "../functions/isInCidRange.js";
|
||||
|
||||
describe("getFullLink", async () => {
|
||||
const server = await createServer();
|
||||
let url: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
url = await server.listen();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("should fail if not a Google Cloud IP", async () => {
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
url: "/1234567890",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it("should fail if not a valid ID", async () => {
|
||||
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
|
||||
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
url: "/123",
|
||||
});
|
||||
|
||||
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
|
||||
const result2 = await server.inject({
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
url: "/1234567890.",
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(404);
|
||||
expect(result2.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it("should redirect to the correct URL", async () => {
|
||||
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
|
||||
|
||||
const { body } = await server.inject({
|
||||
url: `/create/https://${"a".repeat(256)}`,
|
||||
});
|
||||
|
||||
expect(body).toStrictEqual(expect.any(String));
|
||||
|
||||
const result = await server.inject({
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
url: body,
|
||||
});
|
||||
|
||||
expect(result.statusCode).toBe(302);
|
||||
expect(result.headers.location).toBe(`https://${"a".repeat(256)}`);
|
||||
});
|
||||
|
||||
it("should return the correct image", async () => {
|
||||
const imageBuffer = await readFile(new URL("../../fixtures/test.mp4", import.meta.url));
|
||||
const imageBase64 = `data:image/png;base64,${imageBuffer.toString("base64")}`;
|
||||
|
||||
const { body } = await server.inject({
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
method: "POST",
|
||||
payload: imageBase64,
|
||||
url: "/create/base64",
|
||||
});
|
||||
|
||||
expect(body).toStrictEqual(expect.any(String));
|
||||
|
||||
vi.spyOn(isInCIDRRange, "default").mockReturnValueOnce(true);
|
||||
|
||||
const result = await fetch(`${url}${body}`, {
|
||||
headers: {
|
||||
"cf-connecting-ip": "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.status).toBe(200);
|
||||
expect(result.headers.get("content-type")).toBe("image/png");
|
||||
expect(Buffer.from(await result.arrayBuffer())).toStrictEqual(imageBuffer);
|
||||
});
|
||||
});
|
||||
51
apps/pd/src/routes/getFullLink.ts
Normal file
51
apps/pd/src/routes/getFullLink.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import type { RouteHandlerMethod } from "fastify";
|
||||
|
||||
import isInCIDRRange from "../functions/isInCidRange.js";
|
||||
import googleCIDRs from "../googleCIDRs.js";
|
||||
import keyv from "../keyv.js";
|
||||
|
||||
const handler: RouteHandlerMethod = async (request, reply) => {
|
||||
/* c8 ignore next 2 */
|
||||
const ip = request.headers["cf-connecting-ip"]?.toString() || request.socket.remoteAddress || request.ip;
|
||||
|
||||
if (
|
||||
!isInCIDRRange(
|
||||
googleCIDRs,
|
||||
ip,
|
||||
)
|
||||
) {
|
||||
return reply.status(401).send("Not a Google Cloud IP");
|
||||
}
|
||||
|
||||
const id = (request.params as { "*": string })["*"].trim();
|
||||
|
||||
if (id.split(".")[0]?.length !== 10)
|
||||
return reply.code(404).send("Invalid ID");
|
||||
|
||||
const url = await keyv.get(id);
|
||||
if (!url)
|
||||
return reply.code(404).send("Unknown ID");
|
||||
|
||||
const hash = crypto.createHash("sha256").update(url).digest("hex");
|
||||
|
||||
await Promise.all([keyv.set(hash, id, 30 * 60 * 1000), keyv.set(id, url, 30 * 60 * 1000)]);
|
||||
void reply.header("Cache-control", "public, max-age=1800");
|
||||
|
||||
//* If it is not a base64 string, redirect to it
|
||||
if (!url.startsWith("data:image"))
|
||||
return reply.redirect(url);
|
||||
|
||||
const image = Buffer.from(
|
||||
url.replace(/^data:image\/\w+;base64,/, ""),
|
||||
"base64",
|
||||
);
|
||||
|
||||
const mime = url.split(";")[0]!.split(":")[1]!;
|
||||
|
||||
return reply.type(mime).send(image);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
8
apps/pd/tsconfig.app.json
Normal file
8
apps/pd/tsconfig.app.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
8
apps/pd/tsconfig.json
Normal file
8
apps/pd/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@types/node"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["environment.d.ts", "src"]
|
||||
}
|
||||
2
apps/schema-server/.eslintrc.yaml
Normal file
2
apps/schema-server/.eslintrc.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
rules:
|
||||
no-console: off
|
||||
3
apps/schema-server/Readme.md
Normal file
3
apps/schema-server/Readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @premid/schema-server
|
||||
|
||||
This is a simple schema server which serves JSON schemas for Presence Developers.
|
||||
7
apps/schema-server/environment.d.ts
vendored
Normal file
7
apps/schema-server/environment.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
NODE_ENV?: "development" | "production" | "test";
|
||||
PORT?: string;
|
||||
HOST?: string;
|
||||
}
|
||||
}
|
||||
22
apps/schema-server/package.json
Normal file
22
apps/schema-server/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@premid/schema-server",
|
||||
"type": "module",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"description": "A small service to serve the JSON schemas for PreMiD",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"schemas"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node --enable-source-maps .",
|
||||
"dev": "node --watch --enable-source-maps ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"fastify": "^4.26.0",
|
||||
"globby": "^14.0.1"
|
||||
}
|
||||
}
|
||||
241
apps/schema-server/schemas/metadata/1.0.json
Normal file
241
apps/schema-server/schemas/metadata/1.0.json
Normal file
@@ -0,0 +1,241 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://schemas.premid.app/metadata/1.0",
|
||||
|
||||
"title": "Metadata",
|
||||
"type": "object",
|
||||
"description": "Metadata that describes a presence.",
|
||||
|
||||
"definitions": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"description": "User information.",
|
||||
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the user."
|
||||
},
|
||||
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The Discord snowflake of the user.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "id"]
|
||||
}
|
||||
},
|
||||
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"$comment": "This is required otherwise the schema will fail itself when it is applied to a document via $schema. This is optional so that validators that use this schema don't fail if the metadata doesn't have the $schema property.",
|
||||
|
||||
"type": "string",
|
||||
"description": "The metadata schema URL."
|
||||
},
|
||||
|
||||
"author": {
|
||||
"$ref": "#/definitions/user",
|
||||
"description": "The author of this presence."
|
||||
},
|
||||
|
||||
"contributors": {
|
||||
"type": "array",
|
||||
"description": "Any extra contributors to this presence.",
|
||||
|
||||
"items": {
|
||||
"$ref": "#/definitions/user"
|
||||
}
|
||||
},
|
||||
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service this presence is for."
|
||||
},
|
||||
|
||||
"description": {
|
||||
"type": "object",
|
||||
"description": "A description of the presence in multiple languages.",
|
||||
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The language key. The key must be languagecode(_REGIONCODE).",
|
||||
"pattern": "^[a-z]{2}(_[A-Z]{2})?$"
|
||||
},
|
||||
"patternProperties": {
|
||||
"^[a-z]{2}(_[A-Z]{2})?$": {
|
||||
"type": "string",
|
||||
"description": "The description of the presence in the key's language."
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false,
|
||||
"required": ["en"]
|
||||
},
|
||||
|
||||
"url": {
|
||||
"type": ["string", "array"],
|
||||
"description": "The service's website URL, or an array of URLs. Protocols should not be added.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$",
|
||||
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "One of the service's website URLs.",
|
||||
"pattern": "^(([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+|(\\d{1,3}\\.){3}\\d{1,3}|localhost)$"
|
||||
},
|
||||
"minItems": 2
|
||||
},
|
||||
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The SemVer version of the presence. Must just be major.minor.patch.",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
|
||||
"logo": {
|
||||
"type": "string",
|
||||
"description": "The logo of the service this presence is for.",
|
||||
"pattern": "^https?:\\/\\/?(?:[a-z0-9-]+\\.)*[0-9a-z_-]+(?:\\.[a-z]+)+\\/.*$"
|
||||
},
|
||||
|
||||
"thumbnail": {
|
||||
"type": "string",
|
||||
"description": "A thumbnail of the service this presence is for.",
|
||||
"pattern": "^https?:\\/\\/?([a-z0-9-]+\\.)*[0-9a-z_-]+(\\.[a-z]+)+\\/.*$"
|
||||
},
|
||||
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "The theme color of the service this presence is for. Must be either a 6 digit or a 3 digit hex code.",
|
||||
"pattern": "^#([A-Fa-f0-9]{3}){1,2}$"
|
||||
},
|
||||
|
||||
"tags": {
|
||||
"type": ["array"],
|
||||
"description": "The tags for the presence.",
|
||||
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A tag.",
|
||||
"pattern": "^[^A-Z\\s!\"#$%&'()*+,./:;<=>?@\\[\\\\\\]^_`{|}~]+$"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The category the presence falls under.",
|
||||
"enum": ["anime", "games", "music", "socials", "videos", "other"]
|
||||
},
|
||||
|
||||
"iframe": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not the presence should run in IFrames."
|
||||
},
|
||||
|
||||
"regExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match URLs for the presence to inject into."
|
||||
},
|
||||
|
||||
"iFrameRegExp": {
|
||||
"type": "string",
|
||||
"description": "A regular expression used to match IFrames for the presence to inject into."
|
||||
},
|
||||
|
||||
"button": {
|
||||
"type": "boolean",
|
||||
"description": "Controls whether the presence is automatically added when the extension is installed. For partner presences only."
|
||||
},
|
||||
|
||||
"warning": {
|
||||
"type": "boolean",
|
||||
"description": "Shows a warning saying that it requires additional steps for the presence to function correctly."
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"type": "array",
|
||||
"description": "An array of settings the user can change in the presence.",
|
||||
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A setting.",
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The title of the setting. Required only if `multiLanguage` is disabled."
|
||||
},
|
||||
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "The icon of the setting. Required only if `multiLanguage` is disabled.",
|
||||
"pattern": "^fa[bs] fa-[0-9a-z-]+$"
|
||||
},
|
||||
|
||||
"if": {
|
||||
"type": "object",
|
||||
"description": "Restrict showing this setting if another setting is the defined value.",
|
||||
|
||||
"propertyNames": {
|
||||
"type": "string",
|
||||
"description": "The ID of the setting."
|
||||
},
|
||||
|
||||
"patternProperties": {
|
||||
"": {
|
||||
"type": ["string", "number", "boolean"],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"placeholder": {
|
||||
"type": "string",
|
||||
"description": "The placeholder for settings that require input. Shown when the input is empty."
|
||||
},
|
||||
|
||||
"value": {
|
||||
"type": ["string", "number", "boolean"],
|
||||
"description": "The default value of the setting. Not compatible with `values`."
|
||||
},
|
||||
|
||||
"values": {
|
||||
"type": "array",
|
||||
"description": "The default values of the setting. Not compatible with `value`.",
|
||||
|
||||
"items": {
|
||||
"type": ["string", "number", "boolean"],
|
||||
"description": "The value of the setting."
|
||||
}
|
||||
},
|
||||
|
||||
"multiLanguage": {
|
||||
"type": ["string", "boolean", "array"],
|
||||
"description": "When false, multi-localization is disabled. When true, strings from the `general.json` file are available for use. When a string, it is the name of a file (excluding .json) of a used language from the localization GitHub repo. When an array of strings, it is all of the file names (excluding .json) of used languages from the localization GitHub repo.",
|
||||
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The name of a file from the localization GitHub repository."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false,
|
||||
"required": ["author", "service", "description", "url", "version", "logo", "thumbnail", "color", "tags", "category"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user