Credo: split Connection's long command handlers into helpers

7c0d5fe · Gabe Morell · 2026-11-04 20:38

4 files +371 -342

Files changed

modified lib/oh_my_ex_oh_ircd/irc/connection.ex
+371 −342
@@ -765,47 +765,11 @@ defmodule OhMyExOhIrcd.IRC.Connection do
765 765 state
766 766
767 767 channel_target?(target) ->
768 − cond do
769 − not MapSet.member?(state.channels, downcase(target)) ->
770 − send_numeric(socket, state, "404", [state.nick, target, "Cannot send to channel"])
771 −
772 − true ->
773 − case Channel.privmsg(target, prefix(state), text) do
774 − :ok ->
775 − :ok
776 −
777 − {:error, :moderated} ->
778 − send_numeric(socket, state, "404", [
779 − state.nick,
780 − target,
781 − "Cannot send to channel (+m)"
782 − ])
783 −
784 − {:error, :no_external} ->
785 − send_numeric(socket, state, "404", [
786 − state.nick,
787 − target,
788 − "Cannot send to channel (+n)"
789 − ])
790 − end
791 − end
792 −
768 + privmsg_channel(target, text, socket, state)
793 769 state
794 770
795 771 true ->
796 − # PRIVMSG to user — find their connection and deliver via Phoenix.PubSub
797 − # of a per-user topic. Simpler: route through MessageLog with target=nick.
798 − case HordeRegistry.whereis({:user, downcase(target)}) do
799 − pid when is_pid(pid) ->
800 − send(
801 − pid,
802 − {:irc_privmsg, %{sender: prefix(state), target: target, text: text, kind: :privmsg}}
803 − )
804 −
805 − nil ->
806 − send_numeric(socket, state, "401", [state.nick, target, "No such nick/channel"])
807 − end
808 −
772 + privmsg_user(target, text, socket, state)
809 773 state
810 774 end
811 775 end
@@ -913,28 +877,7 @@ defmodule OhMyExOhIrcd.IRC.Connection do
913 877 send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
914 878
915 879 true ->
916 − case Channel.kick(channel, state.nick, target, reason) do
917 − :ok ->
918 − :ok
919 −
920 − {:error, :not_authorized} ->
921 − send_numeric(socket, state, "482", [
922 − state.nick,
923 − channel,
924 − "You're not channel operator"
925 − ])
926 −
927 − {:error, :no_such_member} ->
928 − send_numeric(socket, state, "441", [
929 − state.nick,
930 − target,
931 − channel,
932 − "They aren't on that channel"
933 − ])
934 −
935 − {:error, :not_on_channel} ->
936 − send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
937 − end
880 + kick(channel, target, reason, socket, state)
938 881 end
939 882
940 883 state
@@ -949,29 +892,7 @@ defmodule OhMyExOhIrcd.IRC.Connection do
949 892 send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
950 893
951 894 true ->
952 − case Channel.member_info(channel, state.nick) do
953 − %{access: a} ->
954 − if Channel.rank(a) >= Channel.rank(:halfop) do
955 − case HordeRegistry.whereis({:user, downcase(nick)}) do
956 − pid when is_pid(pid) ->
957 − Channel.record_invite(channel, nick)
958 − send(pid, {:irc_invite, state.nick, channel})
959 − send_numeric(socket, state, "341", [state.nick, nick, channel])
960 −
961 − nil ->
962 − send_numeric(socket, state, "401", [state.nick, nick, "No such nick/channel"])
963 − end
964 − else
965 − send_numeric(socket, state, "482", [
966 − state.nick,
967 − channel,
968 − "You're not channel operator"
969 − ])
970 − end
971 −
972 − nil ->
973 − send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
974 − end
895 + invite(nick, channel, socket, state)
975 896 end
976 897
977 898 state
@@ -1400,19 +1321,23 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1400 1321 # crash the ThousandIsland handler and drop the oper's connection.
1401 1322 # We'd much rather surface the error as a NOTICE and log it.
1402 1323 guard_invite_command(socket, state, "MKINVITE", fn ->
1403 − case OhMyExOhIrcd.Accounts.create_invite(opts) do
1404 − {:ok, invite} ->
1405 − url = signup_base_url() <> "/users/register?invite=#{invite.token}"
1406 − send_server_notice(socket, state, "Invite token: #{invite.token}")
1407 − send_server_notice(socket, state, "Signup URL: #{url}")
1324 + do_create_invite(opts, socket, state)
1325 + end)
1326 + end
1327 + end
1408 1328
1409 − if invite.expires_at,
1410 − do: send_server_notice(socket, state, "Expires: #{invite.expires_at}")
1329 + defp do_create_invite(opts, socket, state) do
1330 + case OhMyExOhIrcd.Accounts.create_invite(opts) do
1331 + {:ok, invite} ->
1332 + url = signup_base_url() <> "/users/register?invite=#{invite.token}"
1333 + send_server_notice(socket, state, "Invite token: #{invite.token}")
1334 + send_server_notice(socket, state, "Signup URL: #{url}")
1411 1335
1412 − {:error, cs} ->
1413 − send_server_notice(socket, state, "Could not create invite: #{inspect(cs.errors)}")
1414 − end
1415 − end)
1336 + if invite.expires_at,
1337 + do: send_server_notice(socket, state, "Expires: #{invite.expires_at}")
1338 +
1339 + {:error, cs} ->
1340 + send_server_notice(socket, state, "Could not create invite: #{inspect(cs.errors)}")
1416 1341 end
1417 1342 end
1418 1343
@@ -1425,19 +1350,21 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1425 1350 invites ->
1426 1351 send_server_notice(socket, state, "Active invites (#{length(invites)}):")
1427 1352
1428 − Enum.each(invites, fn i ->
1429 − note = if i.note in [nil, ""], do: "", else: " — #{i.note}"
1430 −
1431 − send_server_notice(
1432 − socket,
1433 − state,
1434 − " #{i.token} expires=#{i.expires_at || "never"}#{note}"
1435 − )
1436 − end)
1353 + Enum.each(invites, &send_invite_line(&1, socket, state))
1437 1354 end
1438 1355 end)
1439 1356 end
1440 1357
1358 + defp send_invite_line(invite, socket, state) do
1359 + note = if invite.note in [nil, ""], do: "", else: " — #{invite.note}"
1360 +
1361 + send_server_notice(
1362 + socket,
1363 + state,
1364 + " #{invite.token} expires=#{invite.expires_at || "never"}#{note}"
1365 + )
1366 + end
1367 +
1441 1368 # Run an invite command body, converting any crash into a logged error
1442 1369 # plus an error NOTICE — never a dropped connection.
1443 1370 defp guard_invite_command(socket, state, label, fun) do
@@ -1505,17 +1432,21 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1505 1432 send_server_notice(socket, state, "Invalid host: #{host}")
1506 1433
1507 1434 true ->
1508 − case OhMyExOhIrcd.IRC.Vhost.set_live(nick, new_host, state.nick) do
1509 − :ok ->
1510 − send_server_notice(
1511 − socket,
1512 − state,
1513 − "Set host for #{nick} to #{new_host || "(cloak)"}"
1514 − )
1435 + set_live_vhost(nick, new_host, socket, state)
1436 + end
1437 + end
1515 1438
1516 − {:error, :no_such_nick} ->
1517 − send_numeric(socket, state, "401", [state.nick || "*", nick, "No such nick/channel"])
1518 − end
1439 + defp set_live_vhost(nick, new_host, socket, state) do
1440 + case OhMyExOhIrcd.IRC.Vhost.set_live(nick, new_host, state.nick) do
1441 + :ok ->
1442 + send_server_notice(
1443 + socket,
1444 + state,
1445 + "Set host for #{nick} to #{new_host || "(cloak)"}"
1446 + )
1447 +
1448 + {:error, :no_such_nick} ->
1449 + send_numeric(socket, state, "401", [state.nick || "*", nick, "No such nick/channel"])
1519 1450 end
1520 1451 end
1521 1452
@@ -1564,14 +1495,7 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1564 1495 send_join_topic(channel, socket, state)
1565 1496 send_names(channel, socket, state)
1566 1497
1567 − MessageLog.entries(channel)
1568 − |> Enum.each(fn entry ->
1569 − cmd =
1570 − if Map.get(entry, :kind, :privmsg) == :notice, do: "NOTICE", else: "PRIVMSG"
1571 −
1572 − send_msg(socket, Message.build(cmd, [channel, entry.text], entry.sender))
1573 − end)
1574 −
1498 + replay_backlog(channel, socket)
1575 1499 state
1576 1500
1577 1501 {:error, reason} ->
@@ -1582,6 +1506,14 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1582 1506 end
1583 1507 end
1584 1508
1509 + defp replay_backlog(channel, socket) do
1510 + MessageLog.entries(channel)
1511 + |> Enum.each(fn entry ->
1512 + cmd = if Map.get(entry, :kind, :privmsg) == :notice, do: "NOTICE", else: "PRIVMSG"
1513 + send_msg(socket, Message.build(cmd, [channel, entry.text], entry.sender))
1514 + end)
1515 + end
1516 +
1585 1517 # Look up the channel's current flags and (re)cache the `+f` exemption
1586 1518 # locally. Called on JOIN so a returning user re-acquires the bit.
1587 1519 defp refresh_flood_exempt(state, channel) do
@@ -1663,6 +1595,97 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1663 1595 end
1664 1596 end
1665 1597
1598 + defp kick(channel, target, reason, socket, state) do
1599 + case Channel.kick(channel, state.nick, target, reason) do
1600 + :ok ->
1601 + :ok
1602 +
1603 + {:error, :not_authorized} ->
1604 + send_numeric(socket, state, "482", [
1605 + state.nick,
1606 + channel,
1607 + "You're not channel operator"
1608 + ])
1609 +
1610 + {:error, :no_such_member} ->
1611 + send_numeric(socket, state, "441", [
1612 + state.nick,
1613 + target,
1614 + channel,
1615 + "They aren't on that channel"
1616 + ])
1617 +
1618 + {:error, :not_on_channel} ->
1619 + send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
1620 + end
1621 + end
1622 +
1623 + defp invite(nick, channel, socket, state) do
1624 + case Channel.member_info(channel, state.nick) do
1625 + %{access: a} ->
1626 + if Channel.rank(a) >= Channel.rank(:halfop) do
1627 + case HordeRegistry.whereis({:user, downcase(nick)}) do
1628 + pid when is_pid(pid) ->
1629 + Channel.record_invite(channel, nick)
1630 + send(pid, {:irc_invite, state.nick, channel})
1631 + send_numeric(socket, state, "341", [state.nick, nick, channel])
1632 +
1633 + nil ->
1634 + send_numeric(socket, state, "401", [state.nick, nick, "No such nick/channel"])
1635 + end
1636 + else
1637 + send_numeric(socket, state, "482", [
1638 + state.nick,
1639 + channel,
1640 + "You're not channel operator"
1641 + ])
1642 + end
1643 +
1644 + nil ->
1645 + send_numeric(socket, state, "442", [state.nick, channel, "You're not on that channel"])
1646 + end
1647 + end
1648 +
1649 + defp privmsg_channel(target, text, socket, state) do
1650 + if MapSet.member?(state.channels, downcase(target)) do
1651 + case Channel.privmsg(target, prefix(state), text) do
1652 + :ok ->
1653 + :ok
1654 +
1655 + {:error, :moderated} ->
1656 + send_numeric(socket, state, "404", [
1657 + state.nick,
1658 + target,
1659 + "Cannot send to channel (+m)"
1660 + ])
1661 +
1662 + {:error, :no_external} ->
1663 + send_numeric(socket, state, "404", [
1664 + state.nick,
1665 + target,
1666 + "Cannot send to channel (+n)"
1667 + ])
1668 + end
1669 + else
1670 + send_numeric(socket, state, "404", [state.nick, target, "Cannot send to channel"])
1671 + end
1672 + end
1673 +
1674 + # PRIVMSG to user — find their connection and deliver via Phoenix.PubSub
1675 + # of a per-user topic. Simpler: route through MessageLog with target=nick.
1676 + defp privmsg_user(target, text, socket, state) do
1677 + case HordeRegistry.whereis({:user, downcase(target)}) do
1678 + pid when is_pid(pid) ->
1679 + send(
1680 + pid,
1681 + {:irc_privmsg, %{sender: prefix(state), target: target, text: text, kind: :privmsg}}
1682 + )
1683 +
1684 + nil ->
1685 + send_numeric(socket, state, "401", [state.nick, target, "No such nick/channel"])
1686 + end
1687 + end
1688 +
1666 1689 defp send_names(channel, socket, state) do
1667 1690 members =
1668 1691 try do
@@ -1898,145 +1921,144 @@ defmodule OhMyExOhIrcd.IRC.Connection do
1898 1921 GenServer.call(pid, :whois_info, 2_000)
1899 1922 end
1900 1923
1901 − send_numeric(socket, state, "311", [
1902 − state.nick || "*",
1903 − info.nick,
1904 − info.user,
1905 − info.host,
1906 − "*",
1907 − info.realname
1908 − ])
1924 + send_whois_reply(info, socket, state)
1925 + catch
1926 + _, _ ->
1927 + send_numeric(socket, state, "401", [state.nick || "*", target, "No such nick/channel"])
1928 + end
1909 1929
1910 − send_numeric(socket, state, "312", [
1911 − state.nick || "*",
1912 − info.nick,
1913 − state.server_name,
1914 − "oh_my_ex_oh_ircd"
1915 − ])
1930 + nil ->
1931 + send_numeric(socket, state, "401", [state.nick || "*", target, "No such nick/channel"])
1932 + end
1933 + end
1916 1934
1917 − # RPL_WHOISACTUALLY — reveal the real IP behind a cloak/vhost, but
1918 − # only to operators. Skipped when the displayed host already is the
1919 − # real host (cloaking off and no vhost).
1920 − real_host = Map.get(info, :real_host)
1921 −
1922 − if state.operator? and is_binary(real_host) and real_host != info.host do
1923 − send_numeric(socket, state, "338", [
1924 − state.nick || "*",
1925 − info.nick,
1926 − real_host,
1927 − "Is actually using host"
1928 − ])
1929 − end
1935 + defp send_whois_reply(info, socket, state) do
1936 + me = state.nick || "*"
1930 1937
1931 − if info.channels != [] do
1932 − send_numeric(socket, state, "319", [
1933 − state.nick || "*",
1934 − info.nick,
1935 − Enum.join(info.channels, " ")
1936 − ])
1937 − end
1938 + send_numeric(socket, state, "311", [
1939 + me,
1940 + info.nick,
1941 + info.user,
1942 + info.host,
1943 + "*",
1944 + info.realname
1945 + ])
1938 1946
1939 − if info.away do
1940 − send_numeric(socket, state, "301", [state.nick || "*", info.nick, info.away])
1941 − end
1947 + send_numeric(socket, state, "312", [me, info.nick, state.server_name, "oh_my_ex_oh_ircd"])
1942 1948
1943 − if info.account do
1944 − send_numeric(socket, state, "330", [
1945 − state.nick || "*",
1946 − info.nick,
1947 − info.account,
1948 − "is logged in as"
1949 − ])
1950 − end
1949 + send_whois_actually(info, socket, state)
1950 + send_whois_extras(info, socket, state)
1951 1951
1952 − if Map.get(info, :operator?) do
1953 − send_numeric(socket, state, "313", [
1954 − state.nick || "*",
1955 − info.nick,
1956 − "is an IRC operator"
1957 − ])
1958 − end
1952 + send_numeric(socket, state, "317", [
1953 + me,
1954 + info.nick,
1955 + Integer.to_string(info.idle),
1956 + Integer.to_string(info.signon),
1957 + "seconds idle, signon time"
1958 + ])
1959 1959
1960 − send_numeric(socket, state, "317", [
1961 − state.nick || "*",
1962 − info.nick,
1963 − Integer.to_string(info.idle),
1964 − Integer.to_string(info.signon),
1965 − "seconds idle, signon time"
1966 − ])
1960 + send_numeric(socket, state, "318", [me, info.nick, "End of /WHOIS list"])
1961 + end
1967 1962
1968 − send_numeric(socket, state, "318", [state.nick || "*", info.nick, "End of /WHOIS list"])
1969 − catch
1970 − _, _ ->
1971 − send_numeric(socket, state, "401", [state.nick || "*", target, "No such nick/channel"])
1972 − end
1963 + # RPL_WHOISACTUALLY — reveal the real IP behind a cloak/vhost, but
1964 + # only to operators. Skipped when the displayed host already is the
1965 + # real host (cloaking off and no vhost).
1966 + defp send_whois_actually(info, socket, state) do
1967 + real_host = Map.get(info, :real_host)
1973 1968
1974 − nil ->
1975 − send_numeric(socket, state, "401", [state.nick || "*", target, "No such nick/channel"])
1969 + if state.operator? and is_binary(real_host) and real_host != info.host do
1970 + send_numeric(socket, state, "338", [
1971 + state.nick || "*",
1972 + info.nick,
1973 + real_host,
1974 + "Is actually using host"
1975 + ])
1976 + end
1977 + end
1978 +
1979 + defp send_whois_extras(info, socket, state) do
1980 + me = state.nick || "*"
1981 +
1982 + if info.channels != [] do
1983 + send_numeric(socket, state, "319", [me, info.nick, Enum.join(info.channels, " ")])
1984 + end
1985 +
1986 + if info.away do
1987 + send_numeric(socket, state, "301", [me, info.nick, info.away])
1988 + end
1989 +
1990 + if info.account do
1991 + send_numeric(socket, state, "330", [me, info.nick, info.account, "is logged in as"])
1992 + end
1993 +
1994 + if Map.get(info, :operator?) do
1995 + send_numeric(socket, state, "313", [me, info.nick, "is an IRC operator"])
1976 1996 end
1977 1997 end
1978 1998
1979 1999 defp send_who(target, socket, state) do
1980 − cond do
1981 − channel_target?(target) ->
1982 − members = safe_members(target)
1983 −
1984 − Enum.each(members, fn member ->
1985 − {nick, access, user, host} =
1986 − case member do
1987 − %{nick: n, access: a, user: u, host: h} -> {n, a, u || n, h || "unknown"}
1988 − %{nick: n, access: a} -> {n, a, n, "unknown"}
1989 − n when is_binary(n) -> {n, :member, n, "unknown"}
1990 − end
2000 + if channel_target?(target) do
2001 + target
2002 + |> safe_members()
2003 + |> Enum.each(&send_who_member(&1, target, socket, state))
2004 + else
2005 + send_who_nick(target, socket, state)
2006 + end
2007 +
2008 + send_numeric(socket, state, "315", [state.nick || "*", target, "End of /WHO list"])
2009 + end
2010 +
2011 + defp send_who_member(member, target, socket, state) do
2012 + {nick, access, user, host} =
2013 + case member do
2014 + %{nick: n, access: a, user: u, host: h} -> {n, a, u || n, h || "unknown"}
2015 + %{nick: n, access: a} -> {n, a, n, "unknown"}
2016 + n when is_binary(n) -> {n, :member, n, "unknown"}
2017 + end
2018 +
2019 + # Per RFC 2812, flags is `H|G[*][@%+]` — we append the highest-rank prefix.
2020 + flag = "H" <> Channel.prefix(access)
2021 + # Format: <channel> <user> <host> <server> <nick> <flags> :<hopcount> <realname>
2022 + send_numeric(socket, state, "352", [
2023 + state.nick || "*",
2024 + target,
2025 + user,
2026 + host,
2027 + state.server_name,
2028 + nick,
2029 + flag,
2030 + "0 #{nick}"
2031 + ])
2032 + end
2033 +
2034 + # WHO on a nick — just one row if we can find them.
2035 + defp send_who_nick(target, socket, state) do
2036 + case HordeRegistry.whereis({:user, downcase(target)}) do
2037 + pid when is_pid(pid) ->
2038 + try do
2039 + info =
2040 + if pid == self(),
2041 + do: local_whois_info(state),
2042 + else: GenServer.call(pid, :whois_info, 1_000)
2043 +
2044 + flag = if info.away, do: "G", else: "H"
1991 2045
1992 − # Per RFC 2812, flags is `H|G[*][@%+]` — we append the highest-rank prefix.
1993 − flag = "H" <> Channel.prefix(access)
1994 − # Format: <channel> <user> <host> <server> <nick> <flags> :<hopcount> <realname>
1995 2046 send_numeric(socket, state, "352", [
1996 2047 state.nick || "*",
1997 − target,
1998 − user,
1999 − host,
2048 + "*",
2049 + info.user,
2050 + info.host,
2000 2051 state.server_name,
2001 − nick,
2052 + info.nick,
2002 2053 flag,
2003 − "0 #{nick}"
2054 + "0 #{info.realname}"
2004 2055 ])
2005 − end)
2006 −
2007 − send_numeric(socket, state, "315", [state.nick || "*", target, "End of /WHO list"])
2008 −
2009 − true ->
2010 − # WHO on a nick — just one row if we can find them.
2011 − case HordeRegistry.whereis({:user, downcase(target)}) do
2012 − pid when is_pid(pid) ->
2013 − try do
2014 − info =
2015 − if pid == self(),
2016 − do: local_whois_info(state),
2017 − else: GenServer.call(pid, :whois_info, 1_000)
2018 −
2019 − flag = if info.away, do: "G", else: "H"
2020 −
2021 − send_numeric(socket, state, "352", [
2022 − state.nick || "*",
2023 − "*",
2024 − info.user,
2025 − info.host,
2026 − state.server_name,
2027 − info.nick,
2028 − flag,
2029 − "0 #{info.realname}"
2030 − ])
2031 − catch
2032 − _, _ -> :ok
2033 − end
2034 −
2035 − nil ->
2036 − :ok
2056 + catch
2057 + _, _ -> :ok
2037 2058 end
2038 2059
2039 − send_numeric(socket, state, "315", [state.nick || "*", target, "End of /WHO list"])
2060 + nil ->
2061 + :ok
2040 2062 end
2041 2063 end
2042 2064
@@ -2154,59 +2176,61 @@ defmodule OhMyExOhIrcd.IRC.Connection do
2154 2176 end
2155 2177
2156 2178 defp handle_channel_mode(target, [mode_string | params], socket, state) do
2157 − cond do
2158 − not state.registered? ->
2159 − send_numeric(socket, state, "451", ["*", "You have not registered"])
2179 + if state.registered? do
2180 + mode_string
2181 + |> parse_channel_modes(params)
2182 + |> apply_parsed_channel_modes(target, socket, state)
2183 + else
2184 + send_numeric(socket, state, "451", ["*", "You have not registered"])
2185 + end
2186 + end
2160 2187
2161 − true ->
2162 − case parse_channel_modes(mode_string, params) do
2163 − {:ok, {[], list_views}} ->
2164 − Enum.each(list_views, &render_mask_list(&1, target, socket, state))
2165 −
2166 − {:ok, {changes, list_views}} ->
2167 − cond do
2168 − not MapSet.member?(state.channels, downcase(target)) ->
2169 − send_numeric(socket, state, "442", [
2170 − state.nick,
2171 − target,
2172 − "You're not on that channel"
2173 − ])
2174 −
2175 − true ->
2176 − case Channel.apply_mode_changes(target, state.nick, changes,
2177 − operator?: state.operator?
2178 − ) do
2179 − {:ok, _applied} ->
2180 − :ok
2181 −
2182 − {:error, :not_authorized} ->
2183 − send_numeric(socket, state, "482", [
2184 − state.nick,
2185 − target,
2186 − "You're not channel operator"
2187 − ])
2188 −
2189 − {:error, :no_such_member} ->
2190 − send_numeric(socket, state, "401", [state.nick, "*", "No such nick/channel"])
2191 −
2192 − {:error, :not_on_channel} ->
2193 − send_numeric(socket, state, "442", [
2194 − state.nick,
2195 − target,
2196 − "You're not on that channel"
2197 − ])
2198 − end
2199 −
2200 − Enum.each(list_views, &render_mask_list(&1, target, socket, state))
2201 − end
2188 + defp apply_parsed_channel_modes({:ok, {[], list_views}}, target, socket, state) do
2189 + Enum.each(list_views, &render_mask_list(&1, target, socket, state))
2190 + end
2202 2191
2203 − {:error, {:unknown_mode, letter}} ->
2204 − send_numeric(socket, state, "472", [
2205 − state.nick || "*",
2206 − <<letter::utf8>>,
2207 − "is unknown mode"
2208 − ])
2209 − end
2192 + defp apply_parsed_channel_modes({:ok, {changes, list_views}}, target, socket, state) do
2193 + if MapSet.member?(state.channels, downcase(target)) do
2194 + apply_channel_mode_changes(target, changes, socket, state)
2195 + Enum.each(list_views, &render_mask_list(&1, target, socket, state))
2196 + else
2197 + send_numeric(socket, state, "442", [
2198 + state.nick,
2199 + target,
2200 + "You're not on that channel"
2201 + ])
2202 + end
2203 + end
2204 +
2205 + defp apply_parsed_channel_modes({:error, {:unknown_mode, letter}}, _target, socket, state) do
2206 + send_numeric(socket, state, "472", [
2207 + state.nick || "*",
2208 + <<letter::utf8>>,
2209 + "is unknown mode"
2210 + ])
2211 + end
2212 +
2213 + defp apply_channel_mode_changes(target, changes, socket, state) do
2214 + case Channel.apply_mode_changes(target, state.nick, changes, operator?: state.operator?) do
2215 + {:ok, _applied} ->
2216 + :ok
2217 +
2218 + {:error, :not_authorized} ->
2219 + send_numeric(socket, state, "482", [
2220 + state.nick,
2221 + target,
2222 + "You're not channel operator"
2223 + ])
2224 +
2225 + {:error, :no_such_member} ->
2226 + send_numeric(socket, state, "401", [state.nick, "*", "No such nick/channel"])
2227 +
2228 + {:error, :not_on_channel} ->
2229 + send_numeric(socket, state, "442", [
2230 + state.nick,
2231 + target,
2232 + "You're not on that channel"
2233 + ])
2210 2234 end
2211 2235 end
2212 2236
@@ -2232,64 +2256,16 @@ defmodule OhMyExOhIrcd.IRC.Connection do
2232 2256 defp do_parse_modes([letter | rest], op, params, changes, views) do
2233 2257 cond do
2234 2258 Map.has_key?(Channel.mode_letters(), letter) ->
2235 − access = Map.fetch!(Channel.mode_letters(), letter)
2236 −
2237 − case params do
2238 − [target | tail] ->
2239 − do_parse_modes(rest, op, tail, [{op, access, target} | changes], views)
2240 −
2241 − [] ->
2242 − {:error, {:unknown_mode, letter}}
2243 − end
2259 + parse_access_mode(letter, rest, op, params, changes, views)
2244 2260
2245 2261 letter in [?b, ?e, ?I] ->
2246 − type = mask_letter_to_type(letter)
2247 −
2248 − case params do
2249 − [mask | tail] ->
2250 − change =
2251 − if op == :grant, do: {:add_mask, type, mask}, else: {:remove_mask, type, mask}
2252 −
2253 − do_parse_modes(rest, op, tail, [change | changes], views)
2254 −
2255 − [] ->
2256 − do_parse_modes(rest, op, params, changes, [type | views])
2257 − end
2262 + parse_mask_mode(letter, rest, op, params, changes, views)
2258 2263
2259 2264 letter == ?k ->
2260 − # `+k <key>` always consumes an argument; `-k <arg>` also consumes
2261 − # one per RFC. Missing arg = silently ignore (some clients send
2262 − # `MODE #ch k` to inspect; we surface it via 324).
2263 − case params do
2264 − [arg | tail] ->
2265 − change = if op == :grant, do: {:set_key, arg}, else: {:clear_key, arg}
2266 − do_parse_modes(rest, op, tail, [change | changes], views)
2267 −
2268 − [] ->
2269 − do_parse_modes(rest, op, params, changes, views)
2270 − end
2265 + parse_key_mode(rest, op, params, changes, views)
2271 2266
2272 2267 letter == ?l ->
2273 − # `+l N` consumes an int. `-l` doesn't consume.
2274 − case op do
2275 − :grant ->
2276 − case params do
2277 − [arg | tail] ->
2278 − case Integer.parse(arg) do
2279 − {n, ""} when n > 0 ->
2280 − do_parse_modes(rest, op, tail, [{:set_limit, n} | changes], views)
2281 −
2282 − _ ->
2283 − do_parse_modes(rest, op, tail, changes, views)
2284 − end
2285 −
2286 − [] ->
2287 − do_parse_modes(rest, op, params, changes, views)
2288 − end
2289 −
2290 − :revoke ->
2291 − do_parse_modes(rest, op, params, [{:clear_limit} | changes], views)
2292 − end
2268 + parse_limit_mode(rest, op, params, changes, views)
2293 2269
2294 2270 Channel.flag_from_letter(letter) ->
2295 2271 flag = Channel.flag_from_letter(letter)
@@ -2301,6 +2277,59 @@ defmodule OhMyExOhIrcd.IRC.Connection do
2301 2277 end
2302 2278 end
2303 2279
2280 + defp parse_access_mode(letter, rest, op, params, changes, views) do
2281 + access = Map.fetch!(Channel.mode_letters(), letter)
2282 +
2283 + case params do
2284 + [target | tail] ->
2285 + do_parse_modes(rest, op, tail, [{op, access, target} | changes], views)
2286 +
2287 + [] ->
2288 + {:error, {:unknown_mode, letter}}
2289 + end
2290 + end
2291 +
2292 + defp parse_mask_mode(letter, rest, op, params, changes, views) do
2293 + type = mask_letter_to_type(letter)
2294 +
2295 + case params do
2296 + [mask | tail] ->
2297 + change = if op == :grant, do: {:add_mask, type, mask}, else: {:remove_mask, type, mask}
2298 + do_parse_modes(rest, op, tail, [change | changes], views)
2299 +
2300 + [] ->
2301 + do_parse_modes(rest, op, params, changes, [type | views])
2302 + end
2303 + end
2304 +
2305 + # `+k <key>` always consumes an argument; `-k <arg>` also consumes
2306 + # one per RFC. Missing arg = silently ignore (some clients send
2307 + # `MODE #ch k` to inspect; we surface it via 324).
2308 + defp parse_key_mode(rest, op, [arg | tail], changes, views) do
2309 + change = if op == :grant, do: {:set_key, arg}, else: {:clear_key, arg}
2310 + do_parse_modes(rest, op, tail, [change | changes], views)
2311 + end
2312 +
2313 + defp parse_key_mode(rest, op, [], changes, views),
2314 + do: do_parse_modes(rest, op, [], changes, views)
2315 +
2316 + # `+l N` consumes an int. `-l` doesn't consume.
2317 + defp parse_limit_mode(rest, :grant, [arg | tail], changes, views) do
2318 + case Integer.parse(arg) do
2319 + {n, ""} when n > 0 ->
2320 + do_parse_modes(rest, :grant, tail, [{:set_limit, n} | changes], views)
2321 +
2322 + _ ->
2323 + do_parse_modes(rest, :grant, tail, changes, views)
2324 + end
2325 + end
2326 +
2327 + defp parse_limit_mode(rest, :grant, [], changes, views),
2328 + do: do_parse_modes(rest, :grant, [], changes, views)
2329 +
2330 + defp parse_limit_mode(rest, :revoke, params, changes, views),
2331 + do: do_parse_modes(rest, :revoke, params, [{:clear_limit} | changes], views)
2332 +
2304 2333 defp mask_letter_to_type(?b), do: :ban
2305 2334 defp mask_letter_to_type(?e), do: :except
2306 2335 defp mask_letter_to_type(?I), do: :invite

Parents: 77991ba