From 231fbb47345c09efc0401a3db7b6a459de1fd53a Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 19 Aug 2023 22:32:12 +0200 Subject: [PATCH 1/8] Advertise ISUPPORT CHANMODES senpai gets confused when missing --- lib/irc/handler.ex | 1 + test/irc/handler_test.exs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/irc/handler.ex b/lib/irc/handler.ex index 1da108b..acb09b3 100644 --- a/lib/irc/handler.ex +++ b/lib/irc/handler.ex @@ -604,6 +604,7 @@ defmodule M51.IrcConn.Handler do "CASEMAPPING=rfc3454", "CLIENTTAGDENY=*,-draft/react,-draft/reply", "CHANLIMIT=", + "CHANMODES=b,,,i", "CHANTYPES=#!", "CHATHISTORY=100", "MAXTARGETS=1", diff --git a/test/irc/handler_test.exs b/test/irc/handler_test.exs index daa7b6c..fa63dd1 100644 --- a/test/irc/handler_test.exs +++ b/test/irc/handler_test.exs @@ -20,7 +20,7 @@ defmodule M51.IrcConn.HandlerTest do @cap_ls_302 ":server. CAP * LS :account-tag batch draft/account-registration=before-connect draft/channel-rename draft/chathistory draft/message-redaction draft/multiline=max-bytes=8192 draft/no-implicit-names draft/sasl-ir echo-message extended-join labeled-response message-tags sasl=PLAIN server-time soju.im/account-required standard-replies userhost-in-names\r\n" @cap_ls ":server. CAP * LS :account-tag batch draft/account-registration draft/channel-rename draft/chathistory draft/message-redaction draft/multiline draft/no-implicit-names draft/sasl-ir echo-message extended-join labeled-response message-tags sasl server-time soju.im/account-required standard-replies userhost-in-names\r\n" - @isupport "CASEMAPPING=rfc3454 CLIENTTAGDENY=*,-draft/react,-draft/reply CHANLIMIT= CHANTYPES=#! CHATHISTORY=100 MAXTARGETS=1 MSGREFTYPES=msgid PREFIX= TARGMAX=JOIN:1,PART:1 UTF8ONLY :are supported by this server\r\n" + @isupport "CASEMAPPING=rfc3454 CLIENTTAGDENY=*,-draft/react,-draft/reply CHANLIMIT= CHANMODES=b,,,i CHANTYPES=#! CHATHISTORY=100 MAXTARGETS=1 MSGREFTYPES=msgid PREFIX= TARGMAX=JOIN:1,PART:1 UTF8ONLY :are supported by this server\r\n" setup do start_supervised!({MockMatrixClient, {self()}}) From 933276127a3d3c80bf8e88cf8fef541e7dbbd161 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 1 Nov 2023 23:35:28 +0100 Subject: [PATCH 2/8] matrix2irc: Fix white background when only the foreground is set --- lib/format/common.ex | 2 +- lib/format/matrix2irc.ex | 24 +++++++++++++++--------- test/format/common_test.exs | 7 +++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/format/common.ex b/lib/format/common.ex index ad2e857..600588c 100644 --- a/lib/format/common.ex +++ b/lib/format/common.ex @@ -56,7 +56,7 @@ defmodule M51.Format do "foo\nbar" iex> M51.Format.matrix2irc(~s(foo bar baz)) - "foo \x04FF0000,FFFFFFbar\x0399,99 baz" + "foo \x04FF0000bar\x0399,99 baz" """ def matrix2irc(html, homeserver \\ nil) do tree = :mochiweb_html.parse("" <> html <> "") diff --git a/lib/format/matrix2irc.ex b/lib/format/matrix2irc.ex index 7c7f641..f20f6d1 100644 --- a/lib/format/matrix2irc.ex +++ b/lib/format/matrix2irc.ex @@ -109,19 +109,14 @@ defmodule M51.Format.Matrix2Irc do transform_children(children, state) _ -> - fg = String.trim_leading(fg || "000000", "#") - bg = String.trim_leading(bg || "FFFFFF", "#") + fg = fg && String.trim_leading(fg, "#") + bg = bg && String.trim_leading(bg, "#") - restored_colors = - case state.color do - # reset - {nil, nil} -> "\x0399,99" - {fg, bg} -> "\x04#{fg},#{bg}" - end + restored_colors = get_color_code(state.color) state = %M51.Format.Matrix2Irc.State{state | color: {fg, bg}} - ~s(\x04#{fg},#{bg}) <> + get_color_code({fg, bg}) <> transform_children(children, state) <> restored_colors end end @@ -143,6 +138,17 @@ defmodule M51.Format.Matrix2Irc do transform_children(children, state, char) end + def get_color_code({fg, bg}) do + case {fg, bg} do + # reset + {nil, nil} -> "\x0399,99" + {fg, nil} -> "\x04#{fg}" + # set both fg and bg, then reset fg + {nil, bg} -> "\x04000000,#{bg}\x0399" + {fg, bg} -> "\x04#{fg},#{bg}" + end + end + defp transform_children(children, state, char \\ "") do Stream.concat([ [char], diff --git a/test/format/common_test.exs b/test/format/common_test.exs index a13af07..3c7cc71 100644 --- a/test/format/common_test.exs +++ b/test/format/common_test.exs @@ -63,15 +63,18 @@ defmodule M51.FormatTest do test "Matrix colors to IRC" do assert M51.Format.matrix2irc(~s(foo)) == - "\x04FF0000,FFFFFFfoo\x0399,99" + "\x04FF0000foo\x0399,99" assert M51.Format.matrix2irc(~s(foo)) == - "\x04FF0000,FFFFFFfoo\x0399,99" + "\x04FF0000foo\x0399,99" assert M51.Format.matrix2irc( ~s(foo) ) == "\x04FF0000,00FF00foo\x0399,99" + assert M51.Format.matrix2irc(~s(foo)) == + "\x04000000,00FF00\x0399foo\x0399,99" + assert M51.Format.matrix2irc( ~s(foo) <> ~s(bar) <> From 2ee9da0a610233bf113eb22cb74927e8a083bda8 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 1 Nov 2023 23:51:02 +0100 Subject: [PATCH 3/8] :parse_trans removed support for OTP 20 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c072ed9..ab33830 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,13 @@ jobs: fail-fast: false matrix: include: - - otp: '20' # for some reason, erlef/setup-beam@v1 fails on 19 + - otp: '21' # :parse_trans does not support OTP 20 anymore elixir: '1.7.4' os: 'ubuntu-20.04' - otp: '22' elixir: '1.7.4' os: 'ubuntu-20.04' - - otp: '20' + - otp: '21' elixir: '1.9' os: 'ubuntu-20.04' - otp: '22' From 8cf8f99a05af449869586f672cd2d655ddca7b63 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 9 Feb 2024 19:52:26 +0100 Subject: [PATCH 4/8] Increase timeout for login requests --- lib/matrix_client/client.ex | 4 +-- lib/matrix_client/poller.ex | 30 ++++++++++++++------- test/matrix_client/client_test.exs | 42 +++++++++++++++++++++++------- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/lib/matrix_client/client.ex b/lib/matrix_client/client.ex index 94f01ce..31bfb06 100644 --- a/lib/matrix_client/client.ex +++ b/lib/matrix_client/client.ex @@ -78,7 +78,7 @@ defmodule M51.MatrixClient.Client do # Check the server supports password login url = base_url <> "/_matrix/client/r0/login" Logger.debug("(raw) GET #{url}") - response = httpoison.get!(url) + response = httpoison.get!(url, [], timeout: @timeout) Logger.debug(Kernel.inspect(response)) case response do @@ -111,7 +111,7 @@ defmodule M51.MatrixClient.Client do url = base_url <> "/_matrix/client/r0/login" Logger.debug("(raw) POST #{url} " <> Kernel.inspect(body)) - response = httpoison.post!(url, body) + response = httpoison.post!(url, body, [], timeout: @timeout) Logger.debug(Kernel.inspect(response)) case response do diff --git a/lib/matrix_client/poller.ex b/lib/matrix_client/poller.ex index f48b432..6d137ac 100644 --- a/lib/matrix_client/poller.ex +++ b/lib/matrix_client/poller.ex @@ -1157,6 +1157,9 @@ defmodule M51.MatrixClient.Poller do end # Sends self JOIN, RPL_TOPIC/RPL_NOTOPIC, RPL_NAMREPLY + # + # Returns whether the announce was actually sent (ie. if the channel has a canonical + # alias, or was allowed to be sent without a canonical alias) defp send_channel_welcome( sup_pid, room_id, @@ -1172,15 +1175,20 @@ defmodule M51.MatrixClient.Poller do supports_channel_rename = Enum.member?(capabilities, :channel_rename) - if old_canonical_alias == nil || !supports_channel_rename do - announce_new_channel( - M51.IrcConn.Supervisor, - sup_pid, - room_id, - write, - event - ) - end + announced_new_channel = + if old_canonical_alias == nil || !supports_channel_rename do + announce_new_channel( + M51.IrcConn.Supervisor, + sup_pid, + room_id, + write, + event + ) + + true + else + false + end if old_canonical_alias != nil do if supports_channel_rename do @@ -1197,6 +1205,8 @@ defmodule M51.MatrixClient.Poller do command: "RENAME", params: [old_canonical_alias, new_canonical_alias, "Canonical alias changed"] }) + + true else close_renamed_channel( sup_pid, @@ -1205,6 +1215,8 @@ defmodule M51.MatrixClient.Poller do canonical_alias_sender, old_canonical_alias ) + + announced_new_channel end end end diff --git a/test/matrix_client/client_test.exs b/test/matrix_client/client_test.exs index 3d35b5d..67a8e78 100644 --- a/test/matrix_client/client_test.exs +++ b/test/matrix_client/client_test.exs @@ -22,6 +22,8 @@ defmodule M51.MatrixClient.ClientTest do setup :set_mox_from_context setup :verify_on_exit! + @timeout 65000 + setup do start_supervised!({M51.MatrixClient.State, {self()}}) @@ -36,8 +38,10 @@ defmodule M51.MatrixClient.ClientTest do assert url == "https://matrix.example.org/.well-known/matrix/client" {:ok, %HTTPoison.Response{status_code: 404, body: "Error 404"}} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 200, @@ -46,8 +50,10 @@ defmodule M51.MatrixClient.ClientTest do """ } end) - |> expect(:post!, fn url, body -> + |> expect(:post!, fn url, body, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] assert Jason.decode!(body) == %{ "type" => "m.login.password", @@ -95,8 +101,10 @@ defmodule M51.MatrixClient.ClientTest do """ }} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 404, @@ -134,8 +142,10 @@ defmodule M51.MatrixClient.ClientTest do """ }} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 200, @@ -150,8 +160,10 @@ defmodule M51.MatrixClient.ClientTest do """ } end) - |> expect(:post!, fn url, body -> + |> expect(:post!, fn url, body, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] assert Jason.decode!(body) == %{ "type" => "m.login.password", @@ -213,8 +225,10 @@ defmodule M51.MatrixClient.ClientTest do """ }} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://matrix.example.com/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 200, @@ -232,8 +246,10 @@ defmodule M51.MatrixClient.ClientTest do """ } end) - |> expect(:post!, fn url, body -> + |> expect(:post!, fn url, body, headers, options -> assert url == "https://matrix.example.com/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] assert Jason.decode!(body) == %{ "type" => "m.login.password", @@ -293,8 +309,10 @@ defmodule M51.MatrixClient.ClientTest do """ }} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 200, @@ -338,8 +356,10 @@ defmodule M51.MatrixClient.ClientTest do """ }} end) - |> expect(:get!, fn url -> + |> expect(:get!, fn url, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] %HTTPoison.Response{ status_code: 200, @@ -354,8 +374,10 @@ defmodule M51.MatrixClient.ClientTest do """ } end) - |> expect(:post!, fn url, body -> + |> expect(:post!, fn url, body, headers, options -> assert url == "https://matrix.example.org/_matrix/client/r0/login" + assert headers == [] + assert options == [timeout: @timeout] assert Jason.decode!(body) == %{ "type" => "m.login.password", From 80dc1da233a66ab57aa86277d6d6508d78a4036c Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 9 Feb 2024 22:56:20 +0100 Subject: [PATCH 5/8] Fix crash when WHOISing invalid MXID --- lib/irc/handler.ex | 90 ++++++++++++++++++++++----------------- test/irc/handler_test.exs | 14 +++++- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/lib/irc/handler.ex b/lib/irc/handler.ex index acb09b3..ca99223 100644 --- a/lib/irc/handler.ex +++ b/lib/irc/handler.ex @@ -1102,51 +1102,61 @@ defmodule M51.IrcConn.Handler do [_server, target | _] -> target end - [local_name, hostname] = String.split(target, ":", parts: 2) + case String.split(target, ":", parts: 2) do + [_] -> + # return ERR_NOSUCHNICK + if target == "" || String.contains?(target, " ") do + send_numeric.("401", ["*", "No such nick"]) + else + send_numeric.("401", [target, "No such nick"]) + end - [member: memberships] = M51.MatrixClient.State.user(matrix_state, target) + [local_name, hostname] -> + [member: memberships] = M51.MatrixClient.State.user(matrix_state, target) - # TODO: pick the most common display name instead - gecos = target + # TODO: pick the most common display name instead + gecos = target - overhead = make_numeric.("353", [target, ""]) |> M51.Irc.Command.format() |> byte_size() + overhead = + make_numeric.("353", [target, ""]) |> M51.Irc.Command.format() |> byte_size() - first_commands = [ - # RPL_WHOISUSER " * :" - make_numeric.("311", [target, local_name, hostname, "*", gecos]) - ] + first_commands = [ + # RPL_WHOISUSER " * :" + make_numeric.("311", [target, local_name, hostname, "*", gecos]) + ] - channel_commands = - memberships - |> Map.keys() - |> Enum.map(fn room_id -> - M51.MatrixClient.State.room_irc_channel(matrix_state, room_id) - end) - |> Enum.sort() - |> M51.Irc.WordWrap.join_tokens(512 - overhead) - |> Enum.map(fn line -> - line = line |> String.trim_trailing() - - if line != "" do - # RPL_WHOISCHANNELS " :[prefix]{ [prefix]}" - make_numeric.("319", [target, line]) - end - end) - |> Enum.filter(fn line -> line != nil end) - - last_commands = [ - # RPL_WHOISSERVER " :" - make_numeric.("312", [target, hostname, hostname]), - # RPL_WHOISACCOUNT " :is logged in as" - make_numeric.("330", [target, target, "is logged in as"]), - # RPL_ENDOFWHOIS - make_numeric.("318", [target, "End of WHOIS"]) - ] - - send_batch.( - Enum.concat([first_commands, channel_commands, last_commands]), - "labeled-response" - ) + channel_commands = + memberships + |> Map.keys() + |> Enum.map(fn room_id -> + M51.MatrixClient.State.room_irc_channel(matrix_state, room_id) + end) + |> Enum.sort() + |> M51.Irc.WordWrap.join_tokens(512 - overhead) + |> Enum.map(fn line -> + line = line |> String.trim_trailing() + + if line != "" do + # RPL_WHOISCHANNELS " :[prefix]{ [prefix]}" + make_numeric.("319", [target, line]) + end + end) + |> Enum.filter(fn line -> line != nil end) + + last_commands = [ + # RPL_WHOISSERVER " :" + make_numeric.("312", [target, hostname, hostname]), + # RPL_WHOISACCOUNT " :is logged in as" + make_numeric.("330", [target, target, "is logged in as"]), + # RPL_ENDOFWHOIS + make_numeric.("318", [target, "End of WHOIS"]) + ] + + send_batch.( + Enum.concat([first_commands, channel_commands, last_commands]), + "labeled-response" + ) + end {"BATCH", [first_param | params]} -> {first_char, reference_tag} = String.split_at(first_param, 1) diff --git a/test/irc/handler_test.exs b/test/irc/handler_test.exs index fa63dd1..113ff46 100644 --- a/test/irc/handler_test.exs +++ b/test/irc/handler_test.exs @@ -828,7 +828,7 @@ defmodule M51.IrcConn.HandlerTest do assert_line("BATCH :-#{batch_id}\r\n") end - test "WHOIS", %{handler: handler} do + test "WHOIS unknown user", %{handler: handler} do do_connection_registration(handler) send(handler, cmd("@label=l1 WHOIS unknown_user:example.com")) @@ -855,6 +855,18 @@ defmodule M51.IrcConn.HandlerTest do assert_line("BATCH :-#{batch_id}\r\n") end + test "WHOIS non-MXID", %{handler: handler} do + do_connection_registration(handler) + + send(handler, cmd("@label=l1 WHOIS not_enough_colons")) + + assert_line("@label=l1 :server. 401 foo:example.org not_enough_colons :No such nick\r\n") + + send(handler, cmd("@label=l1 WHOIS :with spaces")) + + assert_line("@label=l1 :server. 401 foo:example.org * :No such nick\r\n") + end + test "MODE on user", %{handler: handler} do do_connection_registration(handler) From bac0f5b6711e34d55f28fdaa478cacfa615c61ac Mon Sep 17 00:00:00 2001 From: Rebecca Kelly Date: Sat, 3 Feb 2024 16:04:59 -0500 Subject: [PATCH 6/8] Set LINELEN to 8k for clients that support long messages This matches the value already negotiated for cap multiline. I think we could go up to 16k without difficulty, but 8k is probably sufficient. This also tells gen_tcp to use a 16k buffer; with the default settings, long commands sometimes get split across multiple reads, with unfortunate results. --- lib/irc/handler.ex | 2 ++ lib/irc_server.ex | 9 ++++++++- test/irc/handler_test.exs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/irc/handler.ex b/lib/irc/handler.ex index ca99223..87fec72 100644 --- a/lib/irc/handler.ex +++ b/lib/irc/handler.ex @@ -607,6 +607,8 @@ defmodule M51.IrcConn.Handler do "CHANMODES=b,,,i", "CHANTYPES=#!", "CHATHISTORY=100", + # Matrix limit is 64k for the whole event, so this is fairly conservative. + "LINELEN=#{@multiline_max_bytes}", "MAXTARGETS=1", # https://github.com/ircv3/ircv3-specifications/pull/510 "MSGREFTYPES=msgid", diff --git a/lib/irc_server.ex b/lib/irc_server.ex index 948c417..4269707 100644 --- a/lib/irc_server.ex +++ b/lib/irc_server.ex @@ -40,7 +40,14 @@ defmodule M51.IrcServer do end defp accept(port, retries_left \\ 10) do - case :gen_tcp.listen(port, [:binary, :inet6, packet: :line, active: false, reuseaddr: true]) do + opts = [ + :binary, :inet6, + packet: :line, + active: false, + reuseaddr: true, + buffer: M51.IrcConn.Handler.multiline_max_bytes * 2 + ] + case :gen_tcp.listen(port, opts) do {:ok, server_sock} -> Logger.info("Listening on port #{port}") loop_accept(server_sock) diff --git a/test/irc/handler_test.exs b/test/irc/handler_test.exs index 113ff46..3c35eb7 100644 --- a/test/irc/handler_test.exs +++ b/test/irc/handler_test.exs @@ -20,7 +20,7 @@ defmodule M51.IrcConn.HandlerTest do @cap_ls_302 ":server. CAP * LS :account-tag batch draft/account-registration=before-connect draft/channel-rename draft/chathistory draft/message-redaction draft/multiline=max-bytes=8192 draft/no-implicit-names draft/sasl-ir echo-message extended-join labeled-response message-tags sasl=PLAIN server-time soju.im/account-required standard-replies userhost-in-names\r\n" @cap_ls ":server. CAP * LS :account-tag batch draft/account-registration draft/channel-rename draft/chathistory draft/message-redaction draft/multiline draft/no-implicit-names draft/sasl-ir echo-message extended-join labeled-response message-tags sasl server-time soju.im/account-required standard-replies userhost-in-names\r\n" - @isupport "CASEMAPPING=rfc3454 CLIENTTAGDENY=*,-draft/react,-draft/reply CHANLIMIT= CHANMODES=b,,,i CHANTYPES=#! CHATHISTORY=100 MAXTARGETS=1 MSGREFTYPES=msgid PREFIX= TARGMAX=JOIN:1,PART:1 UTF8ONLY :are supported by this server\r\n" + @isupport "CASEMAPPING=rfc3454 CLIENTTAGDENY=*,-draft/react,-draft/reply CHANLIMIT= CHANMODES=b,,,i CHANTYPES=#! CHATHISTORY=100 LINELEN=8192 MAXTARGETS=1 MSGREFTYPES=msgid PREFIX= TARGMAX=JOIN:1,PART:1 UTF8ONLY :are supported by this server\r\n" setup do start_supervised!({MockMatrixClient, {self()}}) From 3e3e6910d7f32833159778687e32f39899a7c10e Mon Sep 17 00:00:00 2001 From: Rebecca Kelly Date: Mon, 12 Feb 2024 17:39:48 -0500 Subject: [PATCH 7/8] fix: multiline_max_bytes was not exported --- lib/irc/handler.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/irc/handler.ex b/lib/irc/handler.ex index 87fec72..a917e95 100644 --- a/lib/irc/handler.ex +++ b/lib/irc/handler.ex @@ -33,6 +33,7 @@ defmodule M51.IrcConn.Handler do # 8kB should be a reasonable limit to remain under the allowed 65kB even # with large signatures and many escapes. @multiline_max_bytes 8192 + def multiline_max_bytes, do: @multiline_max_bytes # set of capabilities that we will show in CAP LS and accept with ACK; # along with their value (shown in CAP LS 302) From 64ff97ca49ee4eda26f92c5b547817110baaf6bd Mon Sep 17 00:00:00 2001 From: Rebecca Kelly Date: Mon, 12 Feb 2024 09:10:51 -0500 Subject: [PATCH 8/8] Send all enabled capabilities on CAP LIST Notably, this fixes an issue with weechat (and possibly some other clients) where if you ever run /cap or /cap list after connection registration, the client will see the "CAP * LIST sasl" reply as downgrading the connection to drop all capabilities except sasl. This also enables the use of CAP LS after connection registration, although without post-hoc CAP REQ support this is of limited use. --- lib/irc/handler.ex | 62 +++++++++++++++++++++++++-------------- test/irc/handler_test.exs | 20 +++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/lib/irc/handler.ex b/lib/irc/handler.ex index a917e95..d1aadbc 100644 --- a/lib/irc/handler.ex +++ b/lib/irc/handler.ex @@ -105,6 +105,10 @@ defmodule M51.IrcConn.Handler do @capabilities_ls Map.merge(@capabilities, @informative_capabilities) + @capability_names @capabilities + |> Enum.map(fn {name, {atom, _}} -> {atom, name} end) + |> Map.new() + @valid_batch_types ["draft/multiline"] @doc """ @@ -301,6 +305,22 @@ defmodule M51.IrcConn.Handler do end end + defp cap_ls(is_302, send) do + caps = @capabilities_ls + |> Map.to_list() + |> Enum.sort_by(fn {k, _v} -> k end) + |> Enum.map(fn {k, {_, v}} -> + cond do + is_nil(v) -> k + !is_302 -> k + true -> k <> "=" <> v + end + end) + |> Enum.join(" ") + + send.(%M51.Irc.Command{source: "server.", command: "CAP", params: ["*", "LS", caps]}) + end + # Handles a connection registration command, ie. only NICK/USER/CAP/AUTHENTICATE. # Returns nil, {:nick, new_nick}, {:user, new_gecos}, {:authenticate, user_id}, # :got_cap_ls, or :got_cap_end. @@ -341,30 +361,11 @@ defmodule M51.IrcConn.Handler do nil {"CAP", ["LS", "302"]} -> - caps = - @capabilities_ls - |> Map.to_list() - |> Enum.sort_by(fn {k, _v} -> k end) - |> Enum.map(fn {k, {_, v}} -> - case v do - nil -> k - _ -> k <> "=" <> v - end - end) - |> Enum.join(" ") - - send.(%M51.Irc.Command{source: "server.", command: "CAP", params: ["*", "LS", caps]}) + cap_ls(true, send) :got_cap_ls {"CAP", ["LS" | _]} -> - caps = - @capabilities_ls - |> Map.to_list() - |> Enum.sort_by(fn {k, {_, _v}} -> k end) - |> Enum.map(fn {k, _v} -> k end) - |> Enum.join(" ") - - send.(%M51.Irc.Command{source: "server.", command: "CAP", params: ["*", "LS", caps]}) + cap_ls(false, send) :got_cap_ls {"CAP", ["LIST" | _]} -> @@ -789,11 +790,28 @@ defmodule M51.IrcConn.Handler do {"USER", _} -> nil + {"CAP", ["LS", "302"]} -> + cap_ls(true, send) + + {"CAP", ["LS" | _]} -> + cap_ls(false, send) + {"CAP", ["LIST" | _]} -> - send.(%M51.Irc.Command{source: "server.", command: "CAP", params: ["*", "LIST", "sasl"]}) + caps = + M51.IrcConn.State.capabilities(state) + |> Enum.map(fn cap -> @capability_names[cap] end) + |> Enum.filter(fn cap -> !is_nil(cap) end) + |> Enum.join(" ") + + send.(%M51.Irc.Command{ + source: "server.", + command: "CAP", + params: ["*", "LIST", caps] + }) {"CAP", [subcommand | _]} -> # ERR_INVALIDCAPCMD + # TODO: support CAP REQ to turn caps on and off post-registration. send_numeric.("410", [subcommand, "Invalid CAP subcommand"]) {"CAP", []} -> diff --git a/test/irc/handler_test.exs b/test/irc/handler_test.exs index 3c35eb7..4c33d21 100644 --- a/test/irc/handler_test.exs +++ b/test/irc/handler_test.exs @@ -449,6 +449,26 @@ defmodule M51.IrcConn.HandlerTest do Logger.add_backend(:console) end + test "post-registration CAP LS", %{handler: handler} do + do_connection_registration(handler) + + send(handler, cmd("CAP LS 302")) + assert_line(@cap_ls_302) + + send(handler, cmd("CAP LS")) + assert_line(@cap_ls) + end + + test "post-registration CAP LIST", %{handler: handler} do + caps_requested = ["draft/multiline", "extended-join", "message-tags", "server-time"] + caps_expected = Enum.join(["batch", "labeled-response", "sasl"] ++ caps_requested, " ") + + do_connection_registration(handler, caps_requested) + + send(handler, cmd("CAP LIST")) + assert_line(":server. CAP * LIST :" <> caps_expected <> "\r\n") + end + test "labeled response", %{handler: handler} do do_connection_registration(handler)