1{ pkgs, ... }:
2let
3 homeserverDomain = "server";
4 homeserverUrl = "http://server:8008";
5 userName = "alice";
6 botUserName = "instagrambot";
7
8 asToken = "this-is-my-totally-randomly-generated-as-token";
9 hsToken = "this-is-my-totally-randomly-generated-hs-token";
10in
11{
12 name = "mautrix-meta-postgres";
13 meta.maintainers = pkgs.mautrix-meta.meta.maintainers;
14
15 nodes = {
16 server =
17 { config, pkgs, ... }:
18 {
19 services.postgresql = {
20 enable = true;
21
22 ensureUsers = [
23 {
24 name = "mautrix-meta-instagram";
25 ensureDBOwnership = true;
26 }
27 ];
28
29 ensureDatabases = [
30 "mautrix-meta-instagram"
31 ];
32 };
33
34 systemd.services.mautrix-meta-instagram = {
35 wants = [ "postgres.service" ];
36 after = [ "postgres.service" ];
37 };
38
39 services.matrix-synapse = {
40 enable = true;
41 settings = {
42 database.name = "sqlite3";
43
44 enable_registration = true;
45
46 # don't use this in production, always use some form of verification
47 enable_registration_without_verification = true;
48
49 listeners = [
50 {
51 # The default but tls=false
52 bind_addresses = [
53 "0.0.0.0"
54 ];
55 port = 8008;
56 resources = [
57 {
58 "compress" = true;
59 "names" = [ "client" ];
60 }
61 {
62 "compress" = false;
63 "names" = [ "federation" ];
64 }
65 ];
66 tls = false;
67 type = "http";
68 }
69 ];
70 };
71 };
72
73 services.mautrix-meta.instances.instagram = {
74 enable = true;
75
76 environmentFile = pkgs.writeText ''my-secrets'' ''
77 AS_TOKEN=${asToken}
78 HS_TOKEN=${hsToken}
79 '';
80
81 settings = {
82 homeserver = {
83 address = homeserverUrl;
84 domain = homeserverDomain;
85 };
86
87 appservice = {
88 port = 8009;
89
90 as_token = "$AS_TOKEN";
91 hs_token = "$HS_TOKEN";
92
93 database = {
94 type = "postgres";
95 uri = "postgres:///mautrix-meta-instagram?host=/var/run/postgresql";
96 };
97
98 bot.username = botUserName;
99 };
100
101 bridge.permissions."@${userName}:server" = "user";
102 };
103 };
104
105 networking.firewall.allowedTCPPorts = [
106 8008
107 8009
108 ];
109 };
110
111 client =
112 { pkgs, ... }:
113 {
114 environment.systemPackages = [
115 (pkgs.writers.writePython3Bin "do_test"
116 {
117 libraries = [ pkgs.python3Packages.matrix-nio ];
118 flakeIgnore = [
119 # We don't live in the dark ages anymore.
120 # Languages like Python that are whitespace heavy will overrun
121 # 79 characters..
122 "E501"
123 ];
124 }
125 ''
126 import sys
127 import functools
128 import asyncio
129
130 from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse
131
132
133 async def message_callback(matrix: AsyncClient, msg: str, _r, e):
134 print("Received matrix text message: ", e)
135 assert msg in e.body
136 exit(0) # Success!
137
138
139 async def run(homeserver: str):
140 matrix = AsyncClient(homeserver)
141 response = await matrix.register("${userName}", "foobar")
142 print("Matrix register response: ", response)
143
144 # Open a DM with the bridge bot
145 response = await matrix.room_create()
146 print("Matrix create room response:", response)
147 assert isinstance(response, RoomCreateResponse)
148 room_id = response.room_id
149
150 response = await matrix.room_invite(room_id, "@${botUserName}:${homeserverDomain}")
151 assert isinstance(response, RoomInviteResponse)
152
153 callback = functools.partial(
154 message_callback, matrix, "Hello, I'm an Instagram bridge bot."
155 )
156 matrix.add_event_callback(callback, RoomMessageNotice)
157
158 print("Waiting for matrix message...")
159 await matrix.sync_forever(timeout=30000)
160
161
162 if __name__ == "__main__":
163 asyncio.run(run(sys.argv[1]))
164 ''
165 )
166 ];
167 };
168 };
169
170 testScript = ''
171 def extract_token(data):
172 stdout = data[1]
173 stdout = stdout.strip()
174 line = stdout.split('\n')[-1]
175 return line.split(':')[-1].strip("\" '\n")
176
177 def get_token_from(token, file):
178 data = server.execute(f"cat {file} | grep {token}")
179 return extract_token(data)
180
181 def get_as_token_from(file):
182 return get_token_from("as_token", file)
183
184 def get_hs_token_from(file):
185 return get_token_from("hs_token", file)
186
187 config_yaml = "/var/lib/mautrix-meta-instagram/config.yaml"
188 registration_yaml = "/var/lib/mautrix-meta-instagram/meta-registration.yaml"
189
190 expected_as_token = "${asToken}"
191 expected_hs_token = "${hsToken}"
192
193 start_all()
194
195 with subtest("start the server"):
196 # bridge
197 server.wait_for_unit("mautrix-meta-instagram.service")
198
199 # homeserver
200 server.wait_for_unit("matrix-synapse.service")
201
202 server.wait_for_open_port(8008)
203 # Bridge only opens the port after it contacts the homeserver
204 server.wait_for_open_port(8009)
205
206 with subtest("ensure messages can be exchanged"):
207 client.succeed("do_test ${homeserverUrl} >&2")
208
209 with subtest("ensure as_token, hs_token match from environment file"):
210 as_token = get_as_token_from(config_yaml)
211 hs_token = get_hs_token_from(config_yaml)
212 as_token_registration = get_as_token_from(registration_yaml)
213 hs_token_registration = get_hs_token_from(registration_yaml)
214
215 assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})"
216 assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})"
217 assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})"
218 assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})"
219
220 with subtest("ensure as_token and hs_token stays same after restart"):
221 server.systemctl("restart mautrix-meta-instagram")
222 server.wait_for_open_port(8009)
223
224 as_token = get_as_token_from(config_yaml)
225 hs_token = get_hs_token_from(config_yaml)
226 as_token_registration = get_as_token_from(registration_yaml)
227 hs_token_registration = get_hs_token_from(registration_yaml)
228
229 assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})"
230 assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})"
231 assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})"
232 assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})"
233 '';
234}