Compare commits

...
Author SHA1 Message Date
HampusandGitHub 1eed347ffb fix(premium): follow the light theme on the Plutonium page (#3111) 2026-10-02 14:15:24 +02:00
HampusandGitHub 4aa7a3e181 fix(voice): allow stereo mics at 64 kbps and in the mic test (#3110) 2026-10-02 14:11:19 +02:00
HampusandGitHub 69786d3b49 fix(voice): prefer vp8 for automatic screen shares in firefox (#3109) 2026-10-02 14:09:55 +02:00
HampusandGitHub 2fb5fb1abb fix(voice): allow av1 and vp9 screen shares in firefox (#3108) 2026-10-02 14:08:41 +02:00
HampusandGitHub a9265cbb39 perf(gateway): speed up reconnects and pin guilds to nodes (#3107) 2026-10-02 14:02:22 +02:00
HampusandGitHub ee2d11ee0a fix(voice): prefer vp9 over software h264 for screen shares (#3106) 2026-10-02 13:29:29 +02:00
TarekandGitHub 632067b552 feat(gateway,admin): Expand stats for metrics (#3064) 2026-10-02 12:58:16 +02:00
HampusandGitHub 840dc3dfa5 feat(premium): match the Plutonium page to the new site look (#3104) 2026-10-02 12:20:44 +02:00
HampusandGitHub 4e6f9b539c fix(voice): make RNNoise the default noise suppression (#3103) 2026-10-02 11:31:40 +02:00
HampusandGitHub 98cce4815d feat(users): add temporary new conversation limits (#3100) 2026-10-02 01:28:35 +02:00
HampusandGitHub b375abc20a feat(desktop): live-reload linked css theme files (#3099) 2026-10-02 01:02:30 +02:00
HampusandGitHub 21cb7ba69c feat(premium): show App Store and Google Play subs on web (#3098) 2026-10-01 22:51:14 +02:00
HampusandGitHub be69333eaf feat(premium): add the Plutonium page behind an experiment (#3097) 2026-10-01 21:45:44 +02:00
HampusandGitHub 9a074adb11 fix(app): make disabling built-in shortcuts take effect live (#3096) 2026-10-01 20:37:28 +02:00
HampusandGitHub 2df82b2b5e fix(guild): treat very high as high without phone verification (#3095) 2026-10-01 20:33:02 +02:00
HampusandGitHub d691047884 feat(desktop): add start minimized option for launch at login (#3094) 2026-10-01 19:51:43 +02:00
HampusandGitHub c2e7fde5bc test(api): isolate crosspost tests that mock constants (#3091) 2026-10-01 17:13:49 +02:00
HampusandGitHub 7e4d5137f8 feat: add announcement channels, publishing and following (#3090) 2026-10-01 16:57:21 +02:00
HampusandGitHub 376afd2ad6 fix(voice): keep mic publish state in sync with voice state (#3088) 2026-10-01 14:03:04 +02:00
HampusandGitHub e3fcedbec5 fix(voice): stabilize voice input and noise suppression (#3087) 2026-10-01 14:02:12 +02:00
HampusandGitHub 7c9564bcad feat(deploy): add helm charts for the fluxer services (#3082) 2026-10-01 04:11:30 +02:00
HampusandGitHub cfed6cc4e0 perf(media-proxy): gzip static assets on the fly (#3079) 2026-09-30 23:41:16 +02:00
HampusandGitHub c7bd1be3e4 fix(auth): offer every transport for passkeys stored without any (#3077) 2026-09-30 23:01:37 +02:00
HampusandGitHub 2161d84701 fix(self-hosting): grow seaweedfs one volume at a time (#3076) 2026-09-30 22:55:12 +02:00
HampusandGitHub eaeeb3b502 fix(api): report final system DM progress (#3074) 2026-09-30 22:11:34 +02:00
HampusandGitHub dc32a7c70e feat(admin): allow system DMs to all users (#3073) 2026-09-30 21:29:05 +02:00
HampusandGitHub ab0b483fbe perf(gateway): speed up presence and harden guild queries (#3072) 2026-09-30 21:12:13 +02:00
HampusandGitHub 6e2f90b03c fix(premium): drop the grace period after a voluntary cancel (#3071) 2026-09-30 21:03:25 +02:00
HampusandGitHub 5e0806f479 fix(voice): preserve microphone channels during screen sharing (#3070) 2026-09-30 20:47:30 +02:00
HampusandGitHub dfdfffe5de feat(premium): give failed renewals a billing-cycle grace period (#3066) 2026-09-30 18:48:01 +02:00
HampusandGitHub f5e32aed31 fix(ci): correct TTL fixtures and unused dependencies (#3065) 2026-09-30 17:45:07 +02:00
HampusandGitHub 710c1aeaa8 fix(deps): bump yanked yoke-derive to 0.8.4 (#3063) 2026-09-30 16:59:59 +02:00
HampusandGitHub af49cd6cc4 refactor(ban): drop ipinfo cgnat blast-radius guard (#3062) 2026-09-30 16:54:43 +02:00
omsterandGitHub ca719e7b5e feat(admin,api): restrict community creation on self-hosted (#3055) 2026-09-30 16:32:45 +02:00
HampusandGitHub ab4069ed0e fix(app): let hidden sidebar buttons be shown again (#3061) 2026-09-30 15:03:44 +02:00
HampusandGitHub 12bfaa83ba fix(sso): route mobile sign-in through the web callback (#3060) 2026-09-30 14:48:24 +02:00
HampusandGitHub 1076728241 perf(gateway): keep large guilds responsive under floods (#3058) 2026-09-30 12:26:21 +02:00
HampusandGitHub 360b984adc fix(ci): repair admin test config and a ttl race in api tests (#3054) 2026-09-30 02:39:46 +02:00
HampusandGitHub dcdf7e1d93 fix(api): let new channels inherit the adult-only setting (#3053) 2026-09-30 02:31:47 +02:00
HampusandGitHub e8cb167dbf feat(premium): add App Store and Google Play purchases (#3052) 2026-09-30 01:55:19 +02:00
HampusandGitHub 0b3418dcbe fix(app): make unchecked checkbox border visible (#3050) 2026-09-30 01:23:43 +02:00
HampusandGitHub ec7649193c docs(admin): document notify_reporter on report resolve (#3049) 2026-09-30 01:01:22 +02:00
HampusandGitHub 2b8a743dc5 refactor(self-hosting): forward every setting, drop dead config (#3047) 2026-09-30 00:58:43 +02:00
HampusandGitHub 39f9beda5a fix(api): send correct staff emails and allow suppressing them (#3048) 2026-09-29 23:55:54 +02:00
HampusandGitHub f0b3c82cfd fix(app): scroll quick switcher selection after typing (#3044) 2026-09-29 20:54:08 +02:00
HampusandGitHub 2808edf6d0 fix(push): keep notification images within the web push budget (#3043) 2026-09-29 20:35:56 +02:00
HampusandGitHub 071263188a fix(push): run the stale DM read check on presence nodes (#3042) 2026-09-29 19:51:36 +02:00
HampusandGitHub f9108f24ce feat(self-host): add an overlay that turns off bundled seaweedfs (#3041) 2026-09-29 19:49:02 +02:00
HampusandGitHub e98b77a54a fix(api): stop treating users without a birth date as minors (#3040) 2026-09-29 18:27:15 +02:00
HampusandGitHub 2636e9cc13 fix(voice): lower DeepFilterNet attenuation limit to 30 dB (#3039) 2026-09-29 18:16:13 +02:00
JiraliteandGitHub 944b586f22 fix(UseForwardDestinations): hide system user (#3038) 2026-09-29 18:14:19 +02:00
HampusandGitHub 4f968bbc47 feat(captcha): make ALTCHA the only captcha (#3035) 2026-09-29 17:00:15 +02:00
HampusandGitHub d433a039b5 feat(profile): ship profile timezone to everyone (#3034) 2026-09-29 16:28:40 +02:00
HampusandGitHub b30ea361d3 fix(push): stop pushes for read, silent and muted messages (#3033) 2026-09-29 15:58:46 +02:00
HampusandGitHub 9908518f5b feat(app): add quick reply and edit keybinds (#3032) 2026-09-29 15:50:56 +02:00
HampusandGitHub 364084c819 refactor(api): emit moderation events and apply account actions (#3031) 2026-09-29 12:24:15 +02:00
HampusandGitHub c488906131 feat(voice): ship noise suppression treatment to everyone (#3029) 2026-09-29 03:43:58 +02:00
HampusandGitHub 39c72f0fb0 fix(desktop): require readable keyboards for Linux input access (#3026) 2026-09-28 22:27:20 +02:00
HampusandGitHub 3736d94d73 feat(premium): let self-hosted instances sell premium and gifts (#3025) 2026-09-28 21:21:51 +02:00
HampusandGitHub 192cec689a fix(app): keep voice connections of one session across channels (#3023) 2026-09-28 19:50:47 +02:00
HampusandGitHub e895c41bf0 fix(app): tighten the composer status row (#3022) 2026-09-28 19:50:00 +02:00
HampusandGitHub 997d98c65c fix(app): fade messages behind the composer status row (#3020) 2026-09-28 18:47:42 +02:00
HampusandGitHub c9ae5b6ee8 fix(app): smooth the fluxer.com migration and expired re-login (#3019) 2026-09-28 18:11:18 +02:00
HampusandGitHub a728be4062 fix(app): respect time format setting in profile local time (#3018) 2026-09-28 17:48:55 +02:00
HampusandGitHub fce81367fb fix(app): stop message text showing through the slowmode hint (#3017) 2026-09-28 17:29:15 +02:00
HampusandGitHub 5a4edc0b59 fix(api): make read state clear endpoint a no-op (#3015) 2026-09-28 16:31:30 +02:00
HampusandGitHub 713ae5f7f5 feat(api): restrict dms to friends by default for new users (#3013) 2026-09-28 15:02:20 +02:00
HampusandGitHub eaee820216 feat(experiments): target rollouts by guild and premium status (#3012) 2026-09-28 14:34:19 +02:00
HampusandGitHub 564c5ae164 feat(profile): move profile timezone from staff to an experiment (#3011) 2026-09-28 12:57:26 +02:00
HampusandGitHub dd8ed6f205 fix(app): react at once when picking a +: autocomplete emoji (#3010) 2026-09-28 12:30:45 +02:00
HampusandGitHub ed8c412415 perf(gateway): make channel moves cheap in large guilds (#3008) 2026-09-28 02:07:47 +02:00
HampusandGitHub 12417a6942 fix(app): keep the caret after inserted emoji (#3007) 2026-09-28 01:56:08 +02:00
HampusandGitHub d05f6c9aaa fix(gateway): push held users whose sessions end during grace (#3006) 2026-09-28 01:47:55 +02:00
HampusandGitHub 0ca035c547 fix(messages): accept null version on legacy message rows (#3004) 2026-09-28 01:10:52 +02:00
HampusandGitHub dfd46ccc2c ci(gateway): drop cached gateway build output before compiling (#3003) 2026-09-28 01:08:52 +02:00
HampusandGitHub f6df3169ca fix(app): use +:shortcode: for reactions, no space before emoji (#3001) 2026-09-28 00:48:23 +02:00
HampusandGitHub 5b280898c5 refactor(push): retire the push service delivery experiment (#3000) 2026-09-28 00:45:22 +02:00
HampusandGitHub 2a9e25c788 fix(dev): drop the stray -- from the tunnel public URL hint (#2999) 2026-09-28 00:43:32 +02:00
HampusandGitHub 463c03fb6d feat(app): make +emoji react on send and target replies (#2998) 2026-09-28 00:08:40 +02:00
HampusandGitHub 153dad11e1 feat(installer): let upgrades copy the uploads uncompressed (#2995) 2026-09-27 23:51:24 +02:00
HampusandGitHub e2d05a44a8 fix(push): stop retrying relay rate limit refusals (#2993) 2026-09-27 23:29:27 +02:00
HampusandGitHub 30ba55bd4d fix(gateway): parse push relay hosts as binaries (#2989) 2026-09-27 21:22:45 +02:00
HampusandGitHub 9def9fbef6 feat(api): accept CIDR ranges in FLUXER_API_IP_BAN_EXEMPT_IPS (#2988) 2026-09-27 21:19:35 +02:00
HampusandGitHub fa3fd0027c fix(i18n): translate the push relay notice strings (#2987) 2026-09-27 21:15:33 +02:00
HampusandGitHub 7e1b934637 feat(captcha): add ALTCHA proof-of-work captcha experiment (#2986) 2026-09-27 21:02:55 +02:00
HampusandGitHub 33a118d12a docs(readme): list the Google Play beta first for Android (#2985) 2026-09-27 20:49:39 +02:00
HampusandGitHub 01f53a168d feat(push): gate relay delivery on operator consent (#2984) 2026-09-27 20:33:10 +02:00
HampusandGitHub 336b8b7dcd fix(forward): make an @silent comment silence the forward too (#2983) 2026-09-27 20:13:14 +02:00
HampusandGitHub 48d0034239 fix(app-proxy): trust the Play app signing certificate (#2982) 2026-09-27 19:37:40 +02:00
HampusandGitHub 677ef8491e fix(desktop): back off failed app loads and offer a retry (#2980) 2026-09-27 16:18:01 +02:00
HampusandGitHub 6a6119ed1e fix(push): preview forwarded message content (#2979) 2026-09-27 13:33:22 +02:00
HampusandGitHub 931327d1dc fix(push): stop sending notifications for system messages (#2978) 2026-09-27 13:33:18 +02:00
HampusandGitHub 858a2d9e2b fix(oauth): stop granting scopes the user turned off (#2968) 2026-09-26 13:48:23 +02:00
HampusandGitHub 841fb7af41 feat(auth): migrate passkeys to fluxer.com (#2964) 2026-09-25 22:33:50 +02:00
HampusandGitHub 08e65d41c0 fix(api): clear the perks-sanitized latch when premium returns (#2963) 2026-09-25 20:13:00 +02:00
HampusandGitHub f76c4dc041 fix(api): cancel only the subscription the refund belongs to (#2962) 2026-09-25 20:10:54 +02:00
HampusandGitHub f1f8ba2031 fix(app): add copy link to link channel context menus (#2959) 2026-09-25 18:16:20 +02:00
HampusandGitHub 5ab8d745c0 fix(i18n): correct the fluxer.com migration translations (#2958) 2026-09-25 17:46:07 +02:00
HampusandGitHub ff62bc89a4 feat(app): add passkey popup bridge for password managers (#2957) 2026-09-25 17:43:19 +02:00
HampusandGitHub 838bbdb5ec fix(app): only start the domain migration when the app opens (#2956) 2026-09-25 16:44:58 +02:00
HampusandGitHub 1c36a59b2c feat(app): rework quick switcher ranking and show origin icons (#2953) 2026-09-25 13:59:25 +02:00
HampusandGitHub 6730a242db feat(web): prepare the fluxer.com domain migration (#2952) 2026-09-25 13:43:34 +02:00
HampusandGitHub e62ae77643 refactor(config): trim the default passkey origin list (#2951) 2026-09-25 13:42:02 +02:00
HampusandGitHub f4f39e6a89 feat(app): show where forward destinations come from (#2950) 2026-09-25 13:12:17 +02:00
HampusandGitHub 00bf74cef5 fix(app): handle swapped overwrites when comparing channels (#2949) 2026-09-24 23:38:04 +02:00
HampusandGitHub c1c45d835f fix(app): only parse markdown in rich embeds (#2948) 2026-09-24 22:50:34 +02:00
HampusandGitHub bbfe809bef fix(app): crop animated images on web with libwebp (#2947) 2026-09-24 22:45:53 +02:00
HampusandGitHub e0843ac4f5 fix(app): keep guild folder expansion state local (#2944) 2026-09-24 17:52:33 +02:00
HampusandGitHub 43741cdad8 fix(gateway): always trim the connect snapshot for guild connects (#2943) 2026-09-24 17:09:48 +02:00
HampusandGitHub b8e3807262 Revert "fix(push): deliver direct messages without holding them" (#2942) 2026-09-24 17:09:44 +02:00
HampusandGitHub 3304f01a84 chore(i18n): recompile uk error catalog (#2941) 2026-09-24 17:09:36 +02:00
fluxer-weblate[bot]andGitHub 2ba463235b chore(i18n): update translations from Weblate (#2909) 2026-09-24 16:25:26 +02:00
fluxer-weblate[bot]andGitHub 15136fed59 chore(i18n): update translations from Weblate (#2923) 2026-09-24 16:25:05 +02:00
HampusandGitHub 6013581dd9 fix(push): deliver direct messages without holding them (#2938) 2026-09-24 16:21:42 +02:00
HampusandGitHub 7a91f128e9 fix(app-proxy): drop link preview metadata on self-hosted (#2936) 2026-09-24 16:07:00 +02:00
HampusandGitHub 963ffc5550 feat(push): scope read clears to the enrolled cohort (#2935) 2026-09-24 15:15:45 +02:00
HampusandGitHub a90991612c fix(gateway): truncate reads on an expired outbox entry (#2934) 2026-09-24 15:04:24 +02:00
HampusandGitHub 50ad23b760 fix(api): run the notification extension on every iOS alert (#2933) 2026-09-24 15:04:01 +02:00
HampusandGitHub 425dab983b fix(push): restore iOS avatars and stop misrouting relay endpoints (#2932) 2026-09-24 15:03:32 +02:00
HampusandGitHub a0825e77c4 feat(voice): ship the screen share delivery rework to everyone (#2931) 2026-09-24 14:57:40 +02:00
HampusandGitHub 88038a1d5b fix(voice): stop direct input capturing microphones in stereo (#2929) 2026-09-24 14:51:05 +02:00
HampusandGitHub c2c0fdb445 fix(app): make corner volume control the focused stream (#2928) 2026-09-24 14:04:05 +02:00
HampusandGitHub dcd5f09d6a feat(api): add env toggles for automatic phone flagging (#2927) 2026-09-24 03:36:35 +02:00
HampusandGitHub 590b1f36fd docs(downloads): document the canary apt and dnf repositories (#2926) 2026-09-24 03:29:52 +02:00
HampusandGitHub 168ac727f1 fix(desktop): set the deb package synopsis (#2925) 2026-09-24 03:29:33 +02:00
HampusandGitHub deb86dd92e fix(admin): format users list search hint (#2924) 2026-09-24 02:12:37 +02:00
omsterandGitHub 7ccec4d3b8 feat(admin): hint text for * search in user page (#2922) 2026-09-24 01:56:17 +02:00
omsterandGitHub 2f38bcdf26 fix(admin): ordering fixes for admin user search and meilisearch (#2920) 2026-09-24 01:45:56 +02:00
HampusandGitHub f2785941aa fix(app): point self-hosted users at their instance admins (#2921) 2026-09-24 01:42:33 +02:00
HampusandGitHub ea9f83a443 fix(push): keep read-state clears alive as long as the alert (#2919) 2026-09-24 01:26:18 +02:00
HampusandGitHub bd6ca7290e fix(api): allow deleting messages without send permission (#2918) 2026-09-24 01:14:01 +02:00
HampusandGitHub b85e975fb5 feat(push): deliver our own relay endpoints in process (#2917) 2026-09-24 01:07:09 +02:00
HampusandGitHub b6e504f68c fix(push): keep device tokens out of logs (#2916) 2026-09-24 00:33:50 +02:00
HampusandGitHub 5fde6eb484 feat(push): ring Android calls and harden the relay (#2915) 2026-09-24 00:07:15 +02:00
HampusandGitHub b16989d567 feat(push): ring incoming calls on Apple PushKit devices (#2911) 2026-09-23 20:21:08 +02:00
HampusandGitHub c9754ac11a fix(api): exempt internal rpc from the client ip check (#2910) 2026-09-23 18:03:36 +02:00
fluxer-weblate[bot]andGitHub f34e4a5115 chore(i18n): update translations from Weblate (#2903) 2026-09-23 17:28:25 +02:00
fluxer-weblate[bot]andGitHub 44b3615298 chore(i18n): update translations from Weblate (#2904) 2026-09-23 17:27:59 +02:00
HampusandGitHub 211e98307d perf(push): cache endpoint guard dns verdicts (#2907) 2026-09-23 17:27:19 +02:00
HampusandGitHub 18c303abf6 feat(push): relay notifications as encrypted web push (#2906) 2026-09-23 14:04:55 +02:00
JiraliteandGitHub 7021a58090 fix: allow copying message snapshots (#2905) 2026-09-23 14:01:10 +02:00
WagnerandGitHub 320725a587 fix(desktop): capture full pipewire quantum on linux (#2481) 2026-09-22 21:37:20 +02:00
fluxer-weblate[bot]andGitHub 8450edc072 chore(i18n): update translations from Weblate (#2895) 2026-09-22 21:28:24 +02:00
omsterandGitHub a1e2bf2c8d feat(dev/linux): select the wayland backend when reachable in the native desktop app (#2899)
Signed-off-by: omstr <[email protected]>
2026-09-22 21:27:59 +02:00
HampusandGitHub 82b2f4ec5e fix(app): put jxl and other image attachments in the mosaic (#2902) 2026-09-22 21:18:39 +02:00
HampusandGitHub c92e5d03a7 fix(api): accept any image or video attachment as embed media (#2901) 2026-09-22 21:18:35 +02:00
HampusandGitHub 91340c5c84 fix(markdown): compile the parser wasm asynchronously (#2900) 2026-09-22 19:58:38 +02:00
HampusandGitHub 045dd5d027 test(api): make the harvest token tamper test deterministic (#2894) 2026-09-22 02:20:18 +02:00
HampusandGitHub a21b9c4659 docs(readme): clean up the download prose (#2893) 2026-09-22 02:08:42 +02:00
HampusandGitHub 4b1b869802 docs(readme): point Linux installs at Flathub (#2892) 2026-09-22 02:04:06 +02:00
HampusandGitHub 1ab7e7dfcc fix(api): unfurl links to a self-hosted instance's own domain (#2891) 2026-09-22 02:00:51 +02:00
HampusandGitHub 31c53d2dff fix(app): stop pending stickers from reloading the channel (#2890) 2026-09-22 02:00:26 +02:00
HampusandGitHub 412a1ae79d perf(api): stop ledgering session payment reconciliation (#2889) 2026-09-22 01:37:21 +02:00
HampusandGitHub 0b2306ec3d fix(api): honour default TTLs and expire stale job ledger rows (#2887) 2026-09-21 23:16:39 +02:00
HampusandGitHub 242ed3a934 fix(desktop): drop orphaned Squirrel uninstall entry (#2885) 2026-09-21 20:03:33 +02:00
HampusandGitHub 70e1ce682a feat(emoji): add Unicode 17 emoji and fix mixed skin tones (#2883) 2026-09-21 16:26:06 +02:00
HampusandGitHub 7601bf98ee fix(channel): sync a cleared group DM name without a reload (#2882) 2026-09-21 15:34:18 +02:00
c7ec2a0f58 chore(tooling): Ignore .vscode/ in .gitignore (#2868)
Co-authored-by: Hampus <[email protected]>
2026-09-21 13:29:33 +02:00
XeonandGitHub 6a5e0056a8 fix(flatpak): Add a release tag and make small corrections (#2872) 2026-09-21 13:28:36 +02:00
HampusandGitHub 78d105b46e fix(desktop): stop looping on an update that never installs (#2879) 2026-09-21 03:36:11 +02:00
HampusandGitHub c68d62b8a0 fix(voice): darken screen share source titles in light theme (#2878) 2026-09-21 01:20:54 +02:00
HampusandGitHub df58020f4c fix(api): keep premium paid for after a subscription cancels (#2875) 2026-09-20 23:50:49 +02:00
HampusandGitHub f052ce05aa fix(workspace): point the Erlang extension at the repo root (#2871) 2026-09-20 19:49:06 +02:00
HampusandGitHub eedfd9275f fix(api): only require permissions a channel overwrite grants (#2867) 2026-09-20 17:56:22 +02:00
HampusandGitHub 416af4bec4 fix(docs): correct the flatpak and dnf signing instructions (#2865) 2026-09-20 16:42:33 +02:00
HampusandGitHub 108d282ddd chore(deps): pin pnpm 11 so the lockfile parses for packagers (#2864) 2026-09-20 15:30:50 +02:00
HampusandGitHub a6103244b0 docs(readme): fix the license wording and shrink the preview (#2862) 2026-09-20 15:11:06 +02:00
HampusandGitHub 38935c83c5 docs(readme): document every download and install method (#2861) 2026-09-20 15:05:54 +02:00
HampusandGitHub c157ab5752 feat(voice): rework screen share delivery behind an experiment (#2859) 2026-09-20 06:10:20 +02:00
HampusandGitHub 574a93257c docs(downloads): the pacman repository is signed (#2858) 2026-09-20 05:54:28 +02:00
HampusandGitHub ba7d8781cf feat(auth): make passkey two-factor authentication opt-in (#2857) 2026-09-20 05:06:50 +02:00
HampusandGitHub 3256af8d92 refactor(app-proxy): remove the stable time freeze (#2856) 2026-09-20 04:02:47 +02:00
HampusandGitHub 86043212f2 docs(downloads): one pacman repository holds both channels (#2855) 2026-09-20 02:50:08 +02:00
HampusandGitHub 5d85e88532 fix(search): suggest yourself in DM from: and mentions: filters (#2854) 2026-09-20 01:24:43 +02:00
HampusandGitHub e2abfd476a feat(api): redirect desktop downloads to pkgs (#2853) 2026-09-20 01:20:30 +02:00
HampusandGitHub 487febac8e fix(voice): make stereo microphones work in studio and custom (#2852) 2026-09-20 00:19:53 +02:00
HampusandGitHub a3454e8245 fix(installer): name the services that are not ready (#2851) 2026-09-19 23:34:08 +02:00
HampusandGitHub bf7567b768 fix(user): push guild member updates on profile field changes (#2850) 2026-09-19 23:30:25 +02:00
HampusandGitHub ac3450ab32 feat(ci): publish appimage zsync control files (#2849) 2026-09-19 22:22:54 +02:00
HampusandGitHub 5d034becb8 fix(installer): stop waiting for an absent bucket initialiser (#2848) 2026-09-19 22:14:13 +02:00
HampusandGitHub f9397d0db9 feat(ci): publish linux repositories from the desktop release (#2847) 2026-09-19 22:01:06 +02:00
HampusandGitHub 9005139dc8 fix(voice): stop stereo microphones publishing as mono (#2846) 2026-09-19 21:54:38 +02:00
HampusandGitHub d93604afa2 fix(voice): let screen shares use the hardware H.264 encoder (#2845) 2026-09-19 21:54:30 +02:00
HampusandGitHub c4f0b2ece0 feat(desktop): self-update appimages in place (#2843) 2026-09-19 19:06:53 +02:00
HampusandGitHub 98a42f612b fix(desktop): supersede the legacy linux packages on upgrade (#2842) 2026-09-19 18:50:37 +02:00
HampusandGitHub cc75e1318d fix(ci): raise the macos minimum to 13.0 (#2841) 2026-09-19 18:35:05 +02:00
HampusandGitHub 9027cbdf3e fix(voice): send screen shares at the quality the user picked (#2840) 2026-09-19 16:46:22 +02:00
HampusandGitHub 2119e10ed5 chore(static): update marketing screenshots and readme cover (#2839) 2026-09-19 16:44:32 +02:00
HampusandGitHub 87f3eb3c81 feat(desktop): add flatpak and arch packaging inputs (#2838) 2026-09-19 15:31:41 +02:00
HampusandGitHub f9bb8bd585 test(voice): remove the slow screen share delivery proof (#2836) 2026-09-19 02:33:32 +02:00
HampusandGitHub bc47a724af fix(voice): stop screen shares failing to reach their viewers (#2835) 2026-09-19 02:17:25 +02:00
HampusandGitHub f32356801d feat(api): make tor and breached password lookups opt-in (#2834) 2026-09-19 01:22:17 +02:00
HampusandGitHub efd677f32b feat(api): exempt configured ASNs from abusive IP auto-bans (#2833) 2026-09-18 23:01:58 +02:00
HampusandGitHub 3cec27ba57 fix(static): vendor the deepfilternet 1.3.0 assets (#2832) 2026-09-18 18:47:42 +02:00
HampusandGitHub 1f810ba04d fix(api): drop the upload segment signal and dead exports (#2831) 2026-09-18 17:35:58 +02:00
HampusandGitHub 522cf08e61 feat(media-proxy): sign attachment URLs and gate origins (#2830) 2026-09-18 15:57:32 +02:00
HampusandGitHub 025c01ab13 fix(api): chunk guild permission batch RPC over 100 guilds (#2829) 2026-09-18 12:54:03 +02:00
HampusandGitHub dc41b53d60 fix(desktop): drop redundant casts flagged by clippy 1.98 (#2826) 2026-09-17 21:28:48 +02:00
HampusandGitHub 3b552e00ef chore(deps): upgrade all dependencies, toolchains and images (#2825) 2026-09-17 21:08:56 +02:00
HampusandGitHub 56e04e7b53 test(backend): remove duplicate and useless tests (#2820) 2026-09-17 15:32:25 +02:00
HampusandGitHub deac653a9e test(app): remove useless frontend tests (#2819) 2026-09-17 15:05:36 +02:00
HampusandGitHub ed9528834d fix(gateway): stop dead sessions leaving voice states behind (#2818) 2026-09-17 14:55:18 +02:00
HampusandGitHub 4cecbf1f43 fix(auth): disable TOTP with one code instead of two (#2816) 2026-09-17 04:21:50 +02:00
HampusandGitHub b019f4a91f fix(gateway): act on voice states in the voice server (#2815) 2026-09-17 03:59:36 +02:00
HampusandGitHub 34b6ecfbd2 chore(admin): remove the heap snapshot endpoint (#2814) 2026-09-16 18:56:01 +02:00
HampusandGitHub 4ef9c4c65b fix(api): restore commas in geoip location labels (#2812) 2026-09-16 18:27:50 +02:00
HampusandGitHub 3276039e41 feat(admin): audit admin reads and filter the log by access (#2811) 2026-09-16 17:23:03 +02:00
HampusandGitHub 03d1354562 chore(voice): remove voice reconciliation leftovers (#2810) 2026-09-16 17:09:57 +02:00
HampusandGitHub 964845d7a7 chore(voice): remove the recon service (#2808) 2026-09-16 16:53:30 +02:00
HampusandGitHub 3bc5dd8e0f fix(gateway): always clear expired custom statuses (#2807) 2026-09-16 16:52:22 +02:00
HampusandGitHub 17292fd6a5 fix(app): stop plain unicode symbols rendering as color emoji (#2806) 2026-09-16 16:33:13 +02:00
TarekandGitHub f753659899 feat(instance): make the status page URL configurable (#1159) 2026-09-16 15:20:37 +02:00
HampusandGitHub 7412ec3395 refactor(api): purge cache by canonical media prefix (#2802) 2026-09-16 02:32:36 +02:00
HampusandGitHub 570c8776c4 fix(api): require manage messages to remove others' reactions (#2799) 2026-09-15 18:16:55 +02:00
HampusandGitHub 910db6734b feat(experiments): ship seven treatments to everyone (#2798) 2026-09-15 18:03:35 +02:00
HampusandGitHub 9e614026d7 fix(api): stop exporting the change feed stats type (#2797) 2026-09-15 17:27:09 +02:00
HampusandGitHub b38e7c6433 feat(api): publish object storage changes to a JetStream feed (#2796) 2026-09-15 17:20:26 +02:00
HampusandGitHub 83c8e91955 fix(app): fit the user area popout shadow to its card (#2795) 2026-09-15 17:11:43 +02:00
HampusandGitHub 0532dd0440 fix(app): stop guild banner jumps and restore hover animation (#2792) 2026-09-15 09:31:52 +02:00
HampusandGitHub a08615e306 fix(gateway): match member search on username and global name (#2791) 2026-09-15 08:48:33 +02:00
HampusandGitHub f4c5fee17e feat(app): rank forward destinations and preview the message (#2790) 2026-09-15 07:23:26 +02:00
HampusandGitHub c5aaf65a10 fix(app): list friends with closed DMs in the forward modal (#2787) 2026-09-15 00:39:18 +02:00
HampusandGitHub 951e39da3d feat(api): add expression source guild routes (#2786) 2026-09-15 00:31:47 +02:00
HampusandGitHub b693d84d2b fix(openapi): restore named discriminated union branches (#2785) 2026-09-14 23:42:26 +02:00
HampusandGitHub 9bbf6c513b fix(installer): say what the email prompt is for (#2784) 2026-09-14 23:07:03 +02:00
HampusandGitHub 50cec92738 fix(api): batch member user lookups on guild load (#2783) 2026-09-14 23:06:36 +02:00
HampusandGitHub c212d315f4 fix(app): gate reworked typing indicators behind an experiment (#2782) 2026-09-14 21:54:52 +02:00
HampusandGitHub 1861432a53 fix(app): scope message rings and reach the composer by key (#2781) 2026-09-14 21:44:12 +02:00
HampusandGitHub d0c6146429 feat(app): gate a collapsing guild header behind an experiment (#2779) 2026-09-14 21:15:31 +02:00
HampusandGitHub 550e6b05a1 fix(app): show hover highlight and inset the focus ring (#2778) 2026-09-14 20:51:59 +02:00
HampusandGitHub 5b6949170f chore(donations): drop the donor email case backfill script (#2776) 2026-09-14 20:45:38 +02:00
HampusandGitHub f32bc37794 fix(guild): correct activity log sentence presentation (#2777) 2026-09-14 20:43:08 +02:00
HampusandGitHub 8ee2279b4b feat(donations): add Nordic currencies, raise amount ceilings (#2775) 2026-09-14 20:27:24 +02:00
HampusandGitHub 6339c3b8ad feat(app): gate the expression info card behind an experiment (#2773) 2026-09-14 20:14:58 +02:00
HampusandGitHub 7c9274847f feat(gateway): list bot ready guilds as unavailable (#2774) 2026-09-14 20:08:26 +02:00
HampusandGitHub 5d1dddc093 fix(deps): update rustls for RUSTSEC-2026-0285 (#2772) 2026-09-14 19:01:22 +02:00
HampusandGitHub 04481d7235 fix(app): keep blockquotes open across pasted lines (#2771) 2026-09-14 18:54:04 +02:00
Hampus Kraft a3d6cf37cb fix(guild): render activity log entries deterministically (#2766) 2026-09-14 17:39:19 +02:00
HampusandGitHub ed10f9d323 fix(app): gate blocked group rendering behind an experiment (#2763) 2026-09-14 16:08:19 +02:00
HampusandGitHub 4b278a0da8 fix(app): gate one-Tab message focus behind an experiment (#2762) 2026-09-14 15:55:00 +02:00
HampusandGitHub 1281045648 fix(app): gate single-source message hover behind an experiment (#2761) 2026-09-14 15:37:37 +02:00
HampusandGitHub 69c42cff90 docs: reword vague sentences and fix wrong claims (#2760) 2026-09-14 15:18:53 +02:00
HampusandGitHub 7d56481aba fix(app): copy selected message text without markdown (#2759) 2026-09-14 14:53:53 +02:00
HampusandGitHub 91a2604e9e fix(admin): apply audit logs and side effects to bulk actions (#2758) 2026-09-14 14:34:43 +02:00
3337 changed files with 379554 additions and 298810 deletions
+9 -11
View File
@@ -1,14 +1,14 @@
FROM chrislusf/seaweedfs:4.31 AS seaweedfs
FROM chrislusf/seaweedfs:4.47 AS seaweedfs
FROM erlang:28.5.0.1
FROM erlang:28.5.0.6
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000
ARG NODE_MAJOR=24
ARG ELP_VERSION=2026-02-27
ARG PNPM_VERSION=10.29.3
ARG WASM_BINDGEN_VERSION=0.2.123
ARG NODE_MAJOR=26
ARG ELP_VERSION=2026-08-10
ARG PNPM_VERSION=11.27.0
ARG WASM_BINDGEN_VERSION=0.2.128
ENV DEBIAN_FRONTEND=noninteractive
@@ -131,7 +131,8 @@ RUN apt-get update \
RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable
&& npm install -g "pnpm@${PNPM_VERSION}" \
&& pnpm --version
RUN python3 -m pip install --break-system-packages --no-cache-dir awscli
@@ -167,7 +168,7 @@ RUN ARCH="$(dpkg --print-architecture)" \
arm64) ELP_ARCH="aarch64" ;; \
*) echo "Unsupported architecture for ELP: $ARCH" >&2; exit 1 ;; \
esac \
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.tar.gz" -o /tmp/elp.tgz \
&& curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL "https://github.com/WhatsApp/erlang-language-platform/releases/download/${ELP_VERSION}/elp-linux-${ELP_ARCH}-unknown-linux-gnu-otp-28.5.tar.gz" -o /tmp/elp.tgz \
&& tar -C /usr/local/bin -xzf /tmp/elp.tgz elp \
&& chmod +x /usr/local/bin/elp \
&& rm /tmp/elp.tgz
@@ -194,7 +195,4 @@ RUN curl --retry 5 --retry-delay 2 --retry-all-errors -fsSL https://sh.rustup.rs
&& cargo install wasm-bindgen-cli --version "${WASM_BINDGEN_VERSION}" --locked \
&& rm -rf "/home/${USERNAME}/.cargo/registry" "/home/${USERNAME}/.cargo/git"
RUN corepack prepare "pnpm@${PNPM_VERSION}" --activate \
&& pnpm --version
WORKDIR /workspaces/fluxer
+5 -1
View File
@@ -38,7 +38,11 @@
"customizations": {
"vscode": {
"settings": {
"editor.defaultFormatter": "biomejs.biome"
"editor.defaultFormatter": "biomejs.biome",
"erlang.includePaths": ["."],
"search.exclude": {
"**/_build/default/lib/fluxer_gateway": true
}
},
"extensions": [
"biomejs.biome",
+6 -12
View File
@@ -9,7 +9,7 @@ services:
init: true
environment:
DOCKER_HOST: unix:///var/run/docker.sock
npm_config_store_dir: /home/vscode/.local/share/pnpm/store
pnpm_config_store_dir: /home/vscode/.local/share/pnpm/store
FLUXER_PUBLIC_PORT: "${FLUXER_DEV_PROXY_PORT:-8088}"
FLUXER_PUBLIC_URL: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}"
FLUXER_API_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/api"
@@ -23,7 +23,6 @@ services:
FLUXER_S3_PUBLIC_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}"
FLUXER_LIVEKIT_URL: "ws://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/livekit"
FLUXER_LIVEKIT_INTERNAL_URL: "http://livekit:7880"
FLUXER_LIVEKIT_WEBHOOK_URL: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/api/webhooks/livekit"
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/media"
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}/media"
FLUXER_GATEWAY_STATIC_CDN_ENDPOINT: "http://localhost:${FLUXER_DEV_PROXY_PORT:-8088}"
@@ -202,11 +201,6 @@ services:
target: /workspaces/fluxer/fluxer_api/pkgs/rate_limit/node_modules
volume:
nocopy: true
- type: volume
source: fluxer-api-sms-node-modules
target: /workspaces/fluxer/fluxer_api/pkgs/sms/node_modules
volume:
nocopy: true
- type: volume
source: fluxer-api-virus-scan-node-modules
target: /workspaces/fluxer/fluxer_api/pkgs/virus_scan/node_modules
@@ -292,13 +286,13 @@ services:
start_period: 5s
valkey:
image: valkey/valkey:8.1.7-alpine
image: valkey/valkey:9.1.2-alpine
command: ["valkey-server", "--save", "", "--appendonly", "no"]
ports:
- "127.0.0.1:${FLUXER_DEV_VALKEY_PORT:-6379}:6379"
nats:
image: nats:2.14.2-alpine
image: nats:2.14.7-alpine
command: ["-js", "-sd", "/data", "-m", "8222"]
volumes:
- nats-data:/data
@@ -321,9 +315,10 @@ services:
- "127.0.0.1:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}:${FLUXER_DEV_LIVEKIT_UDP_PORT:-7882}/udp"
meilisearch:
image: getmeili/meilisearch:v1.12
image: getmeili/meilisearch:v1.53
environment:
MEILI_NO_ANALYTICS: "true"
MEILI_UPGRADE_DB: "true"
MEILI_MASTER_KEY: fluxer-dev-meilisearch
volumes:
- meilisearch-data:/meili_data
@@ -337,7 +332,7 @@ services:
start_period: 5s
mailpit:
image: axllent/mailpit:v1.30
image: axllent/mailpit:v1.31
environment:
MP_DATABASE: /data/mailpit.db
MP_MAX_MESSAGES: 5000
@@ -383,7 +378,6 @@ volumes:
fluxer-api-mime-utils-node-modules:
fluxer-api-nats-node-modules:
fluxer-api-rate-limit-node-modules:
fluxer-api-sms-node-modules:
fluxer-api-virus-scan-node-modules:
fluxer-api-worker-node-modules:
fluxer-app-list-utils-node-modules:
+1
View File
@@ -32,6 +32,7 @@
/fluxer_docs/.astro/
/fluxer_app/.devserver-cache.json
/fluxer_app/pkgs/libfluxcore/
/fluxer_app/pkgs/libfluxwebp/
/fluxer_app/src/features/i18n/locales/*/messages.mjs
/fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
/fluxer_app/src/features/theme/styles/generated/
+2
View File
@@ -2,3 +2,5 @@
fluxer_static/** -text -diff
fluxer_static/**/*.md text diff
packages/fonts/files/** -text -diff
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.wasm -text -diff
fluxer_app/src/features/voice/utils/noise_suppression/deepfilternet3/*.tar.gz -text -diff
+2 -2
View File
@@ -28,9 +28,9 @@ f:media_proxy:
f:messages:
- changed-files:
- any-glob-to-any-file: fluxer_messages/**/*
f:recon:
f:push:
- changed-files:
- any-glob-to-any-file: fluxer_recon/**/*
- any-glob-to-any-file: fluxer_push/**/*
f:snowflakes:
- changed-files:
- any-glob-to-any-file: fluxer_snowflakes/**/*
+12 -12
View File
@@ -58,13 +58,13 @@ jobs:
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Create token
id: create-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
@@ -101,19 +101,19 @@ jobs:
- platform: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: resolve source date
id: source
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
@@ -141,15 +141,15 @@ jobs:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
toolchain: "1.98.1"
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -43,13 +43,13 @@ jobs:
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: set variables
id: vars
run: >-
@@ -71,20 +71,19 @@ jobs:
BUILD_VERSION: ${{ needs.meta.outputs.build_version }}
PUBLIC_ASSET_BASE_URL: ""
BUNDLE_LOCAL_ASSETS: "true"
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED: "false"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: prepare docker config
run: >-
tools/ci/run.sh build-app-proxy
--step prepare_docker_config
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- name: configure ghcr auth
env:
GHCR_USERNAME: ${{ github.actor }}
@@ -131,19 +130,19 @@ jobs:
- platform: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: resolve source date
id: source
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: .
file: fluxer_app_proxy/Dockerfile
@@ -155,7 +154,6 @@ jobs:
BUILD_VERSION=${{ needs.meta.outputs.build_version }}
SOURCE_SHA=${{ github.sha }}
SOURCE_DATE=${{ steps.source.outputs.date }}
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED=false
APP_ASSETS_REF=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:${{ needs.meta.outputs.build_version }}-assets
APP_ASSETS_PLATFORM=linux/amd64
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy-self-hosted:buildcache-${{ matrix.platform }}
@@ -173,15 +171,15 @@ jobs:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
toolchain: "1.98.1"
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+20 -20
View File
@@ -43,13 +43,13 @@ jobs:
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: set variables
id: vars
run: >-
@@ -67,18 +67,18 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: prepare docker config
run: >-
tools/ci/run.sh build-app-proxy
--step prepare_docker_config
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- name: configure ghcr auth
env:
GHCR_USERNAME: ${{ github.actor }}
@@ -131,13 +131,13 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: resolve source date
id: source
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
@@ -145,7 +145,7 @@ jobs:
run: >-
tools/ci/run.sh build-app-proxy
--step prepare_docker_config
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- name: configure ghcr auth
env:
GHCR_USERNAME: ${{ github.actor }}
@@ -178,19 +178,19 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: resolve source date
id: source
run: echo "date=$(TZ=UTC git log -1 --no-show-signature --pretty=%cd --date=format-local:%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
with:
context: .
file: fluxer_app_proxy/Dockerfile
@@ -219,15 +219,15 @@ jobs:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
toolchain: "1.98.1"
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+68 -109
View File
@@ -11,11 +11,6 @@ on:
- stable
- canary
default: stable
test_build:
description: Stash artifacts under desktop-test/ instead of desktop/ (API will not pick these up as a release).
required: false
default: false
type: boolean
build_version:
description: Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation.
required: false
@@ -32,13 +27,12 @@ permissions:
actions: read
concurrency:
group: desktop-${{ inputs.channel }}-${{ inputs.test_build && 'test' || 'release' }}
group: desktop-${{ inputs.channel }}
cancel-in-progress: true
env:
CHANNEL: ${{ inputs.channel }}
BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
TEST_BUILD: ${{ inputs.test_build && 'true' || 'false' }}
jobs:
meta:
@@ -53,19 +47,17 @@ jobs:
pub_date: ${{ steps.meta.outputs.pub_date }}
channel: ${{ steps.meta.outputs.channel }}
build_channel: ${{ steps.meta.outputs.build_channel }}
test_build: ${{ steps.meta.outputs.test_build }}
s3_prefix: ${{ steps.meta.outputs.s3_prefix }}
source_sha: ${{ steps.meta.outputs.source_sha }}
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: main
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Create token
id: create-token
@@ -85,7 +77,6 @@ jobs:
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step set_metadata
--channel "${{ inputs.channel }}"
--test-build "${{ inputs.test_build }}"
matrix:
name: Resolve build matrix
@@ -98,12 +89,12 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Build platform matrix
id: set-matrix
@@ -113,7 +104,7 @@ jobs:
--skip-targets "${{ inputs.skip_targets }}"
build:
name: Build ${{ matrix.platform }} (${{ matrix.arch }}, ${{ matrix.desktop_variant }})
name: Build ${{ matrix.platform }} (${{ matrix.arch }})
needs:
- meta
- matrix
@@ -137,41 +128,34 @@ jobs:
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
DESKTOP_PLATFORM: ${{ matrix.platform }}
DESKTOP_ARCH: ${{ matrix.arch }}
DESKTOP_VARIANT: ${{ matrix.desktop_variant }}
PLATFORM: ${{ matrix.platform }}
ARCH: ${{ matrix.arch }}
ELECTRON_ARCH: ${{ matrix.electron_arch }}
steps:
- name: Checkout CI helpers
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.meta.outputs.source_sha }}
path: _ci
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.meta.outputs.source_sha }}
path: source
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Set up Python (Windows)
if: runner.os == 'Windows'
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.13"
python-version: "3.14"
- name: Ensure python3 command (Windows)
if: runner.os == 'Windows'
@@ -196,14 +180,14 @@ jobs:
--step set_workdir_unix
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
node-version: 26
- name: Set up pnpm via corepack
- name: Set up pnpm
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step setup_pnpm_corepack
--step setup_pnpm
- name: Resolve pnpm store path (Windows)
if: runner.os == 'Windows'
@@ -247,9 +231,9 @@ jobs:
- name: Set up Rust toolchain (Unix)
if: matrix.platform != 'windows'
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
targets: ${{ matrix.platform == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu') }}
- name: Install MSVC ARM64 build tools
@@ -260,7 +244,7 @@ jobs:
- name: Set up MSVC env (Windows)
if: matrix.platform == 'windows'
uses: TheMrMilchmann/setup-msvc-dev@79dac248aac9d0059f86eae9d8b5bfab4e95e97c
uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9
with:
arch: ${{ matrix.arch == 'arm64' && 'amd64_arm64' || 'amd64' }}
@@ -302,9 +286,9 @@ jobs:
- name: Set up .NET SDK (Windows)
if: matrix.platform == 'windows'
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
with:
dotnet-version: "8.0.x"
dotnet-version: "10.0.x"
- name: Install Velopack CLI
if: matrix.platform == 'windows'
@@ -363,7 +347,7 @@ jobs:
- name: Azure login for Artifact Signing
if: matrix.platform == 'windows'
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -477,6 +461,12 @@ jobs:
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step prepare_artifacts_unix
- name: Build AppImage update feed (Linux)
if: matrix.platform == 'linux'
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step build_appimage_update_feed
- name: Normalize updater YAML (macOS)
if: matrix.platform == 'macos'
run: >-
@@ -495,13 +485,23 @@ jobs:
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step generate_checksums_windows
- name: Upload artifacts to S3 handoff
- name: Stage build artifacts
id: handoff
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step upload_handoff
--step stage_handoff
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: ${{ steps.handoff.outputs.artifact_name }}
path: upload_staging
if-no-files-found: error
retention-days: 1
compression-level: 0
upload:
name: Upload to S3
name: Assemble desktop release assets
if: ${{ !cancelled() && needs.build.result == 'success' }}
needs:
- meta
@@ -520,34 +520,26 @@ jobs:
BUILD_VERSION: ${{ needs.meta.outputs.version }}
PUBLIC_BUILD_VERSION: ${{ needs.meta.outputs.version }}
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
TEST_BUILD: ${{ needs.meta.outputs.test_build }}
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
DESKTOP_HANDOFF_PREFIX: _handoff/desktop/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
PUBLIC_DL_BASE: https://api.fluxer.app/dl
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.meta.outputs.source_sha }}
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Download S3 handoff artifacts
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step download_handoff
- name: Download build artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: artifacts
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-*
- name: Build S3 payload layout (+ manifest.json)
- name: Build payload layout (+ manifest.json)
env:
VERSION: ${{ needs.meta.outputs.version }}
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
@@ -556,42 +548,27 @@ jobs:
--step build_payload
- name: Prepare GitHub release assets
if: needs.meta.outputs.test_build != 'true'
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step prepare_release_assets
- name: Publish GitHub release descriptor
if: needs.meta.outputs.test_build != 'true'
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step publish_release_descriptor
- name: Upload payload to S3
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step upload_payload
- name: Upload GitHub release asset handoff
if: needs.meta.outputs.test_build != 'true'
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step upload_release_assets
- name: Upload GitHub release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: fluxer-desktop-release-assets
path: release_assets
if-no-files-found: error
retention-days: 1
compression-level: 0
- name: Build summary
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step build_summary
- name: Cleanup S3 handoff
if: ${{ success() }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step cleanup_handoff
publish_release:
name: Publish GitHub desktop release
if: ${{ !cancelled() && needs.upload.result == 'success' && needs.meta.outputs.test_build != 'true' }}
if: ${{ !cancelled() && needs.upload.result == 'success' }}
needs:
- meta
- upload
@@ -603,28 +580,22 @@ jobs:
env:
CHANNEL: ${{ needs.meta.outputs.build_channel }}
VERSION: ${{ needs.meta.outputs.version }}
S3_DESKTOP_PREFIX: ${{ needs.meta.outputs.s3_prefix }}
DESKTOP_RELEASE_ASSETS_PREFIX: _handoff/desktop-release-assets/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
DESKTOP_METADATA_PREFIX: _handoff/desktop-metadata/${{ needs.meta.outputs.build_channel }}/${{ needs.meta.outputs.version }}/${{ needs.meta.outputs.source_sha }}
S3_ENDPOINT: ${{ vars.DOWNLOADS_S3_ENDPOINT }}
S3_BUCKET: ${{ vars.DOWNLOADS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.DOWNLOADS_AWS_ACCESS_KEY_ID || secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DOWNLOADS_AWS_SECRET_ACCESS_KEY || secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ needs.meta.outputs.source_sha }}
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Download GitHub release assets
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step download_release_assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: fluxer-desktop-release-assets
path: release_assets
- name: Create token
id: create-token
@@ -656,15 +627,3 @@ jobs:
release_args+=(--prerelease)
fi
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- "${release_args[@]}"
- name: Publish GitHub release readiness marker
env:
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step publish_release_marker
- name: Publish payload metadata to S3
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step publish_payload_metadata
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: build recon
name: build push
on:
workflow_dispatch:
@@ -31,6 +31,6 @@ jobs:
uses: ./.github/workflows/_build-image.yaml
secrets: inherit
with:
image: fluxer-recon
dockerfile: fluxer_recon/Dockerfile
image: fluxer-push
dockerfile: fluxer_push/Dockerfile
build-version: ${{ inputs['build-version'] }}
+6 -6
View File
@@ -19,22 +19,22 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout fluxer
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
+6 -6
View File
@@ -35,24 +35,24 @@ jobs:
permission-pull-requests: write
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
token: ${{ steps.create-token.outputs.token }}
fetch-depth: 0
persist-credentials: false
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "24"
node-version: "26"
cache: "pnpm"
- name: Install dependencies
+6 -6
View File
@@ -40,7 +40,7 @@ jobs:
permission-pull-requests: write
- name: Checkout Weblate branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
token: ${{ steps.create-token.outputs.token }}
ref: ${{ env.WEBLATE_BRANCH }}
@@ -48,17 +48,17 @@ jobs:
persist-credentials: false
- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: "24"
node-version: "26"
cache: "pnpm"
- name: Install dependencies
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
permission-pull-requests: write
- name: Label pull request
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13
with:
repo-token: ${{ steps.create-token.outputs.token }}
configuration-path: .github/labeller.yaml
+5 -5
View File
@@ -56,15 +56,15 @@ jobs:
contents: write
packages: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
toolchain: "1.98.1"
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
with:
registry: ghcr.io
username: ${{ github.actor }}
+81 -56
View File
@@ -28,12 +28,12 @@ jobs:
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
targets: wasm32-unknown-unknown
- name: Restore ci helper
@@ -42,7 +42,7 @@ jobs:
with:
path: target/debug/fluxer-ci
key: >-
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
- name: Build ci helper
@@ -55,16 +55,16 @@ jobs:
with:
path: target/debug/fluxer-ci
key: >-
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -83,12 +83,12 @@ jobs:
PNPM_TEST_WORKSPACE_CONCURRENCY: '2'
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
targets: wasm32-unknown-unknown
- name: Restore ci helper
@@ -97,7 +97,7 @@ jobs:
with:
path: target/debug/fluxer-ci
key: >-
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
- name: Build ci helper
@@ -105,12 +105,12 @@ jobs:
run: cargo build --locked --package fluxer-ci
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -123,12 +123,16 @@ jobs:
with:
path: |
fluxer_app/pkgs/libfluxcore
fluxer_app/pkgs/libfluxwebp
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
key: >-
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
'fluxer_app/rust/libfluxwebp/simd/**',
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
'packages/markdown_parser/rust/src/**') }}
@@ -142,12 +146,16 @@ jobs:
with:
path: |
fluxer_app/pkgs/libfluxcore
fluxer_app/pkgs/libfluxwebp
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
key: >-
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
'fluxer_app/rust/libfluxwebp/simd/**',
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
'packages/markdown_parser/rust/src/**') }}
@@ -156,21 +164,21 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
components: clippy, rustfmt
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Cache cargo
@@ -185,11 +193,14 @@ jobs:
rust-${{ runner.os }}-${{ hashFiles('fluxer_media_proxy/tools/install-native-deps.sh') }}-
- name: Install cargo-deny
run: cargo install cargo-deny --version 0.19.6 --locked
run: cargo install cargo-deny --version 0.20.2 --locked
- name: Check Rust dependencies
run: cargo deny --locked check -D warnings
- name: Check libfluxwebp dependencies
run: cargo deny --manifest-path fluxer_app/rust/libfluxwebp/Cargo.toml --config deny.toml --locked check licenses bans sources
- name: Check desktop native dependencies
run: tools/ci/check-desktop-native-workspaces.sh dependencies
@@ -242,6 +253,9 @@ jobs:
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check formatting (libfluxwebp)
run: cargo fmt --manifest-path fluxer_app/rust/libfluxwebp/Cargo.toml -- --check
- name: Check formatting (desktop native workspaces)
run: tools/ci/check-desktop-native-workspaces.sh fmt
@@ -273,12 +287,12 @@ jobs:
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
- name: Cache cargo (gateway NIFs)
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
@@ -294,7 +308,7 @@ jobs:
with:
path: target/debug/fluxer-ci
key: >-
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
- name: Build ci helper
@@ -305,7 +319,7 @@ jobs:
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124
with:
otp-version: '28'
rebar3-version: '3.24.0'
rebar3-version: '3.27.0'
- name: Restore rebar3 dependencies
id: rebar3-cache
@@ -317,10 +331,13 @@ jobs:
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
key: >-
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
'fluxer_gateway/rebar.config') }}
restore-keys: |
rebar3-${{ runner.os }}-otp28-rebar3.24.0-
rebar3-${{ runner.os }}-otp28-rebar3.27.0-
- name: Drop restored gateway build output
run: rm -rf fluxer_gateway/_build/default/lib/fluxer_gateway fluxer_gateway/_build/test/lib/fluxer_gateway
- name: Check formatting
run: |
@@ -348,7 +365,7 @@ jobs:
!fluxer_gateway/_build/default/lib/fluxer_gateway/**
!fluxer_gateway/_build/test/lib/fluxer_gateway/**
key: >-
rebar3-${{ runner.os }}-otp28-rebar3.24.0-${{ hashFiles('fluxer_gateway/rebar.lock',
rebar3-${{ runner.os }}-otp28-rebar3.27.0-${{ hashFiles('fluxer_gateway/rebar.lock',
'fluxer_gateway/rebar.config') }}
knip:
@@ -358,12 +375,12 @@ jobs:
FLUXER_CI_BIN: ${{ github.workspace }}/target/debug/fluxer-ci
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
toolchain: "1.93.0"
toolchain: "1.98.1"
targets: wasm32-unknown-unknown
- name: Restore ci helper
@@ -372,7 +389,7 @@ jobs:
with:
path: target/debug/fluxer-ci
key: >-
fluxer-ci-bin-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'Cargo.toml',
fluxer-ci-bin-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'Cargo.toml',
'tools/ci/Cargo.toml', 'tools/ci/src/**', 'tools/ci/templates/**') }}
- name: Build ci helper
@@ -380,12 +397,12 @@ jobs:
run: cargo build --locked --package fluxer-ci
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -398,12 +415,16 @@ jobs:
with:
path: |
fluxer_app/pkgs/libfluxcore
fluxer_app/pkgs/libfluxwebp
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
key: >-
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
'fluxer_app/rust/libfluxwebp/simd/**',
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
'packages/markdown_parser/rust/src/**') }}
@@ -417,12 +438,16 @@ jobs:
with:
path: |
fluxer_app/pkgs/libfluxcore
fluxer_app/pkgs/libfluxwebp
fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
key: >-
app-wasm-${{ runner.os }}-1.93.0-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
app-wasm-${{ runner.os }}-1.98.1-${{ hashFiles('Cargo.lock', 'tools/ci/src/app_wasm.rs',
'tools/ci/templates/libfluxcore_wrapper.js', 'tools/ci/templates/libfluxcore_wrapper.d.ts',
'fluxer_app/rust/libfluxcore/Cargo.toml', 'fluxer_app/rust/libfluxcore/Cargo.lock',
'fluxer_app/rust/libfluxcore/.cargo/config.toml', 'fluxer_app/rust/libfluxcore/src/**',
'fluxer_app/rust/libfluxwebp/Cargo.toml', 'fluxer_app/rust/libfluxwebp/Cargo.lock',
'fluxer_app/rust/libfluxwebp/src/**', 'fluxer_app/rust/libfluxwebp/shim/**',
'fluxer_app/rust/libfluxwebp/simd/**',
'packages/markdown_parser/rust/Cargo.toml', 'packages/markdown_parser/rust/.cargo/config.toml',
'packages/markdown_parser/rust/src/**') }}
@@ -431,15 +456,15 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -456,15 +481,15 @@ jobs:
timeout-minutes: 25
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -490,15 +515,15 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
node-version: '26'
cache: 'pnpm'
- name: Install dependencies
@@ -515,12 +540,12 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.13"
python-version: "3.14"
- name: Install font tooling
run: python3 -m pip install -r tools/fonts/requirements.txt
+2
View File
@@ -10,6 +10,7 @@
/.direnv/
/.fluxer/
/.pnpm-store/
/.vscode/
**/*.css.d.ts
**/*.tsbuildinfo
@@ -25,6 +26,7 @@
/fluxer_app/.devserver-cache.json
/fluxer_app/pkgs/libfluxcore/
/fluxer_app/pkgs/libfluxwebp/
/fluxer_app/src/features/i18n/locales/*/messages.mjs
/fluxer_app/src/features/messaging/utils/markdown/parser/MarkdownParserWasmBytes.ts
/fluxer_app/src/features/theme/styles/generated/
Generated
+861 -867
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -7,15 +7,14 @@ members = [
"fluxer_gifs",
"fluxer_svc",
"fluxer_messages",
"fluxer_push",
"fluxer_snowflakes",
"tools/ci",
"tools/content/update-frozen-snapshot",
"tools/dev",
"tools/i18n_auto",
"fluxer_users",
"fluxer_unfurl",
"packages/markdown_parser/rust",
"fluxer_recon",
]
exclude = [
"packages/markdown_parser/rust/fuzz",
+158 -3
View File
@@ -6,18 +6,173 @@
</p>
<p align="center">
<a href="https://fluxer.app/donate">
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
<a href="https://fluxer.app/download">
<img src="https://img.shields.io/badge/Download-fluxer.app-4641D9" alt="Download" /></a>
<a href="https://docs.fluxer.app">
<img src="https://img.shields.io/badge/Docs-docs.fluxer.app-blue" alt="Documentation" /></a>
<a href="https://fluxer.app/donate">
<img src="https://img.shields.io/badge/Donate-fluxer.app%2Fdonate-brightgreen" alt="Donate" /></a>
<a href="./LICENSE">
<img src="https://img.shields.io/badge/License-AGPLv3-purple" alt="AGPLv3 License" /></a>
</p>
<p align="center">
<a href="https://flathub.org/apps/app.fluxer.Fluxer">
<img src="https://dl.flathub.org/assets/badges/flathub-badge-en.svg" alt="Get it on Flathub" height="60" /></a>
</p>
# Fluxer
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
<p align="center">
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer app showcase" width="900">
<img src="./fluxer_static/marketing/screenshots/desktop-readme-1920w.png" alt="Fluxer running side by side on a desktop monitor and a phone" width="640">
</p>
## Download
| Windows | macOS | Linux | Android | iOS |
| --- | --- | --- | --- | --- |
| [Installer (x64)][win-setup-x64] | [Disk image][mac-dmg] | [Flathub][flathub] | [Google Play (beta)][android-play] | [TestFlight][ios-testflight] |
| [Installer (ARM64)][win-setup-arm64] | | [deb (x64)][linux-deb-x64] | [APK (beta)][android-apk] | |
| [Portable (x64)][win-portable-x64] | | [deb (ARM64)][linux-deb-arm64] | [Obtainium (beta)][obtainium] | |
| [Portable (ARM64)][win-portable-arm64] | | [rpm (x64)][linux-rpm-x64] | | |
| | | [rpm (ARM64)][linux-rpm-arm64] | | |
| | | [AppImage (x64)][linux-appimage-x64] | | |
| | | [AppImage (ARM64)][linux-appimage-arm64] | | |
| | | [tar.gz (x64)][linux-targz-x64] | | |
| | | [tar.gz (ARM64)][linux-targz-arm64] | | |
The macOS disk image runs on both Apple silicon and Intel. Windows and Linux need the build matching your processor.
On Linux, prefer a repository over a single file so Fluxer updates with the rest of your system.
## Linux package repositories
The package is `fluxer` for stable and `fluxer-canary` for canary. apt and dnf subscribe to one channel per entry file. pacman and Flatpak serve both from one repository.
### Flatpak
Stable is on [Flathub][flathub], the easiest route on most desktops:
```sh
flatpak install flathub app.fluxer.Fluxer
```
Flathub has stable only. To use Fluxer's own repository, open [the stable][flatpak-ref] or [the canary][flatpak-canary-ref] reference file and your software manager takes over. Some desktops also accept `flatpak+https://pkgs.fluxer.com/flatpak/fluxer.flatpakref` in the address bar.
From a terminal:
```sh
flatpak install https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
```
### Debian and Ubuntu
```sh
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL -o /etc/apt/keyrings/fluxer-archive-keyring.gpg https://pkgs.fluxer.com/keys/fluxer-archive-keyring.gpg
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer.sources https://pkgs.fluxer.com/deb/fluxer.sources
sudo apt update && sudo apt install fluxer
```
For canary, use the canary entry file and package.
```sh
sudo curl -fsSL -o /etc/apt/sources.list.d/fluxer-canary.sources https://pkgs.fluxer.com/deb/fluxer-canary.sources
sudo apt update && sudo apt install fluxer-canary
```
A `.deb` installed from a download only updates once its channel's entry is added.
### Fedora and RHEL
```sh
sudo curl -fsSL -o /etc/yum.repos.d/fluxer.repo https://pkgs.fluxer.com/rpm/fluxer.repo
sudo dnf install fluxer
```
For canary, use the canary entry file and package.
```sh
sudo curl -fsSL -o /etc/yum.repos.d/fluxer-canary.repo https://pkgs.fluxer.com/rpm/fluxer-canary.repo
sudo dnf install fluxer-canary
```
RHEL, Rocky, Alma and CentOS Stream need `sudo dnf install epel-release` first, because their base repositories lack `libXScrnSaver`. Fedora does not.
### Arch Linux
The repository is signed, so pacman needs the key once:
```sh
sudo pacman-key --init
curl -fsSL -o /tmp/fluxer-archive-keyring.asc https://pkgs.fluxer.com/keys/fluxer-archive-keyring.asc
sudo pacman-key --add /tmp/fluxer-archive-keyring.asc
sudo pacman-key --lsign-key 09D01339EE128925F75E675C855C5BDE34D205D2
```
`--lsign-key` is what makes pacman trust it. Then add the repository:
```sh
sudo tee -a /etc/pacman.conf >/dev/null <<'REPO'
[fluxer]
SigLevel = Required TrustedOnly
Server = https://pkgs.fluxer.com/arch/$repo/os/$arch
REPO
sudo pacman -Syu fluxer
```
Write `$repo` and `$arch` literally. Both are pacman variables, not shell ones, hence the quoted heredoc.
Full setup notes, including canary, are in the [Linux repositories documentation][docs-linux].
## Other ways to run it
- [Open Fluxer in a browser](https://web.fluxer.app), no install needed.
- [Host your own instance][docs-selfhost] from this repository.
## Documentation
- [Documentation home][docs]
- [Downloads][docs-downloads]
- [Self-hosting][docs-selfhost]
## License
The source is licensed under the [AGPL-3.0-or-later](./LICENSE) license.
Fluxer branding, icons, default avatars, badge artwork, screenshots and marketing
imagery are copyright Fluxer, all rights reserved, as set out in
[fluxer_static/LICENSE](./fluxer_static/LICENSE). Third-party material keeps its own
terms, listed in
[fluxer_static/THIRD_PARTY_LICENSES.md](./fluxer_static/THIRD_PARTY_LICENSES.md).
Public availability of this repository does not grant trademark, brand, or
endorsement rights.
[win-setup-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/setup
[win-setup-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/setup
[win-portable-x64]: https://pkgs.fluxer.com/desktop/stable/win32/x64/latest/portable
[win-portable-arm64]: https://pkgs.fluxer.com/desktop/stable/win32/arm64/latest/portable
[mac-dmg]: https://pkgs.fluxer.com/desktop/stable/darwin/arm64/latest/dmg
[linux-deb-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/deb
[linux-deb-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/deb
[linux-rpm-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/rpm
[linux-rpm-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/rpm
[linux-appimage-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/appimage
[linux-appimage-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/appimage
[linux-targz-x64]: https://pkgs.fluxer.com/desktop/stable/linux/x64/latest/tar_gz
[linux-targz-arm64]: https://pkgs.fluxer.com/desktop/stable/linux/arm64/latest/tar_gz
[flatpak-ref]: https://pkgs.fluxer.com/flatpak/fluxer.flatpakref
[flatpak-canary-ref]: https://pkgs.fluxer.com/flatpak/fluxer-canary.flatpakref
[flathub]: https://flathub.org/apps/app.fluxer.Fluxer
[android-play]: https://play.google.com/store/apps/details?id=com.fluxer
[android-apk]: https://github.com/fluxerapp/flutter_client/releases
[obtainium]: https://obtainium.imranr.dev/
[ios-testflight]: https://testflight.apple.com/join/PKZR6pK9
[docs]: https://docs.fluxer.app
[docs-downloads]: https://docs.fluxer.app/downloads/overview/
[docs-linux]: https://docs.fluxer.app/downloads/linux-repositories/
[docs-selfhost]: https://docs.fluxer.app/operator/get-started/
+7 -2
View File
@@ -48,7 +48,7 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"preset": "recommended",
"complexity": {
"noForEach": "off",
"noImportantStyles": "off",
@@ -83,6 +83,7 @@
}
},
"useConst": "error",
"noDescendingSpecificity": "off",
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"noRestrictedImports": {
@@ -98,7 +99,7 @@
}
},
"a11y": {
"recommended": true,
"preset": "recommended",
"useAriaPropsForRole": "error",
"useValidAriaRole": "error",
"useValidAriaValues": "error",
@@ -142,6 +143,10 @@
],
"linter": {"rules": {"style": {"noRestrictedImports": "off"}}}
},
{
"includes": ["fluxer_app/src/**/*.worklet.js"],
"javascript": {"globals": ["AudioWorkletProcessor", "registerProcessor", "sampleRate", "currentTime"]}
},
{
"includes": ["**/*.astro"],
"linter": {"rules": {"correctness": {"noUnusedImports": "off", "noUnusedVariables": "off"}}},
+1 -15
View File
@@ -34,7 +34,6 @@ FLUXER_KV_URL=redis://valkey:6379/0
FLUXER_NATS_URL=nats://nats:4222
FLUXER_NATS_JETSTREAM_URL=nats://nats:4222
FLUXER_INTERNAL_API_ENDPOINT=http://127.0.0.1:8080
FLUXER_INTERNAL_GATEWAY_ENDPOINT=http://127.0.0.1:8771
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT=http://127.0.0.1:8082
FLUXER_MEDIA_PROXY_ENDPOINT=http://127.0.0.1:8082
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT=http://localhost:8088/media
@@ -42,7 +41,6 @@ FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT=http://localhost:8088/media
FLUXER_SVC_NATS_URL=nats://nats:4222
FLUXER_SVC_SHARD_COUNT=1
FLUXER_SVC_CACHE_TTL_MS=30000
FLUXER_SVC_CACHE_HARD_TTL_MS=600000
FLUXER_S3_ENDPOINT=http://127.0.0.1:8333
FLUXER_S3_PUBLIC_ENDPOINT=http://localhost:8088
@@ -52,7 +50,6 @@ FLUXER_S3_SECRET_ACCESS_KEY=fluxer-secret
FLUXER_S3_FORCE_PATH_STYLE=true
FLUXER_S3_BUCKET_CDN=fluxer
FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
FLUXER_S3_BUCKET_REPORTS=fluxer-reports
FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
FLUXER_S3_BUCKET_STATIC=fluxer-static
@@ -62,14 +59,8 @@ FLUXER_LIVEKIT_URL=ws://localhost:8088/livekit
FLUXER_LIVEKIT_INTERNAL_URL=http://localhost:7880
FLUXER_LIVEKIT_API_KEY=devkey
FLUXER_LIVEKIT_API_SECRET=fluxer-livekit-development-secret
FLUXER_LIVEKIT_WEBHOOK_URL=http://localhost:8088/api/webhooks/livekit
FLUXER_LIVEKIT_DEFAULT_REGION={"id":"local","name":"Local","emoji":"LC","latitude":59.3293,"longitude":18.0686}
FLUXER_RECON_MODE=observing
FLUXER_RECON_EXPECTED_ROOMS=64
FLUXER_RECON_WARMUP_SECONDS=15
FLUXER_RECON_MAX_HOT_ROOMS=8
FLUXER_API_PORT=8080
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED=true
FLUXER_API_WORKER_MODE=all_lanes
@@ -113,9 +104,6 @@ FLUXER_EMAIL_SMTP_PORT=1025
FLUXER_EMAIL_SMTP_USERNAME=dev
FLUXER_EMAIL_SMTP_PASSWORD=dev
FLUXER_EMAIL_SMTP_SECURE=false
FLUXER_SMS_ENABLED=false
FLUXER_CAPTCHA_ENABLED=false
FLUXER_CAPTCHA_PROVIDER=none
FLUXER_SEARCH_ENGINE=meilisearch
FLUXER_SEARCH_URL=http://meilisearch:7700
FLUXER_SEARCH_API_KEY=fluxer-dev-meilisearch
@@ -135,7 +123,5 @@ PUBLIC_RELEASE_CHANNEL=canary
PUBLIC_BOOTSTRAP_API_ENDPOINT=/api
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT=http://localhost:8088/api
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=Zmx1eGVyLWRldi11cGxvYWQtcmVsYXktc2VjcmV0LTAwMDA=
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=Zmx1eGVyLWRldi1hdHRhY2htZW50LXVybC1zZWNyZXQ=
AWS_EC2_METADATA_DISABLED=true
AWS_ACCESS_KEY_ID=fluxer
AWS_SECRET_ACCESS_KEY=fluxer-secret
AWS_DEFAULT_REGION=us-east-1
+18 -10
View File
@@ -79,34 +79,42 @@ deny = [
{ crate = "fuse-sys", reason = "libfuse2 FFI crate; Fluxer AppImages must not reintroduce libfuse2 through native Rust dependencies" },
]
skip = [
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older crypto API" },
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older hashbrown API" },
{ crate = "[email protected]", reason = "transitive dependency requires the prior hashbrown API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older HTTP body API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI API" },
{ crate = "[email protected].6", reason = "transitive dependency requires the older randomness API" },
{ crate = "[email protected].4", reason = "transitive dependency requires the prior randomness API" },
{ crate = "[email protected].8", reason = "transitive dependency requires the older randomness API" },
{ crate = "[email protected].5", reason = "transitive dependency requires the prior randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older randomness API" },
{ crate = "[email protected]", reason = "transitive dependency requires the prior randomness API" },
{ crate = "[email protected].6", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected].7", reason = "transitive dependency requires the older digest API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older digest API" },
{ crate = "s[email protected]0", reason = "transitive dependency requires the older socket API" },
{ crate = "s[email protected].0", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]+wasi-snapshot-preview1", reason = "transitive dependency requires the legacy WASI API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older release line" },
{ crate = "[email protected]", reason = "transitive dependency requires the older Windows API" },
{ crate = "[email protected]", reason = "transitive dependency requires the prior Windows API" },
{ crate = "[email protected]", reason = "transitive dependency requires the older WASI binding API" },
]
skip-tree = []
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-api
description: Fluxer HTTP API and background job workers
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,244 @@
{{- define "fluxer-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-api.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-api.labels" -}}
{{ include "fluxer-api.selectorLabels" . }}
app.kubernetes.io/component: {{ .component }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-api.chart" .root }}
{{- end }}
{{- define "fluxer-api.image" -}}
{{- $g := .root.Values.image | default dict -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default "fluxer-api") -}}
{{- end -}}
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-api.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-api.str" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- int64 . | toString | quote -}}
{{- else -}}
{{- toString . | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-api.env" -}}
{{- $env := dict -}}
{{- range $k, $val := .root.Values.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := .w.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := $env }}
{{- if not (kindIs "invalid" $val) }}
- name: {{ $k }}
value: {{ include "fluxer-api.str" $val }}
{{- end }}
{{- end }}
{{- with .w.buildVersion }}
- name: BUILD_VERSION
value: {{ include "fluxer-api.str" . }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-api.topologySpread" -}}
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
{{- range $tscs }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-api.selectorLabels" $ | fromYaml)) }}
{{- end }}
- {{- toYaml $c | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-api.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-api.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "fluxer-api.hpa" -}}
{{- with .w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $.name }}
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $.name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
{{- with .targetCPUUtilizationPercentage }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ . }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "fluxer-api.deployment" -}}
{{- $root := .root -}}
{{- $v := $root.Values -}}
{{- $w := .w -}}
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) -}}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) -}}
{{- $wProbes := $w.probes | default dict -}}
{{- $gProbes := .probes | default dict -}}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .name }}
namespace: {{ $root.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" . | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ int $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-api.selectorLabels" . | nindent 6 }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
labels:
{{- include "fluxer-api.labels" . | nindent 8 }}
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-api.topologySpread" . | trim }}
topologySpreadConstraints:
{{- . | nindent 8 }}
{{- end }}
containers:
- name: {{ .name }}
image: {{ include "fluxer-api.image" . }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
{{- with .command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-api.env" . | trim }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
{{ $probe }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-api.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- range $name, $w := .Values.api }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "api" "probes" ($.Values.probes | default dict) }}
{{ include "fluxer-api.deployment" $ctx }}
{{ include "fluxer-api.hpa" $ctx }}
{{ include "fluxer-api.pdb" $ctx }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-api.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-api.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
{{- end }}
{{- end }}
@@ -0,0 +1,8 @@
{{- range $name, $w := .Values.workers }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w "component" "worker" "command" (list "node" "dist/WorkerEntrypoint.js") "probes" (dict) }}
{{ include "fluxer-api.deployment" $ctx }}
{{ include "fluxer-api.hpa" $ctx }}
{{ include "fluxer-api.pdb" $ctx }}
{{- end }}
{{- end }}
+86
View File
@@ -0,0 +1,86 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env:
NODE_ENV: production
FLUXER_ENV: production
FLUXER_PUBLIC_ORIGIN: https://web.example.com
FLUXER_API_ENDPOINT: https://api.example.com
FLUXER_GATEWAY_ENDPOINT: wss://gateway.example.com
FLUXER_MEDIA_ENDPOINT: https://media.example.com
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: https://uploads.example.com
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
FLUXER_KV_URL: redis://valkey:6379/0
FLUXER_NATS_URL: nats://nats:4222
FLUXER_NATS_JETSTREAM_URL: nats://nats:4222
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
periodSeconds: 10
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
api:
api:
replicas: 1
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
memory: 2560Mi
workers:
worker:
replicas: 1
env:
FLUXER_API_WORKER_MODE: all_lanes
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
memory: 2560Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-gateway
description: A Helm chart for the Fluxer realtime gateway.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,280 @@
{{- define "gateway.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "gateway.labels" -}}
{{ include "gateway.selectorLabels" . }}
{{- with .component }}
app.kubernetes.io/component: {{ . }}
{{- end }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "gateway.headlessName" -}}
{{ printf "%s-headless" .Release.Name }}
{{- end }}
{{- define "gateway.pick" -}}
{{- $v := get .root.Values .key }}
{{- if hasKey .w .key }}
{{- $v = get .w .key }}
{{- end }}
{{- with $v }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.string" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
{{- int64 . | toString }}
{{- else }}
{{- toString . }}
{{- end }}
{{- end }}
{{- define "gateway.envList" -}}
{{- $env := deepCopy (.root.Values.env | default dict) }}
{{- range $k, $v := .w.env | default dict }}
{{- if kindIs "invalid" $v }}
{{- $_ := unset $env $k }}
{{- else }}
{{- $_ := set $env $k $v }}
{{- end }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "gateway.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.envFrom" -}}
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.podAnnotations" -}}
{{- with merge (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.probes" -}}
{{- $global := .root.Values.probes | default dict }}
{{- $own := .w.probes | default dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $p := get $global $probe }}
{{- if hasKey $own $probe }}
{{- $p = get $own $probe }}
{{- end }}
{{- with $p }}
{{ $probe }}Probe:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "gateway.topologySpreadConstraints" -}}
{{- $out := list }}
{{- range include "gateway.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
{{- $c := deepCopy . }}
{{- if not (hasKey $c "labelSelector") }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "gateway.selectorLabels" $ | fromYaml)) }}
{{- end }}
{{- $out = append $out $c }}
{{- end }}
{{- with $out }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "gateway.image" -}}
{{- $img := .w.image | default dict }}
{{- $v := .root.Values.image }}
{{- $repo := $img.repository | default (printf "%s/%s" $v.registry ($img.name | default "fluxer-gateway")) }}
{{- $ref := printf "%s:%s" $repo ($img.tag | default $v.tag) }}
{{- with $img.digest }}
{{- $ref = printf "%s@%s" $ref . }}
{{- end }}
{{- $ref | quote }}
{{- end }}
{{- define "gateway.replicas" -}}
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
{{- end }}
{{- define "gateway.env" -}}
{{- $root := .root }}
{{- $w := .w -}}
{{- with $w.role }}
- name: FLUXER_GATEWAY_ROLE
value: {{ . | quote }}
{{- end }}
{{- if not (kindIs "invalid" $w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "gateway.string" $w.buildVersion | quote }}
{{- end }}
- name: POD_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: FLUXER_ERLANG_NODE_NAME
value: fluxer_gateway@$(POD_IP)
- name: FLUXER_ERLANG_DIST_PORT
value: "8081"
- name: FLUXER_GATEWAY_CLUSTER_ENABLED
value: "true"
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME
value: {{ printf "%s.%s.svc.%s" (include "gateway.headlessName" $root) $root.Release.Namespace $root.Values.clusterDomain | quote }}
- name: FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME
value: fluxer_gateway
{{- include "gateway.envList" . }}
{{- end }}
{{- define "gateway.pod" -}}
{{- $root := .root }}
{{- $w := .w -}}
metadata:
labels:
{{- include "gateway.labels" . | nindent 4 }}
{{- with include "gateway.podAnnotations" . }}
annotations:
{{- . | nindent 4 }}
{{- end }}
spec:
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.topologySpreadConstraints" . }}
topologySpreadConstraints:
{{- . | nindent 4 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 4 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: gateway
image: {{ include "gateway.image" . }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $root.Values.image.pullPolicy }}
env:
{{- include "gateway.env" . | trim | nindent 6 }}
{{- with include "gateway.envFrom" . }}
envFrom:
{{- . | nindent 6 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 6 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: epmd
containerPort: 4369
protocol: TCP
- name: erl-dist
containerPort: 8081
protocol: TCP
{{- with include "gateway.probes" . | trim }}
{{- . | nindent 4 }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with include "gateway.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 6 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- define "gateway.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "gateway.labels" $ | nindent 4 }}
spec:
{{- if not (kindIs "invalid" .minAvailable) }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if not (kindIs "invalid" .maxUnavailable) }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "gateway.hpa" -}}
{{- with .w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $.name }}
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "gateway.labels" $ | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $.name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $.name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $.name) .maxReplicas }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,48 @@
{{- range $name, $w := .Values.deployments }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ include "gateway.replicas" $ctx }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
{{- include "gateway.pod" $ctx | nindent 4 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
{{- include "gateway.selectorLabels" $ctx | nindent 4 }}
{{- include "gateway.hpa" $ctx }}
{{- include "gateway.pdb" $ctx }}
{{- end }}
{{- end }}
@@ -0,0 +1,26 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "gateway.headlessName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" (dict "root" . "name" "gateway" "component" "discovery") | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
- name: epmd
port: 4369
protocol: TCP
targetPort: epmd
- name: erl-dist
port: 8081
protocol: TCP
targetPort: erl-dist
selector:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
@@ -0,0 +1,53 @@
{{- $np := .Values.networkPolicy | default dict }}
{{- if $np.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: gateway
namespace: {{ .Release.Namespace }}
labels:
{{- include "gateway.labels" (dict "root" . "name" "gateway") | nindent 4 }}
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
policyTypes:
- Ingress
- Egress
egress:
- {}
ingress:
{{- with $np.ingressNamespace }}
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . }}
ports:
- port: 8080
protocol: TCP
{{- end }}
{{- with $np.clients }}
- from:
{{- range . }}
- podSelector:
matchLabels:
{{- toYaml . | nindent 10 }}
{{- end }}
ports:
- port: 8080
protocol: TCP
{{- end }}
- from:
- podSelector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
ports:
- port: 8080
protocol: TCP
- port: 4369
protocol: TCP
- port: 8081
protocol: TCP
{{- end }}
@@ -0,0 +1,29 @@
{{- range $name, $w := .Values.statefulsets }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "component" $w.role "w" $w }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "gateway.labels" $ctx | nindent 4 }}
spec:
replicas: {{ include "gateway.replicas" $ctx }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
serviceName: {{ include "gateway.headlessName" $ }}
selector:
matchLabels:
{{- include "gateway.selectorLabels" $ctx | nindent 6 }}
{{- with include "gateway.pick" (dict "root" $ "w" $w "key" "updateStrategy") }}
updateStrategy:
{{- . | nindent 4 }}
{{- end }}
template:
{{- include "gateway.pod" $ctx | nindent 4 }}
{{- include "gateway.pdb" $ctx }}
{{- end }}
{{- end }}
+86
View File
@@ -0,0 +1,86 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
clusterDomain: cluster.local
env:
FLUXER_ENV: production
FLUXER_GATEWAY_PORT: "8080"
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: https://media.example.com
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
exec:
command:
- curl
- -fsS
- -o
- /dev/null
- --max-time
- "2"
- http://127.0.0.1:8080/_health/ready
timeoutSeconds: 3
strategy: {}
updateStrategy: {}
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
networkPolicy:
enabled: false
ingressNamespace: ingress-nginx
clients:
- app.kubernetes.io/part-of: fluxer
deployments:
gateway:
role: all
replicas: 1
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8080/_health/drain; sleep 5
resources:
requests:
cpu: 100m
memory: 384Mi
limits:
memory: 1Gi
statefulsets: {}
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-infra
description: NATS and Valkey for a Fluxer installation.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,282 @@
{{- define "fluxer-infra.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-infra.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-infra.labels" -}}
{{ include "fluxer-infra.selectorLabels" . }}
app.kubernetes.io/component: {{ .component }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-infra.chart" .root }}
{{- end }}
{{- define "fluxer-infra.pick" -}}
{{- $v := get .root.Values .key }}
{{- if hasKey .w .key }}
{{- $v = get .w .key }}
{{- end }}
{{- with $v }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.string" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) }}
{{- int64 . | toString }}
{{- else }}
{{- toString . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.envList" -}}
{{- $env := deepCopy (.root.Values.env | default dict) }}
{{- range $k, $v := .w.env | default dict }}
{{- if kindIs "invalid" $v }}
{{- $_ := unset $env $k }}
{{- else }}
{{- $_ := set $env $k $v }}
{{- end }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "fluxer-infra.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.envFrom" -}}
{{- with concat (.root.Values.envFrom | default list) (.w.envFrom | default list) }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.probes" -}}
{{- $global := .root.Values.probes | default dict }}
{{- $own := .w.probes | default dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $p := get $global $probe }}
{{- if hasKey $own $probe }}
{{- $p = get $own $probe }}
{{- end }}
{{- with $p }}
{{ $probe }}Probe:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.topologySpreadConstraints" -}}
{{- $out := list }}
{{- range include "fluxer-infra.pick" (dict "root" .root "w" .w "key" "topologySpreadConstraints") | fromYamlArray }}
{{- $c := deepCopy . }}
{{- if not (hasKey $c "labelSelector") }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-infra.selectorLabels" $ | fromYaml)) }}
{{- end }}
{{- $out = append $out $c }}
{{- end }}
{{- with $out }}
{{- toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.replicas" -}}
{{- if kindIs "invalid" .w.replicas }}1{{ else }}{{ .w.replicas }}{{ end }}
{{- end }}
{{- define "fluxer-infra.image" -}}
{{- $ref := printf "%s:%s" .repository .tag }}
{{- with .digest }}
{{- $ref = printf "%s@%s" $ref . }}
{{- end }}
{{- $ref | quote }}
{{- end }}
{{- define "fluxer-infra.podAnnotations" -}}
{{- with merge (deepCopy (.extra | default dict)) (deepCopy (.w.podAnnotations | default dict)) (deepCopy (.root.Values.podAnnotations | default dict)) }}
annotations:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.podSpec" -}}
{{- $root := .root }}
{{- $w := .w }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.topologySpreadConstraints" . }}
topologySpreadConstraints:
{{- . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 2 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.containerCommon" -}}
{{- $root := .root }}
{{- $w := .w }}
{{- $img := $w.image | default dict }}
image: {{ include "fluxer-infra.image" $img }}
imagePullPolicy: {{ $img.pullPolicy }}
{{- $env := include "fluxer-infra.envList" . | trim }}
{{- if or .env $env }}
env:
{{- with .env }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- with $env }}
{{- . | nindent 2 }}
{{- end }}
{{- end }}
{{- with include "fluxer-infra.envFrom" . }}
envFrom:
{{- . | nindent 2 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- include "fluxer-infra.probes" . }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- with include "fluxer-infra.pick" (dict "root" $root "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 2 }}
{{- end }}
{{- with concat .mounts ($w.extraVolumeMounts | default list) }}
volumeMounts:
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.statefulSetSpec" -}}
{{- $w := .w }}
{{- with include "fluxer-infra.pick" (dict "root" .root "w" $w "key" "updateStrategy") }}
updateStrategy:
{{- . | nindent 2 }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.volumeClaim" -}}
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
{{- with .storageClassName }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .size }}
{{- end }}
{{- define "fluxer-infra.pdb" -}}
{{- with .w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $.name }}-pdb
namespace: {{ $.root.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ | nindent 4 }}
spec:
{{- if not (kindIs "invalid" .minAvailable) }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if not (kindIs "invalid" .maxUnavailable) }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.service" }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .svcName }}
namespace: {{ .root.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" . | nindent 4 }}
spec:
{{- if .headless }}
clusterIP: None
{{- end }}
{{- if .publishNotReady }}
publishNotReadyAddresses: true
{{- end }}
selector:
{{- include "fluxer-infra.selectorLabels" . | nindent 4 }}
ports:
{{- range .ports }}
- name: {{ index . 0 }}
port: {{ index . 1 }}
targetPort: {{ index . 0 }}
{{- end }}
{{- end }}
{{- define "fluxer-infra.natsConf" -}}
{{- $w := .Values.nats -}}
{{- with $w.config -}}
listen: 0.0.0.0:4222
http: 0.0.0.0:8222
max_payload: {{ .maxPayload }}
max_pending: {{ .maxPending }}
max_connections: {{ .maxConnections }}
{{- if $w.jetstream.enabled }}
server_name: $POD_NAME
jetstream {
store_dir: /data
}
{{- end }}
cluster {
name: {{ .clusterName }}
listen: 0.0.0.0:6222
routes = [
{{- range $i := until (int (include "fluxer-infra.replicas" (dict "w" $w))) }}
nats-route://nats-{{ $i }}.nats-headless.{{ $.Release.Namespace }}.svc.{{ $.Values.clusterDomain }}:6222
{{- end }}
]
}
{{ end }}
{{- end }}
@@ -0,0 +1,71 @@
{{- with .Values.nats }}
{{- $ctx := dict "root" $ "w" . "name" "nats" "component" "messaging" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: nats-config
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
data:
nats.conf: {{ include "fluxer-infra.natsConf" $ | toJson }}
{{- include "fluxer-infra.pdb" $ctx }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats" "ports" (list (list "client" 4222))) $ctx) }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "nats-headless" "headless" true "ports" (list (list "client" 4222) (list "cluster" 6222) (list "monitor" 8222))) $ctx) }}
{{- $mounts := list (dict "name" "config" "mountPath" "/etc/nats") }}
{{- $env := list }}
{{- if .jetstream.enabled }}
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
{{- $env = append $env (dict "name" "POD_NAME" "valueFrom" (dict "fieldRef" (dict "fieldPath" "metadata.name"))) }}
{{- end }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nats
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
spec:
replicas: {{ include "fluxer-infra.replicas" $ctx }}
serviceName: nats-headless
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
{{- . | nindent 2 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
template:
metadata:
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
{{- with include "fluxer-infra.podAnnotations" (merge (dict "extra" (dict "checksum/config" (include "fluxer-infra.natsConf" $ | sha256sum))) $ctx) | trim }}
{{- . | nindent 6 }}
{{- end }}
spec:
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
containers:
- name: nats
{{- include "fluxer-infra.containerCommon" (merge (dict "env" $env "mounts" $mounts) $ctx) | trim | nindent 10 }}
args:
- -c
- /etc/nats/nats.conf
ports:
- name: client
containerPort: 4222
- name: cluster
containerPort: 6222
- name: monitor
containerPort: 8222
volumes:
- name: config
configMap:
name: nats-config
{{- with .extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .jetstream.enabled }}
volumeClaimTemplates:
{{- include "fluxer-infra.volumeClaim" .jetstream.storage | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,67 @@
{{- with .Values.valkey }}
{{- $ctx := dict "root" $ "w" . "name" "valkey" "component" "cache" }}
{{- include "fluxer-infra.pdb" $ctx }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey" "ports" (list (list "valkey" 6379))) $ctx) }}
{{- include "fluxer-infra.service" (merge (dict "svcName" "valkey-headless" "headless" true "publishNotReady" true "ports" (list (list "valkey" 6379))) $ctx) }}
{{- $mounts := list }}
{{- if .persistence.enabled }}
{{- $mounts = append $mounts (dict "name" "data" "mountPath" "/data") }}
{{- end }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: valkey
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 4 }}
spec:
replicas: 1
serviceName: valkey-headless
{{- with include "fluxer-infra.statefulSetSpec" $ctx | trim }}
{{- . | nindent 2 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-infra.selectorLabels" $ctx | nindent 6 }}
template:
metadata:
labels:
{{- include "fluxer-infra.labels" $ctx | nindent 8 }}
{{- with include "fluxer-infra.podAnnotations" $ctx | trim }}
{{- . | nindent 6 }}
{{- end }}
spec:
{{- include "fluxer-infra.podSpec" $ctx | trim | nindent 6 }}
containers:
- name: valkey
{{- include "fluxer-infra.containerCommon" (merge (dict "env" list "mounts" $mounts) $ctx) | trim | nindent 10 }}
command:
- valkey-server
{{- if .persistence.enabled }}
- --appendonly
- "yes"
- --dir
- /data
{{- else }}
- --save
- ""
- --appendonly
- "no"
{{- end }}
- --maxmemory
- {{ .maxmemory | quote }}
- --maxmemory-policy
- {{ .maxmemoryPolicy | quote }}
ports:
- name: valkey
containerPort: 6379
{{- with .extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .persistence.enabled }}
volumeClaimTemplates:
{{- include "fluxer-infra.volumeClaim" .persistence | nindent 4 }}
{{- end }}
{{- end }}
+108
View File
@@ -0,0 +1,108 @@
imagePullSecrets: []
clusterDomain: cluster.local
env: {}
extraEnv: []
envFrom: []
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes: {}
updateStrategy: {}
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
nats:
image:
repository: nats
tag: 2.14-alpine
pullPolicy: IfNotPresent
replicas: 3
config:
clusterName: nats
maxPayload: 1MB
maxPending: 64MB
maxConnections: 65536
jetstream:
enabled: true
storage:
size: 10Gi
storageClassName: ""
podSecurityContext:
fsGroup: 65534
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
probes:
liveness:
httpGet:
path: /healthz
port: monitor
initialDelaySeconds: 10
readiness:
httpGet:
path: /healthz?js-enabled-only=true
port: monitor
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
valkey:
image:
repository: valkey/valkey
tag: 9.1-alpine
pullPolicy: IfNotPresent
maxmemory: 192mb
maxmemoryPolicy: noeviction
persistence:
enabled: true
size: 1Gi
storageClassName: ""
podSecurityContext:
fsGroup: 999
runAsGroup: 999
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
probes:
liveness:
exec:
command:
- valkey-cli
- ping
initialDelaySeconds: 10
readiness:
exec:
command:
- valkey-cli
- ping
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-ingress
description: Ingress routing for the public Fluxer endpoints.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,27 @@
{{- define "fluxer-ingress.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-ingress.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "fluxer-ingress.chart" . }}
{{- end }}
{{- define "fluxer-ingress.annotationKey" -}}
{{- if or (contains "/" .key) (not .prefix) -}}
{{- .key -}}
{{- else -}}
{{- printf "%s/%s" .prefix .key -}}
{{- end -}}
{{- end }}
{{- define "fluxer-ingress.string" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- . | int64 | toString -}}
{{- else -}}
{{- . | toString -}}
{{- end -}}
{{- end }}
@@ -0,0 +1,20 @@
{{- with .Values.clusterIssuer }}
{{- if .enabled }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: {{ required "clusterIssuer.name is required" .name }}
labels:
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
spec:
acme:
email: {{ required "clusterIssuer.email is required" .email | quote }}
privateKeySecretRef:
name: {{ required "clusterIssuer.privateKeySecretName is required" .privateKeySecretName }}
server: {{ required "clusterIssuer.server is required" .server }}
solvers:
- http01:
ingress:
class: {{ required "clusterIssuer.solverIngressClass is required" .solverIngressClass }}
{{- end }}
{{- end }}
@@ -0,0 +1,58 @@
{{- $v := .Values }}
{{- $presets := $v.annotationPresets | default dict }}
{{- $issuer := $v.clusterIssuer | default dict }}
{{- range $name, $spec := ($v.ingresses | default dict) }}
{{- if not (kindIs "invalid" $spec) }}
{{- $ann := deepCopy ($v.commonAnnotations | default dict) }}
{{- range ($spec.presets | default list) }}
{{- $ann = mergeOverwrite $ann (deepCopy (required (printf "unknown annotation preset %s" .) (index $presets .))) }}
{{- end }}
{{- if and $spec.tls $issuer.enabled }}
{{- $_ := set $ann "cert-manager.io/cluster-issuer" (required "clusterIssuer.name is required" $issuer.name) }}
{{- end }}
{{- $ann = mergeOverwrite $ann (deepCopy ($spec.annotations | default dict)) }}
{{- range $k, $val := $ann }}
{{- if kindIs "invalid" $val }}
{{- $_ := unset $ann $k }}
{{- end }}
{{- end }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-ingress.labels" $ | nindent 4 }}
{{- with $ann }}
annotations:
{{- range $k, $val := . }}
{{ include "fluxer-ingress.annotationKey" (dict "key" $k "prefix" $v.annotationPrefix) }}: {{ include "fluxer-ingress.string" $val | quote }}
{{- end }}
{{- end }}
spec:
{{- with $spec.ingressClassName | default $v.ingressClassName }}
ingressClassName: {{ . }}
{{- end }}
{{- with $spec.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range $rule := required (printf "ingress %s needs rules" $name) $spec.rules }}
- host: {{ required (printf "ingress %s has a rule without a host" $name) $rule.host | quote }}
http:
paths:
{{- range $p := $rule.paths | default (list dict) }}
{{- $p = $p | default dict }}
- path: {{ $p.path | default "/" | quote }}
pathType: {{ $p.pathType | default "Prefix" }}
backend:
service:
name: {{ required (printf "ingress %s host %s needs a service" $name $rule.host) ($p.service | default $rule.service) }}
port:
number: {{ required (printf "ingress %s host %s needs a port or servicePort" $name $rule.host) ($p.port | default $rule.port | default $v.servicePort) | int64 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+53
View File
@@ -0,0 +1,53 @@
ingressClassName: nginx
annotationPrefix: nginx.ingress.kubernetes.io
servicePort: 8080
commonAnnotations: {}
annotationPresets:
websocket:
proxy-read-timeout: "3600"
proxy-send-timeout: "3600"
stripPrefix:
use-regex: "true"
rewrite-target: /$2
ingresses:
fluxer:
rules:
- host: web.example.com
service: app-proxy
- host: api.example.com
service: api
- host: admin.example.com
service: admin
- host: media.example.com
service: media-proxy
fluxer-web-api:
presets: [stripPrefix]
rules:
- host: web.example.com
service: api
paths:
- path: /api(/(.*))?$
pathType: ImplementationSpecific
fluxer-gateway:
presets: [websocket]
rules:
- host: gateway.example.com
service: gateway
fluxer-uploads:
annotations:
proxy-body-size: 100m
proxy-request-buffering: "off"
rules:
- host: uploads.example.com
service: uploads
clusterIssuer:
enabled: false
name: letsencrypt
email: ""
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretName: letsencrypt-account-key
solverIngressClass: nginx
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-media-proxy
description: Fluxer media proxy and upload relay workloads.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,87 @@
{{- define "fluxer-media-proxy.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-media-proxy.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-media-proxy.labels" -}}
{{ include "fluxer-media-proxy.selectorLabels" . }}
app.kubernetes.io/component: {{ include "fluxer-media-proxy.mode" . }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-media-proxy.chart" .root }}
{{- end }}
{{- define "fluxer-media-proxy.image" -}}
{{- $g := .root.Values.image -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default "fluxer-media-proxy")) -}}
{{- $tag := $i.tag | default $g.tag -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-media-proxy.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-media-proxy.mode" -}}
{{- $mode := required (printf "workloads.%s.mode is required" .name) .w.mode -}}
{{- if not (has $mode (list "mp" "static" "upload" "relay")) -}}
{{- fail (printf "workloads.%s.mode must be mp, static, upload or relay" .name) -}}
{{- end -}}
{{- $mode -}}
{{- end }}
{{- define "fluxer-media-proxy.envValue" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
{{- int64 . | toString -}}
{{- else -}}
{{- toString . -}}
{{- end -}}
{{- end }}
{{- define "fluxer-media-proxy.mergeEnv" -}}
{{- $out := dict -}}
{{- range $layer := . -}}
{{- range $k, $v := ($layer | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $out $k -}}
{{- else -}}
{{- $_ := set $out $k $v -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-media-proxy.topologySpreadConstraints" -}}
{{- $out := list -}}
{{- range .constraints -}}
{{- if .labelSelector -}}
{{- $out = append $out . -}}
{{- else -}}
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-media-proxy.pdb" -}}
{{- $out := dict -}}
{{- range $k := list "minAvailable" "maxUnavailable" -}}
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
{{- $_ := set $out $k (index $ $k) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
@@ -0,0 +1,191 @@
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $mode := include "fluxer-media-proxy.mode" $ctx }}
{{- $sel := include "fluxer-media-proxy.selectorLabels" $ctx | fromYaml }}
{{- $env := include "fluxer-media-proxy.mergeEnv" (list $.Values.env $w.env) | fromYaml }}
{{- $extraEnv := concat ($.Values.extraEnv | default list) ($w.extraEnv | default list) }}
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($.Values.podAnnotations | default dict) }}
{{- $probes := dict }}
{{- range $k, $v := ($.Values.probes | default dict) }}
{{- $_ := set $probes $k $v }}
{{- end }}
{{- range $k, $v := ($w.probes | default dict) }}
{{- $_ := set $probes $k $v }}
{{- end }}
{{- $pick := dict "root" $ "w" $w }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int64 }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ $w.minReadySeconds | int64 }}
{{- end }}
selector:
matchLabels:
{{- toYaml $sel | nindent 6 }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 8 }}
spec:
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int64 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "topologySpreadConstraints") | fromYamlArray }}
topologySpreadConstraints:
{{- include "fluxer-media-proxy.topologySpreadConstraints" (dict "constraints" . "selector" $sel) | nindent 8 }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-media-proxy.image" $ctx }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
env:
{{- if not (kindIs "invalid" $w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-media-proxy.envValue" $w.buildVersion | quote }}
{{- end }}
- name: FLUXER_MEDIA_PROXY_MODE
value: {{ $mode | quote }}
{{- range $k, $v := $env }}
- name: {{ $k }}
value: {{ include "fluxer-media-proxy.envValue" $v | quote }}
{{- end }}
{{- with $extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $k := list "startup" "liveness" "readiness" }}
{{- with get $probes $k }}
{{ $k }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-media-proxy.pick" (set (deepCopy $pick) "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- toYaml $sel | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
{{- with include "fluxer-media-proxy.pdb" ($w.pdb | default dict) | fromYaml }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- toYaml $sel | nindent 6 }}
{{- end }}
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-media-proxy.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int64 }}
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int64 }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,72 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
probes:
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
media-proxy:
mode: mp
replicas: 1
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
uploads:
mode: relay
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 512Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-push
description: Fluxer push notification delivery service
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,71 @@
{{- define "fluxer-push.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-push.labels" -}}
{{ include "fluxer-push.selectorLabels" . }}
app.kubernetes.io/component: {{ include "fluxer-push.mode" . }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "fluxer-push.mode" -}}
{{- $mode := .w.mode | default "delivery" -}}
{{- if not (has $mode (list "delivery" "relay")) -}}
{{- fail (printf "workloads.%s.mode must be delivery or relay" .name) -}}
{{- end -}}
{{- $mode -}}
{{- end }}
{{- define "fluxer-push.port" -}}
{{- .w.port | default (ternary 8127 8126 (eq (include "fluxer-push.mode" .) "relay")) -}}
{{- end }}
{{- define "fluxer-push.image" -}}
{{- $global := .root.Values.image | default dict -}}
{{- $img := .w.image | default dict -}}
{{- $repo := $img.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $global.registry) ($img.name | default "fluxer-push") -}}
{{- end -}}
{{- $ref := printf "%s:%s" $repo (include "fluxer-push.string" (required "image.tag is required" ($img.tag | default $global.tag))) -}}
{{- with $img.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
{{- $ref -}}
{{- end }}
{{- define "fluxer-push.string" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- . | int64 | toString -}}
{{- else -}}
{{- . | toString -}}
{{- end -}}
{{- end }}
{{- define "fluxer-push.env" -}}
{{- $env := deepCopy (.root.Values.env | default dict) -}}
{{- range $k, $v := (.w.env | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $env $k -}}
{{- else -}}
{{- $_ := set $env $k $v -}}
{{- end -}}
{{- end -}}
{{- if not (kindIs "invalid" .w.port) -}}
{{- $_ := set $env "FLUXER_PUSH_SERVICE_PORT" .w.port -}}
{{- end -}}
{{- if not (kindIs "invalid" .w.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-push.string" .w.buildVersion | quote }}
{{- end }}
{{- range $k, $v := $env }}
{{- if not (kindIs "invalid" $v) }}
- name: {{ $k }}
value: {{ include "fluxer-push.string" $v | quote }}
{{- end }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
@@ -0,0 +1,205 @@
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $mode := include "fluxer-push.mode" $ctx }}
{{- $port := include "fluxer-push.port" $ctx | int }}
{{- $globalProbes := $.Values.probes | default dict }}
{{- $workloadProbes := $w.probes | default dict }}
{{- $probes := dict }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- $_ := set $probes $probe (ternary (index $workloadProbes $probe) (index $globalProbes $probe) (hasKey $workloadProbes $probe)) }}
{{- end }}
{{- $annotations := mergeOverwrite (deepCopy ($.Values.podAnnotations | default dict)) (deepCopy ($w.podAnnotations | default dict)) }}
{{- $pullSecrets := ternary $w.imagePullSecrets $.Values.imagePullSecrets (hasKey $w "imagePullSecrets") }}
{{- $podSecurityContext := ternary $w.podSecurityContext $.Values.podSecurityContext (hasKey $w "podSecurityContext") }}
{{- $securityContext := ternary $w.securityContext $.Values.securityContext (hasKey $w "securityContext") }}
{{- $strategy := ternary $w.strategy $.Values.strategy (hasKey $w "strategy") }}
{{- $tsc := ternary $w.topologySpreadConstraints $.Values.topologySpreadConstraints (hasKey $w "topologySpreadConstraints") }}
{{- $nodeSelector := ternary $w.nodeSelector $.Values.nodeSelector (hasKey $w "nodeSelector") }}
{{- $tolerations := ternary $w.tolerations $.Values.tolerations (hasKey $w "tolerations") }}
{{- $affinity := ternary $w.affinity $.Values.affinity (hasKey $w "affinity") }}
{{- $envFrom := concat ($.Values.envFrom | default list) ($w.envFrom | default list) }}
{{- $env := include "fluxer-push.env" $ctx }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ ternary $w.replicas 1 (hasKey $w "replicas") | int }}
{{- end }}
{{- if hasKey $w "minReadySeconds" }}
minReadySeconds: {{ $w.minReadySeconds | int }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
{{- with $strategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
metadata:
{{- with $annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 8 }}
spec:
{{- with $pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if hasKey $w "terminationGracePeriodSeconds" }}
terminationGracePeriodSeconds: {{ $w.terminationGracePeriodSeconds | int }}
{{- end }}
{{- with $nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $tsc }}
topologySpreadConstraints:
{{- range . }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-push.selectorLabels" $ctx | fromYaml)) }}
{{- end }}
{{- toYaml (list $c) | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-push.image" $ctx | quote }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($.Values.image | default dict).pullPolicy | default "IfNotPresent" }}
command:
- /usr/local/bin/fluxer-push
{{- if eq $mode "relay" }}
args:
- --mode
- relay
{{- end }}
{{- with trim $env }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ $port }}
protocol: TCP
{{- with $probes.startup }}
startupProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $probes.liveness }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $probes.readiness }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: {{ $port }}
protocol: TCP
targetPort: http
{{- with $w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-push.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-push.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "workloads.%s.hpa.minReplicas is required" $name) .minReplicas | int }}
maxReplicas: {{ required (printf "workloads.%s.hpa.maxReplicas is required" $name) .maxReplicas | int }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+65
View File
@@ -0,0 +1,65 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
probes:
liveness:
httpGet:
path: /_healthz
port: http
readiness:
httpGet:
path: /_healthz
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
push:
mode: delivery
replicas: 1
env:
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
FLUXER_SVC_NATS_URL: nats://nats:4222
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-svc
description: Fluxer internal services, each a router Deployment and a shard StatefulSet
type: application
version: 0.1.0
appVersion: v1
@@ -0,0 +1,203 @@
{{- define "fluxer-svc.chart" -}}
{{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end }}
{{- define "fluxer-svc.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-svc.labels" -}}
{{ include "fluxer-svc.selectorLabels" . }}
app.kubernetes.io/component: {{ .mode }}
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-svc.chart" .root }}
{{- end }}
{{- define "fluxer-svc.envValue" -}}
{{- if and (kindIs "float64" .) (eq . (float64 (int64 .))) -}}
{{- int64 . | toString -}}
{{- else -}}
{{- toString . -}}
{{- end -}}
{{- end }}
{{- define "fluxer-svc.mergeEnv" -}}
{{- $out := dict -}}
{{- range $layer := . -}}
{{- range $k, $v := ($layer | default dict) -}}
{{- if kindIs "invalid" $v -}}
{{- $_ := unset $out $k -}}
{{- else -}}
{{- $_ := set $out $k $v -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.topologySpreadConstraints" -}}
{{- $out := list -}}
{{- range .constraints -}}
{{- if .labelSelector -}}
{{- $out = append $out . -}}
{{- else -}}
{{- $out = append $out (merge (dict "labelSelector" (dict "matchLabels" $.selector)) .) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.pdb" -}}
{{- $out := dict -}}
{{- range $k := list "minAvailable" "maxUnavailable" -}}
{{- if and (hasKey $ $k) (not (kindIs "invalid" (index $ $k))) -}}
{{- $_ := set $out $k (index $ $k) -}}
{{- end -}}
{{- end -}}
{{- toYaml $out -}}
{{- end }}
{{- define "fluxer-svc.config" -}}
{{- $v := .root.Values -}}
{{- $levels := list (index $v .mode) (index .svc .mode) -}}
{{- $c := dict "extraEnv" ($v.extraEnv | default list) "envFrom" ($v.envFrom | default list) "podAnnotations" (deepCopy ($v.podAnnotations | default dict)) "probes" (deepCopy ($v.probes | default dict)) "image" (deepCopy (.svc.image | default dict)) -}}
{{- range $k := list "imagePullSecrets" "podSecurityContext" "securityContext" "topologySpreadConstraints" "nodeSelector" "tolerations" "affinity" (ternary "updateStrategy" "strategy" (eq .mode "shard")) -}}
{{- $_ := set $c $k (index $v $k) -}}
{{- end -}}
{{- $envLayers := list $v.env -}}
{{- range $level := $levels -}}
{{- range $k, $x := ($level | default dict) -}}
{{- if eq $k "env" -}}
{{- $envLayers = append $envLayers $x -}}
{{- else if has $k (list "podAnnotations" "image") -}}
{{- $_ := set $c $k (mergeOverwrite (index $c $k) (deepCopy ($x | default dict))) -}}
{{- else if has $k (list "extraEnv" "envFrom") -}}
{{- $_ := set $c $k (concat (index $c $k) ($x | default list)) -}}
{{- else if eq $k "probes" -}}
{{- range $name, $p := ($x | default dict) -}}
{{- $_ := set $c.probes $name $p -}}
{{- end -}}
{{- else -}}
{{- $_ := set $c $k $x -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $_ := set $c "env" (include "fluxer-svc.mergeEnv" $envLayers | fromYaml) -}}
{{- toYaml $c }}
{{- end }}
{{- define "fluxer-svc.image" -}}
{{- $g := .root.Values.image -}}
{{- $i := .c.image -}}
{{- $repo := $i.repository | default (printf "%s/%s" $g.registry ($i.name | default (printf "fluxer-%s" .service))) -}}
{{- $ref := printf "%s:%s" $repo ($i.tag | default $g.tag) -}}
{{- with $i.digest }}{{ $ref = printf "%s@%s" $ref . }}{{ end -}}
{{- $ref -}}
{{- end }}
{{- define "fluxer-svc.pod" -}}
{{- $v := .root.Values -}}
{{- $c := .c -}}
metadata:
{{- with $c.podAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "fluxer-svc.labels" . | nindent 4 }}
spec:
{{- with $c.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if not (kindIs "invalid" $c.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ $c.terminationGracePeriodSeconds | int64 }}
{{- end }}
{{- with $c.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.tolerations }}
tolerations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.affinity }}
affinity:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with $c.topologySpreadConstraints }}
topologySpreadConstraints:
{{- include "fluxer-svc.topologySpreadConstraints" (dict "constraints" . "selector" (include "fluxer-svc.selectorLabels" $ | fromYaml)) | nindent 4 }}
{{- end }}
containers:
- name: {{ .mode }}
image: {{ include "fluxer-svc.image" . | quote }}
imagePullPolicy: {{ $c.image.pullPolicy | default $v.image.pullPolicy }}
env:
- name: FLUXER_SVC_MODE
value: {{ .mode | quote }}
- name: FLUXER_SVC_NAME
value: {{ .service | quote }}
- name: FLUXER_SVC_SHARD_COUNT
value: {{ .shardCount | quote }}
- name: FLUXER_SVC_PORT
value: {{ include "fluxer-svc.envValue" $v.port | quote }}
{{- if not (kindIs "invalid" $c.buildVersion) }}
- name: BUILD_VERSION
value: {{ include "fluxer-svc.envValue" $c.buildVersion | quote }}
{{- end }}
{{- if eq .mode "shard" }}
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
{{- end }}
{{- range $name, $value := $c.env }}
- name: {{ $name }}
value: {{ include "fluxer-svc.envValue" $value | quote }}
{{- end }}
{{- with $c.extraEnv }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.envFrom }}
envFrom:
{{- toYaml . | nindent 8 }}
{{- end }}
ports:
- name: http
containerPort: {{ $v.port }}
protocol: TCP
{{- with $c.lifecycle }}
lifecycle:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- range $name := list "startup" "liveness" "readiness" }}
{{- with index $c.probes $name }}
{{ $name }}Probe:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with $c.resources }}
resources:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.securityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $c.extraVolumes }}
volumes:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,145 @@
{{- range $service, $svc := .Values.services }}
{{- if not (kindIs "invalid" $svc) }}
{{- $svc = $svc | default dict }}
{{- $rc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "router")) }}
{{- $sc := fromYaml (include "fluxer-svc.config" (dict "root" $ "svc" $svc "mode" "shard")) }}
{{- $routerReplicas := ternary $rc.replicas 1 (hasKey $rc "replicas") | int64 }}
{{- $shardCount := ternary $sc.replicas 1 (hasKey $sc "replicas") | int64 }}
{{- if lt $shardCount 1 }}
{{- fail (printf "services.%s shard replicas must be at least 1" $service) }}
{{- end }}
{{- $router := dict "root" $ "service" $service "svc" $svc "mode" "router" "name" $service "c" $rc "shardCount" (toString $shardCount) }}
{{- $shard := dict "root" $ "service" $service "svc" $svc "mode" "shard" "name" (printf "%s-shard" $service) "c" $sc "shardCount" (toString $shardCount) }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
{{- if not $rc.hpa }}
replicas: {{ $routerReplicas }}
{{- end }}
{{- if not (kindIs "invalid" $rc.minReadySeconds) }}
minReadySeconds: {{ $rc.minReadySeconds | int64 }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $router | nindent 6 }}
{{- with $rc.strategy }}
strategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
{{- include "fluxer-svc.pod" $router | nindent 4 }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $service }}-shard
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
spec:
replicas: {{ $shardCount }}
{{- if not (kindIs "invalid" $sc.minReadySeconds) }}
minReadySeconds: {{ $sc.minReadySeconds | int64 }}
{{- end }}
podManagementPolicy: Parallel
serviceName: {{ $service }}-shard-headless
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $shard | nindent 6 }}
{{- with $sc.updateStrategy }}
updateStrategy:
{{- toYaml . | nindent 4 }}
{{- end }}
template:
{{- include "fluxer-svc.pod" $shard | nindent 4 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-svc.selectorLabels" $router | nindent 4 }}
ports:
- name: http
port: {{ $.Values.port }}
targetPort: {{ $.Values.port }}
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: {{ $service }}-shard-headless
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $shard | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
selector:
{{- include "fluxer-svc.selectorLabels" $shard | nindent 4 }}
ports:
- name: http
port: {{ $.Values.port }}
targetPort: {{ $.Values.port }}
protocol: TCP
{{- with $rc.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $service }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $router | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $service }}
minReplicas: {{ required (printf "services.%s router hpa.minReplicas is required" $service) .minReplicas | int64 }}
maxReplicas: {{ required (printf "services.%s router hpa.maxReplicas is required" $service) .maxReplicas | int64 }}
{{- if not (kindIs "invalid" .targetCPUUtilizationPercentage) }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .targetCPUUtilizationPercentage | int64 }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- range $ctx := list $router $shard }}
{{- with include "fluxer-svc.pdb" ($ctx.c.pdb | default dict) | fromYaml }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $ctx.name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-svc.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-svc.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+89
View File
@@ -0,0 +1,89 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env:
FLUXER_SVC_NATS_URL: nats://nats:4222
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
liveness:
httpGet:
path: /_healthz
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
updateStrategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
port: 8090
router:
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 192Mi
shard:
replicas: 2
probes:
startup:
httpGet:
path: /_healthz
port: http
periodSeconds: 10
failureThreshold: 30
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
services:
gifs:
shard:
env:
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: https://media.example.com
messages: {}
snowflakes: {}
unfurl:
shard:
env:
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
users: {}
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: fluxer-web
description: Fluxer web app proxy and admin dashboard.
type: application
version: 0.1.0
appVersion: "v1"
@@ -0,0 +1,80 @@
{{- define "fluxer-web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "fluxer-web.selectorLabels" -}}
app.kubernetes.io/name: {{ .name }}
app.kubernetes.io/instance: {{ .root.Release.Name }}
{{- end }}
{{- define "fluxer-web.labels" -}}
{{ include "fluxer-web.selectorLabels" . }}
app.kubernetes.io/component: web
app.kubernetes.io/part-of: fluxer
app.kubernetes.io/managed-by: {{ .root.Release.Service }}
helm.sh/chart: {{ include "fluxer-web.chart" .root }}
{{- end }}
{{- define "fluxer-web.image" -}}
{{- $g := .root.Values.image | default dict -}}
{{- $i := .w.image | default dict -}}
{{- $repo := $i.repository -}}
{{- if not $repo -}}
{{- $repo = printf "%s/%s" (required "image.registry is required" $g.registry) ($i.name | default (printf "fluxer-%s" .name)) -}}
{{- end -}}
{{- $tag := required "image.tag is required" ($i.tag | default $g.tag) -}}
{{- if $i.digest -}}
{{- printf "%s:%s@%s" $repo $tag $i.digest | quote -}}
{{- else -}}
{{- printf "%s:%s" $repo $tag | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-web.pick" -}}
{{- $v := ternary (get .w .key) (get .root.Values .key) (hasKey .w .key) -}}
{{- if $v }}
{{- toYaml $v }}
{{- end }}
{{- end }}
{{- define "fluxer-web.str" -}}
{{- if and (kindIs "float64" .) (eq . (floor .)) -}}
{{- int64 . | toString | quote -}}
{{- else -}}
{{- toString . | quote -}}
{{- end -}}
{{- end }}
{{- define "fluxer-web.env" -}}
{{- $env := dict -}}
{{- range $k, $val := .root.Values.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := .w.env | default dict }}
{{- $_ := set $env $k $val }}
{{- end }}
{{- range $k, $val := $env }}
{{- if not (kindIs "invalid" $val) }}
- name: {{ $k }}
value: {{ include "fluxer-web.str" $val }}
{{- end }}
{{- end }}
{{- with .w.buildVersion }}
- name: BUILD_VERSION
value: {{ include "fluxer-web.str" . }}
{{- end }}
{{- with concat (.root.Values.extraEnv | default list) (.w.extraEnv | default list) }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "fluxer-web.topologySpread" -}}
{{- $tscs := ternary .w.topologySpreadConstraints .root.Values.topologySpreadConstraints (hasKey .w "topologySpreadConstraints") -}}
{{- range $tscs }}
{{- $c := deepCopy . }}
{{- if not $c.labelSelector }}
{{- $_ := set $c "labelSelector" (dict "matchLabels" (include "fluxer-web.selectorLabels" $ | fromYaml)) }}
{{- end }}
- {{- toYaml $c | nindent 2 }}
{{- end }}
{{- end }}
@@ -0,0 +1,172 @@
{{- $v := .Values }}
{{- range $name, $w := .Values.workloads }}
{{- if not (kindIs "invalid" $w) }}
{{- $ctx := dict "root" $ "name" $name "w" $w }}
{{- $envFrom := concat ($v.envFrom | default list) ($w.envFrom | default list) }}
{{- $podAnnotations := merge (dict) ($w.podAnnotations | default dict) ($v.podAnnotations | default dict) }}
{{- $wProbes := $w.probes | default dict }}
{{- $gProbes := $v.probes | default dict }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
{{- if not $w.hpa }}
replicas: {{ if kindIs "invalid" $w.replicas }}1{{ else }}{{ int $w.replicas }}{{ end }}
{{- end }}
{{- if not (kindIs "invalid" $w.minReadySeconds) }}
minReadySeconds: {{ int $w.minReadySeconds }}
{{- end }}
selector:
matchLabels:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "strategy") }}
strategy:
{{- . | nindent 4 }}
{{- end }}
template:
metadata:
labels:
{{- include "fluxer-web.labels" $ctx | nindent 8 }}
{{- with $podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "imagePullSecrets") }}
imagePullSecrets:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "podSecurityContext") }}
securityContext:
{{- . | nindent 8 }}
{{- end }}
{{- if not (kindIs "invalid" $w.terminationGracePeriodSeconds) }}
terminationGracePeriodSeconds: {{ int $w.terminationGracePeriodSeconds }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "nodeSelector") }}
nodeSelector:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "affinity") }}
affinity:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "tolerations") }}
tolerations:
{{- . | nindent 8 }}
{{- end }}
{{- with include "fluxer-web.topologySpread" $ctx | trim }}
topologySpreadConstraints:
{{- . | nindent 8 }}
{{- end }}
containers:
- name: {{ $name }}
image: {{ include "fluxer-web.image" $ctx }}
imagePullPolicy: {{ ($w.image | default dict).pullPolicy | default ($v.image | default dict).pullPolicy | default "IfNotPresent" }}
{{- with include "fluxer-web.env" $ctx | trim }}
env:
{{- . | nindent 12 }}
{{- end }}
{{- with $envFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8080
protocol: TCP
{{- with $w.lifecycle }}
lifecycle:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- range $probe := list "startup" "liveness" "readiness" }}
{{- with hasKey $wProbes $probe | ternary (get $wProbes $probe) (get $gProbes $probe) }}
{{ $probe }}Probe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- with $w.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with include "fluxer-web.pick" (dict "root" $ "w" $w "key" "securityContext") }}
securityContext:
{{- . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $w.extraVolumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
type: ClusterIP
selector:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 4 }}
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
{{- with $w.hpa }}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ $name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ $name }}
minReplicas: {{ required (printf "%s.hpa.minReplicas is required" $name) .minReplicas }}
maxReplicas: {{ required (printf "%s.hpa.maxReplicas is required" $name) .maxReplicas }}
{{- with .targetCPUUtilizationPercentage }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ . }}
{{- end }}
{{- with .behavior }}
behavior:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- with $w.pdb }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $name }}-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "fluxer-web.labels" $ctx | nindent 4 }}
spec:
{{- toYaml . | nindent 2 }}
selector:
matchLabels:
{{- include "fluxer-web.selectorLabels" $ctx | nindent 6 }}
{{- end }}
{{- end }}
{{- end }}
+83
View File
@@ -0,0 +1,83 @@
image:
registry: ghcr.io/fluxerapp
tag: v1
pullPolicy: IfNotPresent
imagePullSecrets: []
env: {}
extraEnv: []
envFrom:
- secretRef:
name: fluxer-env
podAnnotations: {}
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
probes:
startup:
httpGet:
path: /_health
port: http
periodSeconds: 10
failureThreshold: 30
liveness:
httpGet:
path: /_health
port: http
readiness:
httpGet:
path: /_health
port: http
strategy:
type: RollingUpdate
topologySpreadConstraints: []
nodeSelector: {}
tolerations: []
affinity: {}
workloads:
admin:
image:
name: fluxer-admin
replicas: 1
env:
FLUXER_ENV: production
FLUXER_API_ENDPOINT: https://api.example.com
FLUXER_ADMIN_ENDPOINT: https://admin.example.com
FLUXER_MEDIA_ENDPOINT: https://media.example.com
FLUXER_APP_ENDPOINT: https://web.example.com
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
app-proxy:
image:
name: fluxer-app-proxy-self-hosted
replicas: 1
env:
RELEASE_CHANNEL: stable
PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://web.example.com/api
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 384Mi
+427 -197
View File
@@ -1,108 +1,66 @@
# Every variable docker-compose.yml reads is named here, uncommented when it has
# no default and commented with its default when it has one. A name absent from
# this file reaches a service only through a Compose override. Compose expands
# top to bottom, so a line using ${...} must sit below every name it reads.
# Every variable docker-compose.yml reads. A value an install must set is
# uncommented. A commented line shows the default, or an example where its comment
# says so, and nothing after = means the service decides. An empty value keeps the
# default too. Compose expands top to bottom, so a line using ${...} must sit below
# every name it reads.
FLUXER_DOMAIN=chat.example.com
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443
# The lines above are the address browsers use, and every advertised endpoint
# carries FLUXER_PUBLIC_PORT. They do not move what the host publishes.
# FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below do that, and a non-default port
# needs the matching one set as well. Complete recipes sit beside them.
# The address browsers use. FLUXER_HTTP_PORT and FLUXER_HTTPS_PORT below decide
# which host ports Fluxer binds.
# How browsers reach this instance.
#
# Default: Fluxer binds 80 and 443 and gets its own Let's Encrypt certificate.
# Point DNS at this host.
#
# Behind your own reverse proxy (nginx, Traefik, HAProxy, Cloudflare Tunnel,
# another Caddy): uncomment COMPOSE_FILE below. Fluxer then serves plain HTTP on
# 127.0.0.1:8080 instead, and your proxy forwards everything to it. Keep
# FLUXER_PUBLIC_SCHEME and FLUXER_PUBLIC_PORT describing the PUBLIC address your
# proxy serves, not this local port.
# By default Fluxer binds 80 and 443 and gets its own certificate. Point DNS here.
# Behind your own reverse proxy, uncomment this instead: Fluxer then serves plain
# HTTP on 127.0.0.1:8080. Keep the scheme and port above describing the public
# address, not this one.
#COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml
# Where the plain-HTTP port binds when the proxy overlay is in use. Leave it on
# loopback when the proxy runs on this host. Use 0.0.0.0:8080 only when the proxy
# is on another machine, and firewall the port to that machine.
# Where that plain-HTTP port binds. Use 0.0.0.0:8080 only when the proxy is on
# another machine, and firewall it to that machine.
#FLUXER_EDGE_BIND=127.0.0.1:8080
# Which upstream hops may set X-Forwarded-For. Fluxer rewrites the header from
# this to the real client address, so IP bans, rate limits and abuse detection
# see the caller rather than the proxy. The default covers proxies on private or
# loopback addresses, which is every same-host setup. Set it to your proxy's
# address if it reaches Fluxer from a public IP.
# Which hops may set X-Forwarded-For. The default covers private and loopback
# addresses. Set your proxy's address if it reaches Fluxer from a public IP.
#FLUXER_EDGE_TRUSTED_PROXIES=private_ranges
# The origin browsers see, without a trailing slash. Leave it unset and each
# service builds one from the three values at the top of this file. Set it and it
# wins: every service reads the host, the scheme and the port out of it and
# ignores those three names. Use it when browsers reach the instance on a host
# FLUXER_DOMAIN does not name. It has to be a bare origin, a scheme and a host
# and an optional port and nothing after them, or the services refuse to start.
# It does not move the edge listener or the published ports either, so set the
# publish below to the port written here.
# The origin browsers see, no trailing slash. Set it when browsers reach the
# instance on a host FLUXER_DOMAIN does not name, and it wins over the three
# values above. Scheme, host and optional port only. It does not move the
# published ports.
#FLUXER_PUBLIC_ORIGIN=https://chat.example.com
# Overrides the address the edge listens on inside its container. Compose builds
# it from FLUXER_PUBLIC_SCHEME and FLUXER_DOMAIN with no port, and the edge keeps
# its container ports at 80 and 443 whatever the public port is. Caddy matches a
# site by host and ignores the port in the Host header, so a request arriving on
# a non-default published port still lands on this site. Put a port in this value
# only if you also publish that same container port below, or nothing will be
# listening where the publish points. Honoured in the default mode only:
# docker-compose.proxy.yml sets the literal :8080 and tunnel.compose.yml the
# literal :80, and Compose lets the last file win, so a value here is discarded
# under either overlay with no warning. Set it for an unusual default-mode
# layout, such as serving several hostnames. Write the scheme into it: a bare
# hostname means automatic HTTPS on 443 whatever FLUXER_PUBLIC_SCHEME says.
# The address the edge listens on inside its container. Both proxy overlays set
# this themselves, so a value here is ignored under either. Include the scheme.
#FLUXER_EDGE_SITE_ADDRESS=https://chat.example.com
# The old name for the value above, read only when FLUXER_EDGE_SITE_ADDRESS is
# unset, so an existing .env keeps the listener it already had.
# The old name for the line above, read only when it is unset.
#FLUXER_CADDY_SITE_ADDRESS=
# Host side of the edge's publishes, and the only names that decide which host
# ports Fluxer binds. The container side is fixed. Container 80 carries the
# HTTP to HTTPS redirect and the Let's Encrypt HTTP challenge under an https
# scheme, and the site itself under an http one. Container 443 carries the TLS
# site. FLUXER_HTTPS_PORT moves the TCP and the UDP publish together, because
# HTTP/3 needs both on the same port. Both take an optional bind address in front
# of the port, and 127.0.0.1 keeps the publish off every public interface. Give
# them different host ports: the same host port on both is two publishes of one
# port and the edge refuses to start.
# Host ports. Container 80 handles the redirect and the certificate challenge,
# container 443 the TLS site. FLUXER_HTTPS_PORT moves TCP and UDP together, since
# HTTP/3 needs both. Both accept a bind address. Give them different host ports.
#FLUXER_HTTP_PORT=80
#FLUXER_HTTPS_PORT=443
#FLUXER_HTTP_PORT=127.0.0.1:80
#FLUXER_HTTPS_PORT=127.0.0.1:443
# HTTPS on 8443, complete. Host 80 stays published and still answers the ACME
# challenge. Let's Encrypt only ever connects to the public 80 or 443, so the
# certificate is issued if a router in front forwards public 80 to this host and
# is not issued otherwise. Serve your own certificate from the Caddyfile when it
# cannot.
# HTTPS on 8443. Host 80 stays published for the certificate challenge, which
# only ever arrives on public 80 or 443. Serve your own certificate if nothing
# forwards those.
#FLUXER_PUBLIC_PORT=8443
#FLUXER_HTTPS_PORT=8443
# Plain HTTP on 19080, complete. The port 80 publish moves to 19080, so nothing
# binds host 80. Under an http scheme nothing listens on container 443, so the
# last line parks that publish on loopback for a host that wants 443 for
# something else. Drop it and 443 is published and idle, which is what earlier
# releases did.
# Plain HTTP on 19080. Nothing binds host 80, and the last line parks the idle
# 443 publish on loopback.
#FLUXER_PUBLIC_SCHEME=http
#FLUXER_PUBLIC_PORT=19080
#FLUXER_HTTP_PORT=19080
#FLUXER_HTTPS_PORT=127.0.0.1:443
# A tunnel or another proxy in front of the stack needs no HTTPS publish at all.
# tunnel.compose.yml ships beside this file and replaces Caddy's published ports
# with a single loopback HTTP publish, so nothing binds 443, and points the edge
# at plain HTTP on that publish so it stops redirecting to https. FLUXER_HTTP_PORT
# still moves that one publish. Set the line below and plain docker compose
# commands pick the file up, or add it to your own -f flags if you pass any. The
# file uses the !override tag, which needs Compose 2.24.4 or newer.
# A tunnel needs no HTTPS publish. tunnel.compose.yml ships beside this file and
# leaves one loopback HTTP publish. Needs Compose 2.24.4 or newer.
#COMPOSE_FILE=docker-compose.yml:tunnel.compose.yml
FLUXER_REGISTRY_OWNER=fluxerapp
@@ -111,11 +69,10 @@ FLUXER_IMAGE_TAG=v1
POSTGRES_PASSWORD=CHANGE_ME
MEILI_MASTER_KEY=CHANGE_ME
# The stack ships its own Postgres and its own object store, and points at both
# by service name. Set these to run either one outside the stack. Leave them
# unset and the bundled services are used. Taking a service out of the stack
# means an upgrade skips the backup step that reaches into it, and backing that
# store up belongs to whoever runs it.
# Set these to run Postgres or the object store outside the stack. Backing up a
# store you moved out is yours to arrange. An upgrade dumps the bundled postgres
# service and skips the dump only when the stack defines none. The values below
# are examples.
#FLUXER_POSTGRES_HOST=db.example.com
#FLUXER_POSTGRES_PORT=5432
#FLUXER_POSTGRES_DATABASE=fluxer
@@ -125,45 +82,104 @@ MEILI_MASTER_KEY=CHANGE_ME
#FLUXER_S3_PUBLIC_ENDPOINT=https://cdn.example.com
#FLUXER_S3_REGION=eu-central-1
#FLUXER_S3_FORCE_PATH_STYLE=false
# Bucket names. The bundled object store creates whichever names these hold, so
# the two stay in step. An object store outside the stack needs the buckets to
# Bucket names. The bundled store creates these. An outside store needs them to
# exist already.
#FLUXER_S3_BUCKET_CDN=fluxer
#FLUXER_S3_BUCKET_UPLOADS=fluxer-uploads
#FLUXER_S3_BUCKET_DOWNLOADS=fluxer-downloads
#FLUXER_S3_BUCKET_REPORTS=fluxer-reports
#FLUXER_S3_BUCKET_HARVESTS=fluxer-harvests
# With the object store outside the stack, add this overlay to COMPOSE_FILE and
# the bundled seaweedfs no longer starts. Put it after any other overlay, such as
# docker-compose.yml:docker-compose.proxy.yml:external-object-store.compose.yml.
# Needs Compose 2.24.4 or newer.
#COMPOSE_FILE=docker-compose.yml:external-object-store.compose.yml
# The rest of the bundled services, pointed somewhere else the same way. Leave a
# line unset and the service in the stack is used. Taking a service out of the
# stack goes in an override file listed in COMPOSE_FILE, because an upgrade
# replaces docker-compose.yml.
# A full connection URL wins over the host, port and database above. The URL is an
# example. The CA is the PEM text of the certificate, with \n for line breaks.
#FLUXER_POSTGRES_URL=postgres://fluxer:[email protected]:5432/fluxer
#FLUXER_POSTGRES_SSL_CA=
# The Postgres table that holds the key-value store.
#FLUXER_POSTGRES_KV_TABLE=fluxer_kv
# media-proxy reads through these when the store serves reads from another
# address or bucket.
#FLUXER_S3_READ_ENDPOINT=
#FLUXER_S3_READ_BUCKET=
#FLUXER_S3_READ_BUCKET_STYLE=
# A temporary S3 session token, read by media-proxy only.
#FLUXER_S3_SESSION_TOKEN=
# The bundled store refuses unsigned reads. Set false only for a public-read bucket.
#FLUXER_S3_READ_SIGNED=true
# The other bundled services, pointed elsewhere. Removing a service from the
# stack belongs in an override file, since an upgrade replaces docker-compose.yml.
# The URLs below are examples.
#FLUXER_KV_URL=redis://cache.example.com:6379/0
#FLUXER_NATS_URL=nats://mq.example.com:4222
#FLUXER_NATS_JETSTREAM_URL=nats://mq.example.com:4222
#FLUXER_SVC_NATS_URL=nats://mq.example.com:4222
#FLUXER_SEARCH_URL=https://search.example.com
#FLUXER_LIVEKIT_INTERNAL_URL=http://livekit.example.com:7880
# How the stack talks to those services.
#FLUXER_KV_MODE=standalone
#FLUXER_SEARCH_ENGINE=meilisearch
#FLUXER_SEARCH_USERNAME=
#FLUXER_SEARCH_PASSWORD=
#FLUXER_SEARCH_TLS_REJECT_UNAUTHORIZED=true
# Voice off. The livekit service still runs until an override file takes it out.
# Voice off. The livekit service still runs until an override removes it.
#FLUXER_LIVEKIT_ENABLED=false
# Optional systems, each off unless the instance is configured for it.
#FLUXER_SMS_ENABLED=false
# Optional systems, each off unless configured.
#FLUXER_STRIPE_ENABLED=false
#FLUXER_NCMEC_ENABLED=false
#FLUXER_CLAMAV_ENABLED=false
#FLUXER_STRIPE_SECRET_KEY=
#FLUXER_STRIPE_WEBHOOK_SECRET=
# Stripe prices as one JSON object. The admin dashboard can set them instead.
#FLUXER_STRIPE_PRICES={}
#FLUXER_STRIPE_LEGACY_PRICES={}
#FLUXER_API_DONATION_PROXY_KEY=
#FLUXER_VISIONARIES_GUILD_ID=
#FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID=
# The client address. Set the header name a proxy in front actually writes, and
# turn the trust off when nothing sits in front, because a trusted header an
# attacker can set is a spoofed client address.
#FLUXER_CLIENT_IP_HEADER_NAME=cf-connecting-ip
# NCMEC CyberTipline reporting, off by default. All four values are required
# once it is on. The values below are examples.
#FLUXER_NCMEC_ENABLED=true
#FLUXER_NCMEC_BASE_URL=https://report.cybertip.org/ispws
#FLUXER_NCMEC_USERNAME=
#FLUXER_NCMEC_PASSWORD=
#[email protected]
# Upload virus scanning, off by default. No ClamAV container ships, so point
# this at your own. The values below are examples.
#FLUXER_CLAMAV_ENABLED=true
#FLUXER_CLAMAV_HOST=clamav
#FLUXER_CLAMAV_PORT=3310
#FLUXER_CLAMAV_FAIL_OPEN=false
# Outside lookups, off unless turned on. The breached password check asks
# api.pwnedpasswords.com.
#FLUXER_BREACHED_PASSWORD_CHECK_ENABLED=false
#FLUXER_BLOCKLIST_FEEDS_ENABLED=false
# Phone verification needs your own responder on the rpc.phone.v1 NATS
# subjects. Off unless turned on.
#FLUXER_PHONE_VERIFICATION_ENABLED=false
# A local path, or an s3:// URL read with the S3 credentials of this file.
#FLUXER_GEOIP_DB_PATH=
# The client address. The edge sets X-Forwarded-For on every hop, so keep the
# trust on and the default header. Turning the trust off makes the api refuse
# every request outside its exempt routes with a 403.
#FLUXER_CLIENT_IP_HEADER_NAME=x-forwarded-for
#FLUXER_TRUST_CLIENT_IP_HEADER=true
# How much the services write. trace, debug, info, warn, error or fatal. Every
# service names the object storage endpoint and its addressing at info on start,
# so a bucket that answers 404 is visible without raising this.
#LOG_LEVEL=debug
# How much the services write. LOG_LEVEL covers the api and worker and takes trace,
# debug, info, warn, error or fatal. RUST_LOG covers the Rust services and takes
# an EnvFilter such as debug. The gateway takes an Erlang level such as notice,
# and LOGGER_LEVEL beats FLUXER_GATEWAY_LOGGER_LEVEL.
#LOG_LEVEL=info
#RUST_LOG=info
#FLUXER_GATEWAY_LOGGER_LEVEL=info
#LOGGER_LEVEL=
FLUXER_S3_ACCESS_KEY=fluxer
FLUXER_S3_SECRET_KEY=CHANGE_ME
@@ -177,32 +193,73 @@ FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=CHANGE_ME
FLUXER_ADMIN_SECRET_KEY_BASE=CHANGE_ME
FLUXER_ADMIN_OAUTH_CLIENT_SECRET=CHANGE_ME
# The token every service sends to NATS. The bundled NATS runs without
# authentication, so this stays empty unless a Compose override points the stack
# at an external NATS that requires a token. Compose forwards the name to every
# container that connects.
# The token every service sends to NATS. The bundled NATS needs none, so this
# stays empty unless an override points at an external one.
#FLUXER_NATS_AUTH_TOKEN=
FLUXER_VAPID_PUBLIC_KEY=CHANGE_ME
FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
# The VAPID contact address defaults to admin@ followed by FLUXER_DOMAIN. Set it
# only if that mailbox does not exist.
# Defaults to admin@ followed by FLUXER_DOMAIN. Set it if that mailbox does not
# exist.
#[email protected]
# Passkeys follow FLUXER_DOMAIN by default. Set these only if browsers reach the
# instance on a different host, and note that changing FLUXER_PASSKEY_RP_ID
# invalidates every passkey already registered against the old value.
# The passkey RP ID defaults to FLUXER_DOMAIN, whatever FLUXER_PUBLIC_ORIGIN says.
# Changing the RP ID invalidates every passkey registered against the old value.
#FLUXER_PASSKEY_RP_ID=chat.example.com
#FLUXER_PASSKEY_RP_NAME=Fluxer
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=https://chat.example.com
#FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=http://chat.example.com:19080
# Extra Content-Security-Policy sources, appended to the built-in ones. Set these
# only when a browser must reach an origin the defaults do not cover, such as a
# voice server hosted on a domain other than FLUXER_DOMAIN. Separate several
# sources with spaces or commas. Every one of them is empty by default, and the
# three carrying a value below are illustrations, not defaults.
# Notification jobs the push container holds at once, 1 to 1000000.
#FLUXER_PUSH_SERVICE_QUEUE_CAPACITY=10000
# Provider requests the push container sends at once, 1 to 65536.
#FLUXER_PUSH_SERVICE_SEND_CONCURRENCY=256
# The push container's provider addresses and relay hosts.
#FLUXER_PUSH_SERVICE_APNS_BASE_URL=
#FLUXER_PUSH_SERVICE_FCM_BASE_URL=https://fcm.googleapis.com
#FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS=push.fluxer.com
#FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS=
#FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED=false
# Direct mobile push through your own APNs and FCM credentials, off by default.
#FLUXER_PUSH_APNS_ENABLED=false
#FLUXER_PUSH_APNS_TEAM_ID=
#FLUXER_PUSH_APNS_KEY_ID=
#FLUXER_PUSH_APNS_PRIVATE_KEY=
#FLUXER_PUSH_APNS_PRIVATE_KEY_PATH=
#FLUXER_PUSH_APNS_APPS=
#FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT=production
#FLUXER_PUSH_FCM_ENABLED=false
#FLUXER_PUSH_FCM_PROJECT_ID=
#FLUXER_PUSH_FCM_CLIENT_EMAIL=
#FLUXER_PUSH_FCM_PRIVATE_KEY=
#FLUXER_PUSH_FCM_PRIVATE_KEY_PATH=
#FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH=
#FLUXER_PUSH_FCM_TOKEN_URI=https://oauth2.googleapis.com/token
#FLUXER_PUSH_FCM_APPS=
# Optional media policies, both off by default. See the operator docs.
#
# CORS limits which web origins may read media. A request with no Origin is
# always served. Add https://web.fluxer.app if people use the hosted client.
#
# Signatures make an attachment read need a signed URL, so a copied link stops
# working. Needs a secret from openssl rand -base64 32, first entry signs and
# every entry verifies.
#
# Each mode is off, report or enforce, and off is the default. Start at report.
# media-proxy reads these at start, so apply with docker compose up -d
# media-proxy. The values below are examples.
#FLUXER_MEDIA_PROXY_CORS_MODE=enforce
#FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS=https://chat.example.com,https://web.fluxer.app
#FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64=
#FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE=enforce
# Extra Content-Security-Policy sources, appended to the built-in ones. Set one
# only when a browser must reach an origin the defaults do not cover. Separate
# several with spaces or commas. The three values below are illustrations.
#FLUXER_CSP_EXTRA_DEFAULT_SRC=
#FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881
#FLUXER_CSP_EXTRA_IMG_SRC=https://cdn.example.com
@@ -214,45 +271,42 @@ FLUXER_VAPID_PRIVATE_KEY=CHANGE_ME
#FLUXER_CSP_EXTRA_WORKER_SRC=
#FLUXER_CSP_EXTRA_MANIFEST_SRC=
# One report-uri for Content-Security-Policy violation reports. Empty leaves the
# directive off the header.
# One report-uri for CSP violation reports. Empty leaves the directive off.
#FLUXER_CSP_REPORT_URI=
# Allow the SSO identity provider to resolve to a private or internal address.
# Off by default: the API refuses to call non-public addresses so a misconfigured
# provider URL cannot be used to reach internal services. Turn it on only when the
# provider genuinely lives on your own network, such as split-horizon DNS or a LAN
# identity provider, and only when you trust everyone who can configure SSO.
# Let the SSO provider resolve to a private address. Off by default, so a
# misconfigured provider URL cannot reach internal services. Turn it on only for
# a provider on your own network. The value below is an example.
#FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES=true
# Both reach LiveKit as LIVEKIT_KEYS and the webhook signing key, and the API as
# FLUXER_LIVEKIT_API_KEY and FLUXER_LIVEKIT_API_SECRET. Change them together.
# These reach both LiveKit and the api. Change them together.
LIVEKIT_API_KEY=fluxer
LIVEKIT_API_SECRET=CHANGE_ME
# The URL browsers use for voice signalling. Compose builds it from
# FLUXER_PUBLIC_ORIGIN, or from FLUXER_PUBLIC_SCHEME, FLUXER_DOMAIN and
# FLUXER_PUBLIC_PORT, as that origin followed by /livekit. The client rewrites a
# leading http to ws itself. Set it only when LiveKit is served from another
# host.
# The URL browsers use for voice signalling. Built from the public origin plus
# /livekit. Set it only when LiveKit is served from another host.
#FLUXER_LIVEKIT_URL=
# Media ports. LiveKit advertises these in ICE candidates, so the host must
# forward the same numbers.
# Media ports. LiveKit advertises these, so forward the same numbers.
#FLUXER_LIVEKIT_TCP_PORT=7881
#FLUXER_LIVEKIT_UDP_PORT=7882
# LiveKit finds the address browsers dial by asking a STUN server. A host that
# cannot reach one over UDP stops with "could not resolve external IP", and the
# address is then set by hand: put it in FLUXER_LIVEKIT_NODE_IP and set
# FLUXER_LIVEKIT_USE_EXTERNAL_IP to false. Point the STUN entries at another
# server to keep the lookup and leave Google out of it.
# LiveKit finds its public address over STUN. A host that cannot reach one stops
# with "could not resolve external IP", so set the address by hand instead, or
# point STUN elsewhere. The values below are examples.
#FLUXER_LIVEKIT_USE_EXTERNAL_IP=false
#FLUXER_LIVEKIT_NODE_IP=203.0.113.10
#FLUXER_LIVEKIT_STUN_PRIMARY=stun.l.google.com:19302
#FLUXER_LIVEKIT_STUN_SECONDARY=stun1.l.google.com:19302
# The voice region users see, and how much LiveKit logs.
#FLUXER_LIVEKIT_DEFAULT_REGION={"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0}
#FLUXER_LIVEKIT_LOG_LEVEL=info
FLUXER_KLIPY_API_KEY=
#FLUXER_YOUTUBE_API_KEY=
# Hosts the api never unfurls, comma separated.
#FLUXER_API_UNFURL_IGNORED_HOSTS=
FLUXER_EMAIL_ENABLED=false
FLUXER_EMAIL_PROVIDER=none
@@ -264,20 +318,97 @@ FLUXER_EMAIL_SMTP_PORT=587
FLUXER_EMAIL_SMTP_USERNAME=
FLUXER_EMAIL_SMTP_PASSWORD=
FLUXER_EMAIL_SMTP_SECURE=true
#FLUXER_EMAIL_WEBHOOK_SECRET=
FLUXER_CAPTCHA_ENABLED=false
FLUXER_CAPTCHA_PROVIDER=none
FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY=
FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY=
FLUXER_CAPTCHA_TURNSTILE_SITE_KEY=
FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY=
FLUXER_DISCOVERY_ENABLED=true
#FLUXER_DISCOVERY_MIN_MEMBER_COUNT=1
# Container memory. The limits sum to 18.25 GiB, which is a sum of ceilings and
# not an allocation, so the defaults fit a host with 8 GB and are sized for 16 GB.
# The reservations are cgroup memory.low, which biases the kernel away from
# reclaiming from services whose death takes the instance down. They reserve
# nothing. Lower the limits on a smaller host.
# Instance identity and account policy.
#FLUXER_APP_PRODUCT_NAME=Fluxer
#FLUXER_APP_ICON_URL=
#FLUXER_APP_SYMBOL_URL=
#FLUXER_APP_LOGO_URL=
#FLUXER_APP_WORDMARK_URL=
#FLUXER_APP_FAVICON_URL=
#FLUXER_APP_THEME_COLOR=
#FLUXER_APP_STATUS_PAGE_URL=
#FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL=
#FLUXER_INSTANCE_SETUP_CONFIGURED=false
#FLUXER_AUTO_JOIN_INVITE_CODE=
#FLUXER_DELETION_GRACE_PERIOD_HOURS=336
# Sign in with Bluesky, off unless turned on.
#FLUXER_AUTH_BLUESKY_ENABLED=false
#FLUXER_AUTH_BLUESKY_CLIENT_NAME=Fluxer
#FLUXER_AUTH_BLUESKY_CLIENT_URI=
#FLUXER_AUTH_BLUESKY_LOGO_URI=
#FLUXER_AUTH_BLUESKY_TOS_URI=
#FLUXER_AUTH_BLUESKY_POLICY_URI=
#FLUXER_AUTH_BLUESKY_KEYS=
# Public addresses. Each follows the public origin unless set here.
#FLUXER_API_ENDPOINT=
#FLUXER_API_CLIENT_ENDPOINT=
#FLUXER_APP_ENDPOINT=
#FLUXER_GATEWAY_ENDPOINT=
#FLUXER_MEDIA_ENDPOINT=
#FLUXER_STATIC_CDN_ENDPOINT=
#FLUXER_ADMIN_ENDPOINT=
#FLUXER_MARKETING_ENDPOINT=
#FLUXER_INVITE_ENDPOINT=
#FLUXER_GIFT_ENDPOINT=
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT=
#PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT=
# These follow FLUXER_STATIC_CDN_ENDPOINT first, then the public origin.
#FLUXER_GATEWAY_STATIC_CDN_ENDPOINT=
#FLUXER_UNFURL_STATIC_CDN_ENDPOINT=
# These follow FLUXER_MEDIA_ENDPOINT first, then the public origin.
#FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT=
#FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT=
# Extra hosts for static assets, invites, gifts and the web app. Empty by default.
#FLUXER_STATIC_CDN_DOMAIN=
#FLUXER_INVITE_DOMAIN=
#FLUXER_GIFT_DOMAIN=
#FLUXER_APP_ORIGIN_ALIASES=
# The path the admin panel is served under. The edge and the admin service read
# it. The api follows it through the default FLUXER_ADMIN_ENDPOINT, and not when
# FLUXER_ADMIN_ENDPOINT is set. Write it with a leading slash and no trailing
# slash.
#FLUXER_ADMIN_BASE_PATH=/admin
# The compression the edge offers, as Caddy encode arguments.
#FLUXER_EDGE_ENCODE=zstd gzip
# Images of the bundled services, for a mirror or another tag. A new Postgres
# major needs a dump and restore, as the upgrade guide describes.
#FLUXER_CADDY_IMAGE=caddy:2.11-alpine
#FLUXER_POSTGRES_IMAGE=postgres:16-alpine
#FLUXER_VALKEY_IMAGE=valkey/valkey:9.1-alpine
#FLUXER_NATS_IMAGE=nats:2.14-alpine
#FLUXER_MEILISEARCH_IMAGE=getmeili/meilisearch:v1.53
#FLUXER_SEAWEEDFS_IMAGE=chrislusf/seaweedfs:4.47
#FLUXER_LIVEKIT_IMAGE=livekit/livekit-server:v1.12.0
# Restart policy for every long-running service.
#FLUXER_RESTART_POLICY=unless-stopped
# Health checks. Raise the retries or start periods on a slow host.
#FLUXER_HEALTHCHECK_INTERVAL=10s
#FLUXER_HEALTHCHECK_TIMEOUT=5s
#FLUXER_HEALTHCHECK_RETRIES=10
#FLUXER_APP_HEALTHCHECK_RETRIES=30
#FLUXER_APP_HEALTHCHECK_START_PERIOD=90s
#FLUXER_SVC_HEALTHCHECK_START_PERIOD=60s
#FLUXER_WORKER_HEALTHCHECK_RETRIES=3
#FLUXER_SEAWEEDFS_HEALTHCHECK_RETRIES=20
#FLUXER_SEAWEEDFS_HEALTHCHECK_START_PERIOD=60s
#FLUXER_SEAWEEDFS_INIT_ATTEMPTS=60
# Container memory. These are ceilings, not allocations, and the defaults suit a
# 16 GB host. The reservations bias the kernel away from reclaiming from services
# whose death takes the instance down. Lower the limits on a smaller host.
#FLUXER_CADDY_MEMORY_LIMIT=256mb
#FLUXER_POSTGRES_MEMORY_LIMIT=5gb
#FLUXER_POSTGRES_MEMORY_RESERVATION=3gb
@@ -294,6 +425,7 @@ FLUXER_DISCOVERY_ENABLED=true
#FLUXER_GATEWAY_MEMORY_LIMIT=1gb
#FLUXER_GATEWAY_MEMORY_RESERVATION=384mb
#FLUXER_MEDIA_PROXY_MEMORY_LIMIT=512mb
#FLUXER_PUSH_MEMORY_LIMIT=256mb
#FLUXER_STATIC_PROXY_MEMORY_LIMIT=256mb
#FLUXER_APP_PROXY_MEMORY_LIMIT=256mb
#FLUXER_SNOWFLAKES_MEMORY_LIMIT=128mb
@@ -308,80 +440,178 @@ FLUXER_DISCOVERY_ENABLED=true
#FLUXER_UNFURL_SHARD_MEMORY_LIMIT=256mb
#FLUXER_ADMIN_MEMORY_LIMIT=256mb
# Meilisearch indexing memory. Keep it well under FLUXER_MEILISEARCH_MEMORY_LIMIT,
# which is the container ceiling the indexer shares with the search process.
# Meilisearch indexing memory. Keep it well under the container limit above.
#FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=384mb
#FLUXER_MEILISEARCH_ENV=production
#FLUXER_MEILISEARCH_NO_ANALYTICS=true
# SeaweedFS heap ceiling. Go collects against this value instead of against the
# container limit, which it cannot see, so without it an upload burst grows the
# heap past FLUXER_SEAWEEDFS_MEMORY_LIMIT and the kernel OOM-kills the container
# mid-upload (exit 137). Keep it near three quarters of that limit, and raise both
# together: the peak is the parts of one upload in flight at once, which is 25 MB
# times 20 for a 500 MB attachment.
# SeaweedFS heap ceiling. Go cannot see the container limit, so without this an
# upload burst gets the container OOM-killed. Keep it near three quarters of
# FLUXER_SEAWEEDFS_MEMORY_LIMIT and raise both together.
#FLUXER_SEAWEEDFS_GOMEMLIMIT=1536MiB
#FLUXER_SEAWEEDFS_TELEMETRY=false
# Node sizes its own heap from the container memory limit by default, at roughly
# 55 percent of it, which always leaves room for the buffers and stacks that live
# outside the heap. Leave these unset unless you have a reason to pin the value.
# Any value set here must stay well below the container limit above: a heap ceiling
# above the container limit makes the kernel OOM-kill the container (exit 137, no
# diagnostics) instead of Node reporting a JavaScript heap out of memory error.
# Volumes SeaweedFS creates at once when a bucket needs space. Each reserves 1 GB
# of free disk from the start, and SeaweedFS's own default of 7 fills a small
# disk before every bucket has one, so uploads fail with no free volumes left.
#FLUXER_SEAWEEDFS_VOLUME_GROWTH=1
# Node sizes its heap from the container limit by default. Leave these unset
# unless you need to pin it. A heap ceiling above the container limit gets the
# container OOM-killed instead of reporting a heap error. The values below are
# examples.
#FLUXER_API_NODE_HEAP_MB=1792
#FLUXER_WORKER_NODE_HEAP_MB=1792
# Bundled Postgres tuning. Keep these consistent with FLUXER_POSTGRES_MEMORY_LIMIT:
# budget roughly shared_buffers + (server max_connections x 12 MB) +
# (3 x autovacuum_work_mem) + 300 MB for page cache and WAL. Note this is the
# server setting, distinct from the per-service FLUXER_POSTGRES_MAX_CONNECTIONS
# pool sizes used by the api, worker and shards.
# Extra Node flags for api and worker, appended to NODE_OPTIONS. Empty by
# default. The value below is an example.
#FLUXER_API_NODE_OPTIONS=--heapsnapshot-near-heap-limit=1
#FLUXER_WORKER_NODE_OPTIONS=--heapsnapshot-near-heap-limit=1
# Extra CA certificates api and worker trust, as a PEM bundle path inside the
# container. The default is the image's system bundle.
#FLUXER_NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Bundled Postgres tuning. Keep it consistent with the memory limit above. This
# is the server setting, not the per-service pool sizes.
#FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS=150
#FLUXER_POSTGRES_SHARED_BUFFERS=512MB
#FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE=2GB
#FLUXER_POSTGRES_WORK_MEM=8MB
#FLUXER_POSTGRES_MAINTENANCE_WORK_MEM=256MB
#FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM=128MB
#FLUXER_POSTGRES_SHM_SIZE=1gb
#FLUXER_POSTGRES_RANDOM_PAGE_COST=1.1
#FLUXER_POSTGRES_EFFECTIVE_IO_CONCURRENCY=200
#FLUXER_POSTGRES_DEFAULT_STATISTICS_TARGET=200
#FLUXER_POSTGRES_JIT=off
#FLUXER_POSTGRES_MIN_WAL_SIZE=512MB
#FLUXER_POSTGRES_MAX_WAL_SIZE=2GB
#FLUXER_POSTGRES_CHECKPOINT_COMPLETION_TARGET=0.9
#FLUXER_POSTGRES_WAL_BUFFERS=16MB
#FLUXER_POSTGRES_WAL_COMPRESSION=zstd
#FLUXER_POSTGRES_BGWRITER_DELAY=50ms
#FLUXER_POSTGRES_BGWRITER_LRU_MAXPAGES=1000
#FLUXER_POSTGRES_AUTOVACUUM_VACUUM_SCALE_FACTOR=0.05
#FLUXER_POSTGRES_AUTOVACUUM_ANALYZE_SCALE_FACTOR=0.02
#FLUXER_POSTGRES_AUTOVACUUM_VACUUM_COST_LIMIT=2000
#FLUXER_POSTGRES_TRACK_IO_TIMING=on
#FLUXER_POSTGRES_SHARED_PRELOAD_LIBRARIES=pg_stat_statements
# The bundled Valkey holds durable state as well as cache. The bulk message
# deletion queue and the account deletion queue are sorted sets with no expiry,
# and nothing else stores the first of the two. It therefore runs with an
# append-only file on a named volume and with noeviction, so an over-limit write
# fails loudly instead of silently deleting queued work. Distributed locks all
# carry a TTL and are not what the durability is for. Only change the policy if
# you have moved that durable state elsewhere.
# Postgres pool size of each service that opens a pool.
#FLUXER_API_POSTGRES_MAX_CONNECTIONS=25
#FLUXER_WORKER_POSTGRES_MAX_CONNECTIONS=25
#FLUXER_USERS_SHARD_POSTGRES_MAX_CONNECTIONS=20
#FLUXER_MESSAGES_SHARD_POSTGRES_MAX_CONNECTIONS=20
# The bundled Valkey holds durable state as well as cache, so it runs with an
# append-only file and with noeviction, which fails an over-limit write instead
# of dropping queued work. Change the policy only if that state lives elsewhere.
#FLUXER_VALKEY_MAXMEMORY=192mb
#FLUXER_VALKEY_MAXMEMORY_POLICY=noeviction
#FLUXER_VALKEY_APPENDFSYNC=everysec
# The gateway derives its BEAM scheduler count from the container CPU quota,
# clamped to this range. The floor matters: a single scheduler lets one blocking
# operation stall every websocket on the node. The ceiling stops a large host
# from starting far more schedulers than the container can actually use.
# The gateway derives its scheduler count from the CPU quota, clamped here. One
# scheduler lets a single blocking operation stall every websocket on the node.
#FLUXER_ERLANG_SCHEDULERS_MIN=2
#FLUXER_ERLANG_SCHEDULERS_MAX=16
# A fixed scheduler count skips the clamp. Dirty CPU schedulers default to two
# thirds of it.
#FLUXER_ERLANG_SCHEDULERS=
#FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS=
# In-flight request ceiling for the services Compose forwards it to: the users
# and messages routers and their shards. Leave it unset and each service uses its
# built-in default. Set it and the one value replaces that default on all of
# them, so size it for the busiest. The built-in defaults are 192 for
# messages, 320 for snowflakes and 64 elsewhere, and they govern every service
# Compose does not forward this to. A router holds a slot for the whole round
# trip to its shard, so this is a ceiling on requests in flight at once and not a
# rate: too low a value does not slow requests down, it rejects them, and the api
# turns that rejection into a 503.
#FLUXER_SVC_MAX_CONCURRENT_REQUESTS=192
# Gateway push and RPC tuning.
#FLUXER_GATEWAY_PUSH_ENABLED=true
#FLUXER_GATEWAY_PUSH_ENROLLED_CLEAR_NOTIFICATIONS_ENABLED=true
#FLUXER_GATEWAY_PUSH_OUTBOX_REQUEST_TIMEOUT_MS=100000
#FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES=128
#FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES=1048576
#FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY=512
#FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS=512
#FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD=6
#FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS=15000
# The api and the Rust services name their fixed Postgres statement shapes so the
# server can reuse their plans. Named prepared statements require a session that
# outlives the transaction, so set this to false if you put a transaction-pooling
# connection pooler such as PgBouncer in front of Postgres. One setting governs
# every service. The bundled compose talks to Postgres directly, where naming is
# a win and the default is correct.
# In-flight request ceiling for every svc router and shard. Unset, each keeps its
# own default: 192 for messages, 320 for snowflakes and 64 for the rest. One value
# replaces all of them, so size it for the busiest. Too low a value rejects
# requests rather than slowing them, and the api turns that into a 503. The value
# below is an example.
#FLUXER_SVC_MAX_CONCURRENT_REQUESTS=320
# svc caches, and how the api calls the svc services over NATS.
#FLUXER_SVC_CACHE_MAX_ENTRIES=100000
#FLUXER_SVC_CACHE_TTL_MS=30000
#FLUXER_GIFS_SHARD_CACHE_MAX_BYTES=536870912
#FLUXER_GIF_SERVICE_NATS_CLIENT_NAME=fluxer-api-gifs
#FLUXER_GIF_SERVICE_TIMEOUT_MS=12000
#FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS=3000
#FLUXER_USERS_SERVICE_NATS_CLIENT_NAME=fluxer-api-users
#FLUXER_USERS_SERVICE_TIMEOUT_MS=6000
#FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES=10000
#FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME=fluxer-api-snowflakes
#FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE=128
#FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK=
#FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS=5000
#FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS=6000
# Worker concurrency per lane, as a JSON object keyed by lane.
#FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES=
# Named prepared statements need a session that outlives the transaction, so set
# this to false behind a transaction-pooling connection pooler. The bundled
# compose talks to Postgres directly, where the default is correct.
#FLUXER_POSTGRES_PREPARED_STATEMENTS=true
# The api bounds how long a client may take to send a request. The header timeout
# covers the request line and headers only, while the request timeout covers the
# whole exchange, so a slow uploader is bounded by the second value and not by
# the first. Raise both if you front large uploads or serve clients on high
# latency links. The header timeout is clamped down to the request timeout, so
# raising it alone does nothing. Both are milliseconds, between 1000 and 3600000.
# How long a client may take to send a request. The header timeout covers the
# request line and headers, the request timeout the whole exchange, and the first
# is clamped down to the second. Milliseconds, 1000 to 3600000.
#FLUXER_API_HEADERS_TIMEOUT_MS=30000
#FLUXER_API_REQUEST_TIMEOUT_MS=120000
# api request limits and IP bans. A refresh interval of 0 stops the periodic
# ban reload.
#FLUXER_API_MAX_INFLIGHT_REQUESTS=512
#FLUXER_API_IP_BAN_EXEMPT_IPS=
#FLUXER_IP_BAN_REFRESH_INTERVAL_MS=300000
# Uploads and data exports. Presigned exports link to FLUXER_S3_PUBLIC_ENDPOINT,
# so turn them on only once browsers can reach it.
#FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED=true
#FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED=false
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES=524288000
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS=900
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES=
#FLUXER_API_STORAGE_CHANGE_FEED_ENABLED=false
#FLUXER_API_STORAGE_CHANGE_FEED_STREAM=STORAGE_CHANGES
#FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS=
#FLUXER_CACHE_PURGE_ADAPTER=none
#FLUXER_CACHE_PURGE_HTTP_ENDPOINT=
#FLUXER_CACHE_PURGE_HTTP_TOKEN=
#FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS=10000
# media-proxy limits and timeouts.
#FLUXER_MEDIA_PROXY_READ_ONLY=false
#FLUXER_MEDIA_PROXY_NSFW_THRESHOLD=0.85
#FLUXER_NSFW_SERVICE_ENDPOINT=
#FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS=
#FLUXER_MEDIA_PROXY_WORKER_QUEUE_CAPACITY=
#FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS=30000
#FLUXER_MEDIA_PROXY_MAX_ENCODE_FRAMES=20000
#FLUXER_MEDIA_PROXY_TRANSFORM_TIMEOUT_MS=15000
#FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_BYTES=268435456
#FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES=67108864
#FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_TTL_MS=120000
#FLUXER_MEDIA_PROXY_SOCKET_IO_TIMEOUT_MS=30000
#FLUXER_MEDIA_PROXY_SHUTDOWN_GRACE_MS=30000
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_S3_TIMEOUT_MS=900000
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES=33554432
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES=536870912
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_DIR=
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_CHUNK_BYTES=1048576
#FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES=8589934592
# app-proxy discovery refresh, index upstream and manifest scope.
#DISCOVERY_REFRESH_INTERVAL_MS=60000
#FLUXER_APP_PROXY_INDEX_UPSTREAM_URL=
#FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS=
#FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS=
+3 -3
View File
@@ -6,7 +6,7 @@
}
{$FLUXER_EDGE_SITE_ADDRESS} {
encode zstd gzip
encode {$FLUXER_EDGE_ENCODE:zstd gzip}
handle /_health {
respond "OK" 200
@@ -33,12 +33,12 @@
reverse_proxy livekit:7880
}
handle /admin {
handle {$FLUXER_ADMIN_BASE_PATH:/admin} {
rewrite * /
reverse_proxy admin:8080
}
handle_path /admin/* {
handle_path {$FLUXER_ADMIN_BASE_PATH:/admin}/* {
reverse_proxy admin:8080
}
+329 -179
View File
@@ -3,38 +3,82 @@ name: fluxer
x-fluxer-postgres-env: &fluxer-postgres-env
FLUXER_DATABASE_BACKEND: postgres
FLUXER_POSTGRES_HOST: ${FLUXER_POSTGRES_HOST:-postgres}
FLUXER_POSTGRES_PORT: "${FLUXER_POSTGRES_PORT:-5432}"
FLUXER_POSTGRES_DATABASE: ${FLUXER_POSTGRES_DATABASE:-fluxer}
FLUXER_POSTGRES_USERNAME: ${FLUXER_POSTGRES_USERNAME:-fluxer}
FLUXER_POSTGRES_PORT: ${FLUXER_POSTGRES_PORT:-}
FLUXER_POSTGRES_DATABASE: ${FLUXER_POSTGRES_DATABASE:-}
FLUXER_POSTGRES_USERNAME: ${FLUXER_POSTGRES_USERNAME:-}
FLUXER_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
FLUXER_POSTGRES_SSL: "${FLUXER_POSTGRES_SSL:-false}"
FLUXER_POSTGRES_PREPARED_STATEMENTS: ${FLUXER_POSTGRES_PREPARED_STATEMENTS:-true}
FLUXER_POSTGRES_URL: ${FLUXER_POSTGRES_URL:-}
FLUXER_POSTGRES_SSL: ${FLUXER_POSTGRES_SSL:-}
FLUXER_POSTGRES_SSL_CA: ${FLUXER_POSTGRES_SSL_CA:-}
FLUXER_POSTGRES_PREPARED_STATEMENTS: ${FLUXER_POSTGRES_PREPARED_STATEMENTS:-}
FLUXER_POSTGRES_KV_TABLE: ${FLUXER_POSTGRES_KV_TABLE:-}
x-fluxer-env: &fluxer-env
<<: *fluxer-postgres-env
FLUXER_ENV: production
NODE_ENV: production
LOG_LEVEL: ${LOG_LEVEL:-info}
LOG_LEVEL: ${LOG_LEVEL:-}
RUST_LOG: ${RUST_LOG:-}
FLUXER_SELF_HOSTED: "true"
FLUXER_BASE_DOMAIN: ${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}
FLUXER_PUBLIC_SCHEME: ${FLUXER_PUBLIC_SCHEME:-https}
FLUXER_PUBLIC_PORT: ${FLUXER_PUBLIC_PORT:-443}
FLUXER_PUBLIC_ORIGIN: ${FLUXER_PUBLIC_ORIGIN:-}
FLUXER_TRUST_CLIENT_IP_HEADER: "${FLUXER_TRUST_CLIENT_IP_HEADER:-true}"
FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-x-forwarded-for}
FLUXER_API_HEADERS_TIMEOUT_MS: ${FLUXER_API_HEADERS_TIMEOUT_MS:-30000}
FLUXER_API_REQUEST_TIMEOUT_MS: ${FLUXER_API_REQUEST_TIMEOUT_MS:-120000}
FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-}
FLUXER_API_HEADERS_TIMEOUT_MS: ${FLUXER_API_HEADERS_TIMEOUT_MS:-}
FLUXER_API_REQUEST_TIMEOUT_MS: ${FLUXER_API_REQUEST_TIMEOUT_MS:-}
FLUXER_API_MAX_INFLIGHT_REQUESTS: ${FLUXER_API_MAX_INFLIGHT_REQUESTS:-}
FLUXER_API_IP_BAN_EXEMPT_IPS: ${FLUXER_API_IP_BAN_EXEMPT_IPS:-}
FLUXER_IP_BAN_REFRESH_INTERVAL_MS: ${FLUXER_IP_BAN_REFRESH_INTERVAL_MS:-}
FLUXER_APP_ORIGIN_ALIASES: ${FLUXER_APP_ORIGIN_ALIASES:-}
FLUXER_BREACHED_PASSWORD_CHECK_ENABLED: ${FLUXER_BREACHED_PASSWORD_CHECK_ENABLED:-}
FLUXER_BLOCKLIST_FEEDS_ENABLED: ${FLUXER_BLOCKLIST_FEEDS_ENABLED:-}
FLUXER_PHONE_VERIFICATION_ENABLED: ${FLUXER_PHONE_VERIFICATION_ENABLED:-}
FLUXER_GEOIP_DB_PATH: ${FLUXER_GEOIP_DB_PATH:-}
FLUXER_API_ENDPOINT: ${FLUXER_API_ENDPOINT:-}
FLUXER_API_CLIENT_ENDPOINT: ${FLUXER_API_CLIENT_ENDPOINT:-}
FLUXER_APP_ENDPOINT: ${FLUXER_APP_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
FLUXER_GATEWAY_ENDPOINT: ${FLUXER_GATEWAY_ENDPOINT:-}
FLUXER_MEDIA_ENDPOINT: ${FLUXER_MEDIA_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media}
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT:-${FLUXER_MEDIA_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media}}
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_STATIC_CDN_ENDPOINT:-}
FLUXER_ADMIN_ENDPOINT: ${FLUXER_ADMIN_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}${FLUXER_ADMIN_BASE_PATH:-/admin}}
FLUXER_MARKETING_ENDPOINT: ${FLUXER_MARKETING_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
FLUXER_INVITE_ENDPOINT: ${FLUXER_INVITE_ENDPOINT:-}
FLUXER_GIFT_ENDPOINT: ${FLUXER_GIFT_ENDPOINT:-}
FLUXER_STATIC_CDN_DOMAIN: ${FLUXER_STATIC_CDN_DOMAIN:-}
FLUXER_INVITE_DOMAIN: ${FLUXER_INVITE_DOMAIN:-}
FLUXER_GIFT_DOMAIN: ${FLUXER_GIFT_DOMAIN:-}
FLUXER_KV_URL: ${FLUXER_KV_URL:-redis://valkey:6379/0}
FLUXER_KV_MODE: ${FLUXER_KV_MODE:-}
FLUXER_NATS_URL: ${FLUXER_NATS_URL:-nats://nats:4222}
FLUXER_NATS_JETSTREAM_URL: ${FLUXER_NATS_JETSTREAM_URL:-${FLUXER_NATS_URL:-nats://nats:4222}}
FLUXER_NATS_AUTH_TOKEN: ${FLUXER_NATS_AUTH_TOKEN:-}
FLUXER_SVC_NATS_URL: ${FLUXER_SVC_NATS_URL:-${FLUXER_NATS_URL:-nats://nats:4222}}
FLUXER_SVC_SHARD_COUNT: "1"
FLUXER_SVC_MAX_CONCURRENT_REQUESTS: ${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}
FLUXER_SVC_CACHE_MAX_ENTRIES: ${FLUXER_SVC_CACHE_MAX_ENTRIES:-}
FLUXER_SVC_CACHE_TTL_MS: ${FLUXER_SVC_CACHE_TTL_MS:-}
FLUXER_GIF_SERVICE_NATS_CLIENT_NAME: ${FLUXER_GIF_SERVICE_NATS_CLIENT_NAME:-}
FLUXER_GIF_SERVICE_TIMEOUT_MS: ${FLUXER_GIF_SERVICE_TIMEOUT_MS:-}
FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS: ${FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS:-}
FLUXER_USERS_SERVICE_NATS_CLIENT_NAME: ${FLUXER_USERS_SERVICE_NATS_CLIENT_NAME:-}
FLUXER_USERS_SERVICE_TIMEOUT_MS: ${FLUXER_USERS_SERVICE_TIMEOUT_MS:-}
FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES: ${FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES:-}
FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME: ${FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME:-}
FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE: ${FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE:-}
FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK: ${FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK:-}
FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS: ${FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS:-}
FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS: ${FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS:-}
FLUXER_SEARCH_ENGINE: meilisearch
FLUXER_SEARCH_ENGINE: ${FLUXER_SEARCH_ENGINE:-meilisearch}
FLUXER_SEARCH_URL: ${FLUXER_SEARCH_URL:-http://meilisearch:7700}
FLUXER_SEARCH_API_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
FLUXER_SEARCH_USERNAME: ${FLUXER_SEARCH_USERNAME:-}
FLUXER_SEARCH_PASSWORD: ${FLUXER_SEARCH_PASSWORD:-}
FLUXER_SEARCH_TLS_REJECT_UNAUTHORIZED: ${FLUXER_SEARCH_TLS_REJECT_UNAUTHORIZED:-}
FLUXER_S3_ENDPOINT: ${FLUXER_S3_ENDPOINT:-http://seaweedfs:8333}
FLUXER_S3_PUBLIC_ENDPOINT: ${FLUXER_S3_PUBLIC_ENDPOINT:-${FLUXER_S3_ENDPOINT:-http://seaweedfs:8333}}
@@ -44,91 +88,137 @@ x-fluxer-env: &fluxer-env
FLUXER_S3_FORCE_PATH_STYLE: "${FLUXER_S3_FORCE_PATH_STYLE:-true}"
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
AWS_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
AWS_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
AWS_DEFAULT_REGION: ${FLUXER_S3_REGION:-us-east-1}
AWS_EC2_METADATA_DISABLED: "true"
FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED: "${FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED:-false}"
FLUXER_API_STORAGE_CHANGE_FEED_ENABLED: ${FLUXER_API_STORAGE_CHANGE_FEED_ENABLED:-}
FLUXER_API_STORAGE_CHANGE_FEED_STREAM: ${FLUXER_API_STORAGE_CHANGE_FEED_STREAM:-}
FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS: ${FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS:-}
FLUXER_CACHE_PURGE_ADAPTER: ${FLUXER_CACHE_PURGE_ADAPTER:-}
FLUXER_CACHE_PURGE_HTTP_ENDPOINT: ${FLUXER_CACHE_PURGE_HTTP_ENDPOINT:-}
FLUXER_CACHE_PURGE_HTTP_TOKEN: ${FLUXER_CACHE_PURGE_HTTP_TOKEN:-}
FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS: ${FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS:-}
FLUXER_LIVEKIT_ENABLED: "${FLUXER_LIVEKIT_ENABLED:-true}"
FLUXER_LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}
FLUXER_LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?set LIVEKIT_API_SECRET in .env}
FLUXER_LIVEKIT_INTERNAL_URL: ${FLUXER_LIVEKIT_INTERNAL_URL:-http://livekit:7880}
FLUXER_LIVEKIT_WEBHOOK_URL: http://api:8080/webhooks/livekit
FLUXER_LIVEKIT_DEFAULT_REGION: '{"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0}'
FLUXER_LIVEKIT_DEFAULT_REGION: '${FLUXER_LIVEKIT_DEFAULT_REGION:-{"id":"default","name":"Default","emoji":"🌍","latitude":0,"longitude":0}}'
FLUXER_LIVEKIT_URL: ${FLUXER_LIVEKIT_URL:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}/livekit}
FLUXER_KLIPY_API_KEY: ${FLUXER_KLIPY_API_KEY:-}
FLUXER_YOUTUBE_API_KEY: ${FLUXER_YOUTUBE_API_KEY:-}
FLUXER_API_UNFURL_IGNORED_HOSTS: ${FLUXER_API_UNFURL_IGNORED_HOSTS:-}
FLUXER_EMAIL_ENABLED: ${FLUXER_EMAIL_ENABLED:-false}
FLUXER_EMAIL_PROVIDER: ${FLUXER_EMAIL_PROVIDER:-none}
FLUXER_EMAIL_ENABLED: ${FLUXER_EMAIL_ENABLED:-}
FLUXER_EMAIL_PROVIDER: ${FLUXER_EMAIL_PROVIDER:-}
FLUXER_EMAIL_FROM_EMAIL: ${FLUXER_EMAIL_FROM_EMAIL:-noreply@localhost}
FLUXER_EMAIL_FROM_NAME: ${FLUXER_EMAIL_FROM_NAME:-Fluxer}
FLUXER_EMAIL_FROM_NAME: ${FLUXER_EMAIL_FROM_NAME:-}
FLUXER_EMAIL_APP_BASE_URL: ${FLUXER_EMAIL_APP_BASE_URL:-}
FLUXER_EMAIL_WEBHOOK_SECRET: ${FLUXER_EMAIL_WEBHOOK_SECRET:-}
FLUXER_EMAIL_SMTP_HOST: ${FLUXER_EMAIL_SMTP_HOST:-}
FLUXER_EMAIL_SMTP_PORT: ${FLUXER_EMAIL_SMTP_PORT:-587}
FLUXER_EMAIL_SMTP_PORT: ${FLUXER_EMAIL_SMTP_PORT:-}
FLUXER_EMAIL_SMTP_USERNAME: ${FLUXER_EMAIL_SMTP_USERNAME:-}
FLUXER_EMAIL_SMTP_PASSWORD: ${FLUXER_EMAIL_SMTP_PASSWORD:-}
FLUXER_EMAIL_SMTP_SECURE: ${FLUXER_EMAIL_SMTP_SECURE:-true}
FLUXER_EMAIL_SMTP_SECURE: ${FLUXER_EMAIL_SMTP_SECURE:-}
FLUXER_SMS_ENABLED: "${FLUXER_SMS_ENABLED:-false}"
FLUXER_CAPTCHA_ENABLED: ${FLUXER_CAPTCHA_ENABLED:-false}
FLUXER_CAPTCHA_PROVIDER: ${FLUXER_CAPTCHA_PROVIDER:-none}
FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY: ${FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY:-}
FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY: ${FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY:-}
FLUXER_CAPTCHA_TURNSTILE_SITE_KEY: ${FLUXER_CAPTCHA_TURNSTILE_SITE_KEY:-}
FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY: ${FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY:-}
FLUXER_STRIPE_ENABLED: "${FLUXER_STRIPE_ENABLED:-false}"
FLUXER_NCMEC_ENABLED: "${FLUXER_NCMEC_ENABLED:-false}"
FLUXER_CLAMAV_ENABLED: "${FLUXER_CLAMAV_ENABLED:-false}"
FLUXER_DISCOVERY_ENABLED: ${FLUXER_DISCOVERY_ENABLED:-true}
FLUXER_STRIPE_ENABLED: ${FLUXER_STRIPE_ENABLED:-}
FLUXER_STRIPE_SECRET_KEY: ${FLUXER_STRIPE_SECRET_KEY:-}
FLUXER_STRIPE_WEBHOOK_SECRET: ${FLUXER_STRIPE_WEBHOOK_SECRET:-}
FLUXER_STRIPE_PRICES: ${FLUXER_STRIPE_PRICES:-}
FLUXER_STRIPE_LEGACY_PRICES: ${FLUXER_STRIPE_LEGACY_PRICES:-}
FLUXER_API_DONATION_PROXY_KEY: ${FLUXER_API_DONATION_PROXY_KEY:-}
FLUXER_VISIONARIES_GUILD_ID: ${FLUXER_VISIONARIES_GUILD_ID:-}
FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID: ${FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID:-}
FLUXER_NCMEC_ENABLED: ${FLUXER_NCMEC_ENABLED:-}
FLUXER_NCMEC_BASE_URL: ${FLUXER_NCMEC_BASE_URL:-}
FLUXER_NCMEC_USERNAME: ${FLUXER_NCMEC_USERNAME:-}
FLUXER_NCMEC_PASSWORD: ${FLUXER_NCMEC_PASSWORD:-}
FLUXER_NCMEC_REPORTER_EMAIL: ${FLUXER_NCMEC_REPORTER_EMAIL:-}
FLUXER_CLAMAV_ENABLED: ${FLUXER_CLAMAV_ENABLED:-}
FLUXER_CLAMAV_HOST: ${FLUXER_CLAMAV_HOST:-}
FLUXER_CLAMAV_PORT: ${FLUXER_CLAMAV_PORT:-}
FLUXER_CLAMAV_FAIL_OPEN: ${FLUXER_CLAMAV_FAIL_OPEN:-}
FLUXER_APP_PRODUCT_NAME: ${FLUXER_APP_PRODUCT_NAME:-}
FLUXER_APP_ICON_URL: ${FLUXER_APP_ICON_URL:-}
FLUXER_APP_SYMBOL_URL: ${FLUXER_APP_SYMBOL_URL:-}
FLUXER_APP_LOGO_URL: ${FLUXER_APP_LOGO_URL:-}
FLUXER_APP_WORDMARK_URL: ${FLUXER_APP_WORDMARK_URL:-}
FLUXER_APP_FAVICON_URL: ${FLUXER_APP_FAVICON_URL:-}
FLUXER_APP_THEME_COLOR: ${FLUXER_APP_THEME_COLOR:-}
FLUXER_APP_STATUS_PAGE_URL: ${FLUXER_APP_STATUS_PAGE_URL:-}
FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: ${FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL:-}
FLUXER_INSTANCE_SETUP_CONFIGURED: ${FLUXER_INSTANCE_SETUP_CONFIGURED:-}
FLUXER_AUTO_JOIN_INVITE_CODE: ${FLUXER_AUTO_JOIN_INVITE_CODE:-}
FLUXER_DISCOVERY_ENABLED: ${FLUXER_DISCOVERY_ENABLED:-}
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: ${FLUXER_DISCOVERY_MIN_MEMBER_COUNT:-}
FLUXER_DELETION_GRACE_PERIOD_HOURS: ${FLUXER_DELETION_GRACE_PERIOD_HOURS:-}
FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES: ${FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES:-}
FLUXER_AUTH_BLUESKY_ENABLED: ${FLUXER_AUTH_BLUESKY_ENABLED:-}
FLUXER_AUTH_BLUESKY_CLIENT_NAME: ${FLUXER_AUTH_BLUESKY_CLIENT_NAME:-}
FLUXER_AUTH_BLUESKY_CLIENT_URI: ${FLUXER_AUTH_BLUESKY_CLIENT_URI:-}
FLUXER_AUTH_BLUESKY_LOGO_URI: ${FLUXER_AUTH_BLUESKY_LOGO_URI:-}
FLUXER_AUTH_BLUESKY_TOS_URI: ${FLUXER_AUTH_BLUESKY_TOS_URI:-}
FLUXER_AUTH_BLUESKY_POLICY_URI: ${FLUXER_AUTH_BLUESKY_POLICY_URI:-}
FLUXER_AUTH_BLUESKY_KEYS: ${FLUXER_AUTH_BLUESKY_KEYS:-}
FLUXER_PUSH_APNS_ENABLED: ${FLUXER_PUSH_APNS_ENABLED:-}
FLUXER_PUSH_APNS_TEAM_ID: ${FLUXER_PUSH_APNS_TEAM_ID:-}
FLUXER_PUSH_APNS_KEY_ID: ${FLUXER_PUSH_APNS_KEY_ID:-}
FLUXER_PUSH_APNS_PRIVATE_KEY: ${FLUXER_PUSH_APNS_PRIVATE_KEY:-}
FLUXER_PUSH_APNS_PRIVATE_KEY_PATH: ${FLUXER_PUSH_APNS_PRIVATE_KEY_PATH:-}
FLUXER_PUSH_APNS_APPS: ${FLUXER_PUSH_APNS_APPS:-}
FLUXER_SUDO_MODE_SECRET: ${FLUXER_SUDO_MODE_SECRET:?set FLUXER_SUDO_MODE_SECRET in .env}
FLUXER_CONNECTION_INITIATION_SECRET: ${FLUXER_CONNECTION_INITIATION_SECRET:?set FLUXER_CONNECTION_INITIATION_SECRET in .env}
FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES: ${FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES:-false}
FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES: ${FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES:-}
FLUXER_VAPID_PUBLIC_KEY: ${FLUXER_VAPID_PUBLIC_KEY:?set FLUXER_VAPID_PUBLIC_KEY in .env}
FLUXER_VAPID_PRIVATE_KEY: ${FLUXER_VAPID_PRIVATE_KEY:?set FLUXER_VAPID_PRIVATE_KEY in .env}
FLUXER_VAPID_EMAIL: ${FLUXER_VAPID_EMAIL:-admin@${FLUXER_DOMAIN}}
FLUXER_PASSKEY_RP_ID: ${FLUXER_PASSKEY_RP_ID:-${FLUXER_DOMAIN}}
FLUXER_PASSKEY_RP_NAME: ${FLUXER_PASSKEY_RP_NAME:-Fluxer}
FLUXER_PASSKEY_RP_NAME: ${FLUXER_PASSKEY_RP_NAME:-}
FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: ${FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
FLUXER_GATEWAY_RPC_AUTH_TOKEN: ${FLUXER_GATEWAY_RPC_AUTH_TOKEN:?set FLUXER_GATEWAY_RPC_AUTH_TOKEN in .env}
FLUXER_MEDIA_PROXY_SECRET_KEY: ${FLUXER_MEDIA_PROXY_SECRET_KEY:?set FLUXER_MEDIA_PROXY_SECRET_KEY in .env}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64:?set FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 in .env}
FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64: ${FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64:-}
FLUXER_ADMIN_SECRET_KEY_BASE: ${FLUXER_ADMIN_SECRET_KEY_BASE:?set FLUXER_ADMIN_SECRET_KEY_BASE in .env}
FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ${FLUXER_ADMIN_OAUTH_CLIENT_SECRET:?set FLUXER_ADMIN_OAUTH_CLIENT_SECRET in .env}
FLUXER_INTERNAL_API_ENDPOINT: http://api:8080
FLUXER_INTERNAL_GATEWAY_ENDPOINT: http://gateway:8080
FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
FLUXER_MARKETING_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES:-}
x-fluxer-service: &fluxer-service
restart: unless-stopped
restart: ${FLUXER_RESTART_POLICY:-unless-stopped}
networks: [fluxer]
x-fluxer-svc-healthcheck: &fluxer-svc-healthcheck
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8090 && printf 'GET /_health HTTP/1.0\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
interval: 10s
timeout: 5s
retries: 30
start_period: 60s
x-fluxer-app-healthcheck: &fluxer-app-healthcheck
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_APP_HEALTHCHECK_RETRIES:-30}
start_period: ${FLUXER_APP_HEALTHCHECK_START_PERIOD:-90s}
start_interval: 1s
x-fluxer-svc-healthcheck: &fluxer-svc-healthcheck
<<: *fluxer-app-healthcheck
start_period: ${FLUXER_SVC_HEALTHCHECK_START_PERIOD:-60s}
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8090 && printf 'GET /_health HTTP/1.0\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
services:
edge:
image: caddy:2.10-alpine
<<: *fluxer-service
image: ${FLUXER_CADDY_IMAGE:-caddy:2.11-alpine}
deploy:
resources:
limits:
memory: ${FLUXER_CADDY_MEMORY_LIMIT:-256mb}
restart: unless-stopped
networks: [fluxer]
ports:
- "${FLUXER_HTTP_PORT:-80}:80"
- "${FLUXER_HTTPS_PORT:-443}:443"
@@ -136,15 +226,17 @@ services:
environment:
FLUXER_EDGE_SITE_ADDRESS: ${FLUXER_EDGE_SITE_ADDRESS:-${FLUXER_CADDY_SITE_ADDRESS:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}}}
FLUXER_EDGE_TRUSTED_PROXIES: ${FLUXER_EDGE_TRUSTED_PROXIES:-private_ranges}
FLUXER_EDGE_ENCODE: ${FLUXER_EDGE_ENCODE:-zstd gzip}
FLUXER_ADMIN_BASE_PATH: ${FLUXER_ADMIN_BASE_PATH:-/admin}
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- edge-data:/data
- edge-config:/config
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
depends_on:
api: {condition: service_started}
gateway: {condition: service_healthy}
@@ -153,15 +245,14 @@ services:
admin: {condition: service_started}
postgres:
image: postgres:16-alpine
<<: *fluxer-service
image: ${FLUXER_POSTGRES_IMAGE:-postgres:16-alpine}
deploy:
resources:
limits:
memory: ${FLUXER_POSTGRES_MEMORY_LIMIT:-5gb}
reservations:
memory: ${FLUXER_POSTGRES_MEMORY_RESERVATION:-3gb}
restart: unless-stopped
networks: [fluxer]
command: >
postgres
-c max_connections=${FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS:-150}
@@ -170,114 +261,112 @@ services:
-c work_mem=${FLUXER_POSTGRES_WORK_MEM:-8MB}
-c maintenance_work_mem=${FLUXER_POSTGRES_MAINTENANCE_WORK_MEM:-256MB}
-c autovacuum_work_mem=${FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM:-128MB}
-c random_page_cost=1.1
-c effective_io_concurrency=200
-c default_statistics_target=200
-c jit=off
-c min_wal_size=512MB
-c max_wal_size=2GB
-c checkpoint_completion_target=0.9
-c wal_buffers=16MB
-c wal_compression=zstd
-c bgwriter_delay=50ms
-c bgwriter_lru_maxpages=1000
-c autovacuum_vacuum_scale_factor=0.05
-c autovacuum_analyze_scale_factor=0.02
-c autovacuum_vacuum_cost_limit=2000
-c track_io_timing=on
-c shared_preload_libraries=pg_stat_statements
shm_size: 256mb
-c random_page_cost=${FLUXER_POSTGRES_RANDOM_PAGE_COST:-1.1}
-c effective_io_concurrency=${FLUXER_POSTGRES_EFFECTIVE_IO_CONCURRENCY:-200}
-c default_statistics_target=${FLUXER_POSTGRES_DEFAULT_STATISTICS_TARGET:-200}
-c jit=${FLUXER_POSTGRES_JIT:-off}
-c min_wal_size=${FLUXER_POSTGRES_MIN_WAL_SIZE:-512MB}
-c max_wal_size=${FLUXER_POSTGRES_MAX_WAL_SIZE:-2GB}
-c checkpoint_completion_target=${FLUXER_POSTGRES_CHECKPOINT_COMPLETION_TARGET:-0.9}
-c wal_buffers=${FLUXER_POSTGRES_WAL_BUFFERS:-16MB}
-c wal_compression=${FLUXER_POSTGRES_WAL_COMPRESSION:-zstd}
-c bgwriter_delay=${FLUXER_POSTGRES_BGWRITER_DELAY:-50ms}
-c bgwriter_lru_maxpages=${FLUXER_POSTGRES_BGWRITER_LRU_MAXPAGES:-1000}
-c autovacuum_vacuum_scale_factor=${FLUXER_POSTGRES_AUTOVACUUM_VACUUM_SCALE_FACTOR:-0.05}
-c autovacuum_analyze_scale_factor=${FLUXER_POSTGRES_AUTOVACUUM_ANALYZE_SCALE_FACTOR:-0.02}
-c autovacuum_vacuum_cost_limit=${FLUXER_POSTGRES_AUTOVACUUM_VACUUM_COST_LIMIT:-2000}
-c track_io_timing=${FLUXER_POSTGRES_TRACK_IO_TIMING:-on}
-c shared_preload_libraries=${FLUXER_POSTGRES_SHARED_PRELOAD_LIBRARIES:-pg_stat_statements}
shm_size: ${FLUXER_POSTGRES_SHM_SIZE:-1gb}
environment:
POSTGRES_DB: fluxer
POSTGRES_USER: fluxer
POSTGRES_DB: ${FLUXER_POSTGRES_DATABASE:-fluxer}
POSTGRES_USER: ${FLUXER_POSTGRES_USERNAME:-fluxer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fluxer -d fluxer"]
interval: 10s
timeout: 5s
retries: 10
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
valkey:
image: valkey/valkey:8.1-alpine
<<: *fluxer-service
image: ${FLUXER_VALKEY_IMAGE:-valkey/valkey:9.1-alpine}
deploy:
resources:
limits:
memory: ${FLUXER_VALKEY_MEMORY_LIMIT:-256mb}
restart: unless-stopped
networks: [fluxer]
command: ["valkey-server", "--appendonly", "yes", "--appendfsync", "everysec", "--dir", "/data",
command: ["valkey-server", "--appendonly", "yes", "--appendfsync", "${FLUXER_VALKEY_APPENDFSYNC:-everysec}", "--dir", "/data",
"--maxmemory", "${FLUXER_VALKEY_MAXMEMORY:-192mb}",
"--maxmemory-policy", "${FLUXER_VALKEY_MAXMEMORY_POLICY:-noeviction}"]
volumes:
- valkey-data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
nats:
image: nats:2.14-alpine
<<: *fluxer-service
image: ${FLUXER_NATS_IMAGE:-nats:2.14-alpine}
deploy:
resources:
limits:
memory: ${FLUXER_NATS_MEMORY_LIMIT:-256mb}
restart: unless-stopped
networks: [fluxer]
command: ["-js", "-sd", "/data", "-m", "8222"]
volumes:
- nats-data:/data
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8222/healthz"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
meilisearch:
image: getmeili/meilisearch:v1.12
<<: *fluxer-service
image: ${FLUXER_MEILISEARCH_IMAGE:-getmeili/meilisearch:v1.53}
deploy:
resources:
limits:
memory: ${FLUXER_MEILISEARCH_MEMORY_LIMIT:-768mb}
restart: unless-stopped
networks: [fluxer]
environment:
MEILI_ENV: production
MEILI_NO_ANALYTICS: "true"
MEILI_ENV: ${FLUXER_MEILISEARCH_ENV:-production}
MEILI_NO_ANALYTICS: "${FLUXER_MEILISEARCH_NO_ANALYTICS:-true}"
MEILI_UPGRADE_DB: "true"
MEILI_MAX_INDEXING_MEMORY: ${FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY:-384mb}
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY:?set MEILI_MASTER_KEY in .env}
volumes:
- meilisearch-data:/meili_data
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:7700/health"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
seaweedfs:
image: chrislusf/seaweedfs:4.34
<<: *fluxer-service
image: ${FLUXER_SEAWEEDFS_IMAGE:-chrislusf/seaweedfs:4.47}
deploy:
resources:
limits:
memory: ${FLUXER_SEAWEEDFS_MEMORY_LIMIT:-2gb}
restart: unless-stopped
networks: [fluxer]
environment:
GOMEMLIMIT: ${FLUXER_SEAWEEDFS_GOMEMLIMIT:-1536MiB}
command: ["server", "-s3", "-dir=/data"]
WEED_MASTER_VOLUME_GROWTH_COPY_1: ${FLUXER_SEAWEEDFS_VOLUME_GROWTH:-1}
command: ["server", "-s3", "-dir=/data", "-master.telemetry=${FLUXER_SEAWEEDFS_TELEMETRY:-false}"]
volumes:
- seaweedfs-data:/data
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8333/healthz"]
interval: 10s
timeout: 5s
retries: 20
start_period: 60s
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_SEAWEEDFS_HEALTHCHECK_RETRIES:-20}
start_period: ${FLUXER_SEAWEEDFS_HEALTHCHECK_START_PERIOD:-60s}
seaweedfs-init:
image: chrislusf/seaweedfs:4.34
image: ${FLUXER_SEAWEEDFS_IMAGE:-chrislusf/seaweedfs:4.47}
deploy:
resources:
limits:
@@ -291,16 +380,16 @@ services:
FLUXER_S3_SECRET_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
FLUXER_S3_BUCKET_CDN: ${FLUXER_S3_BUCKET_CDN:-fluxer}
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-fluxer-uploads}
FLUXER_S3_BUCKET_DOWNLOADS: ${FLUXER_S3_BUCKET_DOWNLOADS:-fluxer-downloads}
FLUXER_S3_BUCKET_REPORTS: ${FLUXER_S3_BUCKET_REPORTS:-fluxer-reports}
FLUXER_S3_BUCKET_HARVESTS: ${FLUXER_S3_BUCKET_HARVESTS:-fluxer-harvests}
FLUXER_SEAWEEDFS_INIT_ATTEMPTS: ${FLUXER_SEAWEEDFS_INIT_ATTEMPTS:-60}
entrypoint:
- /bin/sh
- -c
- >
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_DOWNLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
buckets="$$FLUXER_S3_BUCKET_CDN $$FLUXER_S3_BUCKET_UPLOADS $$FLUXER_S3_BUCKET_REPORTS $$FLUXER_S3_BUCKET_HARVESTS";
missing="$$buckets";
for attempt in $$(seq 1 60); do
for attempt in $$(seq 1 $$FLUXER_SEAWEEDFS_INIT_ATTEMPTS); do
if ! nc -z seaweedfs 9333 2>/dev/null; then
sleep 2;
continue;
@@ -327,18 +416,17 @@ services:
exit 1;
livekit:
image: livekit/livekit-server:v1.12.0
<<: *fluxer-service
image: ${FLUXER_LIVEKIT_IMAGE:-livekit/livekit-server:v1.12.0}
deploy:
resources:
limits:
memory: ${FLUXER_LIVEKIT_MEMORY_LIMIT:-512mb}
restart: unless-stopped
networks: [fluxer]
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:?set LIVEKIT_API_KEY in .env}: ${LIVEKIT_API_SECRET:?set LIVEKIT_API_SECRET in .env}"
LIVEKIT_CONFIG: |
port: 7880
log_level: info
log_level: ${FLUXER_LIVEKIT_LOG_LEVEL:-info}
rtc:
tcp_port: ${FLUXER_LIVEKIT_TCP_PORT:-7881}
udp_port: ${FLUXER_LIVEKIT_UDP_PORT:-7882}
@@ -356,9 +444,9 @@ services:
- "${FLUXER_LIVEKIT_UDP_PORT:-7882}:${FLUXER_LIVEKIT_UDP_PORT:-7882}/udp"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:7880/"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
api:
<<: *fluxer-service
@@ -372,16 +460,13 @@ services:
environment:
<<: *fluxer-env
FLUXER_API_PORT: "8080"
NODE_OPTIONS: --enable-source-maps${FLUXER_API_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_API_NODE_HEAP_MB}
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED: "true"
FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
NODE_OPTIONS: --enable-source-maps${FLUXER_API_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_API_NODE_HEAP_MB}${FLUXER_API_NODE_OPTIONS:+ $FLUXER_API_NODE_OPTIONS}
NODE_EXTRA_CA_CERTS: ${FLUXER_NODE_EXTRA_CA_CERTS:-/etc/ssl/certs/ca-certificates.crt}
FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_API_POSTGRES_MAX_CONNECTIONS:-25}"
FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED: "${FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED:-true}"
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:8080/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 10s
timeout: 5s
retries: 30
start_period: 90s
start_interval: 1s
<<: *fluxer-app-healthcheck
depends_on:
postgres: {condition: service_healthy}
valkey: {condition: service_healthy}
@@ -406,22 +491,18 @@ services:
memory: ${FLUXER_WORKER_MEMORY_LIMIT:-2560mb}
reservations:
memory: ${FLUXER_WORKER_MEMORY_RESERVATION:-1gb}
working_dir: /usr/src/app/fluxer_api
command: ["sh", "-c", "if [ -f dist/WorkerEntrypoint.js ]; then exec node dist/WorkerEntrypoint.js; else exec ./node_modules/.bin/tsx src/WorkerEntrypoint.ts; fi"]
command: ["node", "dist/WorkerEntrypoint.js"]
environment:
<<: *fluxer-env
NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}
NODE_OPTIONS: --enable-source-maps${FLUXER_WORKER_NODE_HEAP_MB:+ --max-old-space-size=$FLUXER_WORKER_NODE_HEAP_MB}${FLUXER_WORKER_NODE_OPTIONS:+ $FLUXER_WORKER_NODE_OPTIONS}
NODE_EXTRA_CA_CERTS: ${FLUXER_NODE_EXTRA_CA_CERTS:-/etc/ssl/certs/ca-certificates.crt}
FLUXER_API_WORKER_MODE: all_lanes
FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true"
FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION: "true"
FLUXER_POSTGRES_MAX_CONNECTIONS: "25"
FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_WORKER_POSTGRES_MAX_CONNECTIONS:-25}"
healthcheck:
test: ["CMD", "node", "-e", "const age=Date.now()-require('node:fs').statSync('/tmp/fluxer-worker-heartbeat').mtimeMs;if(age>30000){console.error('worker heartbeat is '+Math.round(age)+'ms old');process.exit(1)}"]
interval: 10s
timeout: 5s
retries: 3
start_period: 90s
start_interval: 1s
<<: *fluxer-app-healthcheck
retries: ${FLUXER_WORKER_HEALTHCHECK_RETRIES:-3}
depends_on:
postgres: {condition: service_healthy}
valkey: {condition: service_healthy}
@@ -443,18 +524,30 @@ services:
environment:
<<: *fluxer-env
FLUXER_GATEWAY_PORT: "8080"
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_GATEWAY_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
FLUXER_GATEWAY_LOGGER_LEVEL: info
FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: ${FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT:-${FLUXER_MEDIA_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media}}
FLUXER_GATEWAY_STATIC_CDN_ENDPOINT: ${FLUXER_GATEWAY_STATIC_CDN_ENDPOINT:-${FLUXER_STATIC_CDN_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}}
FLUXER_GATEWAY_LOGGER_LEVEL: ${FLUXER_GATEWAY_LOGGER_LEVEL:-}
LOGGER_LEVEL: ${LOGGER_LEVEL:-}
FLUXER_GATEWAY_PUSH_ENABLED: ${FLUXER_GATEWAY_PUSH_ENABLED:-}
FLUXER_GATEWAY_PUSH_ENROLLED_CLEAR_NOTIFICATIONS_ENABLED: ${FLUXER_GATEWAY_PUSH_ENROLLED_CLEAR_NOTIFICATIONS_ENABLED:-}
FLUXER_GATEWAY_PUSH_OUTBOX_REQUEST_TIMEOUT_MS: ${FLUXER_GATEWAY_PUSH_OUTBOX_REQUEST_TIMEOUT_MS:-}
FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES: ${FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES:-}
FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES: ${FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES:-}
FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY: ${FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY:-}
FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS: ${FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS:-}
FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD: ${FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD:-}
FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS: ${FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS:-}
FLUXER_ERLANG_COOKIE: ${FLUXER_ERLANG_COOKIE:?set FLUXER_ERLANG_COOKIE in .env}
FLUXER_ERLANG_SCHEDULERS_MIN: "${FLUXER_ERLANG_SCHEDULERS_MIN:-2}"
FLUXER_ERLANG_SCHEDULERS_MAX: "${FLUXER_ERLANG_SCHEDULERS_MAX:-16}"
FLUXER_ERLANG_SCHEDULERS: ${FLUXER_ERLANG_SCHEDULERS:-}
FLUXER_ERLANG_SCHEDULERS_MIN: ${FLUXER_ERLANG_SCHEDULERS_MIN:-}
FLUXER_ERLANG_SCHEDULERS_MAX: ${FLUXER_ERLANG_SCHEDULERS_MAX:-}
FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS: ${FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS:-}
healthcheck:
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/_health/ready"]
interval: 10s
timeout: 5s
retries: 30
start_period: 90s
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_APP_HEALTHCHECK_RETRIES:-30}
start_period: ${FLUXER_APP_HEALTHCHECK_START_PERIOD:-90s}
depends_on:
nats: {condition: service_healthy}
valkey: {condition: service_healthy}
@@ -468,16 +561,73 @@ services:
memory: ${FLUXER_MEDIA_PROXY_MEMORY_LIMIT:-512mb}
environment:
<<: *fluxer-env
FLUXER_MEDIA_PROXY_HOST: 0.0.0.0
FLUXER_MEDIA_PROXY_PORT: "8080"
FLUXER_MEDIA_PROXY_MODE: upload
FLUXER_MEDIA_PROXY_STORAGE_BACKEND: s3
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_S3_READ_SIGNED: "true"
FLUXER_MEDIA_PROXY_CORS_MODE: ${FLUXER_MEDIA_PROXY_CORS_MODE:-}
FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS: ${FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}:${FLUXER_PUBLIC_PORT:-443}}}
FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE: ${FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE:-}
FLUXER_MEDIA_PROXY_READ_ONLY: ${FLUXER_MEDIA_PROXY_READ_ONLY:-}
FLUXER_MEDIA_PROXY_NSFW_THRESHOLD: ${FLUXER_MEDIA_PROXY_NSFW_THRESHOLD:-}
FLUXER_NSFW_SERVICE_ENDPOINT: ${FLUXER_NSFW_SERVICE_ENDPOINT:-}
FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS: ${FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS:-}
FLUXER_MEDIA_PROXY_WORKER_QUEUE_CAPACITY: ${FLUXER_MEDIA_PROXY_WORKER_QUEUE_CAPACITY:-}
FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS: ${FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS:-}
FLUXER_MEDIA_PROXY_MAX_ENCODE_FRAMES: ${FLUXER_MEDIA_PROXY_MAX_ENCODE_FRAMES:-}
FLUXER_MEDIA_PROXY_TRANSFORM_TIMEOUT_MS: ${FLUXER_MEDIA_PROXY_TRANSFORM_TIMEOUT_MS:-}
FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_BYTES: ${FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_BYTES:-}
FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES: ${FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES:-}
FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_TTL_MS: ${FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_TTL_MS:-}
FLUXER_MEDIA_PROXY_SOCKET_IO_TIMEOUT_MS: ${FLUXER_MEDIA_PROXY_SOCKET_IO_TIMEOUT_MS:-}
FLUXER_MEDIA_PROXY_SHUTDOWN_GRACE_MS: ${FLUXER_MEDIA_PROXY_SHUTDOWN_GRACE_MS:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_S3_TIMEOUT_MS: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_S3_TIMEOUT_MS:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_DIR: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_DIR:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_CHUNK_BYTES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_CHUNK_BYTES:-}
FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES:-}
FLUXER_S3_SESSION_TOKEN: ${FLUXER_S3_SESSION_TOKEN:-}
FLUXER_S3_READ_ENDPOINT: ${FLUXER_S3_READ_ENDPOINT:-}
FLUXER_S3_READ_BUCKET: ${FLUXER_S3_READ_BUCKET:-}
FLUXER_S3_READ_BUCKET_STYLE: ${FLUXER_S3_READ_BUCKET_STYLE:-}
FLUXER_S3_READ_SIGNED: "${FLUXER_S3_READ_SIGNED:-true}"
depends_on:
seaweedfs-init: {condition: service_completed_successfully}
nats: {condition: service_healthy}
push:
<<: *fluxer-service
image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-push:${FLUXER_IMAGE_TAG:-v1}
deploy:
resources:
limits:
memory: ${FLUXER_PUSH_MEMORY_LIMIT:-256mb}
environment:
<<: *fluxer-env
FLUXER_PUSH_SERVICE_QUEUE_CAPACITY: ${FLUXER_PUSH_SERVICE_QUEUE_CAPACITY:-}
FLUXER_PUSH_SERVICE_SEND_CONCURRENCY: ${FLUXER_PUSH_SERVICE_SEND_CONCURRENCY:-}
FLUXER_PUSH_SERVICE_APNS_BASE_URL: ${FLUXER_PUSH_SERVICE_APNS_BASE_URL:-}
FLUXER_PUSH_SERVICE_FCM_BASE_URL: ${FLUXER_PUSH_SERVICE_FCM_BASE_URL:-}
FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS: ${FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS:-}
FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS: ${FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS:-}
FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED: ${FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED:-}
FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT: ${FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT:-}
FLUXER_PUSH_FCM_ENABLED: ${FLUXER_PUSH_FCM_ENABLED:-}
FLUXER_PUSH_FCM_PROJECT_ID: ${FLUXER_PUSH_FCM_PROJECT_ID:-}
FLUXER_PUSH_FCM_CLIENT_EMAIL: ${FLUXER_PUSH_FCM_CLIENT_EMAIL:-}
FLUXER_PUSH_FCM_PRIVATE_KEY: ${FLUXER_PUSH_FCM_PRIVATE_KEY:-}
FLUXER_PUSH_FCM_PRIVATE_KEY_PATH: ${FLUXER_PUSH_FCM_PRIVATE_KEY_PATH:-}
FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH: ${FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH:-}
FLUXER_PUSH_FCM_TOKEN_URI: ${FLUXER_PUSH_FCM_TOKEN_URI:-}
FLUXER_PUSH_FCM_APPS: ${FLUXER_PUSH_FCM_APPS:-}
healthcheck:
test: ["CMD", "/usr/local/bin/fluxer-push", "healthcheck"]
<<: *fluxer-app-healthcheck
start_period: ${FLUXER_SVC_HEALTHCHECK_START_PERIOD:-60s}
depends_on:
nats: {condition: service_healthy}
api: {condition: service_healthy}
static-proxy:
<<: *fluxer-service
image: ${FLUXER_REGISTRY:-ghcr.io/${FLUXER_REGISTRY_OWNER:-fluxerapp}}/fluxer-static:${FLUXER_IMAGE_TAG:-v1}
@@ -487,9 +637,9 @@ services:
memory: ${FLUXER_STATIC_PROXY_MEMORY_LIMIT:-256mb}
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/avatars/0.png"]
interval: 10s
timeout: 5s
retries: 10
interval: ${FLUXER_HEALTHCHECK_INTERVAL:-10s}
timeout: ${FLUXER_HEALTHCHECK_TIMEOUT:-5s}
retries: ${FLUXER_HEALTHCHECK_RETRIES:-10}
app-proxy:
<<: *fluxer-service
@@ -499,15 +649,29 @@ services:
limits:
memory: ${FLUXER_APP_PROXY_MEMORY_LIMIT:-256mb}
environment:
FLUXER_APP_PROXY_HOST: 0.0.0.0
RUST_LOG: ${RUST_LOG:-}
FLUXER_APP_PROXY_PORT: "8080"
FLUXER_BASE_DOMAIN: ${FLUXER_DOMAIN:?set FLUXER_DOMAIN in .env}
FLUXER_PUBLIC_SCHEME: ${FLUXER_PUBLIC_SCHEME:-https}
FLUXER_PUBLIC_PORT: ${FLUXER_PUBLIC_PORT:-443}
FLUXER_PUBLIC_ORIGIN: ${FLUXER_PUBLIC_ORIGIN:-}
FLUXER_TRUST_CLIENT_IP_HEADER: "${FLUXER_TRUST_CLIENT_IP_HEADER:-true}"
FLUXER_CLIENT_IP_HEADER_NAME: ${FLUXER_CLIENT_IP_HEADER_NAME:-}
FLUXER_GEOIP_DB_PATH: ${FLUXER_GEOIP_DB_PATH:-}
FLUXER_S3_ENDPOINT: ${FLUXER_S3_ENDPOINT:-http://seaweedfs:8333}
FLUXER_S3_PUBLIC_ENDPOINT: ${FLUXER_S3_PUBLIC_ENDPOINT:-}
FLUXER_S3_REGION: ${FLUXER_S3_REGION:-us-east-1}
FLUXER_S3_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY:?set FLUXER_S3_ACCESS_KEY in .env}
FLUXER_S3_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY:?set FLUXER_S3_SECRET_KEY in .env}
FLUXER_S3_BUCKET_UPLOADS: ${FLUXER_S3_BUCKET_UPLOADS:-}
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_STATIC_CDN_ENDPOINT:-}
DISCOVERY_UPSTREAM_URL: http://edge:8088/.well-known/fluxer
DISCOVERY_REFRESH_INTERVAL_MS: ${DISCOVERY_REFRESH_INTERVAL_MS:-}
PUBLIC_BOOTSTRAP_API_ENDPOINT: /api
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/api
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/api}
FLUXER_APP_PROXY_INDEX_UPSTREAM_URL: ${FLUXER_APP_PROXY_INDEX_UPSTREAM_URL:-}
FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS: ${FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS:-}
FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS: ${FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS:-}
FLUXER_CSP_EXTRA_DEFAULT_SRC: ${FLUXER_CSP_EXTRA_DEFAULT_SRC:-}
FLUXER_CSP_EXTRA_CONNECT_SRC: ${FLUXER_CSP_EXTRA_CONNECT_SRC:-}
FLUXER_CSP_EXTRA_IMG_SRC: ${FLUXER_CSP_EXTRA_IMG_SRC:-}
@@ -565,7 +729,6 @@ services:
<<: *fluxer-env
FLUXER_SVC_NAME: users
FLUXER_SVC_MODE: router
FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -582,8 +745,7 @@ services:
FLUXER_SVC_NAME: users
FLUXER_SVC_MODE: shard
FLUXER_SVC_SHARD_ID: "0"
FLUXER_POSTGRES_MAX_CONNECTIONS: "20"
FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_USERS_SHARD_POSTGRES_MAX_CONNECTIONS:-20}"
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -600,7 +762,6 @@ services:
<<: *fluxer-env
FLUXER_SVC_NAME: gifs
FLUXER_SVC_MODE: router
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -617,7 +778,7 @@ services:
FLUXER_SVC_NAME: gifs
FLUXER_SVC_MODE: shard
FLUXER_SVC_SHARD_ID: "0"
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_GIFS_SHARD_CACHE_MAX_BYTES: ${FLUXER_GIFS_SHARD_CACHE_MAX_BYTES:-}
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -633,7 +794,6 @@ services:
<<: *fluxer-env
FLUXER_SVC_NAME: messages
FLUXER_SVC_MODE: router
FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -650,8 +810,7 @@ services:
FLUXER_SVC_NAME: messages
FLUXER_SVC_MODE: shard
FLUXER_SVC_SHARD_ID: "0"
FLUXER_POSTGRES_MAX_CONNECTIONS: "20"
FLUXER_SVC_MAX_CONCURRENT_REQUESTS: "${FLUXER_SVC_MAX_CONCURRENT_REQUESTS:-}"
FLUXER_POSTGRES_MAX_CONNECTIONS: "${FLUXER_MESSAGES_SHARD_POSTGRES_MAX_CONNECTIONS:-20}"
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -668,8 +827,6 @@ services:
<<: *fluxer-env
FLUXER_SVC_NAME: unfurl
FLUXER_SVC_MODE: router
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -686,8 +843,9 @@ services:
FLUXER_SVC_NAME: unfurl
FLUXER_SVC_MODE: shard
FLUXER_SVC_SHARD_ID: "0"
FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080
FLUXER_UNFURL_STATIC_CDN_ENDPOINT: ${FLUXER_UNFURL_STATIC_CDN_ENDPOINT:-}
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_STATIC_CDN_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
healthcheck: *fluxer-svc-healthcheck
depends_on:
nats: {condition: service_healthy}
@@ -701,22 +859,14 @@ services:
memory: ${FLUXER_ADMIN_MEMORY_LIMIT:-256mb}
environment:
<<: *fluxer-env
FLUXER_ADMIN_HOST: 0.0.0.0
FLUXER_ADMIN_PORT: "8080"
FLUXER_ADMIN_BASE_PATH: /admin
FLUXER_ADMIN_BASE_PATH: ${FLUXER_ADMIN_BASE_PATH:-/admin}
FLUXER_API_ENDPOINT: http://api:8080
FLUXER_ADMIN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/admin
FLUXER_APP_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/media
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}
FLUXER_ADMIN_OAUTH_REDIRECT_URI: ${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}/admin/oauth2_callback
FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_STATIC_CDN_ENDPOINT:-${FLUXER_PUBLIC_ORIGIN:-${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}}}
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/8080 && printf 'GET /_health HTTP/1.0\\r\\n\\r\\n' >&3 && head -n 1 <&3 | grep -q ' 200 '"]
interval: 10s
timeout: 5s
retries: 30
start_period: 60s
start_interval: 1s
<<: *fluxer-app-healthcheck
start_period: ${FLUXER_SVC_HEALTHCHECK_START_PERIOD:-60s}
depends_on:
api: {condition: service_healthy}
@@ -0,0 +1,14 @@
services:
seaweedfs:
profiles: [bundled-object-store]
seaweedfs-init:
profiles: [bundled-object-store]
api:
depends_on:
seaweedfs-init: !reset null
worker:
depends_on:
seaweedfs-init: !reset null
media-proxy:
depends_on:
seaweedfs-init: !reset null
+14 -14
View File
@@ -9,32 +9,32 @@ build = "build.rs"
[dependencies]
anyhow = "1.0.104"
axum = { version = "0.8.9", features = ["macros"] }
base64 = "0.22.1"
base64 = "0.23.1"
chrono = { version = "0.4", default-features = false, features = ["serde"] }
cookie = "0.18.1"
cookie = "0.18.2"
fluxer_common = { path = "../fluxer_common" }
hmac = "0.13.0"
maud = { version = "0.27.0", features = ["axum"] }
rand = "0.10"
regress = "0.11"
reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
regress = "0.12"
reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
sha2 = "0.11.0"
time = { version = "0.3.47", features = ["formatting", "parsing"] }
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "signal"] }
time = { version = "0.3.55", features = ["formatting", "parsing"] }
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal"] }
tower = { version = "0.5.3", features = ["util"] }
tower-http = { version = "0.6.11", features = ["compression-gzip", "trace"] }
tower-http = { version = "0.7.1", features = ["compression-gzip", "trace"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracing-subscriber = "0.3.23"
url = "2.5"
urlencoding = "2.1.3"
progenitor-client = { version = "0.14.0", default-features = false }
progenitor-client = { version = "0.15.0", default-features = false }
[build-dependencies]
openapiv3 = "2.2.0"
prettyplease = "0.2"
progenitor = { version = "0.14.0", default-features = false }
prettyplease = "0.3"
progenitor = { version = "0.15.0", default-features = false }
serde_json = "1"
sha2 = "0.11.0"
syn = "2"
syn = "3"
+3 -6
View File
@@ -1,15 +1,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM rust:1-bookworm AS builder
FROM rust:1-trixie AS builder
ARG BUILD_VERSION=""
ARG TARGETARCH
WORKDIR /usr/src/app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates nodejs npm pkg-config \
&& npm install -g pnpm@10.29.3 \
&& npm install -g pnpm@11.27.0 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install --no-audit --no-fund @tailwindcss/[email protected] [email protected]
@@ -45,8 +44,6 @@ RUN printf '%s\n' \
'strip = "symbols"' \
> Cargo.toml
ENV FLUXER_BUILD_VERSION="${BUILD_VERSION}"
RUN cargo build --release -p fluxer_admin \
&& cp target/release/fluxer_admin /usr/local/bin/fluxer-admin
@@ -57,7 +54,7 @@ RUN test "$(ls target/release/build/fluxer_admin-*/out/static/fonts/*.woff2 | wc
&& ls target/release/build/fluxer_admin-*/out/static/fonts/fonts.*.css \
&& echo "Latin-core fonts bundled successfully"
FROM debian:bookworm-slim AS runtime
FROM debian:trixie-slim AS runtime
ARG BUILD_VERSION=""
ARG SOURCE_SHA=""
+331 -2
View File
@@ -38,6 +38,9 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
let mut spec: openapiv3::OpenAPI =
serde_json::from_str(&json_str).expect("failed to parse openapi-admin.json");
adapt_progenitor_throttled_errors(&mut spec);
relax_guild_audit_log_schemas(&mut spec);
relax_progenitor_schema_strictness(&mut spec);
relax_integer_enums(&mut spec);
let mut settings = progenitor::GenerationSettings::new();
settings.with_interface(progenitor::InterfaceStyle::Positional);
@@ -52,9 +55,9 @@ fn generate_admin_api(manifest_dir: &Path, out_dir: &Path) {
.generate_tokens(&spec)
.expect("failed to generate admin API client");
let content = prettyplease::unparse(
let content = relax_required_nullable_fields(&prettyplease::unparse(
&syn::parse2::<syn::File>(tokens).expect("failed to parse generated tokens"),
);
));
let output_path = out_dir.join("admin_api_generated.rs");
fs::write(&output_path, content).expect("failed to write generated API code");
@@ -141,6 +144,332 @@ fn adapt_progenitor_throttled_errors(spec: &mut openapiv3::OpenAPI) {
}
}
fn relax_guild_audit_log_schemas(spec: &mut openapiv3::OpenAPI) {
let components = spec.components.as_mut().expect("missing API components");
let entry = object_schema_mut(components, "GuildAuditLogEntryResponse");
entry.additional_properties = None;
let openapiv3::ReferenceOr::Item(options) = entry
.properties
.get_mut("options")
.expect("GuildAuditLogEntryResponse has no options property")
else {
panic!("GuildAuditLogEntryResponse options must be an inline schema");
};
let openapiv3::SchemaKind::Type(openapiv3::Type::Object(options)) = &mut options.schema_kind
else {
panic!("GuildAuditLogEntryResponse options must be an object schema");
};
options.additional_properties = None;
let change = object_schema_mut(components, "AuditLogChangeSchema");
change.additional_properties = None;
for property in ["old_value", "new_value"] {
change.properties.insert(
property.to_string(),
openapiv3::ReferenceOr::Item(Box::new(openapiv3::Schema {
schema_data: openapiv3::SchemaData::default(),
schema_kind: openapiv3::SchemaKind::Any(openapiv3::AnySchema::default()),
})),
);
}
}
const OPEN_INTEGER_ENUMS: &[&str] = &["ChannelType", "MessageType", "WebhookType"];
fn relax_integer_enums(spec: &mut openapiv3::OpenAPI) {
let components = spec.components.as_mut().expect("missing API components");
for name in OPEN_INTEGER_ENUMS {
let Some(openapiv3::ReferenceOr::Item(schema)) = components.schemas.get_mut(*name) else {
panic!("missing inline {name} schema");
};
let openapiv3::SchemaKind::Type(openapiv3::Type::Integer(integer)) =
&mut schema.schema_kind
else {
panic!("{name} must be an integer schema");
};
integer.enumeration.clear();
}
}
fn object_schema_mut<'a>(
components: &'a mut openapiv3::Components,
name: &str,
) -> &'a mut openapiv3::ObjectType {
let Some(openapiv3::ReferenceOr::Item(schema)) = components.schemas.get_mut(name) else {
panic!("missing inline {name} schema");
};
let openapiv3::SchemaKind::Type(openapiv3::Type::Object(object)) = &mut schema.schema_kind
else {
panic!("{name} must be an object schema");
};
object
}
const MAX_SCHEMA_REFERENCE_DEPTH: usize = 32;
fn relax_required_nullable_fields(generated: &str) -> String {
const PRESENCE_CHECK: &str =
"#[serde(deserialize_with = \"::std::option::Option::deserialize\")]";
generated
.lines()
.filter(|line| line.trim() != PRESENCE_CHECK)
.flat_map(|line| [line, "\n"])
.collect()
}
fn relax_progenitor_schema_strictness(spec: &mut openapiv3::OpenAPI) {
let registry = spec.components.clone().unwrap_or_default();
if let Some(components) = spec.components.as_mut() {
for schema in components.schemas.values_mut() {
relax_schema_reference(schema, &registry);
}
for response in components.responses.values_mut() {
if let openapiv3::ReferenceOr::Item(response) = response {
relax_response(response, &registry);
}
}
for parameter in components.parameters.values_mut() {
if let openapiv3::ReferenceOr::Item(parameter) = parameter {
relax_parameter(parameter, &registry);
}
}
for request_body in components.request_bodies.values_mut() {
if let openapiv3::ReferenceOr::Item(request_body) = request_body {
relax_content(&mut request_body.content, &registry);
}
}
for header in components.headers.values_mut() {
if let openapiv3::ReferenceOr::Item(header) = header {
relax_parameter_format(&mut header.format, &registry);
}
}
}
for path in spec.paths.paths.values_mut() {
let openapiv3::ReferenceOr::Item(path) = path else {
continue;
};
for parameter in &mut path.parameters {
if let openapiv3::ReferenceOr::Item(parameter) = parameter {
relax_parameter(parameter, &registry);
}
}
for operation in [
&mut path.get,
&mut path.put,
&mut path.post,
&mut path.delete,
&mut path.options,
&mut path.head,
&mut path.patch,
&mut path.trace,
]
.into_iter()
.flatten()
{
for parameter in &mut operation.parameters {
if let openapiv3::ReferenceOr::Item(parameter) = parameter {
relax_parameter(parameter, &registry);
}
}
if let Some(openapiv3::ReferenceOr::Item(request_body)) =
operation.request_body.as_mut()
{
relax_content(&mut request_body.content, &registry);
}
for response in operation
.responses
.responses
.values_mut()
.chain(operation.responses.default.iter_mut())
{
if let openapiv3::ReferenceOr::Item(response) = response {
relax_response(response, &registry);
}
}
}
}
}
fn relax_response(response: &mut openapiv3::Response, registry: &openapiv3::Components) {
relax_content(&mut response.content, registry);
for header in response.headers.values_mut() {
if let openapiv3::ReferenceOr::Item(header) = header {
relax_parameter_format(&mut header.format, registry);
}
}
}
fn relax_content(content: &mut openapiv3::Content, registry: &openapiv3::Components) {
for media_type in content.values_mut() {
if let Some(schema) = media_type.schema.as_mut() {
relax_schema_reference(schema, registry);
}
}
}
fn relax_parameter(parameter: &mut openapiv3::Parameter, registry: &openapiv3::Components) {
let format = match parameter {
openapiv3::Parameter::Query { parameter_data, .. }
| openapiv3::Parameter::Header { parameter_data, .. }
| openapiv3::Parameter::Path { parameter_data, .. }
| openapiv3::Parameter::Cookie { parameter_data, .. } => &mut parameter_data.format,
};
relax_parameter_format(format, registry);
}
fn relax_parameter_format(
format: &mut openapiv3::ParameterSchemaOrContent,
registry: &openapiv3::Components,
) {
match format {
openapiv3::ParameterSchemaOrContent::Schema(schema) => {
relax_schema_reference(schema, registry)
}
openapiv3::ParameterSchemaOrContent::Content(content) => relax_content(content, registry),
}
}
fn relax_schema_reference(
schema: &mut openapiv3::ReferenceOr<openapiv3::Schema>,
registry: &openapiv3::Components,
) {
if let openapiv3::ReferenceOr::Item(schema) = schema {
relax_schema(schema, registry);
}
}
fn relax_boxed_schema_reference(
schema: &mut openapiv3::ReferenceOr<Box<openapiv3::Schema>>,
registry: &openapiv3::Components,
) {
if let openapiv3::ReferenceOr::Item(schema) = schema {
relax_schema(schema, registry);
}
}
fn relax_schema(schema: &mut openapiv3::Schema, registry: &openapiv3::Components) {
if flattens_objects_beside_scalars(&schema.schema_kind, registry) {
schema.schema_kind = openapiv3::SchemaKind::Any(openapiv3::AnySchema::default());
return;
}
match &mut schema.schema_kind {
openapiv3::SchemaKind::Type(openapiv3::Type::Object(object)) => {
relax_additional_properties(&mut object.additional_properties, registry);
for property in object.properties.values_mut() {
relax_boxed_schema_reference(property, registry);
}
}
openapiv3::SchemaKind::Type(openapiv3::Type::Array(array)) => {
if let Some(items) = array.items.as_mut() {
relax_boxed_schema_reference(items, registry);
}
}
openapiv3::SchemaKind::Type(_) => {}
openapiv3::SchemaKind::OneOf { one_of: subschemas }
| openapiv3::SchemaKind::AllOf { all_of: subschemas }
| openapiv3::SchemaKind::AnyOf { any_of: subschemas } => {
for subschema in subschemas {
relax_schema_reference(subschema, registry);
}
}
openapiv3::SchemaKind::Not { not } => relax_schema_reference(not, registry),
openapiv3::SchemaKind::Any(any) => {
relax_additional_properties(&mut any.additional_properties, registry);
for property in any.properties.values_mut() {
relax_boxed_schema_reference(property, registry);
}
if let Some(items) = any.items.as_mut() {
relax_boxed_schema_reference(items, registry);
}
for subschema in any
.one_of
.iter_mut()
.chain(any.all_of.iter_mut())
.chain(any.any_of.iter_mut())
{
relax_schema_reference(subschema, registry);
}
if let Some(not) = any.not.as_mut() {
relax_schema_reference(not, registry);
}
}
}
}
fn relax_additional_properties(
additional_properties: &mut Option<openapiv3::AdditionalProperties>,
registry: &openapiv3::Components,
) {
match additional_properties {
Some(openapiv3::AdditionalProperties::Any(false)) => *additional_properties = None,
Some(openapiv3::AdditionalProperties::Schema(schema)) => {
relax_schema_reference(schema, registry)
}
_ => {}
}
}
fn flattens_objects_beside_scalars(
schema_kind: &openapiv3::SchemaKind,
registry: &openapiv3::Components,
) -> bool {
let subschemas = match schema_kind {
openapiv3::SchemaKind::OneOf { one_of } => one_of,
openapiv3::SchemaKind::AnyOf { any_of } => any_of,
_ => return false,
};
let mut objects = false;
let mut scalars = false;
for subschema in subschemas {
if resolves_to_object(subschema, registry, MAX_SCHEMA_REFERENCE_DEPTH) {
objects = true;
} else {
scalars = true;
}
}
objects && scalars
}
fn resolves_to_object(
schema: &openapiv3::ReferenceOr<openapiv3::Schema>,
registry: &openapiv3::Components,
depth: usize,
) -> bool {
let Some(depth) = depth.checked_sub(1) else {
return false;
};
let schema = match schema {
openapiv3::ReferenceOr::Reference { reference } => {
let Some(target) = reference
.strip_prefix("#/components/schemas/")
.and_then(|name| registry.schemas.get(name))
else {
return false;
};
return resolves_to_object(target, registry, depth);
}
openapiv3::ReferenceOr::Item(schema) => schema,
};
match &schema.schema_kind {
openapiv3::SchemaKind::Type(openapiv3::Type::Object(_)) => true,
openapiv3::SchemaKind::Type(_) => false,
openapiv3::SchemaKind::OneOf { one_of: subschemas }
| openapiv3::SchemaKind::AllOf { all_of: subschemas }
| openapiv3::SchemaKind::AnyOf { any_of: subschemas } => subschemas
.iter()
.any(|subschema| resolves_to_object(subschema, registry, depth)),
openapiv3::SchemaKind::Not { .. } => false,
openapiv3::SchemaKind::Any(any) => {
any.typ.as_deref() == Some("object")
|| !any.properties.is_empty()
|| any.additional_properties.is_some()
}
}
}
struct Face {
css_family: String,
weight: u64,
File diff suppressed because it is too large Load Diff
-8
View File
@@ -17,9 +17,6 @@ pub const JOBS_CANCEL: &str = "jobs:cancel";
pub const BAN_EMAIL_ADD: &str = "ban:email:add";
pub const BAN_EMAIL_CHECK: &str = "ban:email:check";
pub const BAN_EMAIL_REMOVE: &str = "ban:email:remove";
pub const SUSPICIOUS_EMAIL_DOMAIN_ADD: &str = "suspicious_email_domain:add";
pub const SUSPICIOUS_EMAIL_DOMAIN_CHECK: &str = "suspicious_email_domain:check";
pub const SUSPICIOUS_EMAIL_DOMAIN_REMOVE: &str = "suspicious_email_domain:remove";
pub const BAN_PHRASE_ADD: &str = "ban:phrase:add";
pub const BAN_PHRASE_CHECK: &str = "ban:phrase:check";
pub const BAN_PHRASE_REMOVE: &str = "ban:phrase:remove";
@@ -79,7 +76,6 @@ pub const REPORT_RESOLVE: &str = "report:resolve";
pub const REPORT_VIEW: &str = "report:view";
pub const REPORT_VIEW_REPORTER_PII: &str = "report:view:reporter_pii";
pub const SYSTEM_DM_SEND: &str = "system_dm:send";
pub const SYSTEM_HEAP_SNAPSHOT: &str = "system:heap_snapshot";
pub const USER_CANCEL_BULK_MESSAGE_DELETION: &str = "user:cancel:bulk_message_deletion";
pub const USER_DELETE: &str = "user:delete";
pub const USER_DISABLE_SUSPICIOUS: &str = "user:disable:suspicious";
@@ -130,9 +126,6 @@ pub const ALL_ACLS: &[&str] = &[
BAN_EMAIL_ADD,
BAN_EMAIL_CHECK,
BAN_EMAIL_REMOVE,
SUSPICIOUS_EMAIL_DOMAIN_ADD,
SUSPICIOUS_EMAIL_DOMAIN_CHECK,
SUSPICIOUS_EMAIL_DOMAIN_REMOVE,
BAN_PHRASE_ADD,
BAN_PHRASE_CHECK,
BAN_PHRASE_REMOVE,
@@ -192,7 +185,6 @@ pub const ALL_ACLS: &[&str] = &[
REPORT_VIEW,
REPORT_VIEW_REPORTER_PII,
SYSTEM_DM_SEND,
SYSTEM_HEAP_SNAPSHOT,
USER_CANCEL_BULK_MESSAGE_DELETION,
USER_DELETE,
USER_DISABLE_SUSPICIOUS,
+17
View File
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::templates::components::tooltip::{Hint, HintLink};
pub fn limit_key_hint(key: &str) -> Option<Hint<'static>> {
match key {
"feature_guild_create" => Some(Hint {
name: Some("Community Creation Access"),
body: "Admins with the wildcard ACL can always create communities.",
link: Some(HintLink::new(
"/instance-config#community-creation",
"Community creation policy",
)),
}),
_ => None,
}
}
+41
View File
@@ -10,6 +10,7 @@ pub struct SearchAuditLogsParams {
pub admin_user_id: Option<String>,
pub target_id: Option<String>,
pub target_type: Option<String>,
pub access: Option<String>,
pub sort_by: Option<String>,
pub sort_order: Option<String>,
pub limit: u32,
@@ -21,6 +22,12 @@ impl AdminApiClient {
&self,
params: &SearchAuditLogsParams,
) -> ApiResult<AuditLogsListResponse> {
let access = params
.access
.as_deref()
.map(audit_access)
.transpose()?
.map(|value| value.to_string());
let sort_by = params
.sort_by
.as_deref()
@@ -46,6 +53,7 @@ impl AdminApiClient {
params.target_type.as_deref().unwrap_or_default(),
),
("target_id", params.target_id.as_deref().unwrap_or_default()),
("access", access.as_deref().unwrap_or_default()),
("sort_by", sort_by.as_deref().unwrap_or_default()),
("sort_order", sort_order.as_deref().unwrap_or_default()),
("limit", limit.as_str()),
@@ -55,6 +63,11 @@ impl AdminApiClient {
}
}
fn audit_access(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsAccess> {
generated_types::ListAdminAuditLogsAccess::try_from(value)
.map_err(|e| ApiError::Parse(e.to_string()))
}
fn audit_sort_by(value: &str) -> ApiResult<generated_types::ListAdminAuditLogsSortBy> {
let value = match value {
"created_at" => "createdAt",
@@ -83,6 +96,13 @@ mod tests {
assert_eq!(audit_sort_order("desc").unwrap().to_string(), "desc");
}
#[test]
fn accepts_only_known_access_filters() {
assert_eq!(audit_access("read").unwrap().to_string(), "read");
assert_eq!(audit_access("write").unwrap().to_string(), "write");
assert!(audit_access("all").is_err());
}
#[test]
fn rejects_lossy_audit_totals() {
for total in [serde_json::json!(1.5), serde_json::json!(-1)] {
@@ -99,6 +119,7 @@ mod tests {
"admin_user_id": "234567890123456789",
"admin_user": null,
"action": "USER_UPDATE",
"access": "write",
"target_id": "345678901234567890",
"target_type": "user",
"target_user": null,
@@ -118,6 +139,26 @@ mod tests {
assert_eq!(generated.logs[0].action.to_string(), "USER_UPDATE");
let response: AuditLogsListResponse = serde_json::from_value(json.clone()).unwrap();
assert_eq!(response.logs[0].access.as_deref(), Some("write"));
assert_eq!(serde_json::to_value(response).unwrap(), json);
}
#[test]
fn deserializes_audit_entries_from_an_api_without_access() {
let json = serde_json::json!({
"logs": [{
"log_id": "123456789012345678",
"admin_user_id": "234567890123456789",
"action": "USER_UPDATE",
"target_id": "345678901234567890",
"target_type": "user",
"audit_log_reason": null,
"metadata": {},
"created_at": "2026-09-11T12:00:00.000Z"
}],
"total": 1
});
let response: AuditLogsListResponse = serde_json::from_value(json).unwrap();
assert_eq!(response.logs[0].access, None);
}
}
+30 -79
View File
@@ -9,12 +9,11 @@ impl AdminApiClient {
pub async fn ban_email(&self, email: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
self.create_blocklist_entry(
"email",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_1: Some(generated_types::BanEmailRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanEmailRequest {
email: generated_types::EmailType::from(email.to_owned()),
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -32,10 +31,9 @@ impl AdminApiClient {
pub async fn ban_ip(&self, ip: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
self.create_blocklist_entry(
"ip",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_0: Some(generated_types::BanIpRequest { ip: ip.to_owned() }),
..Default::default()
},
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanIpRequest { ip: ip.to_owned() },
),
audit_log_reason,
)
.await
@@ -50,45 +48,14 @@ impl AdminApiClient {
self.check_blocklist_entry("ip", ip, None).await
}
pub async fn add_suspicious_email_domain(
&self,
domain: &str,
audit_log_reason: Option<&str>,
) -> ApiResult<()> {
self.create_blocklist_entry(
SUSPICIOUS_EMAIL_DOMAIN_LIST,
generated_types::AdminBlocklistEntryCreateRequest {
subtype_2: Some(suspicious_email_domain_request(domain)?),
..Default::default()
},
audit_log_reason,
)
.await
}
pub async fn remove_suspicious_email_domain(
&self,
domain: &str,
audit_log_reason: Option<&str>,
) -> ApiResult<()> {
self.delete_blocklist_entry(SUSPICIOUS_EMAIL_DOMAIN_LIST, domain, None, audit_log_reason)
.await
}
pub async fn check_suspicious_email_domain(&self, domain: &str) -> ApiResult<BanCheckResult> {
self.check_blocklist_entry(SUSPICIOUS_EMAIL_DOMAIN_LIST, domain, None)
.await
}
pub async fn ban_phrase(&self, phrase: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
self.create_blocklist_entry(
"phrase",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_3: Some(generated_types::BanPhraseRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanPhraseRequest {
phrase: phrase.to_owned(),
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -110,16 +77,15 @@ impl AdminApiClient {
pub async fn ban_url(&self, url: &str, audit_log_reason: Option<&str>) -> ApiResult<()> {
self.create_blocklist_entry(
"url",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_4: Some(generated_types::BanUrlRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanUrlRequest {
category: None,
notes: None,
severity: None,
source_url: None,
url: url.to_owned(),
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -142,17 +108,16 @@ impl AdminApiClient {
) -> ApiResult<()> {
self.create_blocklist_entry(
"url-domain",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_5: Some(generated_types::BanUrlDomainRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanUrlDomainRequest {
category: None,
domain: domain.to_owned(),
match_subdomains,
notes: None,
severity: None,
source_url: None,
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -178,17 +143,16 @@ impl AdminApiClient {
) -> ApiResult<()> {
self.create_blocklist_entry(
"file-sha",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_6: Some(generated_types::BanFileShaRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanFileShaRequest {
category: None,
content_type: None,
notes: None,
severity: None,
sha256_hex: sha256_hex.to_owned(),
source_url: None,
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -231,17 +195,16 @@ impl AdminApiClient {
) -> ApiResult<()> {
self.create_blocklist_entry(
"avatar-hash",
generated_types::AdminBlocklistEntryCreateRequest {
subtype_7: Some(generated_types::BanAvatarHashRequest {
generated_types::AdminBlocklistEntryCreateRequest::from(
generated_types::BanAvatarHashRequest {
category: None,
hashes: vec![hash_short.to_owned()],
notes: None,
reason: None,
severity: None,
source_url: None,
}),
..Default::default()
},
},
),
audit_log_reason,
)
.await
@@ -279,10 +242,9 @@ impl AdminApiClient {
) -> ApiResult<()> {
self.create_blocklist_entry(
PROFILE_SUBSTRING_LIST,
generated_types::AdminBlocklistEntryCreateRequest {
subtype_8: Some(profile_substring_request(scope, substring)?),
..Default::default()
},
generated_types::AdminBlocklistEntryCreateRequest::from(profile_substring_request(
scope, substring,
)?),
audit_log_reason,
)
.await
@@ -359,8 +321,6 @@ impl AdminApiClient {
}
}
const SUSPICIOUS_EMAIL_DOMAIN_LIST: &str = "email-domain-suspicious";
const PROFILE_SUBSTRING_LIST: &str = "profile-substring";
fn blocklist_list_type(list_type: &str) -> ApiResult<generated_types::AdminBlocklistListType> {
@@ -380,15 +340,6 @@ fn blocklist_delete_scope(
.map_err(|e| ApiError::Parse(e.to_string()))
}
fn suspicious_email_domain_request(
domain: &str,
) -> ApiResult<generated_types::SuspiciousEmailDomainRequest> {
Ok(generated_types::SuspiciousEmailDomainRequest {
domain: generated_types::SuspiciousEmailDomainRequestDomain::try_from(domain)
.map_err(|e| ApiError::Parse(e.to_string()))?,
})
}
fn profile_substring_request(
scope: &str,
substring: &str,
+9 -5
View File
@@ -14,8 +14,8 @@ impl AdminApiClient {
audit_log_reason: Option<&str>,
) -> ApiResult<BulkJobResponse> {
let body = generated_types::AdminBulkJobCreateRequest::UpdateUserFlags {
add_flags: user_flags(add_flags),
remove_flags: user_flags(remove_flags),
add_flags: user_flags(add_flags)?,
remove_flags: user_flags(remove_flags)?,
user_ids: snowflakes(user_ids),
};
self.post_typed_with_reason("/admin/bulk-jobs", &body, audit_log_reason)
@@ -86,6 +86,7 @@ impl AdminApiClient {
reason_code: u32,
days_until_deletion: u32,
public_reason: Option<&str>,
notify_user: bool,
audit_log_reason: Option<&str>,
) -> ApiResult<BulkJobResponse> {
let body = generated_types::AdminBulkJobCreateRequest::ScheduleUserDeletion {
@@ -95,6 +96,7 @@ impl AdminApiClient {
)
.map_err(ApiError::Parse)?
.into(),
notify_user,
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
reason_code: crate::api::generated::deletion_reason_code(
i32::try_from(reason_code).map_err(|e| ApiError::Parse(e.to_string()))?,
@@ -112,11 +114,13 @@ fn snowflakes(values: &[String]) -> Vec<generated_types::SnowflakeType> {
values.iter().map(|value| snowflake(value)).collect()
}
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
values
.iter()
.cloned()
.map(generated_types::UserFlags::from)
.map(|value| {
generated_types::UserFlags::try_from(value.as_str())
.map_err(|error| ApiError::Parse(error.to_string()))
})
.collect()
}
+17 -4
View File
@@ -90,10 +90,7 @@ impl AdminApiClient {
fn headers_with_reason(&self, audit_log_reason: Option<&str>) -> ApiResult<HeaderMap> {
let mut headers = self.generated.inner().clone();
if let Some(reason) = audit_log_reason {
let mut value = HeaderValue::from_str(reason)
.map_err(|_| ApiError::Parse("invalid audit log reason header".to_owned()))?;
value.set_sensitive(true);
headers.insert("x-audit-log-reason", value);
headers.insert("x-audit-log-reason", audit_log_reason_header(reason)?);
}
Ok(headers)
}
@@ -422,11 +419,27 @@ impl std::fmt::Display for ApiError {
}
}
fn audit_log_reason_header(reason: &str) -> ApiResult<HeaderValue> {
let mut value = HeaderValue::from_bytes(reason.as_bytes())
.map_err(|_| ApiError::Parse("invalid audit log reason header".to_owned()))?;
value.set_sensitive(true);
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Value, json};
#[test]
fn audit_log_reason_header_carries_utf8_bytes() {
let reason = "§ 3 Regel – wiederholt 日本";
let value = audit_log_reason_header(reason).expect("valid reason header");
assert_eq!(value.as_bytes(), reason.as_bytes());
assert!(value.is_sensitive());
assert!(audit_log_reason_header("line one\nline two").is_err());
}
fn response(status: u16, body: &'static str) -> reqwest::Response {
axum::http::Response::builder()
.status(status)
+5
View File
@@ -4,6 +4,7 @@ use super::client::{AdminApiClient, ApiResult};
use super::types::{
CreateRegistrationUrlRequest, CreateRegistrationUrlResponse, InstanceConfigResponse,
InstanceConfigUpdateRequest, InstanceEmailSmtpTestRequest, InstanceEmailSmtpTestResponse,
InstancePremiumDiscovery,
};
impl AdminApiClient {
@@ -11,6 +12,10 @@ impl AdminApiClient {
self.get("/admin/instance/config", None).await
}
pub async fn get_instance_premium_discovery(&self) -> ApiResult<InstancePremiumDiscovery> {
self.get("/.well-known/fluxer", None).await
}
pub async fn update_instance_config(
&self,
update: &InstanceConfigUpdateRequest,
+2
View File
@@ -56,12 +56,14 @@ impl AdminApiClient {
&self,
report_id: &str,
public_comment: Option<&str>,
notify_reporter: bool,
audit_log_reason: Option<&str>,
) -> ApiResult<ResolveReportResponse> {
let mut body = serde_json::json!({"status": "resolved"});
if let Some(public_comment) = public_comment {
body["public_comment"] = serde_json::Value::from(public_comment);
}
body["notify_reporter"] = serde_json::Value::from(notify_reporter);
self.patch_with_reason(
&format!("/admin/reports/{}", urlencoding::encode(report_id)),
Some(&body),
+7 -2
View File
@@ -8,13 +8,18 @@ use super::types::SendSystemDmResponse;
impl AdminApiClient {
pub async fn send_system_dm(
&self,
user_ids: &[String],
user_ids: Option<&[String]>,
content: &str,
) -> ApiResult<SendSystemDmResponse> {
let body = generated_types::SendSystemDmRequest {
content: generated_types::SendSystemDmRequestContent::try_from(content)
.map_err(|e| ApiError::Parse(e.to_string()))?,
user_ids: user_ids.iter().map(|id| snowflake(id)).collect(),
user_ids: user_ids
.unwrap_or_default()
.iter()
.map(|id| snowflake(id))
.collect(),
all_users: user_ids.is_none().then_some(true),
};
let response = self
.generated()
+2
View File
@@ -9,6 +9,8 @@ pub struct AuditLogEntry {
#[serde(default)]
pub admin_user: Option<AuditLogUserSummary>,
pub action: String,
#[serde(default)]
pub access: Option<String>,
pub target_id: String,
pub target_type: String,
#[serde(default)]
+6
View File
@@ -122,6 +122,12 @@ pub struct AdminUser {
pub pending_bulk_message_deletion_at: Option<String>,
pub deletion_reason_code: Option<i32>,
pub deletion_public_reason: Option<String>,
#[serde(default)]
pub deletion_audit_log_reason: Option<String>,
#[serde(default)]
pub deletion_scheduled_by: Option<String>,
#[serde(default)]
pub deletion_scheduled_at: Option<String>,
pub last_active_at: Option<String>,
pub last_active_ip: Option<String>,
pub last_active_ip_reverse: Option<String>,
@@ -0,0 +1,332 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::{InstanceConfigResponse, PremiumMode};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const BILLING_MAX_CURRENCIES: usize = 64;
pub const BILLING_MAX_COUNTRY_CURRENCIES: usize = 300;
pub const BILLING_MAX_LEGACY_SLOTS: usize = 256;
pub const BILLING_MAX_LEGACY_PRICES_PER_SLOT: usize = 32;
pub const BILLING_PRICE_SLOTS: [&str; 4] = ["monthly", "yearly", "gift_1_month", "gift_1_year"];
pub const PREMIUM_PRODUCT_NAME_MAX_CHARS: usize = 40;
pub const TRI_STATE_DEFAULT: &str = "default";
pub const TRI_STATE_ON: &str = "on";
pub const TRI_STATE_OFF: &str = "off";
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BillingCatalogMode {
#[default]
Env,
Operator,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct BillingPriceSet {
pub monthly: Option<String>,
pub yearly: Option<String>,
pub gift_1_month: Option<String>,
pub gift_1_year: Option<String>,
}
impl BillingPriceSet {
pub fn has_recurring_pair(&self) -> bool {
self.monthly.is_some() && self.yearly.is_some()
}
pub fn is_empty(&self) -> bool {
self.monthly.is_none()
&& self.yearly.is_none()
&& self.gift_1_month.is_none()
&& self.gift_1_year.is_none()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct InstanceBillingResponse {
pub enabled: Option<bool>,
#[serde(default)]
pub effective_enabled: bool,
#[serde(default)]
pub stripe_secret_key_set: bool,
#[serde(default)]
pub stripe_webhook_secret_set: bool,
#[serde(default)]
pub stripe_secret_key_stored: bool,
#[serde(default)]
pub stripe_webhook_secret_stored: bool,
pub default_currency: Option<String>,
pub prices: Option<BTreeMap<String, BillingPriceSet>>,
pub country_currencies: Option<BTreeMap<String, String>>,
pub legacy_prices: Option<BTreeMap<String, Vec<String>>>,
#[serde(default)]
pub billing_active: bool,
#[serde(default)]
pub stripe_serviceable: bool,
#[serde(default)]
pub catalog_mode: BillingCatalogMode,
#[serde(default)]
pub webhook_url: String,
pub automatic_tax: Option<bool>,
pub tax_id_collection: Option<bool>,
pub terms_consent_required: Option<bool>,
#[serde(default)]
pub effective_automatic_tax: bool,
#[serde(default)]
pub effective_tax_id_collection: bool,
#[serde(default)]
pub effective_terms_consent_required: bool,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct InstanceBillingUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<Option<bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stripe_secret_key: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stripe_webhook_secret: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_currency: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prices: Option<Option<BTreeMap<String, BillingPriceSet>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country_currencies: Option<Option<BTreeMap<String, String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legacy_prices: Option<Option<BTreeMap<String, Vec<String>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub automatic_tax: Option<Option<bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tax_id_collection: Option<Option<bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_consent_required: Option<Option<bool>>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct InstancePremiumDiscovery {
#[serde(default)]
pub app_public: InstancePremiumDiscoveryAppPublic,
#[serde(default)]
pub features: InstancePremiumDiscoveryFeatures,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct InstancePremiumDiscoveryAppPublic {
#[serde(default)]
pub branding: InstancePremiumDiscoveryBranding,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct InstancePremiumDiscoveryBranding {
pub premium_product_name: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct InstancePremiumDiscoveryFeatures {
#[serde(default)]
pub premium_enabled: bool,
}
impl InstancePremiumDiscovery {
pub fn premium_product_name(&self) -> Option<&str> {
self.app_public
.branding
.premium_product_name
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PremiumBranding {
pub name: Option<String>,
pub premium_enabled: bool,
}
impl PremiumBranding {
pub fn from_discovery(discovery: &InstancePremiumDiscovery) -> Self {
Self {
name: discovery.premium_product_name().map(str::to_owned),
premium_enabled: discovery.features.premium_enabled,
}
}
pub fn from_instance_config(config: &InstanceConfigResponse) -> Self {
Self::from_config_parts(
config.self_hosted,
&config.app_public.branding.premium_product_name,
config.policy.premium_mode,
)
}
fn from_config_parts(self_hosted: bool, name: &str, premium_mode: PremiumMode) -> Self {
let name = name.trim();
Self {
name: (!name.is_empty()).then(|| name.to_owned()),
premium_enabled: !self_hosted || matches!(premium_mode, PremiumMode::Mirror),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::generated::types as generated_types;
use serde_json::json;
#[test]
fn billing_response_round_trips_through_the_generated_contract() {
let value = json!({
"enabled": true,
"effective_enabled": true,
"stripe_secret_key_set": true,
"stripe_webhook_secret_set": false,
"stripe_secret_key_stored": true,
"stripe_webhook_secret_stored": false,
"default_currency": "GBP",
"prices": {
"GBP": {
"monthly": "price_1Monthly",
"yearly": "price_1Yearly",
"gift_1_month": null,
"gift_1_year": null
}
},
"country_currencies": {"GB": "GBP"},
"legacy_prices": {"monthly_GBP": ["price_1Old"]},
"billing_active": false,
"stripe_serviceable": false,
"catalog_mode": "operator",
"webhook_url": "https://api.example.com/stripe/webhook",
"automatic_tax": null,
"tax_id_collection": false,
"terms_consent_required": true,
"effective_automatic_tax": false,
"effective_tax_id_collection": false,
"effective_terms_consent_required": true
});
let generated: generated_types::InstanceBillingResponse =
serde_json::from_value(value.clone()).expect("generated billing response");
let ours: InstanceBillingResponse =
serde_json::from_value(value.clone()).expect("hand-written billing response");
assert_eq!(ours.catalog_mode, BillingCatalogMode::Operator);
assert!(ours.stripe_secret_key_stored);
assert_eq!(ours.automatic_tax, None);
assert_eq!(ours.tax_id_collection, Some(false));
assert!(ours.effective_terms_consent_required);
assert!(ours.prices.as_ref().expect("prices")["GBP"].has_recurring_pair());
assert_eq!(serde_json::to_value(&ours).expect("serializable"), value);
assert_eq!(
serde_json::to_value(generated).expect("serializable generated"),
value
);
}
#[test]
fn default_billing_response_matches_the_generated_contract() {
let value = serde_json::to_value(InstanceBillingResponse::default()).expect("serializable");
serde_json::from_value::<generated_types::InstanceBillingResponse>(value.clone())
.expect("generated billing response");
assert_eq!(value["catalog_mode"], json!("env"));
assert_eq!(value["prices"], json!(null));
}
#[test]
fn billing_update_preserves_explicit_nulls_and_omits_untouched_fields() {
let mut prices = BTreeMap::new();
prices.insert(
"SEK".to_owned(),
BillingPriceSet {
monthly: Some("price_1Monthly".to_owned()),
yearly: Some("price_1Yearly".to_owned()),
..Default::default()
},
);
let update = InstanceBillingUpdateRequest {
enabled: Some(None),
stripe_secret_key: Some(None),
default_currency: Some(None),
prices: Some(Some(prices)),
country_currencies: Some(None),
legacy_prices: Some(Some(BTreeMap::new())),
automatic_tax: Some(None),
tax_id_collection: Some(Some(true)),
terms_consent_required: Some(Some(false)),
..Default::default()
};
let value = serde_json::to_value(update).expect("serializable update");
serde_json::from_value::<generated_types::InstanceBillingUpdateRequest>(value.clone())
.expect("generated update contract");
assert_eq!(
value,
json!({
"enabled": null,
"stripe_secret_key": null,
"default_currency": null,
"prices": {
"SEK": {
"monthly": "price_1Monthly",
"yearly": "price_1Yearly",
"gift_1_month": null,
"gift_1_year": null
}
},
"country_currencies": null,
"legacy_prices": {},
"automatic_tax": null,
"tax_id_collection": true,
"terms_consent_required": false
})
);
assert_eq!(
serde_json::to_value(InstanceBillingUpdateRequest::default())
.expect("serializable update"),
json!({})
);
}
#[test]
fn premium_discovery_reads_the_name_and_feature_flag() {
let discovery: InstancePremiumDiscovery = serde_json::from_value(json!({
"app_public": {"branding": {"product_name": "Example", "premium_product_name": " Gold "}},
"features": {"premium_enabled": true, "stripe_enabled": false}
}))
.expect("discovery");
assert_eq!(discovery.premium_product_name(), Some("Gold"));
assert!(discovery.features.premium_enabled);
let empty: InstancePremiumDiscovery =
serde_json::from_value(json!({})).expect("empty discovery");
assert_eq!(empty.premium_product_name(), None);
assert!(!empty.features.premium_enabled);
assert_eq!(
PremiumBranding::from_discovery(&discovery),
PremiumBranding {
name: Some("Gold".to_owned()),
premium_enabled: true
}
);
}
#[test]
fn premium_branding_from_instance_config_matches_discovery_rules() {
assert_eq!(
PremiumBranding::from_config_parts(true, " Gold ", PremiumMode::Everyone),
PremiumBranding {
name: Some("Gold".to_owned()),
premium_enabled: false
}
);
assert!(
PremiumBranding::from_config_parts(true, "Gold", PremiumMode::Mirror).premium_enabled
);
assert_eq!(
PremiumBranding::from_config_parts(false, " ", PremiumMode::Everyone),
PremiumBranding {
name: None,
premium_enabled: true
}
);
}
}
+214 -155
View File
@@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize};
pub use crate::api::generated::types::VoiceNoiseSuppressionBackendSchema as NoiseSuppressionBackend;
use super::{InstanceBillingResponse, InstanceBillingUpdateRequest};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InstanceConfigResponse {
@@ -21,9 +21,17 @@ pub struct InstanceConfigResponse {
#[serde(default)]
pub media: InstanceMediaResponse,
#[serde(default)]
pub voice_noise_suppression: VoiceNoiseSuppressionConfigResponse,
pub push_relay: PushRelayConfigResponse,
#[serde(default)]
pub domain_migration: DomainMigrationConfigResponse,
#[serde(default)]
pub plutonium_page: PlutoniumPageConfigResponse,
#[serde(default)]
pub captcha: CaptchaConfigResponse,
#[serde(default)]
pub experiment_delivery: ExperimentDeliveryConfigResponse,
#[serde(default)]
pub billing: InstanceBillingResponse,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -37,34 +45,18 @@ pub struct InstancePolicyResponse {
pub direct_messages_locked: bool,
#[serde(default)]
pub premium_mode: PremiumMode,
#[serde(default = "default_guild_create_access")]
pub guild_create_access: bool,
#[serde(default)]
pub services: InstanceServicesOverrides,
#[serde(default)]
pub services_resolved: InstanceServicesResolved,
#[serde(default)]
pub services_available: InstanceServicesAvailable,
#[serde(default)]
pub deferred_phone_gate: DeferredPhoneGateResponse,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DeferredPhoneGateResponse {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub window_hours: f64,
#[serde(default)]
pub member_threshold: i64,
}
impl Default for DeferredPhoneGateResponse {
fn default() -> Self {
Self {
enabled: true,
window_hours: 6.0,
member_threshold: 50,
}
}
fn default_guild_create_access() -> bool {
true
}
impl Default for InstancePolicyResponse {
@@ -75,10 +67,10 @@ impl Default for InstancePolicyResponse {
direct_messages_disabled: false,
direct_messages_locked: false,
premium_mode: PremiumMode::Everyone,
guild_create_access: default_guild_create_access(),
services: InstanceServicesOverrides::default(),
services_resolved: InstanceServicesResolved::default(),
services_available: InstanceServicesAvailable::default(),
deferred_phone_gate: DeferredPhoneGateResponse::default(),
}
}
}
@@ -117,8 +109,6 @@ pub struct InstanceIntegrationsResponse {
#[serde(default)]
pub youtube: InstanceYoutubeIntegrationResponse,
#[serde(default)]
pub captcha: InstanceCaptchaIntegrationResponse,
#[serde(default)]
pub email: InstanceEmailIntegrationResponse,
#[serde(default)]
pub bluesky: InstanceBlueskyIntegrationResponse,
@@ -139,21 +129,6 @@ pub struct InstanceYoutubeIntegrationResponse {
pub effective_available: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct InstanceCaptchaIntegrationResponse {
pub provider: Option<String>,
#[serde(default)]
pub effective_provider: String,
pub hcaptcha_site_key: Option<String>,
#[serde(default)]
pub hcaptcha_secret_key_set: bool,
pub turnstile_site_key: Option<String>,
#[serde(default)]
pub turnstile_secret_key_set: bool,
#[serde(default)]
pub effective_enabled: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct InstanceEmailIntegrationResponse {
pub enabled: Option<bool>,
@@ -326,6 +301,11 @@ pub struct AppBrandingConfigResponse {
pub wordmark_url: Option<String>,
pub favicon_url: Option<String>,
pub theme_color: Option<String>,
pub status_page_url: Option<String>,
pub status_page_incident_history_url: Option<String>,
#[serde(default = "default_premium_product_name")]
pub premium_product_name: String,
pub premium_info_url: Option<String>,
}
impl Default for AppBrandingConfigResponse {
@@ -338,6 +318,10 @@ impl Default for AppBrandingConfigResponse {
wordmark_url: None,
favicon_url: None,
theme_color: None,
status_page_url: None,
status_page_incident_history_url: None,
premium_product_name: default_premium_product_name(),
premium_info_url: None,
}
}
}
@@ -346,6 +330,10 @@ fn default_product_name() -> String {
"Fluxer".to_owned()
}
fn default_premium_product_name() -> String {
"Premium".to_owned()
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AppSetupConfigResponse {
#[serde(default)]
@@ -442,99 +430,152 @@ impl VoiceE2eeScope {
}
}
pub const VOICE_NS_MAX_TARGETED_USERS: usize = 1_000;
pub const VOICE_NS_MAX_GUILD_OVERRIDES: usize = 200;
pub const EXPERIMENT_MAX_TARGETED_USERS: usize = 1_000;
pub const DOMAIN_MIGRATION_DEFAULT_SALT: &str = "domain-migration-v1";
pub const PLUTONIUM_PAGE_DEFAULT_SALT: &str = "plutonium-page-v1";
pub const CAPTCHA_COST_RANGE: std::ops::RangeInclusive<u32> = 1_000..=20_000;
pub const CAPTCHA_MAX_COUNTER_RANGE: std::ops::RangeInclusive<u32> = 100..=20_000;
impl NoiseSuppressionBackend {
pub const ALL: [Self; 7] = [
Self::None,
Self::Standard,
Self::Gate,
Self::Speex,
Self::Rnnoise,
Self::Gtcrn,
Self::DeepFilter,
];
pub fn label(&self) -> &'static str {
match self {
Self::None => "None (pass-through)",
Self::Standard => "Standard (WebRTC)",
Self::Gate => "Noise gate",
Self::Speex => "Speex",
Self::Rnnoise => "RNNoise",
Self::Gtcrn => "GTCRN",
Self::DeepFilter => "DeepFilterNet",
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct PushRelayConfigResponse {
pub relay_consent_accepted: bool,
pub relay_consent_accepted_at: Option<String>,
pub relay_consent_accepted_by: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VoiceNoiseSuppressionGuildOverride {
pub guild_id: String,
pub backend: NoiseSuppressionBackend,
#[derive(Clone, Debug, Default, Serialize)]
pub struct PushRelayConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub relay_consent_accepted: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct VoiceNoiseSuppressionConfigResponse {
pub struct DomainMigrationConfigResponse {
pub enabled: bool,
pub config_version: u64,
pub default_backend: NoiseSuppressionBackend,
pub enabled_backends: Vec<NoiseSuppressionBackend>,
pub allow_user_override: bool,
pub rollout_basis_points: u32,
pub rollout_salt: String,
pub included_user_ids: Vec<String>,
pub included_guild_ids: Vec<String>,
pub include_premium_users: bool,
pub excluded_user_ids: Vec<String>,
pub guild_overrides: Vec<VoiceNoiseSuppressionGuildOverride>,
pub stereo_enabled: bool,
pub suppression_strength: u32,
pub anonymous_rollout_basis_points: u32,
pub standalone_forwarding: bool,
}
impl Default for VoiceNoiseSuppressionConfigResponse {
impl Default for DomainMigrationConfigResponse {
fn default() -> Self {
Self {
enabled: false,
config_version: 0,
default_backend: NoiseSuppressionBackend::Standard,
enabled_backends: NoiseSuppressionBackend::ALL.to_vec(),
allow_user_override: true,
rollout_basis_points: 0,
rollout_salt: "voice-ns-v1".to_owned(),
rollout_salt: DOMAIN_MIGRATION_DEFAULT_SALT.to_owned(),
included_user_ids: Vec::new(),
included_guild_ids: Vec::new(),
include_premium_users: false,
excluded_user_ids: Vec::new(),
guild_overrides: Vec::new(),
stereo_enabled: false,
suppression_strength: 80,
anonymous_rollout_basis_points: 0,
standalone_forwarding: false,
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct VoiceNoiseSuppressionConfigUpdateRequest {
pub struct DomainMigrationConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_backend: Option<NoiseSuppressionBackend>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled_backends: Option<Vec<NoiseSuppressionBackend>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_user_override: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_basis_points: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_salt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_guild_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_premium_users: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guild_overrides: Option<Vec<VoiceNoiseSuppressionGuildOverride>>,
pub anonymous_rollout_basis_points: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stereo_enabled: Option<bool>,
pub standalone_forwarding: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct PlutoniumPageConfigResponse {
pub enabled: bool,
pub config_version: u64,
pub rollout_basis_points: u32,
pub rollout_salt: String,
pub included_user_ids: Vec<String>,
pub included_guild_ids: Vec<String>,
pub include_premium_users: bool,
pub excluded_user_ids: Vec<String>,
}
impl Default for PlutoniumPageConfigResponse {
fn default() -> Self {
Self {
enabled: false,
config_version: 0,
rollout_basis_points: 0,
rollout_salt: PLUTONIUM_PAGE_DEFAULT_SALT.to_owned(),
included_user_ids: Vec::new(),
included_guild_ids: Vec::new(),
include_premium_users: false,
excluded_user_ids: Vec::new(),
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PlutoniumPageConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub suppression_strength: Option<u32>,
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_basis_points: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rollout_salt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub included_guild_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_premium_users: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_user_ids: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct CaptchaConfigResponse {
pub enabled: bool,
pub cost: u32,
pub max_counter: u32,
}
impl Default for CaptchaConfigResponse {
fn default() -> Self {
Self {
enabled: true,
cost: 5_000,
max_counter: 1_000,
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct CaptchaConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_counter: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -651,9 +692,17 @@ pub struct InstanceConfigUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<InstanceMediaUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice_noise_suppression: Option<VoiceNoiseSuppressionConfigUpdateRequest>,
pub push_relay: Option<PushRelayConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_migration: Option<DomainMigrationConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plutonium_page: Option<PlutoniumPageConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captcha: Option<CaptchaConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experiment_delivery: Option<ExperimentDeliveryConfigUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing: Option<InstanceBillingUpdateRequest>,
}
#[derive(Clone, Debug, Default, Serialize)]
@@ -665,21 +714,11 @@ pub struct InstancePolicyUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub direct_messages_disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guild_create_access: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub premium_mode: Option<PremiumMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub services: Option<InstanceServicesUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deferred_phone_gate: Option<DeferredPhoneGateUpdateRequest>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct DeferredPhoneGateUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_hours: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub member_threshold: Option<i64>,
}
#[derive(Clone, Debug, Default, Serialize)]
@@ -699,8 +738,6 @@ pub struct InstanceIntegrationsUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub youtube: Option<InstanceYoutubeIntegrationUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captcha: Option<InstanceCaptchaIntegrationUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<InstanceEmailIntegrationUpdateRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bluesky: Option<InstanceBlueskyIntegrationUpdateRequest>,
@@ -718,20 +755,6 @@ pub struct InstanceYoutubeIntegrationUpdateRequest {
pub api_key: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct InstanceCaptchaIntegrationUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hcaptcha_site_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hcaptcha_secret_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub turnstile_site_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub turnstile_secret_key: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct InstanceEmailIntegrationUpdateRequest {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -858,6 +881,14 @@ pub struct AppBrandingConfigUpdateRequest {
pub favicon_url: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_color: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_page_url: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_page_incident_history_url: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub premium_product_name: Option<Option<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub premium_info_url: Option<Option<String>>,
}
#[derive(Clone, Debug, Default, Serialize)]
@@ -963,40 +994,47 @@ mod tests {
use serde_json::json;
#[test]
fn noise_suppression_backend_choices_use_the_generated_wire_contract() {
assert_eq!(
serde_json::to_value(NoiseSuppressionBackend::ALL).expect("serializable backends"),
json!([
"none",
"standard",
"gate",
"speex",
"rnnoise",
"gtcrn",
"deep_filter"
])
);
assert!(serde_json::from_value::<NoiseSuppressionBackend>(json!("deepfilter")).is_err());
}
#[test]
fn default_instance_experiment_config_matches_the_published_contract() {
fn default_instance_config_sections_match_the_published_contract() {
let schema: serde_json::Value =
serde_json::from_str(include_str!("../../../openapi-admin.json"))
.expect("admin schema");
let noise = serde_json::from_value::<VoiceNoiseSuppressionConfigResponse>(json!({}))
.expect("default noise config");
let domain_migration = serde_json::from_value::<DomainMigrationConfigResponse>(json!({}))
.expect("default domain migration config");
let plutonium_page = serde_json::from_value::<PlutoniumPageConfigResponse>(json!({}))
.expect("default plutonium page config");
let captcha = serde_json::from_value::<CaptchaConfigResponse>(json!({}))
.expect("default captcha config");
let delivery = serde_json::from_value::<ExperimentDeliveryConfigResponse>(json!({}))
.expect("default delivery config");
let noise = serde_json::to_value(noise).expect("serializable noise config");
let domain_migration =
serde_json::to_value(domain_migration).expect("serializable domain migration config");
let plutonium_page =
serde_json::to_value(plutonium_page).expect("serializable plutonium page config");
let captcha = serde_json::to_value(captcha).expect("serializable captcha config");
let delivery = serde_json::to_value(delivery).expect("serializable delivery config");
let generated_noise: generated_types::VoiceNoiseSuppressionConfigResponse =
serde_json::from_value(noise.clone()).expect("generated noise config contract");
let generated_domain_migration: generated_types::DomainMigrationConfigResponse =
serde_json::from_value(domain_migration.clone())
.expect("generated domain migration config contract");
let generated_plutonium_page: generated_types::PlutoniumPageConfigResponse =
serde_json::from_value(plutonium_page.clone())
.expect("generated plutonium page config contract");
let generated_captcha: generated_types::CaptchaConfigResponse =
serde_json::from_value(captcha.clone()).expect("generated captcha config contract");
let generated_delivery: generated_types::ExperimentDeliveryConfigResponse =
serde_json::from_value(delivery.clone()).expect("generated delivery config contract");
assert_eq!(
serde_json::to_value(generated_noise).expect("serializable generated noise config"),
noise
serde_json::to_value(generated_domain_migration)
.expect("serializable generated domain migration config"),
domain_migration
);
assert_eq!(
serde_json::to_value(generated_plutonium_page)
.expect("serializable generated plutonium page config"),
plutonium_page
);
assert_eq!(
serde_json::to_value(generated_captcha).expect("serializable generated captcha config"),
captcha
);
assert_eq!(
serde_json::to_value(generated_delivery)
@@ -1004,7 +1042,9 @@ mod tests {
delivery
);
for (name, value) in [
("VoiceNoiseSuppressionConfigResponse", noise),
("DomainMigrationConfigResponse", domain_migration),
("PlutoniumPageConfigResponse", plutonium_page),
("CaptchaConfigResponse", captcha),
("ExperimentDeliveryConfigResponse", delivery),
] {
for (field, value) in value.as_object().expect("config object") {
@@ -1017,25 +1057,44 @@ mod tests {
}
#[test]
fn noise_suppression_update_preserves_empty_lists_and_omitted_fields() {
let update = VoiceNoiseSuppressionConfigUpdateRequest {
enabled_backends: Some(Vec::new()),
fn domain_migration_update_preserves_empty_lists_and_omitted_fields() {
let update = DomainMigrationConfigUpdateRequest {
included_user_ids: Some(Vec::new()),
excluded_user_ids: Some(Vec::new()),
guild_overrides: Some(Vec::new()),
..Default::default()
};
let value = serde_json::to_value(update).expect("serializable update");
serde_json::from_value::<generated_types::VoiceNoiseSuppressionConfigUpdateRequest>(
serde_json::from_value::<generated_types::DomainMigrationConfigUpdateRequest>(
value.clone(),
)
.expect("generated update contract");
assert_eq!(
value,
json!({"enabled_backends": [], "included_user_ids": [], "excluded_user_ids": [], "guild_overrides": []})
json!({"included_user_ids": [], "excluded_user_ids": []})
);
assert_eq!(
serde_json::to_value(VoiceNoiseSuppressionConfigUpdateRequest::default())
serde_json::to_value(DomainMigrationConfigUpdateRequest::default())
.expect("serializable update"),
json!({})
);
}
#[test]
fn plutonium_page_update_preserves_empty_lists_and_omitted_fields() {
let update = PlutoniumPageConfigUpdateRequest {
included_user_ids: Some(Vec::new()),
excluded_user_ids: Some(Vec::new()),
..Default::default()
};
let value = serde_json::to_value(update).expect("serializable update");
serde_json::from_value::<generated_types::PlutoniumPageConfigUpdateRequest>(value.clone())
.expect("generated update contract");
assert_eq!(
value,
json!({"included_user_ids": [], "excluded_user_ids": []})
);
assert_eq!(
serde_json::to_value(PlutoniumPageConfigUpdateRequest::default())
.expect("serializable update"),
json!({})
);
+2
View File
@@ -9,6 +9,7 @@ mod codes;
mod common;
mod discovery;
mod guild_assets;
mod instance_billing;
mod instance_config;
mod jobs;
mod limit_config;
@@ -28,6 +29,7 @@ pub use codes::*;
pub use common::*;
pub use discovery::*;
pub use guild_assets::*;
pub use instance_billing::*;
pub use instance_config::*;
pub use jobs::*;
pub use limit_config::*;
+1 -1
View File
@@ -4,5 +4,5 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SendSystemDmResponse {
pub recipient_count: i64,
pub recipient_count: Option<i64>,
}
+60 -15
View File
@@ -95,8 +95,8 @@ impl AdminApiClient {
remove_flags: &[String],
) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserFlagsUpdateRequest {
add_flags: user_flags(add_flags),
remove_flags: user_flags(remove_flags),
add_flags: user_flags(add_flags)?,
remove_flags: user_flags(remove_flags)?,
};
let response = self
.generated()
@@ -393,12 +393,14 @@ impl AdminApiClient {
user_id: &str,
duration_hours: u32,
reason: Option<&str>,
notify_user: bool,
private_reason: Option<&str>,
) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserBanRequest {
duration_hours: i32::try_from(duration_hours)
.map_err(|e| ApiError::Parse(e.to_string()))?
.into(),
notify_user,
reason: reason.map(std::borrow::ToOwned::to_owned),
};
let resp: UserMutationResponse = self
@@ -411,10 +413,20 @@ impl AdminApiClient {
Ok(resp.user)
}
pub async fn unban_user(&self, user_id: &str) -> ApiResult<AdminUser> {
pub async fn unban_user(
&self,
user_id: &str,
public_reason: Option<&str>,
notify_user: bool,
private_reason: Option<&str>,
) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserUnbanRequest {
notify_user,
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
};
let response = self
.generated()
.unban_admin_user(&snowflake(user_id))
.generated_with_reason(private_reason)?
.unban_admin_user(&snowflake(user_id), &body)
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
@@ -427,6 +439,7 @@ impl AdminApiClient {
reason_code: i32,
public_reason: Option<&str>,
days_until_deletion: u32,
notify_user: bool,
audit_log_reason: Option<&str>,
) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserDeletionScheduleRequest {
@@ -436,9 +449,11 @@ impl AdminApiClient {
)
.map_err(ApiError::Parse)?
.into(),
notify_user,
public_reason: public_reason.map(std::borrow::ToOwned::to_owned),
reason_code: crate::api::generated::deletion_reason_code(reason_code, "reason_code")
.map_err(ApiError::Parse)?,
replace_pending_deletion_at: None,
};
let response = self
.generated_with_reason(audit_log_reason)?
@@ -449,16 +464,44 @@ impl AdminApiClient {
Ok(resp.user)
}
pub async fn cancel_deletion(&self, user_id: &str) -> ApiResult<AdminUser> {
let response = self
.generated()
.cancel_admin_user_deletion(&snowflake(user_id))
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
pub async fn cancel_deletion(
&self,
user_id: &str,
expected_pending_deletion_at: &str,
notify_user: bool,
audit_log_reason: Option<&str>,
) -> ApiResult<AdminUser> {
let body = serde_json::json!({
"expected_pending_deletion_at": expected_pending_deletion_at,
"notify_user": notify_user,
});
let resp: UserMutationResponse = self
.delete_with_reason(
&format!("/admin/users/{}/deletion", urlencoding::encode(user_id)),
Some(&body),
audit_log_reason,
)
.await?;
Ok(resp.user)
}
pub async fn annotate_ban(
&self,
user_id: &str,
ban_audit_log_id: &str,
note: &str,
) -> ApiResult<()> {
let body = serde_json::json!({
"ban_audit_log_id": ban_audit_log_id,
"note": note,
});
self.post_void(
&format!("/admin/users/{}/ban/notes", urlencoding::encode(user_id)),
Some(&body),
)
.await
}
pub async fn change_dob(&self, user_id: &str, dob: &str) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserDobUpdateRequest {
date_of_birth: dob.to_owned(),
@@ -567,11 +610,13 @@ fn bool_param(value: bool) -> &'static str {
if value { "true" } else { "false" }
}
fn user_flags(values: &[String]) -> Vec<generated_types::UserFlags> {
fn user_flags(values: &[String]) -> ApiResult<Vec<generated_types::UserFlags>> {
values
.iter()
.cloned()
.map(generated_types::UserFlags::from)
.map(|value| {
generated_types::UserFlags::try_from(value.as_str())
.map_err(|error| ApiError::Parse(error.to_string()))
})
.collect()
}
+14 -68
View File
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use fluxer_common::config::normalize_public_endpoint_from_env;
use std::env;
use fluxer_common::config::{
normalize_base_path, normalize_public_endpoint_from_env, read_bool_env, read_env,
read_first_env, trim_trailing_slash,
};
const DEFAULT_ADMIN_OAUTH_CLIENT_ID: &str = "1234567890123456789";
@@ -17,12 +19,10 @@ pub struct AdminConfig {
pub static_cdn_endpoint: String,
pub admin_endpoint: String,
pub web_app_endpoint: String,
pub kv_url: String,
pub oauth_client_id: String,
pub oauth_client_secret: String,
pub oauth_redirect_uri: String,
pub build_version: String,
pub release_channel: String,
pub self_hosted: bool,
pub proxy: ProxyConfig,
}
@@ -47,8 +47,8 @@ impl AdminConfig {
"FLUXER_ADMIN_ENDPOINT",
"https://admin.fluxer.app",
)));
let oauth_redirect_uri = normalize_public_endpoint_from_env(&read_env_preferred(
&["FLUXER_ADMIN_OAUTH_REDIRECT_URI"],
let oauth_redirect_uri = normalize_public_endpoint_from_env(&read_env(
"FLUXER_ADMIN_OAUTH_REDIRECT_URI",
&format!("{admin_endpoint}/oauth2_callback"),
));
let secret_key_base = read_env("FLUXER_ADMIN_SECRET_KEY_BASE", "");
@@ -82,38 +82,22 @@ impl AdminConfig {
"FLUXER_APP_ENDPOINT",
"https://app.fluxer.app",
))),
kv_url: read_env("FLUXER_KV_URL", ""),
oauth_client_id: read_env(
"FLUXER_ADMIN_OAUTH_CLIENT_ID",
DEFAULT_ADMIN_OAUTH_CLIENT_ID,
),
oauth_client_secret: read_env("FLUXER_ADMIN_OAUTH_CLIENT_SECRET", ""),
oauth_redirect_uri,
build_version: read_env_preferred(
build_version: read_first_env(
&["BUILD_VERSION", "FLUXER_BUILD_VERSION"],
env!("CARGO_PKG_VERSION"),
),
release_channel: read_env_preferred(
&["RELEASE_CHANNEL", "FLUXER_RELEASE_CHANNEL"],
"stable",
),
self_hosted: read_bool_env(&["FLUXER_SELF_HOSTED"], false),
self_hosted: read_bool_env("FLUXER_SELF_HOSTED", false),
proxy: ProxyConfig {
trust_client_ip_header: read_bool_env(
&["FLUXER_TRUST_CLIENT_IP_HEADER", "TRUST_CLIENT_IP_HEADER"],
false,
),
client_ip_header_name: read_env_preferred(
&[
"FLUXER_CLIENT_IP_HEADER_NAME",
"FLUXER_CLIENT_IP_HEADER",
"CLIENT_IP_HEADER_NAME",
"CLIENT_IP_HEADER",
],
"x-forwarded-for",
)
.trim()
.to_ascii_lowercase(),
trust_client_ip_header: read_bool_env("FLUXER_TRUST_CLIENT_IP_HEADER", false),
client_ip_header_name: read_env("FLUXER_CLIENT_IP_HEADER_NAME", "x-forwarded-for")
.trim()
.to_ascii_lowercase(),
},
})
}
@@ -146,55 +130,21 @@ impl RuntimeEnv {
}
}
pub fn normalize_base_path(value: &str) -> String {
let trimmed = value.trim().trim_matches('/');
if trimmed.is_empty() {
String::new()
} else {
format!("/{trimmed}")
}
}
pub fn trim_trailing_slash(value: &str) -> String {
value.trim_end_matches('/').to_owned()
}
pub(crate) fn read_env(name: &str, fallback: &str) -> String {
env::var(name).unwrap_or_else(|_| fallback.to_owned())
}
pub(crate) fn read_env_preferred(names: &[&str], fallback: &str) -> String {
names
.iter()
.find_map(|name| env::var(name).ok().filter(|value| !value.trim().is_empty()))
.unwrap_or_else(|| fallback.to_owned())
}
pub(crate) fn read_bool_env(names: &[&str], fallback: bool) -> bool {
let Some(value) = names.iter().find_map(|name| env::var(name).ok()) else {
return fallback;
};
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const MANAGED_ENV: [&str; 11] = [
const MANAGED_ENV: [&str; 10] = [
"FLUXER_ENV",
"FLUXER_ADMIN_HOST",
"FLUXER_ADMIN_PORT",
"FLUXER_ADMIN_ENDPOINT",
"FLUXER_ADMIN_OAUTH_CLIENT_ID",
"FLUXER_ADMIN_OAUTH_REDIRECT_URI",
"FLUXER_MASTER_CONFIG",
"FLUXER_APP_ENDPOINT",
"FLUXER_MEDIA_ENDPOINT",
"FLUXER_STATIC_CDN_ENDPOINT",
@@ -291,12 +241,10 @@ mod tests {
admin_endpoint: String::new(),
web_app_endpoint: String::new(),
kv_url: String::new(),
oauth_client_id: String::new(),
oauth_client_secret: String::new(),
oauth_redirect_uri: String::new(),
build_version: String::new(),
release_channel: String::new(),
self_hosted: false,
proxy: ProxyConfig {
trust_client_ip_header: false,
@@ -321,12 +269,10 @@ mod tests {
admin_endpoint: String::new(),
web_app_endpoint: String::new(),
kv_url: String::new(),
oauth_client_id: String::new(),
oauth_client_secret: String::new(),
oauth_redirect_uri: String::new(),
build_version: String::new(),
release_channel: String::new(),
self_hosted: false,
proxy: ProxyConfig {
trust_client_ip_header: false,
+1
View File
@@ -2,6 +2,7 @@
pub mod acl;
pub mod admin_flags;
pub mod admin_hints;
pub mod api;
pub mod config;
pub mod fonts;
+1 -3
View File
@@ -8,9 +8,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.with(fluxer_common::config::env_filter("info"))
.with(tracing_subscriber::fmt::layer())
.init();
-2
View File
@@ -215,12 +215,10 @@ mod tests {
static_cdn_endpoint: String::new(),
admin_endpoint: admin_endpoint.to_owned(),
web_app_endpoint: String::new(),
kv_url: String::new(),
oauth_client_id: String::new(),
oauth_client_secret: String::new(),
oauth_redirect_uri: String::new(),
build_version: "test".to_owned(),
release_channel: String::new(),
self_hosted: false,
proxy: ProxyConfig {
trust_client_ip_header: false,
-6
View File
@@ -23,10 +23,6 @@ pub fn router() -> Router<AppState> {
Router::new()
.route("/ip-bans", get(ip_bans).post(ip_bans_post))
.route("/email-bans", get(email_bans).post(email_bans_post))
.route(
"/suspicious-email-domains",
get(suspicious_email_domains).post(suspicious_email_domains_post),
)
.route("/phrase-bans", get(phrase_bans).post(phrase_bans_post))
.route("/url-bans", get(url_bans).post(url_bans_post))
.route(
@@ -72,7 +68,6 @@ macro_rules! ban_get {
ban_get!(ip_bans, "ip-bans");
ban_get!(email_bans, "email-bans");
ban_get!(suspicious_email_domains, "suspicious-email-domains");
ban_get!(phrase_bans, "phrase-bans");
ban_get!(url_bans, "url-bans");
ban_get!(file_sha_bans, "file-sha-bans");
@@ -141,7 +136,6 @@ macro_rules! ban_post {
ban_post!(ip_bans_post, "ip-bans");
ban_post!(email_bans_post, "email-bans");
ban_post!(suspicious_email_domains_post, "suspicious-email-domains");
ban_post!(phrase_bans_post, "phrase-bans");
ban_post!(url_bans_post, "url-bans");
ban_post!(file_sha_bans_post, "file-sha-bans");
-11
View File
@@ -116,11 +116,6 @@ async fn execute_single_ban(
let result = match ban_type {
"ip-bans" => client.ban_ip(value, audit_log_reason).await,
"email-bans" => client.ban_email(value, audit_log_reason).await,
"suspicious-email-domains" => {
client
.add_suspicious_email_domain(value, audit_log_reason)
.await
}
"phrase-bans" => client.ban_phrase(value, audit_log_reason).await,
"url-bans" => client.ban_url(value, audit_log_reason).await,
"file-sha-bans" => client.ban_file_sha(value, audit_log_reason).await,
@@ -143,11 +138,6 @@ async fn execute_single_unban(
let result = match ban_type {
"ip-bans" => client.unban_ip(value, audit_log_reason).await,
"email-bans" => client.unban_email(value, audit_log_reason).await,
"suspicious-email-domains" => {
client
.remove_suspicious_email_domain(value, audit_log_reason)
.await
}
"phrase-bans" => client.unban_phrase(value, audit_log_reason).await,
"url-bans" => client.unban_url(value, audit_log_reason).await,
"file-sha-bans" => client.unban_file_sha(value, audit_log_reason).await,
@@ -169,7 +159,6 @@ async fn execute_check(
let result = match ban_type {
"ip-bans" => client.check_ip_ban(value).await,
"email-bans" => client.check_email_ban(value).await,
"suspicious-email-domains" => client.check_suspicious_email_domain(value).await,
"phrase-bans" => client.check_phrase_ban(value).await,
"url-bans" => client.check_url_ban(value).await,
"file-sha-bans" => client.check_file_sha_ban(value).await,
+597
View File
@@ -0,0 +1,597 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::{
api::{
client::ApiError,
types::{
AppBrandingConfigUpdateRequest, AppPublicConfigUpdateRequest,
BILLING_MAX_COUNTRY_CURRENCIES, BILLING_MAX_CURRENCIES,
BILLING_MAX_LEGACY_PRICES_PER_SLOT, BILLING_MAX_LEGACY_SLOTS, BILLING_PRICE_SLOTS,
BillingPriceSet, InstanceBillingUpdateRequest, InstanceConfigUpdateRequest,
PREMIUM_PRODUCT_NAME_MAX_CHARS, TRI_STATE_DEFAULT, TRI_STATE_OFF, TRI_STATE_ON,
},
},
middleware::flash::FlashData,
utils::forms::MultiValueForm,
};
use std::collections::BTreeMap;
const PRICE_ID_MAX_CHARS: usize = 255;
const INFO_URL_MAX_CHARS: usize = 2048;
pub(super) fn build_billing_update(
form: &MultiValueForm,
) -> Result<InstanceConfigUpdateRequest, String> {
let premium_product_name = if form.contains_key("billing_premium_product_name") {
Some(parse_premium_product_name(
form.clean("billing_premium_product_name"),
)?)
} else {
None
};
let premium_info_url = if form.contains_key("billing_premium_info_url") {
Some(parse_info_url(form.clean("billing_premium_info_url"))?)
} else {
None
};
let branding = (premium_product_name.is_some() || premium_info_url.is_some()).then(|| {
AppBrandingConfigUpdateRequest {
premium_product_name,
premium_info_url,
..Default::default()
}
});
let prices = if form.contains_key("billing_price_currency") {
Some(parse_price_rows(form)?)
} else {
None
};
let default_currency = if form.contains_key("billing_default_currency") {
Some(
form.clean("billing_default_currency")
.map(|value| parse_currency(&value))
.transpose()?,
)
} else {
None
};
let country_currencies = if form.contains_key("billing_country_currencies") {
Some(parse_country_currencies(
form.first("billing_country_currencies").unwrap_or(""),
)?)
} else {
None
};
let legacy_prices = if form.contains_key("billing_legacy_prices") {
Some(parse_legacy_prices(
form.first("billing_legacy_prices").unwrap_or(""),
)?)
} else {
None
};
if let Some(Some(prices)) = &prices {
if let Some(Some(currency)) = &default_currency
&& !prices.contains_key(currency)
{
return Err(format!(
"Default currency {currency} has no row in the price table"
));
}
if let Some(Some(countries)) = &country_currencies
&& let Some((country, currency)) = countries
.iter()
.find(|(_, currency)| !prices.contains_key(*currency))
{
return Err(format!(
"{country} maps to {currency}, which has no row in the price table"
));
}
}
Ok(InstanceConfigUpdateRequest {
app_public: branding.map(|branding| AppPublicConfigUpdateRequest {
branding: Some(branding),
..Default::default()
}),
billing: Some(InstanceBillingUpdateRequest {
enabled: parse_tri_state(form, "billing_enabled")?,
stripe_secret_key: secret_update(
form,
"billing_stripe_secret_key",
"billing_clear_stripe_secret_key",
),
stripe_webhook_secret: secret_update(
form,
"billing_stripe_webhook_secret",
"billing_clear_stripe_webhook_secret",
),
default_currency,
prices,
country_currencies,
legacy_prices,
automatic_tax: parse_tri_state(form, "billing_automatic_tax")?,
tax_id_collection: parse_tri_state(form, "billing_tax_id_collection")?,
terms_consent_required: parse_tri_state(form, "billing_terms_consent_required")?,
}),
..Default::default()
})
}
fn parse_tri_state(form: &MultiValueForm, key: &str) -> Result<Option<Option<bool>>, String> {
if !form.contains_key(key) {
return Ok(None);
}
match form.first(key).map(str::trim).unwrap_or("") {
TRI_STATE_DEFAULT => Ok(Some(None)),
TRI_STATE_ON => Ok(Some(Some(true))),
TRI_STATE_OFF => Ok(Some(Some(false))),
other => Err(format!("Invalid choice \"{other}\" for {key}")),
}
}
pub(super) fn billing_result<T>(result: Result<T, ApiError>) -> FlashData {
match result {
Ok(_) => FlashData::success("Premium and billing settings updated"),
Err(error) => {
tracing::warn!(%error, "admin API request failed: update billing config");
match validation_message(&error) {
Some(message) => FlashData::error(format!(
"Failed to update premium and billing settings: {message}"
)),
None => FlashData::error("Failed to update premium and billing settings"),
}
}
}
}
fn validation_message(error: &ApiError) -> Option<String> {
let ApiError::Http {
status: 400,
message,
} = error
else {
return None;
};
let body: serde_json::Value = serde_json::from_str(message).ok()?;
let first = body["errors"].as_array().and_then(|errors| errors.first());
let detail = first.and_then(|error| {
let message = error["message"].as_str()?;
Some(
match error["path"].as_str().filter(|path| !path.is_empty()) {
Some(path) => format!("{path}: {message}"),
None => message.to_owned(),
},
)
});
detail.or_else(|| body["message"].as_str().map(str::to_owned))
}
fn secret_update(form: &MultiValueForm, key: &str, clear_key: &str) -> Option<Option<String>> {
match form.clean(key) {
Some(secret) => Some(Some(secret)),
None if form.bool_value(clear_key) => Some(None),
None => None,
}
}
fn parse_premium_product_name(value: Option<String>) -> Result<Option<String>, String> {
match value {
Some(name) if name.encode_utf16().count() > PREMIUM_PRODUCT_NAME_MAX_CHARS => Err(format!(
"Premium name must be at most {PREMIUM_PRODUCT_NAME_MAX_CHARS} characters"
)),
other => Ok(other),
}
}
fn parse_info_url(value: Option<String>) -> Result<Option<String>, String> {
let Some(value) = value else {
return Ok(None);
};
let valid = value.chars().count() <= INFO_URL_MAX_CHARS
&& url::Url::parse(&value).is_ok_and(|url| {
matches!(url.scheme(), "http" | "https")
&& url.host_str().is_some_and(|h| !h.is_empty())
});
if valid {
Ok(Some(value))
} else {
Err("Premium info URL must be an absolute http or https URL".to_owned())
}
}
fn parse_currency(value: &str) -> Result<String, String> {
let currency = value.trim().to_ascii_uppercase();
if currency.len() == 3 && currency.bytes().all(|byte| byte.is_ascii_uppercase()) {
Ok(currency)
} else {
Err(format!(
"Invalid currency \"{}\": use a 3-letter ISO 4217 code such as GBP",
value.trim()
))
}
}
fn parse_country(value: &str) -> Result<String, String> {
let country = value.trim().to_ascii_uppercase();
if country.len() == 2 && country.bytes().all(|byte| byte.is_ascii_uppercase()) {
Ok(country)
} else {
Err(format!(
"Invalid country \"{}\": use a 2-letter ISO 3166 code such as SE",
value.trim()
))
}
}
fn parse_price_id(value: &str) -> Result<String, String> {
let id = value.trim();
let valid = id.len() <= PRICE_ID_MAX_CHARS
&& id.strip_prefix("price_").is_some_and(|rest| {
!rest.is_empty() && rest.bytes().all(|b| b.is_ascii_alphanumeric())
});
if valid {
Ok(id.to_owned())
} else {
Err(format!(
"Invalid Stripe price ID \"{id}\": it must look like price_1AbC"
))
}
}
fn parse_optional_price_id(value: Option<&String>) -> Result<Option<String>, String> {
match value
.map(|value| value.trim())
.filter(|value| !value.is_empty())
{
Some(id) => parse_price_id(id).map(Some),
None => Ok(None),
}
}
fn parse_price_rows(
form: &MultiValueForm,
) -> Result<Option<BTreeMap<String, BillingPriceSet>>, String> {
let currencies = form.values("billing_price_currency");
let column = |key: &str, index: usize| form.values(key).get(index);
let mut prices = BTreeMap::new();
for (index, currency) in currencies.iter().enumerate() {
if currency.trim().is_empty() {
continue;
}
let currency = parse_currency(currency)?;
let set = BillingPriceSet {
monthly: parse_optional_price_id(column("billing_price_monthly", index))?,
yearly: parse_optional_price_id(column("billing_price_yearly", index))?,
gift_1_month: parse_optional_price_id(column("billing_price_gift_1_month", index))?,
gift_1_year: parse_optional_price_id(column("billing_price_gift_1_year", index))?,
};
if set.is_empty() {
return Err(format!("{currency} needs at least one price ID"));
}
if prices.insert(currency.clone(), set).is_some() {
return Err(format!(
"{currency} appears more than once in the price table"
));
}
}
if prices.len() > BILLING_MAX_CURRENCIES {
return Err(format!(
"The price table holds at most {BILLING_MAX_CURRENCIES} currencies"
));
}
Ok((!prices.is_empty()).then_some(prices))
}
fn key_value_lines(value: &str) -> impl Iterator<Item = Result<(&str, &str), String>> {
value
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| {
line.split_once('=')
.map(|(key, value)| (key.trim(), value.trim()))
.ok_or_else(|| format!("Line \"{line}\" must use the form KEY=VALUE"))
})
}
fn parse_country_currencies(value: &str) -> Result<Option<BTreeMap<String, String>>, String> {
let mut countries = BTreeMap::new();
for line in key_value_lines(value) {
let (country, currency) = line?;
let country = parse_country(country)?;
let currency = parse_currency(currency)?;
if countries.insert(country.clone(), currency).is_some() {
return Err(format!("{country} is mapped more than once"));
}
}
if countries.len() > BILLING_MAX_COUNTRY_CURRENCIES {
return Err(format!(
"At most {BILLING_MAX_COUNTRY_CURRENCIES} country mappings are allowed"
));
}
Ok((!countries.is_empty()).then_some(countries))
}
fn parse_legacy_slot(value: &str) -> Result<String, String> {
let invalid = || {
format!(
"Invalid legacy price slot \"{value}\": use monthly, yearly, gift_1_month or gift_1_year followed by _ and a currency, such as monthly_GBP"
)
};
let (slot, currency) = value.rsplit_once('_').ok_or_else(invalid)?;
let slot = slot.to_ascii_lowercase();
if !BILLING_PRICE_SLOTS.contains(&slot.as_str()) {
return Err(invalid());
}
let currency = parse_currency(currency).map_err(|_| invalid())?;
Ok(format!("{slot}_{currency}"))
}
fn parse_legacy_prices(value: &str) -> Result<Option<BTreeMap<String, Vec<String>>>, String> {
let mut legacy: BTreeMap<String, Vec<String>> = BTreeMap::new();
for line in key_value_lines(value) {
let (slot, ids) = line?;
let slot = parse_legacy_slot(slot)?;
let entry = legacy.entry(slot.clone()).or_default();
for id in ids.split(',').map(str::trim).filter(|id| !id.is_empty()) {
let id = parse_price_id(id)?;
if !entry.contains(&id) {
entry.push(id);
}
}
if entry.is_empty() {
return Err(format!("{slot} needs at least one price ID"));
}
if entry.len() > BILLING_MAX_LEGACY_PRICES_PER_SLOT {
return Err(format!(
"{slot} holds at most {BILLING_MAX_LEGACY_PRICES_PER_SLOT} legacy price IDs"
));
}
}
if legacy.len() > BILLING_MAX_LEGACY_SLOTS {
return Err(format!(
"At most {BILLING_MAX_LEGACY_SLOTS} legacy price slots are allowed"
));
}
Ok((!legacy.is_empty()).then_some(legacy))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::generated::types as generated_types;
use serde_json::json;
fn full_form(extra: &str) -> MultiValueForm {
let base = "billing_premium_product_name=%20Gold%20\
&billing_premium_info_url=https%3A%2F%2Fexample.com%2Fgold\
&billing_enabled=on\
&billing_automatic_tax=default&billing_tax_id_collection=on&billing_terms_consent_required=off\
&billing_stripe_secret_key=\
&billing_stripe_webhook_secret=whsec_new\
&billing_default_currency=gbp\
&billing_price_currency=gbp&billing_price_monthly=price_1GbpM&billing_price_yearly=price_1GbpY\
&billing_price_gift_1_month=&billing_price_gift_1_year=price_1GbpG\
&billing_price_currency=SEK&billing_price_monthly=price_1SekM&billing_price_yearly=price_1SekY\
&billing_price_gift_1_month=&billing_price_gift_1_year=\
&billing_price_currency=&billing_price_monthly=&billing_price_yearly=\
&billing_price_gift_1_month=&billing_price_gift_1_year=\
&billing_country_currencies=se%3Dsek%0D%0AGB%20%3D%20GBP%0D%0A\
&billing_legacy_prices=monthly_GBP%3Dprice_1OldA%0Amonthly_gbp%3Dprice_1OldB%2Cprice_1OldA%0Ayearly_SEK%3Dprice_1OldC";
MultiValueForm::parse(format!("{base}{extra}").as_bytes())
}
#[test]
fn full_billing_form_builds_the_expected_patch() {
let update = build_billing_update(&full_form("")).expect("valid form");
let value = serde_json::to_value(&update).expect("serializable");
serde_json::from_value::<generated_types::InstanceConfigUpdateRequest>(value.clone())
.expect("generated update contract");
assert_eq!(
value,
json!({
"app_public": {
"branding": {
"premium_product_name": "Gold",
"premium_info_url": "https://example.com/gold"
}
},
"billing": {
"enabled": true,
"stripe_webhook_secret": "whsec_new",
"default_currency": "GBP",
"prices": {
"GBP": {
"monthly": "price_1GbpM",
"yearly": "price_1GbpY",
"gift_1_month": null,
"gift_1_year": "price_1GbpG"
},
"SEK": {
"monthly": "price_1SekM",
"yearly": "price_1SekY",
"gift_1_month": null,
"gift_1_year": null
}
},
"country_currencies": {"GB": "GBP", "SE": "SEK"},
"legacy_prices": {
"monthly_GBP": ["price_1OldA", "price_1OldB"],
"yearly_SEK": ["price_1OldC"]
},
"automatic_tax": null,
"tax_id_collection": true,
"terms_consent_required": false
}
})
);
}
#[test]
fn blank_fields_clear_and_the_default_choice_sends_null() {
let form = MultiValueForm::parse(
b"billing_premium_product_name=&billing_premium_info_url=&billing_enabled=default\
&billing_stripe_secret_key=&billing_clear_stripe_secret_key=true\
&billing_stripe_webhook_secret=\
&billing_default_currency=\
&billing_price_currency=&billing_price_monthly=price_1Ignored\
&billing_country_currencies=&billing_legacy_prices=",
);
let value = serde_json::to_value(build_billing_update(&form).expect("valid form")).unwrap();
assert_eq!(
value,
json!({
"app_public": {
"branding": {"premium_product_name": null, "premium_info_url": null}
},
"billing": {
"enabled": null,
"stripe_secret_key": null,
"default_currency": null,
"prices": null,
"country_currencies": null,
"legacy_prices": null
}
})
);
}
#[test]
fn a_new_secret_wins_over_the_clear_checkbox() {
let form = MultiValueForm::parse(
b"billing_stripe_secret_key=%20sk_live_x%20&billing_clear_stripe_secret_key=true",
);
let billing = build_billing_update(&form)
.expect("valid form")
.billing
.expect("billing");
assert_eq!(
billing.stripe_secret_key,
Some(Some("sk_live_x".to_owned()))
);
assert_eq!(billing.stripe_webhook_secret, None);
}
#[test]
fn absent_form_keys_leave_their_fields_untouched() {
let update = build_billing_update(&MultiValueForm::parse(b"billing_enabled=off"))
.expect("valid form");
assert!(update.app_public.is_none());
assert_eq!(
serde_json::to_value(update.billing).unwrap(),
json!({"enabled": false})
);
let untouched = build_billing_update(&MultiValueForm::parse(b"")).expect("valid form");
assert_eq!(serde_json::to_value(untouched.billing).unwrap(), json!({}));
}
#[test]
fn invalid_input_is_rejected_with_a_message() {
let cases: &[(&str, &str)] = &[
(
"billing_premium_info_url=ftp%3A%2F%2Fexample.com",
"http or https",
),
("billing_premium_info_url=example.com", "http or https"),
("billing_default_currency=GB", "Invalid currency"),
("billing_enabled=true", "Invalid choice"),
("billing_automatic_tax=maybe", "Invalid choice"),
(
"billing_price_currency=GBPX&billing_price_monthly=price_1A",
"Invalid currency",
),
(
"billing_price_currency=GBP&billing_price_monthly=prod_1A",
"Invalid Stripe price ID",
),
(
"billing_price_currency=GBP&billing_price_monthly=price_1-A",
"Invalid Stripe price ID",
),
("billing_price_currency=GBP", "needs at least one price ID"),
(
"billing_price_currency=GBP&billing_price_monthly=price_1A&billing_price_currency=gbp&billing_price_monthly=price_1B",
"more than once",
),
("billing_country_currencies=SWE%3DSEK", "Invalid country"),
("billing_country_currencies=SE", "KEY=VALUE"),
(
"billing_country_currencies=SE%3DSEK%0ASE%3DEUR",
"mapped more than once",
),
(
"billing_legacy_prices=weekly_GBP%3Dprice_1A",
"Invalid legacy price slot",
),
(
"billing_legacy_prices=monthly_GBP%3D",
"needs at least one price ID",
),
(
"billing_default_currency=EUR&billing_price_currency=GBP&billing_price_monthly=price_1A",
"Default currency EUR has no row",
),
(
"billing_country_currencies=SE%3DSEK&billing_price_currency=GBP&billing_price_monthly=price_1A",
"SE maps to SEK",
),
];
let long_name = format!("billing_premium_product_name={}", "A".repeat(41));
let emoji_name = format!(
"billing_premium_product_name=Gold{}",
"%F0%9F%92%8E".repeat(20)
);
let long_case = [
(long_name.as_str(), "at most 40"),
(emoji_name.as_str(), "at most 40"),
];
for (body, expected) in long_case.iter().chain(cases.iter()) {
let error =
build_billing_update(&MultiValueForm::parse(body.as_bytes())).expect_err(body);
assert!(error.contains(expected), "{body}: {error}");
}
}
#[test]
fn premium_name_limit_counts_utf16_units() {
let name = format!("{}{}", "A".repeat(39), "\u{1F48E}");
assert_eq!(name.chars().count(), 40);
assert!(parse_premium_product_name(Some(name)).is_err());
let fits = format!("{}{}", "A".repeat(38), "\u{E9}\u{E9}");
assert_eq!(
parse_premium_product_name(Some(fits.clone())),
Ok(Some(fits))
);
}
#[test]
fn country_currencies_are_not_cross_checked_without_a_price_table() {
let form = MultiValueForm::parse(b"billing_country_currencies=SE%3DSEK");
let billing = build_billing_update(&form).unwrap().billing.unwrap();
assert_eq!(
billing.country_currencies,
Some(Some(BTreeMap::from([("SE".to_owned(), "SEK".to_owned())])))
);
}
#[test]
fn validation_errors_surface_the_first_api_message() {
let error = ApiError::Http {
status: 400,
message: json!({
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [{"path": "billing.enabled", "code": "X", "message": "Switch the premium model to mirror first"}]
})
.to_string(),
};
assert_eq!(
validation_message(&error).as_deref(),
Some("billing.enabled: Switch the premium model to mirror first")
);
let server_error = ApiError::Http {
status: 500,
message: "{}".to_owned(),
};
assert_eq!(validation_message(&server_error), None);
}
}
+11 -9
View File
@@ -8,12 +8,15 @@ use crate::{
flash::{self, FlashData},
},
state::AppState,
templates::{self, pages::gift_codes::MAX_GIFT_CODES},
templates::{
self,
pages::gift_codes::{GiftCodesPremium, MAX_GIFT_CODES},
},
};
use axum::{
Form, Router,
extract::{FromRequest, Query, Request, State},
response::{Html, IntoResponse, Redirect, Response},
response::{Html, IntoResponse, Response},
routing::get,
};
use serde::Deserialize;
@@ -46,10 +49,11 @@ async fn gift_codes_page(
Query(query): Query<GiftCodesQuery>,
) -> Response {
let config = state.config();
if config.self_hosted {
return Redirect::to(&format!("{}/dashboard", config.base_path)).into_response();
}
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
let premium = GiftCodesPremium::from_branding(
config.self_hosted,
state.premium_branding(&client).await.as_ref(),
);
let generated_codes: Option<Vec<String>> = query
.codes
@@ -60,6 +64,7 @@ async fn gift_codes_page(
config,
&auth.0,
&csrf.0.0,
&premium,
generated_codes.as_deref(),
);
Html(markup.into_string()).into_response()
@@ -72,9 +77,6 @@ async fn gift_codes_post(
) -> Response {
let config = state.config();
let base = &config.base_path;
if config.self_hosted {
return Redirect::to(&format!("{base}/dashboard")).into_response();
}
let form: GiftCodesForm = match Form::from_request(request, &state).await {
Ok(Form(f)) => f,
Err(error) => {
+2 -1
View File
@@ -121,6 +121,7 @@ pub async fn render(
admin_user_id: None,
target_id: Some(guild_id.to_owned()),
target_type: Some("guild".to_owned()),
access: Some("write".to_owned()),
sort_by: Some("created_at".to_owned()),
sort_order: Some("desc".to_owned()),
limit: 50,
@@ -134,7 +135,7 @@ pub async fn render(
@if let Some(resp) = resp {
@if resp.logs.is_empty() {
p class="text-sm text-neutral-500" {
"No admin audit log entries for this guild."
"No admin write actions have been recorded for this guild."
}
} @else {
(table_container(table(maud::html! {
+4 -1
View File
@@ -171,7 +171,8 @@ pub(crate) async fn system_dms_post(
let flash = if let Some(content) = content.as_deref()
&& !user_ids.is_empty()
{
match client.send_system_dm(&user_ids, content).await {
let recipients = (user_ids != ["*"]).then_some(user_ids.as_slice());
match client.send_system_dm(recipients, content).await {
Ok(_) => FlashData::success("System DM sent"),
Err(error) => {
tracing::warn!(%error, "admin API request failed: send system DM");
@@ -261,12 +262,14 @@ pub(crate) async fn bulk_actions_post(
);
};
let public_reason = form.clean("public_reason");
let notify_user = form.opt_out_value("notify_user");
client
.bulk_schedule_user_deletion(
&user_ids,
reason_code.unwrap_or(2),
days.unwrap_or(14),
public_reason.as_deref(),
notify_user,
audit_log_reason.as_deref(),
)
.await
+1
View File
@@ -5,6 +5,7 @@ pub mod applications;
pub mod auth;
pub mod bans;
mod bans_actions;
mod billing_actions;
pub mod codes;
pub mod discovery;
mod guild_tabs;
+8 -2
View File
@@ -52,6 +52,10 @@ struct ResolveForm {
_csrf: Option<String>,
#[serde(default)]
resolution: Option<String>,
#[serde(default)]
notify_reporter: Option<String>,
#[serde(default)]
notify_reporter_present: Option<String>,
}
pub fn router() -> Router<AppState> {
@@ -80,7 +84,7 @@ async fn reports_list(
return reports_error_page(
config,
&auth.0,
"That page is out of range. The reports search returns at most the first 10000 reports, so narrow the filters and start again.",
"That page is out of range. The reports search returns at most the first 10000 reports. Narrow the filters and start again.",
);
}
let search_query = query.q.as_deref().and_then(clean_string);
@@ -227,8 +231,10 @@ async fn report_resolve(
};
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
let public_comment = clean_string(form.resolution.as_deref().unwrap_or(""));
let notify_reporter =
form.notify_reporter_present.is_none() || form.notify_reporter.as_deref() == Some("true");
let result = client
.resolve_report(&report_id, public_comment.as_deref(), None)
.resolve_report(&report_id, public_comment.as_deref(), notify_reporter, None)
.await;
match result {
Ok(_) => {
+8
View File
@@ -28,6 +28,7 @@ struct AuditLogsQuery {
admin_user_id: Option<String>,
target_id: Option<String>,
target_type: Option<String>,
access: Option<String>,
sort_by: Option<String>,
sort_order: Option<String>,
limit: Option<u32>,
@@ -119,6 +120,7 @@ async fn audit_logs_page(
admin_user_id: query.admin_user_id.as_deref().unwrap_or(""),
target_id: query.target_id.as_deref().unwrap_or(""),
target_type: query.target_type.as_deref().unwrap_or(""),
access: query.access.as_deref().unwrap_or(""),
sort_by: query.sort_by.as_deref().unwrap_or("createdAt"),
sort_order: query.sort_order.as_deref().unwrap_or("desc"),
limit,
@@ -131,6 +133,7 @@ async fn audit_logs_page(
admin_user_id: nonempty(params.admin_user_id),
target_id: nonempty(params.target_id),
target_type: nonempty(params.target_type),
access: nonempty(params.access),
sort_by: Some(params.sort_by.to_owned()),
sort_order: Some(params.sort_order.to_owned()),
limit,
@@ -218,6 +221,11 @@ async fn instance_config_page(
.get_instance_config()
.await
.log_error("load instance config");
if let Some(instance_config) = &instance_config {
state.remember_premium_branding(crate::api::types::PremiumBranding::from_instance_config(
instance_config,
));
}
let limit_config = client
.get_limit_config()
.await
File diff suppressed because it is too large Load Diff

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