neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Credo: split Connection's long command handlers into helpers
7c0d5fe · Gabe Morell · 2026-11-04 20:38
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