Credo: split Channel's join and topic loading into helpers

4bbe618 · Gabe Morell · 2026-11-04 20:39

4 files +114 -89

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