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