# `PhoenixGenApi.Relay`
[🔗](https://github.com/ohhi-vn/phoenix_gen_api/blob/main/lib/phoenix_gen_api/relay/relay.ex#L1)

Relay message feature for PhoenixGenApi.

Provides group-based message relaying where a user sends a message to a group
and all members (including the sender) receive it.

## Group Types

### :public
- Anyone can join immediately as `:active`.
- All members can send and receive messages.

### :private
- New members join with `:pending` status.
- Any existing active member can accept pending members.
- Only `:active` members can send and receive messages.

### :strict_private
- New members join with `:pending` status.
- Only `:admin` members can accept pending members.
- Admins can `:mute` and `:unmute` members.
- Muted members can receive but cannot send messages.

## Architecture

- **RelayServer** (`PhoenixGenApi.RelayServer`) is a GenServer that owns the
  ETS table and serializes all group operations to prevent race conditions.
  All public API functions in this module delegate to the GenServer.

- **ETS table** (`:phoenix_gen_api_relay_groups`) stores group metadata:
  `{group_id, group_type, members_map}` where `members_map` is
  `%{user_id => %{roles: MapSet, status: atom, joined_at: DateTime}}`.

- **Registry** (`PhoenixGenApi.RelayRegistry`) with `:duplicate` keys maps
  `group_id` to `{user_id, channel_pid}` for dispatching messages to channel
  processes via `send/2`.

# `group_type`

```elixir
@type group_type() :: :public | :private | :strict_private
```

# `member_info`

```elixir
@type member_info() :: %{
  roles: MapSet.t(atom()),
  status: member_status(),
  joined_at: DateTime.t()
}
```

# `member_status`

```elixir
@type member_status() :: :active | :pending | :muted
```

# `accept_member`

```elixir
@spec accept_member(String.t(), String.t(), String.t()) ::
  :ok
  | {:error, :not_found | :user_not_in_group | :not_admin | :user_not_pending}
```

Accepts a pending member into a group.

- **private**: any active member can accept.
- **strict_private**: only admin can accept.

Returns `:ok` or `{:error, reason}`.

# `create_group`

```elixir
@spec create_group(String.t(), group_type(), String.t(), pid()) ::
  :ok | {:error, :already_exists}
```

Creates a new group. The creator becomes the admin with `:active` status.

Returns `:ok` or `{:error, :already_exists}`.

# `delete_group`

```elixir
@spec delete_group(String.t()) :: :ok | {:error, :not_found}
```

Deletes a group from ETS. Registry entries are cleaned up when channel
processes terminate.

Returns `:ok` or `{:error, :not_found}`.

# `get_group_info`

```elixir
@spec get_group_info(String.t()) :: {:ok, map()} | {:error, :not_found}
```

Returns group info including type and members map.

# `handle_relay`

```elixir
@spec handle_relay(
  PhoenixGenApi.Structs.Request.t(),
  PhoenixGenApi.Structs.FunConfig.t()
) ::
  PhoenixGenApi.Structs.Response.t()
```

Handles a relay message request. This is the MFA target configured in
`FunConfig` for `request_type: "relay_msg"`.

Extracts `group_id` and `message` from `request.args`, validates the
sender's membership and permissions, then sends `{:relay_message, response}`
to all group members' channel pids via the Registry.

Returns a `Response` with relay status.

# `join_group`

```elixir
@spec join_group(String.t(), String.t(), pid()) ::
  {:ok, :active | :pending} | {:error, :not_found | :already_member}
```

Joins a user to a group.

- **public**: user becomes `:active` immediately.
- **private** / **strict_private**: user becomes `:pending`, needs acceptance.

Returns `{:ok, :active}` or `{:ok, :pending}` depending on group type,
or `{:error, reason}`.

# `leave_group`

```elixir
@spec leave_group(String.t(), String.t()) ::
  :ok | {:error, :not_found | :user_not_in_group}
```

Removes a user from a group and unregisters them from the Registry.

Returns `:ok` or `{:error, reason}`.

# `mute_member`

```elixir
@spec mute_member(String.t(), String.t(), String.t()) ::
  :ok
  | {:error,
     :not_found
     | :not_strict_private
     | :user_not_in_group
     | :not_admin
     | :cannot_mute}
```

Mutes a member in a strict_private group. Only admins can mute.
Muted members can receive but cannot send messages.

Returns `:ok` or `{:error, reason}`.

# `table`

```elixir
@spec table() :: atom()
```

Returns the ETS table name for group metadata storage.

# `unmute_member`

```elixir
@spec unmute_member(String.t(), String.t(), String.t()) ::
  :ok
  | {:error,
     :not_found
     | :not_strict_private
     | :user_not_in_group
     | :not_admin
     | :not_muted}
```

Unmutes a member in a strict_private group. Only admins can unmute.

Returns `:ok` or `{:error, reason}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
