1{ lib, pkgs, config, ... }:
2with lib;
3let
4 cfg = config.services.lemmy;
5 settingsFormat = pkgs.formats.json { };
6in
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 partOf = [ "lemmy.service" ];
218 script = with cfg.settings.database; ''
219 PSQL() {
220 ${config.services.postgresql.package}/bin/psql --port=${toString cfg.settings.database.port} "$@"
221 }
222 # check if the database already exists
223 if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${database} ; then
224 PSQL -tAc "CREATE ROLE ${user} WITH LOGIN;"
225 PSQL -tAc "CREATE DATABASE ${database} WITH OWNER ${user};"
226 fi
227 '';
228 serviceConfig = {
229 User = config.services.postgresql.superUser;
230 Type = "oneshot";
231 RemainAfterExit = true;
232 };
233 };
234 };
235
236}