@@ -0,0 +1,238 @@
1+{ lib, pkgs, config, ... }:
2+with lib;
3+let
4+ cfg = config.services.lemmy;
5+ settingsFormat = pkgs.formats.json { };
6+in
7+{
8+ meta.maintainers = with maintainers; [ happysalada ];
9+ # Don't edit the docbook xml directly, edit the md and generate it:
10+ # `pandoc lemmy.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > lemmy.xml`
11+ meta.doc = ./lemmy.xml;
12+
13+ options.services.lemmy = {
14+
15+ enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
16+
17+ jwtSecretPath = mkOption {
18+ type = types.path;
19+ description = "Path to read the jwt secret from.";
20+ };
21+
22+ ui = {
23+ port = mkOption {
24+ type = types.port;
25+ default = 1234;
26+ description = "Port where lemmy-ui should listen for incoming requests.";
27+ };
28+ };
29+
30+ caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
31+
32+ settings = mkOption {
33+ default = { };
34+ description = "Lemmy configuration";
35+
36+ type = types.submodule {
37+ freeformType = settingsFormat.type;
38+
39+ options.hostname = mkOption {
40+ type = types.str;
41+ default = null;
42+ description = "The domain name of your instance (eg 'lemmy.ml').";
43+ };
44+
45+ options.port = mkOption {
46+ type = types.port;
47+ default = 8536;
48+ description = "Port where lemmy should listen for incoming requests.";
49+ };
50+
51+ options.federation = {
52+ enabled = mkEnableOption "activitypub federation";
53+ };
54+
55+ options.captcha = {
56+ enabled = mkOption {
57+ type = types.bool;
58+ default = true;
59+ description = "Enable Captcha.";
60+ };
61+ difficulty = mkOption {
62+ type = types.enum [ "easy" "medium" "hard" ];
63+ default = "medium";
64+ description = "The difficultly of the captcha to solve.";
65+ };
66+ };
67+
68+ options.database.createLocally = mkEnableOption "creation of database on the instance";
69+
70+ };
71+ };
72+
73+ };
74+
75+ config =
76+ let
77+ localPostgres = (cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql");
78+ in
79+ lib.mkIf cfg.enable {
80+ services.lemmy.settings = (mapAttrs (name: mkDefault)
81+ {
82+ bind = "127.0.0.1";
83+ tls_enabled = true;
84+ pictrs_url = with config.services.pict-rs; "http://${address}:${toString port}";
85+ actor_name_max_length = 20;
86+
87+ rate_limit.message = 180;
88+ rate_limit.message_per_second = 60;
89+ rate_limit.post = 6;
90+ rate_limit.post_per_second = 600;
91+ rate_limit.register = 3;
92+ rate_limit.register_per_second = 3600;
93+ rate_limit.image = 6;
94+ rate_limit.image_per_second = 3600;
95+ } // {
96+ database = mapAttrs (name: mkDefault) {
97+ user = "lemmy";
98+ host = "/run/postgresql";
99+ port = 5432;
100+ database = "lemmy";
101+ pool_size = 5;
102+ };
103+ });
104+
105+ services.postgresql = mkIf localPostgres {
106+ enable = mkDefault true;
107+ };
108+
109+ services.pict-rs.enable = true;
110+
111+ services.caddy = mkIf cfg.caddy.enable {
112+ enable = mkDefault true;
113+ virtualHosts."${cfg.settings.hostname}" = {
114+ extraConfig = ''
115+ handle_path /static/* {
116+ root * ${pkgs.lemmy-ui}/dist
117+ file_server
118+ }
119+ @for_backend {
120+ path /api/* /pictrs/* feeds/* nodeinfo/*
121+ }
122+ handle @for_backend {
123+ reverse_proxy 127.0.0.1:${toString cfg.settings.port}
124+ }
125+ @post {
126+ method POST
127+ }
128+ handle @post {
129+ reverse_proxy 127.0.0.1:${toString cfg.settings.port}
130+ }
131+ @jsonld {
132+ header Accept "application/activity+json"
133+ header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
134+ }
135+ handle @jsonld {
136+ reverse_proxy 127.0.0.1:${toString cfg.settings.port}
137+ }
138+ handle {
139+ reverse_proxy 127.0.0.1:${toString cfg.ui.port}
140+ }
141+ '';
142+ };
143+ };
144+
145+ assertions = [{
146+ assertion = cfg.settings.database.createLocally -> localPostgres;
147+ message = "if you want to create the database locally, you need to use a local database";
148+ }];
149+
150+ systemd.services.lemmy = {
151+ description = "Lemmy server";
152+
153+ environment = {
154+ LEMMY_CONFIG_LOCATION = "/run/lemmy/config.hjson";
155+
156+ # Verify how this is used, and don't put the password in the nix store
157+ LEMMY_DATABASE_URL = with cfg.settings.database;"postgres:///${database}?host=${host}";
158+ };
159+
160+ documentation = [
161+ "https://join-lemmy.org/docs/en/administration/from_scratch.html"
162+ "https://join-lemmy.org/docs"
163+ ];
164+
165+ wantedBy = [ "multi-user.target" ];
166+
167+ after = [ "pict-rs.service " ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
168+
169+ requires = lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
170+
171+ # script is needed here since loadcredential is not accessible on ExecPreStart
172+ script = ''
173+ ${pkgs.coreutils}/bin/install -m 600 ${settingsFormat.generate "config.hjson" cfg.settings} /run/lemmy/config.hjson
174+ jwtSecret="$(< $CREDENTIALS_DIRECTORY/jwt_secret )"
175+ ${pkgs.jq}/bin/jq ".jwt_secret = \"$jwtSecret\"" /run/lemmy/config.hjson | ${pkgs.moreutils}/bin/sponge /run/lemmy/config.hjson
176+ ${pkgs.lemmy-server}/bin/lemmy_server
177+ '';
178+
179+ serviceConfig = {
180+ DynamicUser = true;
181+ RuntimeDirectory = "lemmy";
182+ LoadCredential = "jwt_secret:${cfg.jwtSecretPath}";
183+ };
184+ };
185+
186+ systemd.services.lemmy-ui = {
187+ description = "Lemmy ui";
188+
189+ environment = {
190+ LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
191+ LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
192+ LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
193+ LEMMY_HTTPS = "false";
194+ };
195+
196+ documentation = [
197+ "https://join-lemmy.org/docs/en/administration/from_scratch.html"
198+ "https://join-lemmy.org/docs"
199+ ];
200+
201+ wantedBy = [ "multi-user.target" ];
202+
203+ after = [ "lemmy.service" ];
204+
205+ requires = [ "lemmy.service" ];
206+
207+ serviceConfig = {
208+ DynamicUser = true;
209+ WorkingDirectory = "${pkgs.lemmy-ui}";
210+ ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.lemmy-ui}/dist/js/server.js";
211+ };
212+ };
213+
214+ systemd.services.lemmy-postgresql = mkIf cfg.settings.database.createLocally {
215+ description = "Lemmy postgresql db";
216+ after = [ "postgresql.service" ];
217+ bindsTo = [ "postgresql.service" ];
218+ requiredBy = [ "lemmy.service" ];
219+ partOf = [ "lemmy.service" ];
220+ script = with cfg.settings.database; ''
221+ PSQL() {
222+ ${config.services.postgresql.package}/bin/psql --port=${toString cfg.settings.database.port} "$@"
223+ }
224+ # check if the database already exists
225+ if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${database} ; then
226+ PSQL -tAc "CREATE ROLE ${user} WITH LOGIN;"
227+ PSQL -tAc "CREATE DATABASE ${database} WITH OWNER ${user};"
228+ fi
229+ '';
230+ serviceConfig = {
231+ User = config.services.postgresql.superUser;
232+ Type = "oneshot";
233+ RemainAfterExit = true;
234+ };
235+ };
236+ };
237+
238+}