Compare commits

...

220 Commits

Author SHA1 Message Date
Florian Metz
a668add973 chore: release v0.0.31 2024-09-17 10:00:06 +02:00
Florian Metz
42b70b1259 chore: optimize active presence gauge update with concurrency limit 2024-09-17 09:59:41 +02:00
Bas950
253b680d3e chore: release v0.0.30 2024-09-17 09:36:49 +02:00
Bas950
e9a40dc553 chore: small updates 2024-09-17 09:36:43 +02:00
Bas950
b25880d4cd chore: release v0.0.29 2024-09-17 09:11:12 +02:00
Bas950
fb06227aeb chore: release v0.0.29 2024-09-17 09:07:58 +02:00
Bas950
ff3d00497b chore: release v0.0.29 2024-09-17 09:07:21 +02:00
Bas950
a06780f85a chore: reduce memory 2024-09-17 09:06:10 +02:00
Bas950
5b1969c7ab chore: release v0.0.28 2024-09-16 23:22:15 +02:00
Bas950
bedd34594c chore: disable ip stuff for now 2024-09-16 23:22:11 +02:00
Bas950
47feaa5c70 chore: release v0.0.27 2024-09-16 22:56:14 +02:00
Bas950
9fb32f53ae chore: reduce batch size 2024-09-16 22:56:10 +02:00
Bas950
bfb84bb080 chore: release v0.0.26 2024-09-16 22:32:52 +02:00
Bas950
f545b174bd chore: lint 2024-09-16 22:32:42 +02:00
Bas950
4a492cf275 fix: store ip data in postgres 2024-09-16 22:30:29 +02:00
Bas950
3f65f678b1 chore: release v0.0.25 2024-09-16 20:58:24 +02:00
Bas950
a71b66540b chore: release v0.0.14 2024-09-16 20:58:07 +02:00
Bas van Zanten
e675f74983 feat: update tracing (#1067) 2024-09-16 20:18:35 +02:00
Florian Metz
e9e6639492 chore: release v0.0.13 2024-09-15 03:09:23 +02:00
Florian Metz
3258179040 chore: release v0.0.24 2024-09-15 03:09:13 +02:00
Florian Metz
086d476af2 chore: update hash 2024-09-15 03:09:04 +02:00
Florian Metz
146bf9e270 chore: release v0.0.23 2024-09-15 02:48:56 +02:00
Florian Metz
a02f25ba29 chore: test 2024-09-15 02:48:41 +02:00
Florian Metz
416b65f0d4 chore: release v0.0.12 2024-09-15 02:41:31 +02:00
Florian Metz
f8e9fc832d chore: test 2024-09-15 02:41:16 +02:00
Florian Metz
86b0f07216 chore: test 2024-09-15 02:31:38 +02:00
Florian Metz
9eb5c03877 chore: release v0.0.11 2024-09-15 02:25:50 +02:00
Florian Metz
e63e1270aa chore: release v0.0.22 2024-09-15 02:25:38 +02:00
Florian Metz
f730e71bbf chore: test 2024-09-15 02:25:10 +02:00
Bas950
8b68bf85c8 chore: release v0.0.10 2024-09-13 17:27:44 +02:00
Bas950
e4c794a9ad chore: 202 on disabled flag 2024-09-13 17:27:38 +02:00
Bas950
6e8258d76f chore: release v0.0.21 2024-09-13 15:08:16 +02:00
Bas950
56b796c621 chore: use ky 2024-09-13 15:08:08 +02:00
Bas950
0de59c48b4 chore: release v0.0.20 2024-09-13 14:37:31 +02:00
Bas950
60056e069d chore: update log 2024-09-13 14:37:24 +02:00
Bas950
b6bad90919 chore: release v0.0.9 2024-09-13 14:33:34 +02:00
Bas950
ee21bb9dec chore: release v0.0.20 2024-09-13 14:31:39 +02:00
Bas950
6efac4fef1 feat: use scienceId 2024-09-13 14:31:27 +02:00
Bas950
93424793bd chore: release v0.0.19 2024-09-13 13:46:33 +02:00
Bas950
affcb6a0cf chore: add reason 2024-09-13 13:46:27 +02:00
Bas950
bb56949dfb chore: release v0.0.18 2024-09-13 13:02:31 +02:00
Bas950
c06fe04b65 chore: fix time 2024-09-13 13:02:26 +02:00
Florian Metz
ef976341ba chore: release v0.0.17 2024-09-13 12:33:19 +02:00
Florian Metz
38893891af chore: why does it not abort 2024-09-13 12:33:10 +02:00
Florian Metz
63eeeefda7 chore: release v0.0.16 2024-09-13 12:05:42 +02:00
Florian Metz
056db21cb0 chore: add p-limit dependency for session cleanup 2024-09-13 12:05:37 +02:00
Bas950
d8dc08c6c3 chore: release v0.0.15 2024-09-13 11:55:36 +02:00
Bas950
634391b6e3 chore: always return the key 2024-09-13 11:55:32 +02:00
Florian Metz
c46cf6975a chore: release v0.0.14 2024-09-13 11:52:23 +02:00
Florian Metz
68c6b4fcdc chore: add p-limit dependency for session cleanup 2024-09-13 11:52:00 +02:00
Florian Metz
55fa07d5b5 chore: release v0.0.13 2024-09-13 11:38:49 +02:00
Florian Metz
903c238b33 chore: add timeout to headless session deletion 2024-09-13 11:38:40 +02:00
Bas950
acd9afb2b1 chore: release v0.0.12 2024-09-13 11:32:55 +02:00
Bas950
4bd42390eb chore: move some code 2024-09-13 11:32:44 +02:00
Florian Metz
c014504464 chore: release v0.0.11 2024-09-13 11:00:16 +02:00
Florian Metz
24fe349b60 chore: optimize session cleanup with batch deletion 2024-09-13 10:59:13 +02:00
Bas950
ee5428ce08 chore: release v0.0.10 2024-09-13 10:38:38 +02:00
Bas950
e4b1010160 chore: skip clearOldSesssions if another in progress 2024-09-13 10:38:21 +02:00
Bas950
34c42d59ed chore: release v0.0.9 2024-09-12 15:45:16 +02:00
Bas950
d9267361aa feat: use scan 2024-09-12 15:45:10 +02:00
Bas950
0d5382fd50 chore: release v0.0.8 2024-09-12 14:49:01 +02:00
Bas950
e9015b1204 chore: iodk 2024-09-12 14:47:31 +02:00
Bas950
cea36426ab chore: idk kek 2024-09-12 14:46:13 +02:00
Bas950
48c141094e chore: release v0.0.8 2024-09-12 14:41:56 +02:00
Bas950
e67fb97e14 chore: update lockfile 2024-09-12 14:41:51 +02:00
Bas950
0bd0d759f6 chore: release v0.0.7 2024-09-12 14:38:34 +02:00
Bas950
60b7f63409 feat(api-master): add metrics 2024-09-12 14:38:10 +02:00
Bas950
78b482be4f chore: release v0.0.8 2024-09-11 21:33:32 +02:00
Bas950
9db9e931b6 chore: release v0.0.6 2024-09-11 21:33:21 +02:00
Bas950
665263e9b5 chore: revert redis stuff 2024-09-11 21:33:14 +02:00
Bas950
60257dbe53 chore: release v0.0.5 2024-09-11 21:03:22 +02:00
Bas950
411a70f567 chore: release v0.0.7 2024-09-11 21:02:59 +02:00
Bas950
4d1b092ee5 chore: hash the key 2024-09-11 21:02:44 +02:00
Bas950
aa41f1cdae chore: release v0.0.6 2024-09-11 20:31:04 +02:00
Bas950
04b5d54697 chore: update arktype 2024-09-11 20:30:48 +02:00
Bas950
fb096bc4be chore: release v0.0.4 2024-09-11 20:13:30 +02:00
Bas950
6e9e4ae1b6 chore: release v0.0.5 2024-09-11 20:13:16 +02:00
Bas950
d8f73202b9 chore: fix build 2024-09-11 18:42:46 +02:00
Bas950
b175a08dce feat: add session-keep-alive 2024-09-11 18:37:30 +02:00
Florian Metz
2284ee94ad chore: update npm dependencies 2024-09-09 17:44:17 +02:00
Florian Metz
78a3311342 chore: release v0.0.4 2024-08-18 02:53:01 +02:00
Florian Metz
a1fabd3fd6 feat: metrics? 2024-08-18 02:52:51 +02:00
Florian Metz
93c62cc38f chore: release v0.0.3 2024-08-18 00:34:59 +02:00
Florian Metz
8553613593 feat: add feature flags 2024-08-18 00:34:15 +02:00
Bas950
bf83dc4452 chore: bump dep 2024-08-08 11:04:12 +02:00
Bas950
91bf2237c2 chore: release v0.0.3 2024-08-04 19:35:05 +02:00
Bas950
ae9b579e84 feat(api-master): add logs 2024-08-04 19:34:54 +02:00
Bas950
2488d98ede chore: release v0.0.2 2024-08-04 19:06:10 +02:00
Bas950
3eedb8ba81 chore: release v0.0.2 2024-08-04 19:05:53 +02:00
Bas950
bc5cc3054b chore(api): remove old sentry tracing 2024-08-04 19:05:13 +02:00
Florian Metz
5340b6ec2e chore: release v0.0.1 2024-08-04 01:08:37 +02:00
Florian Metz
0a28b2181b chore: deploy on tag 2024-08-04 01:07:21 +02:00
Florian Metz
389ee87757 chore: release v0.0.1 2024-08-04 01:03:01 +02:00
Florian Metz
f3976f5aaf chore: wip api 2024-08-04 00:58:54 +02:00
Florian Metz
e85015725d chore: wip api 2024-08-04 00:56:34 +02:00
Bas van Zanten
a624b7a1cb feat: add more api endpoints (#1059)
* chore: worked on the api and lint

* chore: small fixes

* chore: uhm I think this sort is broken

* chore: worked on the api and lint

* chore: small fixes

* chore: uhm I think this sort is broken

* feat: heartbeat

* chore: add prettier ignore

* feat: websocket

* chore: update tsconfig

* chore: lint

* chore: dont require unused fields

* chore: use djs rest

* fix: websocket

* chore: v5

* chore: fix build

---------

Co-authored-by: Florian Metz <me@timeraa.dev>
2024-08-04 00:31:03 +02:00
Florian Metz
6c85d66eab chore: release v1.0.3 2024-08-01 10:30:44 +02:00
Florian Metz
175f5ec430 chore: release v1.1.9 2024-08-01 10:30:34 +02:00
Florian Metz
18dc9fd594 chore: fix docker 2024-08-01 10:29:00 +02:00
Florian Metz
6dd3c5ffcb chore: release v1.0.2 2024-08-01 10:21:27 +02:00
Florian Metz
dad3fbf44d chore: release v1.1.8 2024-08-01 10:21:15 +02:00
Florian Metz
c42fbfe8b0 chore: fix build 2024-08-01 10:21:02 +02:00
Florian Metz
3a30457944 chore: release v1.1.7 2024-08-01 10:09:29 +02:00
Florian Metz
2eae287e18 chore: release v1.0.1 2024-08-01 10:09:02 +02:00
Florian Metz
457882c515 chore: add crowdin badge 2024-07-26 14:39:43 +02:00
Florian Metz
224925c772 chore: update crowdin configuration to skip untranslated strings and files 2024-07-24 23:04:17 +02:00
Florian Metz
b5fca1c943 chore: update language folder 2024-07-24 22:56:39 +02:00
Bas950
265a4ffffd chore: lint 2024-07-24 10:40:56 +02:00
Florian Metz
ec5ff30ef3 chore: improve crowdin pr 2024-07-24 02:30:34 +02:00
Florian Metz
947b3b91a9 chore: improve crowdin pr 2024-07-24 02:26:59 +02:00
Florian Metz
71eefae4b4 chore: change dest in crowdin 2024-07-24 02:19:42 +02:00
Florian Metz
6800649154 feat: Add Crowdin configuration for website localization. (#1054) 2024-07-24 02:14:15 +02:00
Florian Metz
2100e4420b style: make footer follow max width (#1052) 2024-07-23 20:16:00 +02:00
Florian Metz
d4fc4c97ce ci: use alpine base (#1051) 2024-07-19 22:45:20 +02:00
Florian Metz
ad658b1433 style: render 3 columns (#1049) 2024-07-18 10:54:54 +02:00
Florian Metz
40c94fa8e5 style: fix centering (#1050) 2024-07-18 10:46:34 +02:00
veryCrunchy
902b1e4887 chore: remove docs prefix from scripts (#1046) 2024-07-18 01:55:21 +02:00
Florian Metz
2e98a5a24e chore: setup qemu 2024-07-18 01:27:44 +02:00
Florian Metz
fbb9c815df chore: test (#1048) 2024-07-18 01:13:03 +02:00
Florian Metz
0ed9f5b264 chore: use full build image 2024-07-17 23:00:38 +02:00
Florian Metz
692fb2a8a5 chore: update CI workflow to trigger on 'monorepo' branch 2024-07-17 22:54:38 +02:00
Florian Metz
151f7df388 ci: testing 2024-07-17 22:49:52 +02:00
Florian Metz
9281ce0984 style: small layout fixes 2024-07-17 22:11:16 +02:00
Florian Metz
a82d435e22 chore: update depencies 2024-07-17 22:11:02 +02:00
Florian Metz
6deda1ec8f chore: set client host 2024-07-17 19:14:24 +02:00
Florian Metz
258585efa0 fix: broken lockfile 2024-07-17 17:59:50 +02:00
Florian Metz
2d1eaafbf9 chore: bump dependencies & add missing 2024-07-17 17:56:19 +02:00
Florian Metz
5837c5be5b chore: update npm dependencies 2024-07-16 15:03:17 +02:00
Florian Metz
2106300635 chore: remove comments 2024-07-16 15:00:40 +02:00
Florian Metz
82e7a260fb chore: update security headers in nuxt.config.ts 2024-07-16 13:50:01 +02:00
Florian Metz
64f5cd6c12 chore: update security headers in nuxt.config.ts 2024-07-16 13:21:01 +02:00
Florian Metz
8e56c12b29 wip: csp 2024-07-16 12:55:57 +02:00
Florian Metz
febb0e1927 wip: csp 2024-07-16 07:08:50 +02:00
Florian Metz
4ea97b6b42 wip: csp 2024-07-16 06:00:34 +02:00
Florian Metz
8322b09221 wip: csp 2024-07-16 05:45:40 +02:00
Florian Metz
854cb4145c chore: use correct port 2024-07-16 05:17:41 +02:00
Florian Metz
c36f60d960 wip: cd 2024-07-16 05:02:13 +02:00
Florian Metz
6bf1d58daa wip: cd 2024-07-16 04:44:34 +02:00
Florian Metz
1c4cb32a6e wip: cd 2024-07-16 04:41:33 +02:00
Florian Metz
009384a20d wip: cd 2024-07-16 04:36:53 +02:00
Florian Metz
6e17a2d382 wip: cd 2024-07-16 04:30:02 +02:00
Florian Metz
0e75dab8ff wip: cd 2024-07-16 04:20:39 +02:00
Florian Metz
517119365e fix(ci): use correct context 2024-07-16 03:05:52 +02:00
Florian Metz
3e15ddf988 wip: csp 2024-07-16 03:03:06 +02:00
Florian Metz
fb279a418a wip: csp 2024-07-16 02:36:42 +02:00
Florian Metz
3e86393b25 chore: remove blank 2024-07-15 22:31:43 +02:00
Florian Metz
14c476101b chore: add lib folder to .gitignore 2024-07-15 22:05:07 +02:00
Florian Metz
10cdf6ff47 chore: cleanup 2024-07-15 22:04:46 +02:00
Florian Metz
975c354617 wip: website 2024-07-15 04:05:59 +02:00
Florian Metz
744a252469 wip: website 2024-07-15 04:03:42 +02:00
Florian Metz
e0305773c7 wip: website 2024-07-15 04:00:54 +02:00
Florian Metz
af25311e36 wip: website 2024-07-15 03:59:37 +02:00
Florian Metz
0877e706d2 wip: api 2024-04-02 00:23:24 +02:00
Bas950
ec516f2d02 wip: docs 2024-03-29 20:19:15 +01:00
Bas950
311b1d96fa wip: docs 2024-03-29 17:37:55 +01:00
Bas950
be181718e3 chore: release v1.1.6 2024-03-24 14:12:10 +01:00
Bas950
85ec19bda3 fix(pd): use correct env variable 2024-03-24 14:11:44 +01:00
Bas950
6314acbf81 chore: release v1.1.5 2024-03-08 16:54:55 +01:00
Bas950
c8136da505 chore: update name 2024-03-08 16:54:47 +01:00
Bas950
3a41060d91 chore: release v1.1.4 2024-03-08 16:47:25 +01:00
Bas950
9e12a9f84f feat: use sentinels 2024-03-08 16:45:54 +01:00
Florian Metz
664e5c3e73 chore: release v1.1.3 2024-02-28 14:04:01 +01:00
Florian Metz
34bf4daa6a chore: improve memory usage 2024-02-28 14:03:42 +01:00
Florian Metz
b2f4051e11 chore: release v1.1.2 2024-02-28 12:06:47 +01:00
Florian Metz
03cc674601 feat: add ratelimit environment variables 2024-02-28 12:06:25 +01:00
Florian Metz
da3d007a35 chore: release v1.1.1 2024-02-28 08:23:07 +01:00
Florian Metz
80d9888b37 chore: pd stuff 2024-02-28 08:19:34 +01:00
Florian Metz
3ae265d6fe chore: release v1.1.0 2024-02-27 16:52:15 +01:00
Florian Metz
3c9e7708f8 feat: cache ratelimit in redis 2024-02-27 16:51:57 +01:00
Florian Metz
7663eacb58 chore: release v1.0.1 2024-02-27 16:33:42 +01:00
Florian Metz
85d5fa14fa chore: update readme 2024-02-27 16:33:03 +01:00
Florian Metz
2b55d34c19 chore: release v1.0.1 2024-02-27 16:32:19 +01:00
Florian Metz
9d1c2167be chore: release v1.0.1 2024-02-27 16:25:37 +01:00
Florian Metz
ce52ce9a97 fix: redis url passing 2024-02-27 16:25:11 +01:00
Florian Metz
d73db7e5a9 chore: update funding 2024-02-26 08:29:44 +01:00
Florian Metz
98ce2ff939 chore: readme stuff 2024-02-26 06:34:16 +01:00
Florian Metz
f98d615e7b fix: ci 2024-02-26 06:18:39 +01:00
Florian Metz
9c461c83ff chore: release v1.0.0 2024-02-26 06:16:43 +01:00
Florian Metz
dc8dacd5f5 chore: add release instructions 2024-02-26 06:16:31 +01:00
Florian Metz
db47ae9043 chore: release v1.0.0 2024-02-26 06:16:13 +01:00
Florian Metz
a0f4e141b5 chore: release v0.0.6 2024-02-26 06:12:05 +01:00
Florian Metz
5bc4c0e5c0 chore: testing 2024-02-26 06:11:39 +01:00
Florian Metz
c4765b8eeb chore: testing 2024-02-26 06:07:19 +01:00
Florian Metz
ebc953b549 chore: testing 2024-02-26 06:05:02 +01:00
Florian Metz
b407003450 chore: testing 2024-02-26 06:03:28 +01:00
Florian Metz
3857702040 chore: release v0.0.5 2024-02-26 06:02:21 +01:00
Florian Metz
e997e946a0 chore: testing 2024-02-26 06:02:11 +01:00
Florian Metz
2bcb87ec9c chore: release v0.0.4 2024-02-26 05:53:40 +01:00
Florian Metz
1d5af93390 chore: testing 2024-02-26 05:53:30 +01:00
Florian Metz
8a0e10eab0 chore: release v0.0.3 2024-02-26 05:50:59 +01:00
Florian Metz
13e413329c chore: testing 2024-02-26 05:50:46 +01:00
Florian Metz
d0fac8c495 chore: release v0.0.2 2024-02-26 05:41:01 +01:00
Florian Metz
74525de586 fix: token 2024-02-26 05:39:13 +01:00
Florian Metz
115923b935 fix: add login 2024-02-26 05:36:29 +01:00
Florian Metz
b7af6bb80e chore: release v0.0.2 2024-02-26 05:32:27 +01:00
Florian Metz
b053e6395c chore: release v0.0.2 2024-02-26 05:31:33 +01:00
Florian Metz
698ea34848 chore: testing 2024-02-26 05:31:06 +01:00
Florian Metz
d689324e62 chore: release v0.0.2 2024-02-26 05:27:59 +01:00
Florian Metz
fee8133965 chore: release v0.0.2 2024-02-26 05:24:03 +01:00
Florian Metz
b93db93236 chore: testing 2024-02-26 05:23:31 +01:00
Florian Metz
1efa4b9bb0 feat: add 1.10 schema 2024-02-26 04:59:32 +01:00
Florian Metz
4cf00f5ca9 fix: post-checkout 2024-02-26 04:59:32 +01:00
Florian Metz
94303a9bca chore: rename container 2024-02-16 08:53:44 +00:00
Florian Metz
fa611923b4 chore: change post-checkout 2024-02-16 08:45:28 +00:00
Florian Metz
c57d4db5a3 chore: devcontainer 2024-02-16 08:16:06 +01:00
Florian Metz
5d7760caa3 test: give up on index 2024-02-13 01:09:48 +01:00
Florian Metz
0d5fe8e5ab test: mock better 2024-02-13 01:00:43 +01:00
Florian Metz
1ac552f3cd chore: tests 2024-02-13 00:51:36 +01:00
Florian Metz
4cf4e01286 chore: reusable action 2024-02-13 00:18:55 +01:00
Florian Metz
e96e2c0cb6 ci: fix docker build 2024-02-13 00:03:58 +01:00
Florian Metz
8fb93a09f9 ci: add docker build 2024-02-12 23:45:50 +01:00
Florian Metz
4d20ba95b1 chore: update schemas 2024-02-12 23:45:32 +01:00
Florian Metz
85f428a295 chore: update gitignore 2024-02-12 21:59:49 +01:00
Florian Metz
a3018f528d chore: schemas 2024-02-12 21:59:31 +01:00
Florian Metz
3d8431c4d5 chore: tests 2024-02-12 21:01:24 +01:00
Florian Metz
074d55b6d4 chore: update readme 2024-02-11 04:02:27 +01:00
Florian Metz
d22d88b3a6 wip: docs 2024-02-11 04:01:28 +01:00
Florian Metz
f55a83977b wip: pd 2024-02-10 05:12:56 +01:00
Florian Metz
4ccc02f16c chore: commit-msg hook 2024-02-08 23:51:30 +01:00
Florian Metz
759b2abef9 wip: repo refactor 2024-02-08 23:42:28 +01:00
253 changed files with 31443 additions and 5218 deletions

1
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM mcr.microsoft.com/devcontainers/base:bullseye

View 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"
}

View 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
View File

@@ -0,0 +1,11 @@
.vscode
.DS_Store
.Trashes
.nuxt
.output
dist
node_modules
.env
Dockerfile
.dockerignore
generated

13
.gitattributes vendored Normal file
View 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

View File

@@ -1,30 +0,0 @@
# Contributing
## Requiered knowledge
- JavaScript
- html5
- NodeJS
Additional:
- CSS
- [VueJS](https://vuejs.org/)
- [ElectronJS](https://electronjs.org/)
- [NPMjs](https://www.npmjs.com/)
A source code editor is also requiered. We recommend [Visual Studio Code](https://code.visualstudio.com/).
### Installing the components
1. Install [Git](https://git-scm.com/)
2. Install [Node](https://nodejs.org/en/)
### Cloning the project
1. Fork the [repository](https://github.com/PreMiD/PreMiD)
2. Open a terminal and type `git clone https://github.com/PreMiD/PreMiD`
### Coding your vision
Please keep the structure. We don't want to disorganize our project. Chaotic files may not be accepted.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: Timeraa
github: [PreMiD, Timeraa]
patreon: Timeraa
ko_fi: Timeraa

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
<!-- NOTE -->
<!-- Keep all presence related bugs in our Presences repository! -->
<!-- https://github.com/PreMiD/Presences/issues -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,16 +0,0 @@
---
name: Feature request
about: Suggest an idea for PreMiD
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

BIN
.github/Logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
.github/Patreon.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

1
.github/PayPal.svg vendored

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.1 KiB

13
.github/SUPPORT.md vendored
View File

@@ -1,13 +0,0 @@
# How to get support
## Take a look at the [wiki](https://wiki.premid.app)
Our GitHub wiki is full of information around PreMiD.<br>
Take a look and feel free to contribute if you want to add something new.
## [Open a issue](https://github.com/PreMiD/PreMiD/issues/new/choose) on [GitHub](https://github.com/PreMiD/PreMiD)
Simply open a issue if you don't feel allright.<br>
*Aand there he goes...*
## Ask a staff member in [#support](https://discord.premid.app)
The team is ready to tell you the secrets of the underworld.<br>
Join our [Discord server](https://discord.premid.app) and find out what we're hiding.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View 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

View 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

View File

@@ -1,20 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: chore
include: scope
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: chore
include: scope
labels:
- "dependencies"

BIN
.github/example.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

5
.github/renovate.json vendored Normal file
View 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
View 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
View 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 }}

View File

@@ -1,98 +0,0 @@
name: DePloY
on:
release:
types: [published]
env:
NODE_ENV: DePloY
jobs:
package:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macOS-latest, windows-latest]
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
- name: Install Dependencies
run: |
npm i
npm i -g typescript rimraf
- name: Prepare to package
run: npm run init
- name: Package
run: |
npm run pkg
rimraf dist/app
node util/zip dist ${{ matrix.os }}.zip --zip
- name: Upload bundle
env:
SSHHOST: ${{ secrets.MAIN_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
tsc util/uploadFile util/zip
node util/uploadFile ${{ matrix.os }}.zip /home/PreMiD/download/util/${{ matrix.os }}.zip
createInstallers:
runs-on: "ubuntu-latest"
needs: package
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
- name: Install dependencies
run: |
sudo npm i
sudo npm i -g typescript
- name: Download InstallBuilder
run: |
wget https://clients.bitrock.com/installbuilder/installbuilder-enterprise-20.12.0-linux-x64-installer.run
chmod u+x installbuilder-enterprise-20.12.0-linux-x64-installer.run
- name: Install InstallBuilder
run: |
./installbuilder-enterprise-20.12.0-linux-x64-installer.run --installer-language en --prefix ./installbuilder --mode unattended
echo "${{ secrets.IBLICENSE }}" > ./installbuilder/license.xml
- name: Prepare Upgrade Installer
run: |
tsc util/prepare
node util/prepare
- name: Create Upgrade Installer (MacOS 64bit)
run: |
installbuilder/bin/builder build installer_assets/PreMiD-Upgrade.xml osx
- name: Create Upgrade Installer (Windows)
run: |
installbuilder/bin/builder build installer_assets/PreMiD-Upgrade.xml windows
- name: Upload files
env:
SSHHOST: ${{ secrets.MAIN_HOST }}
SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
tsc util/uploadFile util/zip
node util/uploadFile dist/installer/upgrader.exe /home/PreMiD/download/upgrader.exe
node util/uploadFile dist/installer/upgrader.app.zip /home/PreMiD/download/util/upgrader.app.zip
- name: Finalize build
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.MAIN_HOST }}
username: ${{ secrets.SSH_USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /home/PreMiD/download/util
unzip upgrader.app.zip
tar -czvf upgrader.app.tgz upgrader.app
mv upgrader.app.tgz ../
rm -rf upgrader.app upgrader.app.zip
unzip windows-latest.zip
cd windows-latest/PreMiD-win32-x64/
zip -r ../../PreMiD-win32-x64.zip .
mv ../../PreMiD-win32-x64.zip /home/PreMiD/download/
cd ../PreMiD-win32-ia32/
zip -r ../../PreMiD-win32-x86.zip .
mv ../../PreMiD-win32-x86.zip /home/PreMiD/download/
cd ../..
rm -rf windows-latest windows-latest.zip
unzip macOS-latest.zip
cd macOS-latest/PreMiD-darwin-x64/
zip -r ../../PreMiD-darwin-x64.zip .
mv ../../PreMiD-darwin-x64.zip /home/PreMiD/download/
cd ../..
rm -rf macOS-latest macOS-latest.zip

7
.gitignore vendored
View File

@@ -2,6 +2,8 @@ node_modules
out
dist
tmp
lib
data
.vscode
.env
@@ -18,3 +20,8 @@ src/update.ini
*.app
*.xml.backup
*.js
!eslint.config.js
coverage
*.tsbuildinfo
.DS_Store

2
.husky/commit-msg Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
pnpm exec commitlint --edit $1

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
shamefully-hoist=true

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
*.js
*.ts
*.json

View File

@@ -1,22 +0,0 @@
export default interface ExtensionSettings {
/**
* If extension is enabled
*/
enabled: boolean;
/**
* Autolaunch enabled
*/
autoLaunch: boolean;
/**
* Media keys enabled
*/
mediaKeys: boolean;
/**
* title menubar (TrayTitle)
*/
titleMenubar: boolean;
/**
* language of extension
*/
language: string;
}

View File

@@ -1,16 +0,0 @@
import * as Discord from "discord-rpc";
export default interface Presence {
/**
* Client ID of presence
*/
clientId: string;
/**
* Rich Procedual call connection
*/
rpc: Discord.Client;
/**
* Connection ready?
*/
ready: Boolean;
}

View File

@@ -1,33 +0,0 @@
import * as Discord from "discord-rpc";
export default interface PresenceData {
/**
* Client ID of presence
*/
clientId: string;
/**
* Tray title to be shown in Mac OS tray
*/
trayTitle: string;
/**
* service name of presence
* @deprecated
*/
service: string;
/**
* Determines if the service is currently playing something back or not, if false it will automatically hide after 1 minute
*/
playback: boolean;
/**
* Discord Presence which gets sent directly to Discord app
*/
presenceData: Discord.Presence;
/**
* Determines if the service should be hidden (clearActivity)
*/
hidden: boolean;
/**
* Determines if the service is mediaKey able / uses them
*/
mediaKeys: boolean;
}

View File

@@ -1 +1 @@
* @Timeraa
* @Timeraa

View File

@@ -2,65 +2,38 @@
## 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.
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:
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
- 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
- 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 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.
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.
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.
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.
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
View 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"]

View File

@@ -1,46 +1,54 @@
<div align="center">
<img src=".github/Logo.png" width="150px" draggable="false"><br>
<img src="https://cdn.rcd.gg/PreMiD.png" width="150px" />
# PreMiD
## Your Rich Presence for web services!
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/PreMiD/PreMiD)
[![Crowdin](https://badges.crowdin.net/premid/localized.svg)](https://crowdin.com/project/premid)
![GitHub](https://img.shields.io/github/license/PreMiD/PreMiD?style=for-the-badge)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/premid/premid?label=Application&style=for-the-badge)
![Chrome Web Store](https://img.shields.io/chrome-web-store/v/agjnjboanicjcpenljmaaigopkgdnihi?label=Extension&style=for-the-badge)
[![Chrome Web Store](https://img.shields.io/chrome-web-store/d/agjnjboanicjcpenljmaaigopkgdnihi.svg?label=Chrome&logo=google%20chrome&logoColor=white&colorA=4285F4&style=for-the-badge)](https://chrome.google.com/webstore/detail/premid/agjnjboanicjcpenljmaaigopkgdnihi)
![Website](https://img.shields.io/website?down_message=offline&label=PreMiD.app&style=for-the-badge&up_message=online&url=https%3A%2F%2Fpremid.app)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD?ref=badge_shield)
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.
<img src=".github/example.png" draggable="false"><br>
## Getting Started
# About
**If you are a user looking to install PreMiD, please visit the [official website](https://premid.app).**
**PreMiD** is a simple, configurable utility that allows you to show what you're doing on the web in your Discord **now playing status**. It supports many different websites, and will support multiple users watching the same content simultaneously in an upcoming update.
If you are a developer looking to contribute to PreMiD, read along.
# Features
## Table of Contents
· Displays your current web service in Discord as your status.<br>
· Grants full control over Presences.<br>
· Supports over 1,000 web services, still rising!<br>
· _Watch parties and more are coming soon!_
- [Packages](#packages)
- [License](#license)
# Installation/Troubleshooting
## Packages
### Installation instructions, Troubleshooting guides etc. can be located at our [**docs**](https://docs.premid.app).
This monorepo is split into multiple packages / projects. Here's a list of them:
# Support us
- [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.
<div>
<a target="_blank" href="https://www.patreon.com/bePatron?u=4610890" data-patreon-widget-type="become-patron-button" title="Support me on Patreon!">
<img height="75px" draggable="false" src=".github/Patreon.png">
</a>
<a target="_blank" href="https://discord.premid.app/" title="Join our Discord!">
<img src="https://discordapp.com/api/guilds/493130730549805057/widget.png?style=banner2" height="76px" draggable="false" alt="Join our Discord!">
</a>
</div>
## 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
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FPreMiD%2FPreMiD?ref=badge_large)
This project is licensed under the [MPL-2.0 License](LICENSE).

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dbCredentials: {
url: "postgresql://metrics:metrics@localhost:5432/metrics",
},
dialect: "postgresql",
schema: "./src/db.ts",
out: "./drizzle",
});

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS "online_users_ip_data" (
"uuid" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"ip" varchar(45) NOT NULL,
"country" varchar(2) NOT NULL,
"latitude" numeric(10, 8) NOT NULL,
"longitude" numeric(11, 8) NOT NULL,
"name" varchar(255),
"timestamp" timestamp DEFAULT now()
);

View File

@@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS "idx_online_users_uuid" ON "online_users_ip_data" USING btree ("uuid");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_online_users_timestamp" ON "online_users_ip_data" USING btree ("timestamp");

View File

@@ -0,0 +1 @@
ALTER TABLE "online_users_ip_data" ALTER COLUMN "timestamp" SET DATA TYPE timestamp with time zone;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "online_users_ip_data" ADD COLUMN "presences" jsonb DEFAULT '[]' NOT NULL;--> statement-breakpoint
ALTER TABLE "online_users_ip_data" DROP COLUMN IF EXISTS "name";

View File

@@ -0,0 +1 @@
ALTER TABLE "online_users_ip_data" ADD COLUMN "sessions" integer DEFAULT 0 NOT NULL;

View File

@@ -0,0 +1,70 @@
{
"id": "e29a6708-01f1-455a-b345-63dac1e124dc",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.online_users_ip_data": {
"name": "online_users_ip_data",
"schema": "",
"columns": {
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ip": {
"name": "ip",
"type": "varchar(45)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "numeric(10, 8)",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "numeric(11, 8)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"timestamp": {
"name": "timestamp",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,101 @@
{
"id": "4aa32a8e-f573-43b9-976a-2d078a0df0ea",
"prevId": "e29a6708-01f1-455a-b345-63dac1e124dc",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.online_users_ip_data": {
"name": "online_users_ip_data",
"schema": "",
"columns": {
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ip": {
"name": "ip",
"type": "varchar(45)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "numeric(10, 8)",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "numeric(11, 8)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"timestamp": {
"name": "timestamp",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"idx_online_users_uuid": {
"name": "idx_online_users_uuid",
"columns": [
{
"expression": "uuid",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"idx_online_users_timestamp": {
"name": "idx_online_users_timestamp",
"columns": [
{
"expression": "timestamp",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,101 @@
{
"id": "c1b8dbed-b232-4d66-9e74-b9af333095bc",
"prevId": "4aa32a8e-f573-43b9-976a-2d078a0df0ea",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.online_users_ip_data": {
"name": "online_users_ip_data",
"schema": "",
"columns": {
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ip": {
"name": "ip",
"type": "varchar(45)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "numeric(10, 8)",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "numeric(11, 8)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"timestamp": {
"name": "timestamp",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"idx_online_users_uuid": {
"name": "idx_online_users_uuid",
"columns": [
{
"expression": "uuid",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"idx_online_users_timestamp": {
"name": "idx_online_users_timestamp",
"columns": [
{
"expression": "timestamp",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,102 @@
{
"id": "e409a4d0-f698-484a-b412-38966a7b3a19",
"prevId": "c1b8dbed-b232-4d66-9e74-b9af333095bc",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.online_users_ip_data": {
"name": "online_users_ip_data",
"schema": "",
"columns": {
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ip": {
"name": "ip",
"type": "varchar(45)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "numeric(10, 8)",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "numeric(11, 8)",
"primaryKey": false,
"notNull": true
},
"presences": {
"name": "presences",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'"
},
"timestamp": {
"name": "timestamp",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"idx_online_users_uuid": {
"name": "idx_online_users_uuid",
"columns": [
{
"expression": "uuid",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"idx_online_users_timestamp": {
"name": "idx_online_users_timestamp",
"columns": [
{
"expression": "timestamp",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,109 @@
{
"id": "179435b5-dc15-4a42-9539-c3f336699d63",
"prevId": "e409a4d0-f698-484a-b412-38966a7b3a19",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.online_users_ip_data": {
"name": "online_users_ip_data",
"schema": "",
"columns": {
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"ip": {
"name": "ip",
"type": "varchar(45)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"latitude": {
"name": "latitude",
"type": "numeric(10, 8)",
"primaryKey": false,
"notNull": true
},
"longitude": {
"name": "longitude",
"type": "numeric(11, 8)",
"primaryKey": false,
"notNull": true
},
"sessions": {
"name": "sessions",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"presences": {
"name": "presences",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'[]'"
},
"timestamp": {
"name": "timestamp",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {
"idx_online_users_uuid": {
"name": "idx_online_users_uuid",
"columns": [
{
"expression": "uuid",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"idx_online_users_timestamp": {
"name": "idx_online_users_timestamp",
"columns": [
{
"expression": "timestamp",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,41 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1726516195146,
"tag": "0000_flippant_marrow",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1726516348344,
"tag": "0001_white_lifeguard",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1726516660134,
"tag": "0002_new_darkhawk",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1726517073510,
"tag": "0003_narrow_mastermind",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1726517405363,
"tag": "0004_tiresome_puff_adder",
"breakpoints": true
}
]
}

16
apps/api-master/environment.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
declare module "ip-location-api" {
export function lookup(ip: string): Promise<{
latitude: number;
longitude: number;
country: string;
} | null>;
export function updateDb(options: { fields?: string[]; dataDir?: string; tmpDataDir?: string; smallMemory?: boolean }): Promise<void>;
export function reload(options: { fields?: string[]; dataDir?: string; tmpDataDir?: string; smallMemory?: boolean }): Promise<void>;
}
declare namespace NodeJS {
export interface ProcessEnv {
METRICS_DATABASE_URL?: string;
}
}

View File

@@ -0,0 +1,39 @@
{
"name": "@premid/api-master",
"type": "module",
"version": "0.0.31",
"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 .",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:update": "pnpm db:generate && pnpm db:migrate",
"db:studio": "drizzle-kit studio"
},
"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",
"drizzle-orm": "^0.33.0",
"ioredis": "^5.3.2",
"ip-location-api": "^1.0.0",
"ky": "^1.7.2",
"p-limit": "^6.1.0",
"postgres": "^3.4.4"
},
"devDependencies": {
"@types/debug": "^4.1.12",
"drizzle-kit": "^0.24.2"
}
}

27
apps/api-master/src/db.ts Normal file
View File

@@ -0,0 +1,27 @@
import process from "node:process";
import { decimal, index, integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
// Define the schema
export const onlineUsersIpData = pgTable("online_users_ip_data", {
uuid: uuid("uuid").primaryKey().defaultRandom(),
ip: varchar("ip", { length: 45 }).notNull(),
country: varchar("country", { length: 2 }).notNull(),
latitude: decimal("latitude", { precision: 10, scale: 8 }).notNull(),
longitude: decimal("longitude", { precision: 11, scale: 8 }).notNull(),
sessions: integer("sessions").notNull().default(0),
presences: jsonb("presences").notNull().default("[]").$type<string[]>(),
timestamp: timestamp("timestamp", { withTimezone: true }).defaultNow(),
}, table => ({
idxOnlineUsersUuid: index("idx_online_users_uuid").on(table.uuid),
idxOnlineUsersTimestamp: index("idx_online_users_timestamp").on(table.timestamp),
}));
if (!process.env.METRICS_DATABASE_URL) {
throw new Error("METRICS_DATABASE_URL is not set");
}
export const sql = postgres(process.env.METRICS_DATABASE_URL);
export const db = drizzle(sql);

View File

@@ -0,0 +1,10 @@
import { lt, sql } from "drizzle-orm";
import { db, onlineUsersIpData } from "../db.js";
import { mainLog } from "../index.js";
export async function cleanupOldUserData(retentionDays: number) {
mainLog("Cleaning up old user ip data");
const interval = `'${retentionDays} days'`;
await db.delete(onlineUsersIpData)
.where(lt(onlineUsersIpData.timestamp, sql`now() - interval ${sql.raw(interval)}`));
}

View File

@@ -0,0 +1,100 @@
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();
const pattern = "pmd-api.sessions.*";
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 [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); //* Use SCAN with COUNT for memory efficiency
cursor = newCursor;
totalSessions += keys.length;
const deletePromises: Promise<string>[] = [];
for (const key of keys) {
const session = await redis.hgetall(key) as unknown 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.del(...keysToDelete);
keysToDelete = [];
}
} while (cursor !== "0");
if (keysToDelete.length > 0) {
await redis.del(...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;
}

View File

@@ -0,0 +1,83 @@
import type { ServerResponse } from "node:http";
import type { Attributes } from "@opentelemetry/api";
import { ValueType, diag } from "@opentelemetry/api";
import type { PrometheusExporter, PrometheusSerializer } from "@opentelemetry/exporter-prometheus";
import { AggregationTemporality, DataPointType, type GaugeMetricData, InstrumentType } from "@opentelemetry/sdk-metrics";
const registeredMetrics = new Map<string, ClearableGaugeMetric>();
//* Custom gauge metric class
export class ClearableGaugeMetric {
private data = new Map<string, { value: number; attributes: Attributes }>();
constructor(private readonly name: string, private readonly description: string) {
registeredMetrics.set(name, this);
}
set(key: string, value: number, attributes: Attributes) {
this.data.set(key, { value, attributes });
}
clear({ except }: { except?: string[] }) {
for (const key of this.data.keys()) {
if (except && except.includes(key))
continue;
this.data.delete(key);
}
}
toMetricData(): GaugeMetricData {
return {
descriptor: {
name: this.name,
description: this.description,
unit: "",
type: InstrumentType.GAUGE,
valueType: ValueType.INT,
},
dataPointType: DataPointType.GAUGE,
dataPoints: Array.from(this.data.values()).map(({ value, attributes }) => ({
value,
attributes,
startTime: [0, 0],
endTime: [0, 0],
})),
aggregationTemporality: AggregationTemporality.CUMULATIVE,
};
}
get hasData() {
return this.data.size > 0;
}
}
export function updatePrometheusMetrics(prometheusExporter: PrometheusExporter) {
// @ts-expect-error We are modifying a private method
prometheusExporter._exportMetrics = function (this: PrometheusExporter, response: ServerResponse) {
response.statusCode = 200;
response.setHeader("content-type", "text/plain");
this.collect().then(
(collectionResult) => {
const { resourceMetrics, errors } = collectionResult;
if (errors.length) {
diag.error(
"PrometheusExporter: metrics collection errors",
...errors,
);
}
for (const metric of registeredMetrics.values()) {
if (metric.hasData) {
resourceMetrics.scopeMetrics[0]!.metrics.push(metric.toMetricData());
}
}
response.end((this as unknown as { _serializer: PrometheusSerializer })._serializer.serialize(resourceMetrics));
},
(err) => {
response.end(`# failed to export metrics: ${err}`);
},
);
}.bind(prometheusExporter);
}

View 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;
}

View File

@@ -0,0 +1,40 @@
import type { InferInsertModel } from "drizzle-orm";
import { db, onlineUsersIpData } from "../db.js";
import { lookupIp } from "./lookupIp.js";
const batchSize = 1000;
export async function insertIpData(
data: Map<string, {
presences: string[];
sessions: number;
}>,
) {
const timestamp = new Date();
const list = [...data.keys()];
//* Split into batches of batchSize
for (let i = 0; i < list.length; i += batchSize) {
const batch = list.slice(i, i + batchSize);
const mapped = await Promise.all(batch.map(async (ip) => {
const parsed = await lookupIp(ip);
if (parsed) {
const { presences, sessions } = data.get(ip)!;
return {
ip,
country: parsed.country,
latitude: parsed.latitude.toString(),
longitude: parsed.longitude.toString(),
presences,
sessions,
timestamp,
} satisfies InferInsertModel<typeof onlineUsersIpData>;
}
}));
const toInsert = mapped.filter(Boolean) as InferInsertModel<typeof onlineUsersIpData>[];
if (toInsert.length > 0) {
await db.insert(onlineUsersIpData).values(toInsert);
}
}
}

View File

@@ -0,0 +1,47 @@
import { join } from "node:path";
import process from "node:process";
import { lookup, reload, updateDb } from "ip-location-api";
import { mainLog } from "../index.js";
const fields = ["latitude", "longitude", "country"];
const dataDir = join(process.cwd(), "data");
const tmpDataDir = join(process.cwd(), "tmp");
const smallMemory = true;
let initialized = false;
export async function lookupIp(ip: string): Promise<{ latitude: number; longitude: number; country: string } | undefined> {
if (!initialized) {
reloadIpLocationApi();
return undefined;
}
try {
return await lookup(ip) ?? undefined;
}
catch {
return undefined;
}
}
let reloading: Promise<void> | undefined = Promise.resolve();
let log: debug.Debugger | undefined;
export async function reloadIpLocationApi() {
log ??= mainLog.extend("IP-Location-API");
if (reloading)
return reloading;
reloading = new Promise((resolve, reject) => {
log?.("Reloading IP location API");
updateDb({ fields, dataDir, tmpDataDir, smallMemory }).then(async () => {
await reload({ fields, dataDir, tmpDataDir, smallMemory });
log?.("IP location API reloaded");
initialized = true;
reloading = undefined;
resolve();
}).catch(reject);
});
return reloading;
}

View File

@@ -0,0 +1,13 @@
import { redis } from "../index.js";
import { activeSessionsCounter } from "../tracing.js";
let activeActivities = 0;
activeSessionsCounter.add(0);
export async function setSessionCounter() {
const length = await redis.hlen("pmd-api.sessions");
if (length === activeActivities)
return;
const diff = length - activeActivities;
activeActivities = length;
activeSessionsCounter.add(diff);
}

View File

@@ -0,0 +1,67 @@
import pLimit from "p-limit";
import { mainLog, redis } from "../index.js";
import { activePresenceGauge } from "../tracing.js";
import { insertIpData } from "./insertIpData.js";
export const updateActivePresenceGaugeLimit = pLimit(1);
let log: debug.Debugger | undefined;
//* Function to update the gauge with per-service counts
export async function updateActivePresenceGauge() {
await updateActivePresenceGaugeLimit(async () => {
log ??= mainLog.extend("Heartbeat-Updates");
const pattern = "pmd-api.heartbeatUpdates.*";
let cursor: string = "0";
const serviceCounts = new Map<string, number>();
const ips = new Map<string, {
presences: string[];
sessions: number;
}>();
do {
const [newCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000);
cursor = newCursor;
const hashes = await Promise.all(keys.map(key => redis.hmget(key, "service", "version", "ip_address")));
for (const hash of hashes) {
const service = hash[0];
const version = hash[1];
const ip = hash[2];
if (service && version) {
serviceCounts.set(`${service}:${version}`, (serviceCounts.get(`${service}:${version}`) || 0) + 1);
}
else {
serviceCounts.set("none", (serviceCounts.get("none") || 0) + 1);
}
if (ip) {
const presenceName = service && version ? `${service}:${version}` : undefined;
const ipData = ips.get(ip) || { presences: [], sessions: 0 };
if (presenceName) {
ipData.presences.push(presenceName);
ipData.presences = Array.from(new Set(ipData.presences.filter(Boolean)));
}
ipData.sessions++;
ips.set(ip, ipData);
}
}
} while (cursor !== "0");
log?.("Updating active presence gauge");
// Clear previous data
activePresenceGauge.clear({ except: [...serviceCounts.keys()] });
// Set new data
for (const [serviceVersion, count] of serviceCounts.entries()) {
const [presence_name, version] = serviceVersion.split(":");
activePresenceGauge.set(serviceVersion, count, {
presence_name,
version,
});
}
insertIpData(ips);
});
}

View File

@@ -0,0 +1,61 @@
import process from "node:process";
import { CronJob } from "cron";
import debug from "debug";
import { clearOldSessions } from "./functions/clearOldSessions.js";
import createRedis from "./functions/createRedis.js";
import { setSessionCounter } from "./functions/setSessionCounter.js";
import "./tracing.js";
import { updateActivePresenceGauge, updateActivePresenceGaugeLimit } from "./functions/updateActivePresenceGauge.js";
// import { reloadIpLocationApi } from "./functions/lookupIp.js";
import { cleanupOldUserData } from "./functions/cleanupOldUserData.js";
export const redis = createRedis();
export const mainLog = debug("api-master");
debug("Starting cron jobs");
void new CronJob(
// Every 5 seconds
"*/5 * * * * *",
() => {
if (process.env.DISABLE_CLEAR_OLD_SESSIONS !== "true") {
clearOldSessions();
}
if (process.env.DISABLE_SET_SESSION_COUNTER !== "true") {
setSessionCounter();
}
if (process.env.DISABLE_ACTIVE_PRESENCE_GAUGE !== "true") {
updateActivePresenceGaugeLimit.clearQueue();
updateActivePresenceGauge();
}
},
undefined,
true,
);
// void new CronJob(
// // Every day at 9am
// "0 9 * * *",
// () => {
// reloadIpLocationApi();
// },
// undefined,
// true,
// undefined,
// undefined,
// true,
// );
void new CronJob(
// Every day at 1am
"0 1 * * *",
() => {
cleanupOldUserData(14); // Keep 14 days of data
},
undefined,
true,
undefined,
undefined,
true,
);

View File

@@ -0,0 +1,26 @@
import { ValueType } from "@opentelemetry/api";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { ClearableGaugeMetric, updatePrometheusMetrics } from "./functions/clearableGaugeMetric.js";
const prometheusExporter = new PrometheusExporter();
const provider = new MeterProvider({
readers: [prometheusExporter],
});
const meter = provider.getMeter("nice");
export const activeSessionsCounter = meter.createUpDownCounter("active_sessions", {
description: "Number of active sessions",
valueType: ValueType.INT,
});
export const activePresenceGauge = new ClearableGaugeMetric(
"active_presences",
"Per presence name+version, active number of users",
);
updatePrometheusMetrics(prometheusExporter);
prometheusExporter.startServer();

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"types": ["./environment.d.ts"],
"outDir": "dist"
},
"include": ["src/**/*"]
}

View 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
View File

@@ -0,0 +1 @@
generated

View 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
View File

@@ -0,0 +1,7 @@
declare namespace NodeJS {
export interface ProcessEnv {
NODE_ENV?: "development" | "production" | "test";
DATABASE_URL?: string;
SESSION_KEEP_ALIVE_INTERVAL?: string;
}
}

View File

@@ -0,0 +1,49 @@
{
"name": "@premid/api-worker",
"type": "module",
"version": "0.0.14",
"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"
}
}

View 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);
}
}

View 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,
});

View 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;
}

View 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);
});
});

View File

@@ -0,0 +1,96 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
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();

View File

@@ -0,0 +1,33 @@
import { type } from "arktype";
import { GraphQLError } from "graphql";
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 GraphQLError(out.summary);
await redis.hset(
"pmd-api.scienceUpdates",
out.identifier,
JSON.stringify(out),
);
return {
__typename: "AddScienceResult",
...out,
};
};
export default mutation;

View File

@@ -0,0 +1,54 @@
import { type } from "arktype";
import { GraphQLError } from "graphql";
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, context) => {
const out = heartbeatSchema(input);
if (out instanceof type.errors)
throw new GraphQLError(out.summary);
//* Get the user's IP address from Cloudflare headers or fallback to the request IP
const userIp = context.request.headers.get("cf-connecting-ip") || context.request.ip;
// * 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(),
ip_address: userIp,
});
await redis.expire(redisKey, 300);
return {
__typename: "HeartbeatResult",
...out,
};
};
export default mutation;

View File

@@ -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,
};

View File

@@ -0,0 +1,6 @@
import type { QueryResolvers } from "../../../../generated/graphql-v5.js";
import presences from "./presences.js";
export const Query: QueryResolvers = {
presences,
};

View 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;

View 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,
};

View 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!
}

View 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!
}

View File

@@ -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!]!
}

View 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!
}

View 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
}

View File

@@ -0,0 +1 @@
scalar Scalar

View File

@@ -0,0 +1 @@
scalar StringOrStringArray

View 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}`);

View File

@@ -0,0 +1,62 @@
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" });
const redisKey = `pmd-api.sessions.${out.scienceId}`;
await redis.hset(redisKey, {
session: out.session,
token: out.token,
lastUpdated: Date.now(),
});
await redis.expire(redisKey, 300); // 5 minutes
const interval = Number.parseInt(process.env.SESSION_KEEP_ALIVE_INTERVAL ?? "5000"); // 5 seconds
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;
}
}

View 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();

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"types": ["@ark/schema"],
"outDir": "dist"
},
"include": ["src/**/*"]
}

View 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
View File

@@ -0,0 +1 @@
cache

View 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,
});

View 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.

View 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.

View File

View 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
View 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
View 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"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 288 KiB

View 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
View File

@@ -0,0 +1,3 @@
# @premid/pd
A simple url shortener service to shorten urls longer than 256 characters.

Some files were not shown because too many files have changed in this diff Show More