neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
opers
0429b57 · gmorell · 2026-05-21 00:12
Files changed
modified
config/config.exs
+22
−0
| 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 | 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 | 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 | 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
| 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 | 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 | 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