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