1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.omnom;
9 settingsFormat = pkgs.formats.yaml { };
10
11 configFile = settingsFormat.generate "omnom-config.yml" cfg.settings;
12in
13{
14 options = {
15 services.omnom = {
16 enable = lib.mkEnableOption "Omnom, a webpage bookmarking and snapshotting service";
17 package = lib.mkPackageOption pkgs "omnom" { };
18
19 dataDir = lib.mkOption {
20 type = lib.types.path;
21 default = "/var/lib/omnom";
22 description = "The directory where Omnom stores its data files.";
23 };
24
25 port = lib.mkOption {
26 type = lib.types.port;
27 default = 7331;
28 description = "The Omnom service port.";
29 };
30
31 openFirewall = lib.mkOption {
32 type = lib.types.bool;
33 default = false;
34 description = "Whether to open ports in the firewall.";
35 };
36
37 user = lib.mkOption {
38 type = lib.types.nonEmptyStr;
39 default = "omnom";
40 description = "The Omnom service user.";
41 };
42
43 group = lib.mkOption {
44 type = lib.types.nonEmptyStr;
45 default = "omnom";
46 description = "The Omnom service group.";
47 };
48
49 passwordFile = lib.mkOption {
50 type = lib.types.nullOr lib.types.path;
51 default = null;
52 description = "File containing the password for the SMTP user.";
53 };
54
55 settings = lib.mkOption {
56 description = ''
57 Configuration options for the /etc/omnom/config.yml file.
58 '';
59 type = lib.types.submodule {
60 freeformType = settingsFormat.type;
61 options = {
62 app = {
63 debug = lib.mkEnableOption "debug mode";
64 disable_signup = lib.mkEnableOption "restricting user creation";
65 results_per_page = lib.mkOption {
66 type = lib.types.int;
67 default = 20;
68 description = "Number of results per page.";
69 };
70 };
71 db = {
72 connection = lib.mkOption {
73 type = lib.types.str;
74 default = "${cfg.dataDir}/db.sqlite3";
75 description = "Database connection URI.";
76 defaultText = lib.literalExpression ''
77 "''${config.services.omnom.dataDir}/db.sqlite3"
78 '';
79 };
80 type = lib.mkOption {
81 type = lib.types.enum [ "sqlite" ];
82 default = "sqlite";
83 description = "Database type.";
84 };
85 };
86 server = {
87 address = lib.mkOption {
88 type = lib.types.str;
89 default = "127.0.0.1:${toString cfg.port}";
90 description = "Server address.";
91 defaultText = lib.literalExpression ''
92 "127.0.0.1:''${config.services.omnom.port}"
93 '';
94 };
95 secure_cookie = lib.mkOption {
96 type = lib.types.bool;
97 default = true;
98 description = "Whether to limit cookies to a secure channel.";
99 };
100 };
101 storage = {
102 type = lib.mkOption {
103 type = lib.types.str;
104 default = "fs";
105 description = "Storage type.";
106 };
107 };
108 smtp = {
109 tls = lib.mkEnableOption "Whether TLS encryption should be used.";
110 tls_allow_insecure = lib.mkEnableOption "Whether to allow insecure TLS.";
111 host = lib.mkOption {
112 type = lib.types.str;
113 default = "";
114 description = "SMTP server hostname.";
115 };
116 port = lib.mkOption {
117 type = lib.types.port;
118 default = 25;
119 description = "SMTP server port address.";
120 };
121 sender = lib.mkOption {
122 type = lib.types.str;
123 default = "Omnom <omnom@127.0.0.1>";
124 description = "Omnom sender e-mail.";
125 };
126 send_timeout = lib.mkOption {
127 type = lib.types.int;
128 default = 10;
129 description = "Send timeout duration in seconds.";
130 };
131 connection_timeout = lib.mkOption {
132 type = lib.types.int;
133 default = 5;
134 description = "Connection timeout duration in seconds.";
135 };
136 };
137 activitypub = {
138 pubkey = lib.mkOption {
139 type = lib.types.path;
140 default = "${cfg.dataDir}/public.pem";
141 defaultText = lib.literalExpression ''
142 "''${config.services.omnom.dataDir}/public.pem"
143 '';
144 description = "ActivityPub public key. Will be generated, by default.";
145 };
146 privkey = lib.mkOption {
147 type = lib.types.path;
148 default = "${cfg.dataDir}/private.pem";
149 defaultText = lib.literalExpression ''
150 "''${config.services.omnom.dataDir}/private.pem"
151 '';
152 description = "ActivityPub private key. Will be generated, by default.";
153 };
154 };
155 };
156 };
157 default = { };
158 };
159 };
160 };
161
162 config = lib.mkIf cfg.enable {
163 services.omnom = {
164 settings.app = {
165 static_dir = "${cfg.dataDir}/static";
166 template_dir = "${cfg.package}/share/templates";
167 };
168 };
169
170 assertions = [
171 {
172 assertion = !lib.hasAttr "password" cfg.settings.smtp;
173 message = ''
174 `services.omnom.settings.smtp.password` must be defined in `services.omnom.passwordFile`.
175 '';
176 }
177 ];
178
179 systemd.services.omnom = {
180 path = with pkgs; [
181 yq-go # needed by startup script
182 ];
183
184 serviceConfig = {
185 User = cfg.user;
186 Group = cfg.group;
187 StateDirectory = "omnom";
188 WorkingDirectory = cfg.dataDir;
189 Restart = "on-failure";
190 RestartSec = "10s";
191 LoadCredential = lib.optional (cfg.passwordFile != null) "PASSWORD_FILE:${cfg.passwordFile}";
192 };
193 script = ''
194 install -m 600 ${configFile} $STATE_DIRECTORY/config.yml
195
196 ${lib.optionalString (cfg.passwordFile != null) ''
197 # merge password into main config
198 yq -i '.smtp.password = load(env(CREDENTIALS_DIRECTORY) + "/PASSWORD_FILE")' \
199 "$STATE_DIRECTORY/config.yml"
200 ''}
201
202 ${lib.getExe cfg.package} listen --config "$STATE_DIRECTORY/config.yml"
203 '';
204 after = [
205 "network.target"
206 "systemd-tmpfiles-setup.service"
207 ];
208 wantedBy = [ "multi-user.target" ];
209 };
210
211 # TODO: The program needs to run from the dataDir for it the work, which
212 # is difficult to do with a DynamicUser.
213 # After this has been fixed upstream, remove this and use DynamicUser, instead.
214 # See: https://github.com/asciimoo/omnom/issues/21
215 users = {
216 users = lib.mkIf (cfg.user == "omnom") {
217 omnom = {
218 group = cfg.group;
219 home = cfg.dataDir;
220 isSystemUser = true;
221 };
222 };
223 groups = lib.mkIf (cfg.group == "omnom") { omnom = { }; };
224 };
225
226 systemd.tmpfiles.settings."10-omnom" =
227 let
228 settings = {
229 inherit (cfg) user group;
230 };
231 in
232 {
233 "${cfg.dataDir}"."d" = settings;
234 "${cfg.settings.app.static_dir}"."C" = settings // {
235 argument = "${cfg.package}/share/static";
236 };
237 "${cfg.settings.app.static_dir}/data"."d" = settings;
238 };
239
240 networking.firewall = lib.mkIf cfg.openFirewall {
241 allowedTCPPorts = [ cfg.port ];
242 };
243
244 environment.systemPackages =
245 let
246 omnom-wrapped = pkgs.writeScriptBin "omnom" ''
247 #! ${pkgs.runtimeShell}
248 cd ${cfg.dataDir}
249 sudo=exec
250 if [[ "$USER" != ${cfg.user} ]]; then
251 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
252 fi
253 $sudo ${lib.getExe cfg.package} "$@"
254 '';
255 in
256 [ omnom-wrapped ];
257 };
258}