neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Credo: split Channel's join and topic loading into helpers
4bbe618 · Gabe Morell · 2026-11-04 20:39
Files changed
modified
lib/oh_my_ex_oh_ircd/irc/channel.ex
+114
−89
@@ -314,29 +314,119 @@ defmodule OhMyExOhIrcd.IRC.Channel do
| 314 | 314 | # Topic source of truth is `channel_topics` (covers unregistered |
| 315 | 315 | # channels too). Fall back to the registered channel's legacy |
| 316 | 316 | # `channels.topic` for rows written before this table existed. |
| 317 | − {topic, set_by, set_at} = | |
| 318 | − cond do | |
| 319 | − topic_rec -> | |
| 320 | − {topic_rec.topic || "", topic_rec.set_by || "", topic_rec.set_at || 0} | |
| 321 | − | |
| 322 | − rec -> | |
| 323 | − legacy_set_at = | |
| 324 | − case rec.updated_at do | |
| 325 | − %DateTime{} = dt -> DateTime.to_unix(dt) | |
| 326 | − _ -> 0 | |
| 327 | − end | |
| 328 | − | |
| 329 | − {rec.topic || "", if(founder_id, do: "ChanServ", else: ""), legacy_set_at} | |
| 330 | − | |
| 331 | − true -> | |
| 332 | − {"", "", 0} | |
| 333 | − end | |
| 317 | + {topic, set_by, set_at} = persisted_topic(topic_rec, rec, founder_id) | |
| 334 | 318 | |
| 335 | 319 | {topic, set_by, set_at, founder_id, modes} |
| 336 | 320 | rescue |
| 337 | 321 | _ -> {"", "", 0, nil, nil} |
| 338 | 322 | end |
| 339 | 323 | |
| 324 | + defp persisted_topic(nil, nil, _founder_id), do: {"", "", 0} | |
| 325 | + | |
| 326 | + defp persisted_topic(nil, rec, founder_id) do | |
| 327 | + legacy_set_at = | |
| 328 | + case rec.updated_at do | |
| 329 | + %DateTime{} = dt -> DateTime.to_unix(dt) | |
| 330 | + _ -> 0 | |
| 331 | + end | |
| 332 | + | |
| 333 | + {rec.topic || "", if(founder_id, do: "ChanServ", else: ""), legacy_set_at} | |
| 334 | + end | |
| 335 | + | |
| 336 | + defp persisted_topic(topic_rec, _rec, _founder_id), | |
| 337 | + do: {topic_rec.topic || "", topic_rec.set_by || "", topic_rec.set_at || 0} | |
| 338 | + | |
| 339 | + # Same pid already joined: a no-op. A rejoin under a new pid skips the | |
| 340 | + # entry checks; a fresh join has to pass all of them. | |
| 341 | + defp join_check(%{pid: pid}, pid, _prefix, _key, _supplied_key, _state), do: :already_joined | |
| 342 | + | |
| 343 | + defp join_check(nil, _pid, prefix, key, supplied_key, state) do | |
| 344 | + cond do | |
| 345 | + prefix != nil and banned?(prefix, state) -> | |
| 346 | + {:error, :banned} | |
| 347 | + | |
| 348 | + needs_invite?(state, key) -> | |
| 349 | + {:error, :invite_only} | |
| 350 | + | |
| 351 | + state.key != nil and supplied_key != state.key -> | |
| 352 | + {:error, :bad_key} | |
| 353 | + | |
| 354 | + state.limit != nil and map_size(state.members) >= state.limit -> | |
| 355 | + {:error, :channel_full} | |
| 356 | + | |
| 357 | + true -> | |
| 358 | + :ok | |
| 359 | + end | |
| 360 | + end | |
| 361 | + | |
| 362 | + defp join_check(_existing, _pid, _prefix, _key, _supplied_key, _state), do: :ok | |
| 363 | + | |
| 364 | + defp needs_invite?(state, key), | |
| 365 | + do: MapSet.member?(state.flags, :invite_only) and not MapSet.member?(state.invited, key) | |
| 366 | + | |
| 367 | + defp admit_member(state, existing, nick, key, pid, user_id, prefix) do | |
| 368 | + if existing, do: Process.demonitor(existing.ref, [:flush]) | |
| 369 | + ref = Process.monitor(pid) | |
| 370 | + access = join_access(existing, user_id, state) | |
| 371 | + {user, host} = parse_prefix(prefix, nick) | |
| 372 | + | |
| 373 | + member = %{ | |
| 374 | + pid: pid, | |
| 375 | + ref: ref, | |
| 376 | + access: access, | |
| 377 | + nick: nick, | |
| 378 | + user_id: user_id, | |
| 379 | + user: user, | |
| 380 | + host: host | |
| 381 | + } | |
| 382 | + | |
| 383 | + members = Map.put(state.members, key, member) | |
| 384 | + # Consume any pending invite. | |
| 385 | + invited = MapSet.delete(state.invited, key) | |
| 386 | + | |
| 387 | + Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), { | |
| 388 | + :irc_presence, | |
| 389 | + {:join, member_prefix(member), state.name} | |
| 390 | + }) | |
| 391 | + | |
| 392 | + # Restore the access prefix on rejoin with a synthetic MODE line. | |
| 393 | + if existing == nil and access not in [:member] do | |
| 394 | + letter = mode_letter(access) || "v" | |
| 395 | + | |
| 396 | + Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), { | |
| 397 | + :irc_presence, | |
| 398 | + {:mode, service_prefix(), state.name, "+" <> letter, [nick]} | |
| 399 | + }) | |
| 400 | + end | |
| 401 | + | |
| 402 | + %{state | members: members, invited: invited} | |
| 403 | + end | |
| 404 | + | |
| 405 | + defp member_access(_state, nil), do: nil | |
| 406 | + | |
| 407 | + defp member_access(state, nick) do | |
| 408 | + case Map.get(state.members, downcase(nick)) do | |
| 409 | + %{access: a} -> a | |
| 410 | + _ -> nil | |
| 411 | + end | |
| 412 | + end | |
| 413 | + | |
| 414 | + defp join_access(existing, user_id, state) do | |
| 415 | + cond do | |
| 416 | + existing -> | |
| 417 | + existing.access | |
| 418 | + | |
| 419 | + user_id != nil and user_id == state.founder_user_id -> | |
| 420 | + :founder | |
| 421 | + | |
| 422 | + user_id != nil and Map.has_key?(state.persisted_access, user_id) -> | |
| 423 | + Map.get(state.persisted_access, user_id) | |
| 424 | + | |
| 425 | + true -> | |
| 426 | + :member | |
| 427 | + end | |
| 428 | + end | |
| 429 | + | |
| 340 | 430 | ## Persistence helper for per-user prefix access (channel_memberships). |
| 341 | 431 | ## Updates the cached `persisted_access` map AND writes to the database |
| 342 | 432 | ## (no-op for unregistered channels and anonymous users). |
@@ -380,76 +470,16 @@ defmodule OhMyExOhIrcd.IRC.Channel do
| 380 | 470 | def handle_call({:join, nick, pid, user_id, prefix, supplied_key}, _from, state) do |
| 381 | 471 | key = downcase(nick) |
| 382 | 472 | existing = Map.get(state.members, key) |
| 383 | − rejoining? = existing != nil | |
| 384 | 473 | |
| 385 | − cond do | |
| 386 | − match?(%{pid: ^pid}, existing) -> | |
| 474 | + case join_check(existing, pid, prefix, key, supplied_key, state) do | |
| 475 | + :already_joined -> | |
| 387 | 476 | {:reply, :ok, state} |
| 388 | 477 | |
| 389 | − not rejoining? and prefix != nil and banned?(prefix, state) -> | |
| 390 | − {:reply, {:error, :banned}, state} | |
| 478 | + {:error, _reason} = err -> | |
| 479 | + {:reply, err, state} | |
| 391 | 480 | |
| 392 | − not rejoining? and MapSet.member?(state.flags, :invite_only) and | |
| 393 | − not MapSet.member?(state.invited, key) -> | |
| 394 | − {:reply, {:error, :invite_only}, state} | |
| 395 | − | |
| 396 | − not rejoining? and state.key != nil and supplied_key != state.key -> | |
| 397 | − {:reply, {:error, :bad_key}, state} | |
| 398 | − | |
| 399 | − not rejoining? and state.limit != nil and map_size(state.members) >= state.limit -> | |
| 400 | − {:reply, {:error, :channel_full}, state} | |
| 401 | − | |
| 402 | − true -> | |
| 403 | − if existing, do: Process.demonitor(existing.ref, [:flush]) | |
| 404 | − ref = Process.monitor(pid) | |
| 405 | − | |
| 406 | − access = | |
| 407 | − cond do | |
| 408 | − existing -> | |
| 409 | − existing.access | |
| 410 | − | |
| 411 | − user_id != nil and user_id == state.founder_user_id -> | |
| 412 | − :founder | |
| 413 | − | |
| 414 | − user_id != nil and Map.has_key?(state.persisted_access, user_id) -> | |
| 415 | − Map.get(state.persisted_access, user_id) | |
| 416 | − | |
| 417 | − true -> | |
| 418 | − :member | |
| 419 | − end | |
| 420 | − | |
| 421 | − {user, host} = parse_prefix(prefix, nick) | |
| 422 | − | |
| 423 | − member = %{ | |
| 424 | − pid: pid, | |
| 425 | − ref: ref, | |
| 426 | − access: access, | |
| 427 | − nick: nick, | |
| 428 | − user_id: user_id, | |
| 429 | − user: user, | |
| 430 | − host: host | |
| 431 | − } | |
| 432 | − | |
| 433 | − members = Map.put(state.members, key, member) | |
| 434 | − # Consume any pending invite. | |
| 435 | − invited = MapSet.delete(state.invited, key) | |
| 436 | − | |
| 437 | − Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), { | |
| 438 | − :irc_presence, | |
| 439 | − {:join, member_prefix(member), state.name} | |
| 440 | − }) | |
| 441 | − | |
| 442 | − # Restore the access prefix on rejoin with a synthetic MODE line. | |
| 443 | − if existing == nil and access not in [:member] do | |
| 444 | − letter = mode_letter(access) || "v" | |
| 445 | − | |
| 446 | − Phoenix.PubSub.broadcast(@pubsub, MessageLog.channel_topic(state.name), { | |
| 447 | − :irc_presence, | |
| 448 | − {:mode, service_prefix(), state.name, "+" <> letter, [nick]} | |
| 449 | − }) | |
| 450 | − end | |
| 451 | − | |
| 452 | − {:reply, :ok, %{state | members: members, invited: invited}} | |
| 481 | + :ok -> | |
| 482 | + {:reply, :ok, admit_member(state, existing, nick, key, pid, user_id, prefix)} | |
| 453 | 483 | end |
| 454 | 484 | end |
| 455 | 485 |
@@ -469,12 +499,7 @@ defmodule OhMyExOhIrcd.IRC.Channel do
| 469 | 499 | |
| 470 | 500 | def handle_call({:message, sender, text, kind}, _from, state) do |
| 471 | 501 | sender_nick = sender |> String.split("!", parts: 2) |> List.first() |
| 472 | − | |
| 473 | − sender_access = | |
| 474 | − case sender_nick && Map.get(state.members, downcase(sender_nick)) do | |
| 475 | − %{access: a} -> a | |
| 476 | − _ -> nil | |
| 477 | − end | |
| 502 | + sender_access = member_access(state, sender_nick) | |
| 478 | 503 | |
| 479 | 504 | cond do |
| 480 | 505 | # +n: only members can speak. |
Parents: 7c0d5fe