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 root = lib.mkOption {
108 type = lib.types.path;
109 default = "${cfg.dataDir}/static/data";
110 defaultText = lib.literalExpression ''
111 "''${config.services.omnom.dataDir}/static/data"
112 '';
113 description = "Where the snapshots are saved.";
114 };
115 };
116 smtp = {
117 tls = lib.mkEnableOption "Whether TLS encryption should be used.";
118 tls_allow_insecure = lib.mkEnableOption "Whether to allow insecure TLS.";
119 host = lib.mkOption {
120 type = lib.types.str;
121 default = "";
122 description = "SMTP server hostname.";
123 };
124 port = lib.mkOption {
125 type = lib.types.port;
126 default = 25;
127 description = "SMTP server port address.";
128 };
129 sender = lib.mkOption {
130 type = lib.types.str;
131 default = "Omnom <omnom@127.0.0.1>";
132 description = "Omnom sender e-mail.";
133 };
134 send_timeout = lib.mkOption {
135 type = lib.types.int;
136 default = 10;
137 description = "Send timeout duration in seconds.";
138 };
139 connection_timeout = lib.mkOption {
140 type = lib.types.int;
141 default = 5;
142 description = "Connection timeout duration in seconds.";
143 };
144 };
145 };
146 };
147 default = { };
148 };
149 };
150 };
151
152 config = lib.mkIf cfg.enable {
153 assertions = [
154 {
155 assertion = !lib.hasAttr "password" cfg.settings.smtp;
156 message = ''
157 `services.omnom.settings.smtp.password` must be defined in `services.omnom.passwordFile`.
158 '';
159 }
160 {
161 assertion = !(cfg.settings.storage.root != "${cfg.dataDir}/static/data");
162 message = ''
163 For Omnom to access the snapshots, it needs the storage root
164 directory to be inside the service's working directory.
165
166 As such, `services.omnom.settings.storage.root` must be the same as
167 `''${services.omnom.dataDir}/static/data`.
168 '';
169 }
170 ];
171
172 systemd.services.omnom = {
173 path = with pkgs; [
174 yq-go # needed by startup script
175 ];
176
177 serviceConfig = {
178 User = cfg.user;
179 Group = cfg.group;
180 StateDirectory = "omnom";
181 WorkingDirectory = cfg.dataDir;
182 Restart = "on-failure";
183 RestartSec = "10s";
184 LoadCredential = lib.optional (cfg.passwordFile != null) "PASSWORD_FILE:${cfg.passwordFile}";
185 };
186 script = ''
187 install -m 600 ${configFile} $STATE_DIRECTORY/config.yml
188
189 ${lib.optionalString (cfg.passwordFile != null) ''
190 # merge password into main config
191 yq -i '.smtp.password = load(env(CREDENTIALS_DIRECTORY) + "/PASSWORD_FILE")' \
192 "$STATE_DIRECTORY/config.yml"
193 ''}
194
195 ${lib.getExe cfg.package} listen --config "$STATE_DIRECTORY/config.yml"
196 '';
197 after = [
198 "network.target"
199 "systemd-tmpfiles-setup.service"
200 ];
201 wantedBy = [ "multi-user.target" ];
202 };
203
204 # TODO: The program needs to run from the dataDir for it the work, which
205 # is difficult to do with a DynamicUser.
206 # After this has been fixed upstream, remove this and use DynamicUser, instead.
207 # See: https://github.com/asciimoo/omnom/issues/21
208 users = {
209 users = lib.mkIf (cfg.user == "omnom") {
210 omnom = {
211 group = cfg.group;
212 home = cfg.dataDir;
213 isSystemUser = true;
214 };
215 };
216 groups = lib.mkIf (cfg.group == "omnom") { omnom = { }; };
217 };
218
219 systemd.tmpfiles.settings."10-omnom" =
220 let
221 settings = {
222 inherit (cfg) user group;
223 };
224 in
225 {
226 "${cfg.dataDir}"."d" = settings;
227 "${cfg.dataDir}/templates"."L+" = settings // {
228 argument = "${cfg.package}/share/templates";
229 };
230 "${cfg.settings.storage.root}"."d" = settings;
231 };
232
233 networking.firewall = lib.mkIf cfg.openFirewall {
234 allowedTCPPorts = [ cfg.port ];
235 };
236
237 environment.systemPackages =
238 let
239 omnom-wrapped = pkgs.writeScriptBin "omnom" ''
240 #! ${pkgs.runtimeShell}
241 cd ${cfg.dataDir}
242 sudo=exec
243 if [[ "$USER" != ${cfg.user} ]]; then
244 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
245 fi
246 $sudo ${lib.getExe cfg.package} "$@"
247 '';
248 in
249 [ omnom-wrapped ];
250 };
251}