1import ../make-test-python.nix ({ pkgs, ... }:
2 let
3 homeserverUrl = "http://homeserver:8008";
4 in
5 {
6 name = "matrix-appservice-irc";
7 meta = {
8 maintainers = pkgs.matrix-appservice-irc.meta.maintainers;
9 };
10
11 nodes = {
12 homeserver = { pkgs, ... }: {
13 # We'll switch to this once the config is copied into place
14 specialisation.running.configuration = {
15 services.matrix-synapse = {
16 enable = true;
17 settings = {
18 database.name = "sqlite3";
19 app_service_config_files = [ "/registration.yml" ];
20
21 enable_registration = true;
22
23 # don't use this in production, always use some form of verification
24 enable_registration_without_verification = true;
25
26 listeners = [ {
27 # The default but tls=false
28 bind_addresses = [
29 "0.0.0.0"
30 ];
31 port = 8008;
32 resources = [ {
33 "compress" = true;
34 "names" = [ "client" ];
35 } {
36 "compress" = false;
37 "names" = [ "federation" ];
38 } ];
39 tls = false;
40 type = "http";
41 } ];
42 };
43 };
44
45 networking.firewall.allowedTCPPorts = [ 8008 ];
46 };
47 };
48
49 ircd = { pkgs, ... }: {
50 services.ngircd = {
51 enable = true;
52 config = ''
53 [Global]
54 Name = ircd.ircd
55 Info = Server Info Text
56 AdminInfo1 = _
57
58 [Channel]
59 Name = #test
60 Topic = a cool place
61
62 [Options]
63 PAM = no
64 '';
65 };
66 networking.firewall.allowedTCPPorts = [ 6667 ];
67 };
68
69 appservice = { pkgs, ... }: {
70 services.matrix-appservice-irc = {
71 enable = true;
72 registrationUrl = "http://appservice:8009";
73
74 settings = {
75 homeserver.url = homeserverUrl;
76 homeserver.domain = "homeserver";
77
78 ircService.servers."ircd" = {
79 name = "IRCd";
80 port = 6667;
81 dynamicChannels = {
82 enabled = true;
83 aliasTemplate = "#irc_$CHANNEL";
84 };
85 };
86 };
87 };
88
89 networking.firewall.allowedTCPPorts = [ 8009 ];
90 };
91
92 client = { pkgs, ... }: {
93 environment.systemPackages = [
94 (pkgs.writers.writePython3Bin "do_test"
95 {
96 libraries = [ pkgs.python3Packages.matrix-nio ];
97 flakeIgnore = [
98 # We don't live in the dark ages anymore.
99 # Languages like Python that are whitespace heavy will overrun
100 # 79 characters..
101 "E501"
102 ];
103 } ''
104 import sys
105 import socket
106 import functools
107 from time import sleep
108 import asyncio
109
110 from nio import AsyncClient, RoomMessageText, JoinResponse
111
112
113 async def matrix_room_message_text_callback(matrix: AsyncClient, msg: str, _r, e):
114 print("Received matrix text message: ", e)
115 if msg in e.body:
116 print("Received hi from IRC")
117 await matrix.close()
118 exit(0) # Actual exit point
119
120
121 class IRC:
122 def __init__(self):
123 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
124 sock.connect(("ircd", 6667))
125 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
126 sock.send(b"USER bob bob bob :bob\n")
127 sock.send(b"NICK bob\n")
128 self.sock = sock
129
130 def join(self, room: str):
131 self.sock.send(f"JOIN {room}\n".encode())
132
133 def privmsg(self, room: str, msg: str):
134 self.sock.send(f"PRIVMSG {room} :{msg}\n".encode())
135
136 def expect_msg(self, body: str):
137 buffer = ""
138 while True:
139 buf = self.sock.recv(1024).decode()
140 buffer += buf
141 if body in buffer:
142 return
143
144
145 async def run(homeserver: str):
146 irc = IRC()
147
148 matrix = AsyncClient(homeserver)
149 response = await matrix.register("alice", "foobar")
150 print("Matrix register response: ", response)
151
152 response = await matrix.join("#irc_#test:homeserver")
153 print("Matrix join room response:", response)
154 assert isinstance(response, JoinResponse)
155 room_id = response.room_id
156
157 irc.join("#test")
158 # FIXME: what are we waiting on here? Matrix? IRC? Both?
159 # 10s seem bad for busy hydra machines.
160 sleep(10)
161
162 # Exchange messages
163 print("Sending text message to matrix room")
164 response = await matrix.room_send(
165 room_id=room_id,
166 message_type="m.room.message",
167 content={"msgtype": "m.text", "body": "hi from matrix"},
168 )
169 print("Matrix room send response: ", response)
170 irc.privmsg("#test", "hi from irc")
171
172 print("Waiting for the matrix message to appear on the IRC side...")
173 irc.expect_msg("hi from matrix")
174
175 callback = functools.partial(
176 matrix_room_message_text_callback, matrix, "hi from irc"
177 )
178 matrix.add_event_callback(callback, RoomMessageText)
179
180 print("Waiting for matrix message...")
181 await matrix.sync_forever()
182
183 exit(1) # Unreachable
184
185
186 if __name__ == "__main__":
187 asyncio.run(run(sys.argv[1]))
188 ''
189 )
190 ];
191 };
192 };
193
194 testScript = ''
195 import pathlib
196 import os
197
198 start_all()
199
200 ircd.wait_for_unit("ngircd.service")
201 ircd.wait_for_open_port(6667)
202
203 with subtest("start the appservice"):
204 appservice.wait_for_unit("matrix-appservice-irc.service")
205 appservice.wait_for_open_port(8009)
206
207 with subtest("copy the registration file"):
208 appservice.copy_from_vm("/var/lib/matrix-appservice-irc/registration.yml")
209 homeserver.copy_from_host(
210 str(pathlib.Path(os.environ.get("out", os.getcwd())) / "registration.yml"), "/"
211 )
212 homeserver.succeed("chmod 444 /registration.yml")
213
214 with subtest("start the homeserver"):
215 homeserver.succeed(
216 "/run/current-system/specialisation/running/bin/switch-to-configuration test >&2"
217 )
218
219 homeserver.wait_for_unit("matrix-synapse.service")
220 homeserver.wait_for_open_port(8008)
221
222 with subtest("ensure messages can be exchanged"):
223 client.succeed("do_test ${homeserverUrl} >&2")
224 '';
225 })