1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.sharkey;
10
11 settingsFormat = pkgs.formats.yaml { };
12 configFile = settingsFormat.generate "config.yml" cfg.settings;
13in
14{
15 options.services.sharkey =
16 let
17 inherit (lib)
18 mkEnableOption
19 mkOption
20 mkPackageOption
21 types
22 ;
23 in
24 {
25 enable = mkEnableOption "Sharkey, a Sharkish microblogging platform";
26 package = mkPackageOption pkgs "sharkey" { };
27
28 environmentFiles = mkOption {
29 type = types.listOf types.path;
30 default = [ ];
31 example = [ "/run/secrets/sharkey-env" ];
32 description = ''
33 List of paths to files containing environment variables for Sharkey to use at runtime.
34
35 This is useful for keeping secrets out of the Nix store. See
36 <https://docs.joinsharkey.org/docs/install/configuration/> for how to configure Sharkey using environment
37 variables.
38 '';
39 };
40
41 openFirewall = mkOption {
42 type = types.bool;
43 default = false;
44 example = true;
45 description = ''
46 Whether to open ports in the NixOS firewall for Sharkey.
47 '';
48 };
49
50 setupMeilisearch = mkOption {
51 type = types.bool;
52 default = false;
53 example = true;
54 description = ''
55 Whether to automatically set up a local Meilisearch instance and configure Sharkey to use it.
56
57 You need to ensure `services.meilisearch.masterKeyFile` is correctly configured for a working
58 Meilisearch setup. You also need to configure Sharkey to use an API key obtained from Meilisearch with the
59 `MK_CONFIG_MEILISEARCH_APIKEY` environment variable, and set `services.sharkey.settings.meilisearch.index` to
60 the created index. See <https://docs.joinsharkey.org/docs/customisation/search/meilisearch/> for how to create
61 an API key and index.
62 '';
63 };
64
65 setupPostgresql = mkOption {
66 type = types.bool;
67 default = true;
68 example = false;
69 description = ''
70 Whether to automatically set up a local PostgreSQL database and configure Sharkey to use it.
71 '';
72 };
73
74 setupRedis = mkOption {
75 type = types.bool;
76 default = true;
77 example = false;
78 description = ''
79 Whether to automatically set up a local Redis cache and configure Sharkey to use it.
80 '';
81 };
82
83 settings = mkOption {
84 type = types.submodule {
85 freeformType = settingsFormat.type;
86 options = {
87 url = mkOption {
88 type = types.str;
89 example = "https://blahaj.social/";
90 description = ''
91 The full URL that the Sharkey instance will be publically accessible on.
92
93 Do NOT change this after initial setup!
94 '';
95 };
96
97 port = mkOption {
98 type = types.port;
99 default = 3000;
100 description = ''
101 The port that Sharkey will listen on.
102 '';
103 };
104
105 address = mkOption {
106 type = types.str;
107 default = "0.0.0.0";
108 example = "127.0.0.1";
109 description = ''
110 The address that Sharkey binds to.
111 '';
112 };
113
114 socket = mkOption {
115 type = types.nullOr types.path;
116 default = null;
117 example = "/run/sharkey/sharkey.sock";
118 description = ''
119 If specified, creates a UNIX socket at the given path that Sharkey listens on.
120 '';
121 };
122
123 mediaDirectory = mkOption {
124 type = types.path;
125 default = "/var/lib/sharkey";
126 description = ''
127 Path to the folder where Sharkey stores uploaded media such as images and attachments.
128 '';
129 };
130
131 fulltextSearch.provider = mkOption {
132 type = types.enum [
133 "sqlLike"
134 "sqlPgroonga"
135 "sqlTsvector"
136 "meilisearch"
137 ];
138 default = "sqlLike";
139 example = "sqlPgroonga";
140 description = ''
141 Which provider to use for full text search.
142
143 All options other than `sqlLike` require extra setup - see the comments in
144 <https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml> for details.
145
146 If `sqlPgroonga` is set, and `services.sharkey.setupPostgres` is `true`, the pgroonga extension will
147 automatically be setup. You still need to create an index manually.
148
149 If using Meilisearch, consider setting `services.sharkey.setupMeilisearch` instead, which will
150 configure Meilisearch for you.
151 '';
152 };
153
154 id = mkOption {
155 type = types.enum [
156 "aid"
157 "aidx"
158 "meid"
159 "ulid"
160 "objectid"
161 ];
162 default = "aidx";
163 description = ''
164 The ID generation method for Sharkey to use.
165
166 Do NOT change this after initial setup!
167 '';
168 };
169 };
170 };
171 default = { };
172 description = ''
173 Configuration options for Sharkey.
174
175 See <https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/.config/example.yml> for a list of all
176 available configuration options.
177 '';
178 };
179 };
180
181 config =
182 let
183 inherit (lib) mkDefault mkIf mkMerge;
184 in
185 mkIf cfg.enable (mkMerge [
186 {
187 systemd.services.sharkey = {
188 description = "Sharkey";
189 documentation = [ "https://docs.joinsharkey.org/" ];
190 wantedBy = [ "multi-user.target" ];
191 startLimitBurst = 5;
192 startLimitIntervalSec = 60;
193 environment.MISSKEY_CONFIG_DIR = "/etc/sharkey";
194
195 serviceConfig = {
196 Type = "simple";
197 ExecStart = "${lib.getExe cfg.package} migrateandstart";
198 EnvironmentFile = cfg.environmentFiles;
199 DynamicUser = true;
200 TimeoutSec = 60;
201 Restart = "always";
202 SyslogIdentifier = "sharkey";
203 ConfigurationDirectory = "sharkey";
204 RuntimeDirectory = "sharkey";
205 StateDirectory = "sharkey";
206 CapabilityBoundingSet = "";
207 LockPersonality = true;
208 NoNewPrivileges = true;
209 PrivateDevices = true;
210 PrivateUsers = true;
211 PrivateTmp = true;
212 ProcSubset = "pid";
213 ProtectClock = true;
214 ProtectControlGroups = true;
215 ProtectHome = true;
216 ProtectHostname = true;
217 ProtectKernelLogs = true;
218 ProtectKernelModules = true;
219 ProtectKernelTunables = true;
220 ProtectProc = "invisible";
221 ProtectSystem = "strict";
222 ReadWritePaths = [ cfg.settings.mediaDirectory ];
223 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
224 RestrictNamespaces = true;
225 RestrictRealtime = true;
226 SystemCallArchitectures = "native";
227 SystemCallFilter = [
228 "~@cpu-emulation @debug @mount @obsolete @privileged @resources"
229 "@chown"
230 ];
231 UMask = "0077";
232 };
233 };
234
235 environment.etc."sharkey/default.yml".source = configFile;
236 }
237 (mkIf cfg.openFirewall {
238 networking.firewall.allowedTCPPorts = [ cfg.settings.port ];
239 })
240 (mkIf cfg.setupMeilisearch {
241 services.meilisearch = {
242 enable = mkDefault true;
243 settings.env = mkDefault "production";
244 };
245
246 services.sharkey.settings = {
247 fulltextSearch.provider = "meilisearch";
248 meilisearch = {
249 host = config.services.meilisearch.listenAddress;
250 port = config.services.meilisearch.listenPort;
251 };
252 };
253
254 systemd.services.sharkey = {
255 after = [ "meilisearch.service" ];
256 wants = [ "meilisearch.service" ];
257 };
258 })
259 (mkIf cfg.setupPostgresql {
260 services.postgresql = {
261 enable = mkDefault true;
262 ensureDatabases = [ "sharkey" ];
263 ensureUsers = [
264 {
265 name = "sharkey";
266 ensureDBOwnership = true;
267 }
268 ];
269
270 extensions = mkIf (cfg.settings.fulltextSearch.provider == "sqlPgroonga") (ps: [ ps.pgroonga ]);
271 };
272
273 services.sharkey.settings.db = {
274 host = "/run/postgresql";
275 db = "sharkey";
276 };
277
278 systemd.services.sharkey = {
279 after = [ "postgresql.target" ];
280 bindsTo = [ "postgresql.target" ];
281 };
282 })
283 (mkIf cfg.setupRedis {
284 services.redis.servers.sharkey.enable = mkDefault true;
285
286 services.sharkey.settings.redis.path = config.services.redis.servers.sharkey.unixSocket;
287
288 systemd.services.sharkey = {
289 after = [ "redis-sharkey.service" ];
290 bindsTo = [ "redis-sharkey.service" ];
291
292 serviceConfig.SupplementaryGroups = [
293 config.services.redis.servers.sharkey.group
294 ];
295 };
296 })
297 ]);
298
299 meta.maintainers = with lib.maintainers; [
300 tmarkus
301 ];
302}