opers

0429b57 · gmorell · 2026-05-21 00:12

42 files +2417 -35

Files changed

modified config/config.exs
+22 −0
@@ -31,6 +31,19 @@ config :oh_my_ex_oh_ircd, :services,
31 31 chanserv_nick: "ChanServ",
32 32 service_host: "services."
33 33
34 +# WebIRC LiveView toggle. When enabled, logged-in users can open
35 +# /webirc to participate in the cluster from a browser tab.
36 +config :oh_my_ex_oh_ircd, :webirc, enabled: false
37 +
38 +# IRC operators (OPER command). Each entry's password is a bcrypt hash:
39 +#
40 +# iex> Bcrypt.hash_pwd_salt("yourpassword")
41 +#
42 +# `host_mask` is an IRC glob applied to `user@host` of the connecting
43 +# client; `"*@*"` allows opers from anywhere. Override per-env (likely
44 +# in runtime.exs or env-specific config files).
45 +config :oh_my_ex_oh_ircd, :opers, %{}
46 +
34 47 # IRCd listener defaults; overridden in runtime.exs from env.
35 48 # `allow_anonymous: false` means connections must authenticate via SASL or PASS
36 49 # before completing registration. Flip to `true` (or set IRC_ALLOW_ANONYMOUS=1)
@@ -85,6 +98,15 @@ config :oh_my_ex_oh_ircd, OhMyExOhIrcd.IRC.Listener,
85 98 # ping_timeout_ms or we drop the connection with `Ping timeout`.
86 99 ping_interval_ms: 120_000,
87 100 ping_timeout_ms: 60_000,
101 + # Flood control: each parsed command adds a per-command penalty
102 + # (`flood_penalty_default` by default — see Connection for the
103 + # per-command overrides) to a counter that drains at 1 sec/sec.
104 + # When the counter exceeds `flood_threshold_seconds` the connection
105 + # is dropped with `ERROR :Excess Flood`.
106 + # 20s gives ~10 normal commands or ~13 mode changes in a tight burst
107 + # before the connection is killed. The bucket drains at 1 sec/sec.
108 + flood_threshold_seconds: 20,
109 + flood_penalty_default: 1.0,
88 110 motd: [
89 111 "Welcome to OhMyExOhIRC.",
90 112 "",
modified config/dev.exs
+12 −0
@@ -12,6 +12,10 @@ config :oh_my_ex_oh_ircd, OhMyExOhIrcd.IRC.TLSListener,
12 12 enabled: true,
13 13 port: 6697
14 14
15 +# Enable WebIRC in dev — handy for poking at the LiveView client without
16 +# installing an IRC client locally.
17 +config :oh_my_ex_oh_ircd, :webirc, enabled: true
18 +
15 19 # Configure your database
16 20 config :oh_my_ex_oh_ircd, OhMyExOhIrcd.Repo,
17 21 username: "postgres",
@@ -65,6 +69,14 @@ config :oh_my_ex_oh_ircd, OhMyExOhIrcdWeb.Endpoint,
65 69 # configured to run both http and https servers on
66 70 # different ports.
67 71
72 +config :oh_my_ex_oh_ircd, :opers, %{
73 + "admin" => %{
74 + password_hash: "$2b$12$7ZFGtxWehSNu6j2qcqAcL.hlDG/trb1KaAq2v1lxLfzLRLMfnZrDW",
75 + host_mask: "*@*" # restrict where this oper can log in from
76 + }
77 +}
78 +
79 +
68 80 # Reload browser tabs when matching files change.
69 81 config :oh_my_ex_oh_ircd, OhMyExOhIrcdWeb.Endpoint,
70 82 live_reload: [
added lib/mix/tasks/opers.ex
+89 −0
@@ -0,0 +1,89 @@
1 +defmodule Mix.Tasks.Opers.Grant do
2 + @shortdoc "Grant IRC operator status to a user account"
3 +
4 + @moduledoc """
5 + Flip a user's `is_oper` flag. After this they can `OPER` over IRC
6 + with no arguments (assuming they SASL-authenticate when connecting).
7 +
8 + mix opers.grant alice@example.com
9 + """
10 +
11 + use Mix.Task
12 +
13 + @impl Mix.Task
14 + def run([email]) do
15 + Mix.Task.run("app.start")
16 + do_set(email, true)
17 + end
18 +
19 + def run(_), do: Mix.raise("Usage: mix opers.grant <email>")
20 +
21 + defp do_set(email, flag) do
22 + case OhMyExOhIrcd.Accounts.get_user_by_email(email) do
23 + nil ->
24 + Mix.raise("No account with email #{email}")
25 +
26 + user ->
27 + case OhMyExOhIrcd.Accounts.set_oper(user, flag) do
28 + {:ok, updated} ->
29 + Mix.shell().info(
30 + "#{updated.email}: is_oper=#{updated.is_oper}"
31 + )
32 +
33 + {:error, cs} ->
34 + Mix.raise("Update failed: #{inspect(cs.errors)}")
35 + end
36 + end
37 + end
38 +end
39 +
40 +defmodule Mix.Tasks.Opers.Revoke do
41 + @shortdoc "Revoke IRC operator status from a user account"
42 + @moduledoc "See `mix help opers.grant` — this is the inverse."
43 +
44 + use Mix.Task
45 +
46 + @impl Mix.Task
47 + def run([email]) do
48 + Mix.Task.run("app.start")
49 +
50 + case OhMyExOhIrcd.Accounts.get_user_by_email(email) do
51 + nil ->
52 + Mix.raise("No account with email #{email}")
53 +
54 + user ->
55 + case OhMyExOhIrcd.Accounts.set_oper(user, false) do
56 + {:ok, updated} -> Mix.shell().info("#{updated.email}: is_oper=false")
57 + {:error, cs} -> Mix.raise("Update failed: #{inspect(cs.errors)}")
58 + end
59 + end
60 + end
61 +
62 + def run(_), do: Mix.raise("Usage: mix opers.revoke <email>")
63 +end
64 +
65 +defmodule Mix.Tasks.Opers.List do
66 + @shortdoc "List accounts flagged as IRC operators"
67 +
68 + use Mix.Task
69 +
70 + @impl Mix.Task
71 + def run(_) do
72 + Mix.Task.run("app.start")
73 +
74 + import Ecto.Query
75 +
76 + OhMyExOhIrcd.Repo.all(
77 + from u in OhMyExOhIrcd.Accounts.User,
78 + where: u.is_oper == true,
79 + order_by: [asc: u.email]
80 + )
81 + |> case do
82 + [] ->
83 + Mix.shell().info("(no IRC operators flagged)")
84 +
85 + users ->
86 + Enum.each(users, fn u -> Mix.shell().info(u.email) end)
87 + end
88 + end
89 +end
modified lib/oh_my_ex_oh_ircd/accounts.ex
+15 −0
@@ -281,6 +281,21 @@ defmodule OhMyExOhIrcd.Accounts do
281 281 :ok
282 282 end
283 283
284 + ## IRC operator status
285 +
286 + @doc """
287 + Flip the `is_oper` bit on a user. Returns the updated `%User{}` or an
288 + `{:error, changeset}` tuple. Used by the `mix opers.grant/.revoke`
289 + tasks and by future admin tooling.
290 + """
291 + def set_oper(%User{} = user, is_oper) when is_boolean(is_oper) do
292 + user |> User.oper_changeset(is_oper) |> Repo.update()
293 + end
294 +
295 + @doc "True iff the user is flagged as an IRC operator."
296 + def oper?(%User{is_oper: true}), do: true
297 + def oper?(_), do: false
298 +
284 299 ## Token helper
285 300
286 301 defp update_user_and_delete_all_tokens(changeset) do
modified lib/oh_my_ex_oh_ircd/accounts/user.ex
+9 −0
@@ -8,10 +8,19 @@ defmodule OhMyExOhIrcd.Accounts.User do
8 8 field :hashed_password, :string, redact: true
9 9 field :confirmed_at, :utc_datetime
10 10 field :authenticated_at, :utc_datetime, virtual: true
11 + # IRC operator flag — see OhMyExOhIrcd.IRC.Connection's OPER command
12 + # and `Accounts.set_oper/2`. The static `:opers` app-env map remains
13 + # available as a break-glass fallback for bootstrap.
14 + field :is_oper, :boolean, default: false
11 15
12 16 timestamps(type: :utc_datetime)
13 17 end
14 18
19 + @doc "Changeset that flips the IRC-operator bit."
20 + def oper_changeset(user, is_oper) when is_boolean(is_oper) do
21 + change(user, is_oper: is_oper)
22 + end
23 +
15 24 @doc """
16 25 A user changeset for registering or changing the email.
17 26
modified lib/oh_my_ex_oh_ircd/channels.ex
+34 −0

Click to load diff…

added lib/oh_my_ex_oh_ircd/irc/broadcast.ex
+64 −0

Click to load diff…

modified lib/oh_my_ex_oh_ircd/irc/channel.ex
+19 −6

Click to load diff…

modified lib/oh_my_ex_oh_ircd/irc/connection.ex
+575 −25

Click to load diff…

added lib/oh_my_ex_oh_ircd/irc/klines.ex
+78 −0

Click to load diff…

added lib/oh_my_ex_oh_ircd/irc/klines/kline.ex
+28 −0

Click to load diff…

modified lib/oh_my_ex_oh_ircd/irc/listener.ex
+3 −1

Click to load diff…

added lib/oh_my_ex_oh_ircd/irc/opers.ex
+54 −0

Click to load diff…

modified lib/oh_my_ex_oh_ircd/irc/tls_listener.ex
+3 −1

Click to load diff…

added lib/oh_my_ex_oh_ircd/irc/web_session.ex
+247 −0

Click to load diff…

modified lib/oh_my_ex_oh_ircd_web/components/layouts.ex
+18 −0

Click to load diff…

added lib/oh_my_ex_oh_ircd_web/live/user_live/channels.ex
+154 −0

Click to load diff…

added lib/oh_my_ex_oh_ircd_web/live/web_irc_live.ex
+688 −0

Click to load diff…

modified lib/oh_my_ex_oh_ircd_web/router.ex
+2 −0

Click to load diff…

added priv/repo/migrations/20260523000000_create_klines.exs
+18 −0

Click to load diff…

added priv/repo/migrations/20260524000000_add_is_oper_to_users.exs
+11 −0

Click to load diff…

modified test/oh_my_ex_oh_ircd/irc/channel_modes_test.exs
+2 −2

Click to load diff…

added test/oh_my_ex_oh_ircd/irc/flood_test.exs
+117 −0

Click to load diff…

added test/oh_my_ex_oh_ircd/irc/oper_test.exs
+155 −0

Click to load diff…

Parents: cac0cb2