testing changes

38c4d7b · gmorell · 2026-06-06 18:11

13 files +113 -29

Files changed

modified lib/oh_my_ex_oh_ircd/irc/channel.ex
+38 −9
@@ -348,7 +348,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
348 348
349 349 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
350 350 :irc_presence,
351 − {:join, nick, state.name}
351 + {:join, member_prefix(member), state.name}
352 352 })
353 353
354 354 # Restore the access prefix on rejoin with a synthetic MODE line.
@@ -357,7 +357,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
357 357
358 358 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
359 359 :irc_presence,
360 − {:mode, "ChanServ", state.name, "+" <> letter, [nick]}
360 + {:mode, service_prefix(), state.name, "+" <> letter, [nick]}
361 361 })
362 362 end
363 363
@@ -369,10 +369,11 @@ defmodule OhMyExOhIrcd.IRC.Channel do
369 369 key = downcase(nick)
370 370 {info, members} = Map.pop(state.members, key)
371 371 if info, do: Process.demonitor(info.ref, [:flush])
372 + sender = if info, do: member_prefix(info), else: "#{nick}!#{nick}@unknown"
372 373
373 374 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
374 375 :irc_presence,
375 − {:part, nick, state.name, reason}
376 + {:part, sender, state.name, reason}
376 377 })
377 378
378 379 {:reply, :ok, %{state | members: members}}
@@ -488,7 +489,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
488 489
489 490 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
490 491 :irc_presence,
491 − {:topic, set_by, state.name, text}
492 + {:topic, sender_prefix(state, set_by), state.name, text}
492 493 })
493 494
494 495 {:reply, :ok, state}
@@ -522,7 +523,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
522 523 if flag_letter do
523 524 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
524 525 :irc_presence,
525 − {:mode, granter, state.name, sign <> flag_letter, [member.nick]}
526 + {:mode, sender_prefix(state, granter), state.name, sign <> flag_letter, [member.nick]}
526 527 })
527 528 end
528 529
@@ -535,7 +536,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
535 536 nil ->
536 537 {:reply, {:error, :not_on_channel}, state}
537 538
538 − %{access: kicker_access} ->
539 + %{access: kicker_access} = kicker_member ->
539 540 case Map.get(state.members, downcase(target)) do
540 541 nil ->
541 542 {:reply, {:error, :no_such_member}, state}
@@ -554,7 +555,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
554 555
555 556 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
556 557 :irc_presence,
557 − {:kick, kicker, state.name, tnick, reason}
558 + {:kick, member_prefix(kicker_member), state.name, tnick, reason}
558 559 })
559 560
560 561 send(pid, {:irc_kicked, state.name})
@@ -792,7 +793,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
792 793
793 794 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
794 795 :irc_presence,
795 − {:mode, granter, state.name, flags, targets}
796 + {:mode, sender_prefix(state, granter), state.name, flags, targets}
796 797 })
797 798
798 799 # Persist if any of the applied changes were channel-level (not just
@@ -856,7 +857,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
856 857 if member do
857 858 Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), {
858 859 :irc_presence,
859 − {:quit, member.nick, state.name, "Connection closed"}
860 + {:quit, member_prefix(member), state.name, "Connection closed"}
860 861 })
861 862
862 863 {:noreply, %{state | members: Map.delete(state.members, key)}}
@@ -890,4 +891,32 @@ defmodule OhMyExOhIrcd.IRC.Channel do
890 891 {nick, "unknown"}
891 892 end
892 893 end
894 +
895 + # Full `nick!user@host` prefix for a member. Presence broadcasts
896 + # (JOIN/PART/QUIT/KICK/…) must carry this full form — clients like
897 + # irssi take the host from the message prefix to populate their
898 + # nicklists and assert it's present (`nicklist_set_host: host != NULL`).
899 + defp member_prefix(m),
900 + do: "#{m.nick}!#{Map.get(m, :user) || m.nick}@#{Map.get(m, :host) || "unknown"}"
901 +
902 + # Same, but by nick lookup — for senders that may not be plain members
903 + # (services, or a setter that raced a part). Falls back to a synthetic
904 + # but well-formed prefix so the host token is never empty on the wire.
905 + defp sender_prefix(state, nick) do
906 + case Map.get(state.members, downcase(nick)) do
907 + nil ->
908 + if downcase(nick) == downcase(OhMyExOhIrcd.IRC.ChanServ.nick()) do
909 + service_prefix()
910 + else
911 + "#{nick}!#{nick}@unknown"
912 + end
913 +
914 + m ->
915 + member_prefix(m)
916 + end
917 + end
918 +
919 + defp service_prefix do
920 + "#{OhMyExOhIrcd.IRC.ChanServ.nick()}!service@#{OhMyExOhIrcd.IRC.ChanServ.host()}"
921 + end
893 922 end
modified lib/oh_my_ex_oh_ircd/irc/connection.ex
+19 −10
@@ -186,25 +186,28 @@ defmodule OhMyExOhIrcd.IRC.Connection do
186 186 {:noreply, {socket, state}}
187 187 end
188 188
189 − def handle_info({:irc_presence, {:join, nick, channel}}, {socket, state}) do
190 − if MapSet.member?(state.channels, downcase(channel)) and nick != state.nick do
191 − send_msg(socket, Message.build("JOIN", [channel], nick))
189 + # Presence senders arrive as the full `nick!user@host` prefix and are
190 + # relayed verbatim — clients (irssi in particular) need the host part to
191 + # maintain their nicklists. Self-comparisons strip the nick back out.
192 + def handle_info({:irc_presence, {:join, sender, channel}}, {socket, state}) do
193 + if MapSet.member?(state.channels, downcase(channel)) and prefix_nick(sender) != state.nick do
194 + send_msg(socket, Message.build("JOIN", [channel], sender))
192 195 end
193 196
194 197 {:noreply, {socket, state}}
195 198 end
196 199
197 − def handle_info({:irc_presence, {:part, nick, channel, reason}}, {socket, state}) do
198 − if MapSet.member?(state.channels, downcase(channel)) and nick != state.nick do
200 + def handle_info({:irc_presence, {:part, sender, channel, reason}}, {socket, state}) do
201 + if MapSet.member?(state.channels, downcase(channel)) and prefix_nick(sender) != state.nick do
199 202 params = if reason == "", do: [channel], else: [channel, reason]
200 − send_msg(socket, Message.build("PART", params, nick))
203 + send_msg(socket, Message.build("PART", params, sender))
201 204 end
202 205
203 206 {:noreply, {socket, state}}
204 207 end
205 208
206 209 def handle_info({:irc_presence, {:topic, set_by, channel, text}}, {socket, state}) do
207 − if MapSet.member?(state.channels, downcase(channel)) and set_by != state.nick do
210 + if MapSet.member?(state.channels, downcase(channel)) and prefix_nick(set_by) != state.nick do
208 211 send_msg(socket, Message.build("TOPIC", [channel, text], set_by))
209 212 end
210 213
@@ -285,9 +288,9 @@ defmodule OhMyExOhIrcd.IRC.Connection do
285 288 {:noreply, {socket, state}}
286 289 end
287 290
288 − def handle_info({:irc_presence, {:quit, nick, channel, reason}}, {socket, state}) do
289 − if MapSet.member?(state.channels, downcase(channel)) and nick != state.nick do
290 − send_msg(socket, Message.build("QUIT", [reason], nick))
291 + def handle_info({:irc_presence, {:quit, sender, channel, reason}}, {socket, state}) do
292 + if MapSet.member?(state.channels, downcase(channel)) and prefix_nick(sender) != state.nick do
293 + send_msg(socket, Message.build("QUIT", [reason], sender))
291 294 end
292 295
293 296 {:noreply, {socket, state}}
@@ -1623,6 +1626,12 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1623 1626 end
1624 1627 end
1625 1628
1629 + # Bare nick from a `nick!user@host` prefix (or a bare nick, unchanged).
1630 + defp prefix_nick(prefix) when is_binary(prefix),
1631 + do: prefix |> String.split("!", parts: 2) |> List.first()
1632 +
1633 + defp prefix_nick(_), do: nil
1634 +
1626 1635 defp owns_sender?(%State{nick: nil}, _sender), do: false
1627 1636
1628 1637 defp owns_sender?(%State{nick: nick}, sender) when is_binary(sender) do
modified lib/oh_my_ex_oh_ircd_web/live/web_irc_live.ex
+17 −6
@@ -307,19 +307,28 @@ defmodule OhMyExOhIrcdWeb.WebIrcLive do
307 307 push_line(socket, channel_name, line)
308 308 end
309 309
310 − defp push_presence(socket, {:join, nick, channel}) do
310 + # Presence senders arrive as full `nick!user@host` prefixes (built by
311 + # Channel for the benefit of native IRC clients); strip to the bare
312 + # nick for display and member-list bookkeeping.
313 + defp push_presence(socket, {:join, sender, channel}) do
314 + nick = prefix_nick(sender)
315 +
311 316 socket
312 317 |> push_line(channel, %{kind: :join, nick: nick})
313 318 |> update_members(channel, fn members -> Enum.uniq([nick | members]) end)
314 319 end
315 320
316 − defp push_presence(socket, {:part, nick, channel, reason}) do
321 + defp push_presence(socket, {:part, sender, channel, reason}) do
322 + nick = prefix_nick(sender)
323 +
317 324 socket
318 325 |> push_line(channel, %{kind: :part, nick: nick, reason: reason})
319 326 |> update_members(channel, fn members -> Enum.reject(members, &same_nick?(&1, nick)) end)
320 327 end
321 328
322 − defp push_presence(socket, {:quit, nick, channel, reason}) do
329 + defp push_presence(socket, {:quit, sender, channel, reason}) do
330 + nick = prefix_nick(sender)
331 +
323 332 socket
324 333 |> push_line(channel, %{kind: :quit, nick: nick, reason: reason})
325 334 |> update_members(channel, fn members -> Enum.reject(members, &same_nick?(&1, nick)) end)
@@ -332,7 +341,7 @@ defmodule OhMyExOhIrcdWeb.WebIrcLive do
332 341 end
333 342
334 343 defp push_presence(socket, {:nick_change, old_prefix, new_nick, channel}) do
335 − old_nick = old_prefix |> String.split("!", parts: 2) |> List.first()
344 + old_nick = prefix_nick(old_prefix)
336 345
337 346 socket
338 347 |> push_line(channel, %{kind: :status, text: "#{old_nick} is now known as #{new_nick}"})
@@ -344,19 +353,21 @@ defmodule OhMyExOhIrcdWeb.WebIrcLive do
344 353 end
345 354
346 355 defp push_presence(socket, {:topic, set_by, channel, text}),
347 − do: push_line(socket, channel, %{kind: :topic, set_by: set_by, text: text})
356 + do: push_line(socket, channel, %{kind: :topic, set_by: prefix_nick(set_by), text: text})
348 357
349 358 defp push_presence(socket, {:mode, granter, channel, flags, targets}),
350 359 do:
351 360 push_line(socket, channel, %{
352 361 kind: :mode,
353 − granter: granter,
362 + granter: prefix_nick(granter),
354 363 flags: flags,
355 364 targets: targets
356 365 })
357 366
358 367 defp push_presence(socket, _), do: socket
359 368
369 + defp prefix_nick(prefix), do: prefix |> String.split("!", parts: 2) |> List.first()
370 +
360 371 # The member list in `channels[name].members` is whatever we
361 372 # populated at JOIN — `push_presence/2` keeps it in sync as
362 373 # join/part/quit/kick/nick-change events arrive.
modified test/oh_my_ex_oh_ircd/irc/channel_ops_test.exs
+6 −4
@@ -108,12 +108,13 @@ defmodule OhMyExOhIrcd.IRC.ChannelOpsTest do
108 108 a_lines = drain(a, 300)
109 109 b_lines = drain(b, 300)
110 110
111 + # The relayed MODE carries the granter's full nick!user@host prefix.
111 112 assert Enum.any?(a_lines, fn l ->
112 − l =~ ~r/^:#{a_nick} MODE #{Regex.escape(chan)} \+o #{b_nick}$/
113 + l =~ ~r/^:#{a_nick}!\S+@\S+ MODE #{Regex.escape(chan)} \+o #{b_nick}$/
113 114 end)
114 115
115 116 assert Enum.any?(b_lines, fn l ->
116 − l =~ ~r/^:#{a_nick} MODE #{Regex.escape(chan)} \+o #{b_nick}$/
117 + l =~ ~r/^:#{a_nick}!\S+@\S+ MODE #{Regex.escape(chan)} \+o #{b_nick}$/
117 118 end)
118 119 end
119 120
@@ -180,12 +181,13 @@ defmodule OhMyExOhIrcd.IRC.ChannelOpsTest do
180 181 a_lines = drain(a, 300)
181 182 b_lines = drain(b, 300)
182 183
184 + # The relayed KICK carries the kicker's full nick!user@host prefix.
183 185 assert Enum.any?(a_lines, fn l ->
184 − l =~ ~r/^:#{a_nick} KICK #{Regex.escape(chan)} #{b_nick} :?bye$/
186 + l =~ ~r/^:#{a_nick}!\S+@\S+ KICK #{Regex.escape(chan)} #{b_nick} :?bye$/
185 187 end)
186 188
187 189 assert Enum.any?(b_lines, fn l ->
188 − l =~ ~r/^:#{a_nick} KICK #{Regex.escape(chan)} #{b_nick} :?bye$/
190 + l =~ ~r/^:#{a_nick}!\S+@\S+ KICK #{Regex.escape(chan)} #{b_nick} :?bye$/
189 191 end)
190 192
191 193 send_line(b, "PRIVMSG #{chan} :still here?")
modified test/oh_my_ex_oh_ircd/irc/connection_integration_test.exs
+33 −0
@@ -123,6 +123,39 @@ defmodule OhMyExOhIrcd.IRC.ConnectionIntegrationTest do
123 123 end
124 124 end
125 125
126 + describe "presence relays" do
127 + # Relayed JOIN/PART/QUIT must carry the full `nick!user@host` prefix.
128 + # A bare-nick prefix makes irssi insert a nicklist entry with a NULL
129 + # host (`nicklist_set_host: assertion 'host != NULL' failed`).
130 + test "relayed JOIN and QUIT carry the sender's full prefix" do
131 + port = start_listener()
132 + chan = unique_channel("presence")
133 + a_nick = unique_nick("alice")
134 + b_nick = unique_nick("bob")
135 +
136 + a = register(port, a_nick)
137 + send_line(a, "JOIN #{chan}")
138 + drain(a, 200)
139 +
140 + b = register(port, b_nick)
141 + send_line(b, "JOIN #{chan}")
142 + drain(b, 200)
143 +
144 + join_lines = drain(a, 300)
145 +
146 + assert Enum.any?(join_lines, fn l ->
147 + l =~ ~r/^:#{b_nick}!\S+@\S+ JOIN #{Regex.escape(chan)}$/
148 + end)
149 +
150 + send_line(b, "QUIT :bye")
151 + quit_lines = drain(a, 300)
152 +
153 + assert Enum.any?(quit_lines, fn l ->
154 + l =~ ~r/^:#{b_nick}!\S+@\S+ (QUIT|PART)/
155 + end)
156 + end
157 + end
158 +
126 159 describe "TOPIC" do
127 160 test "set + read back with 332 and 333 (setter + timestamp)" do
128 161 port = start_listener()

Parents: a0ebd53