# `ExWapp.Call`
[🔗](https://github.com/elchemista/ex_wapp/blob/v0.1.5/lib/ex_wapp/call.ex#L1)

Call log storage and queries.

One `%ExWapp.Call{}` describes one call: who started it, where it belongs,
how it ended, and how long it lasted. Records are built from three sources
and merged by call ID, so a call first seen live as `<call>` stanzas can
later be enriched by the authoritative `CallLogRecord` the phone syncs:

- `:live` — inbound `<call>` signalling stanzas (offer/accept/terminate/...)
- `:app_state` — `call_log` app-state mutations carrying `CallLogRecord`
- `:history_sync` — `HistorySync.callLogRecords` entries

## Storage

Records are persisted via the session's store adapter under the `:call_log`
key, bounded to `max_records` entries (oldest dropped first) so an active
account cannot grow the store without limit. The bound is configurable via
`config.calls.max_records`.

## Usage

    # Most recent calls, newest first (limit defaults to 50)
    calls = ExWapp.Call.list(session, limit: 20)

    # Only missed incoming calls since a unix timestamp
    missed = ExWapp.Call.list(session, status: :missed, since: 1_754_000_000)

    # Deferred chronological traversal over the bounded retained log
    ExWapp.Call.stream(session) |> Enum.each(&IO.inspect/1)

    # Timestamps are unix seconds
    DateTime.from_unix!(call.start_time)

# `participant`

```elixir
@type participant() :: %{jid: String.t() | nil, status: status()}
```

# `status`

```elixir
@type status() ::
  :ringing
  | :ongoing
  | :connected
  | :missed
  | :rejected
  | :cancelled
  | :accepted_elsewhere
  | :failed
  | :unavailable
  | :upcoming
  | :abandoned
  | :invalid
  | :silenced
  | :unknown
```

Lifecycle status of a call.

`:ringing` and `:ongoing` are transient live states; everything else is a
final outcome. `:missed` means an incoming call ended unanswered,
`:cancelled` an outgoing call ended before the peer accepted, and
`:accepted_elsewhere` that another device of this account took the call.

# `t`

```elixir
@type t() :: %ExWapp.Call{
  accepted_at: non_neg_integer() | nil,
  call_type: :regular | :scheduled_call | :voice_chat | nil,
  chat_jid: String.t() | nil,
  creator_jid: String.t() | nil,
  duration: non_neg_integer() | nil,
  end_reason: String.t() | nil,
  ended_at: non_neg_integer() | nil,
  group_jid: String.t() | nil,
  id: String.t() | nil,
  is_group: boolean(),
  is_incoming: boolean() | nil,
  is_video: boolean() | nil,
  participants: [participant()],
  source: :live | :app_state | :history_sync | nil,
  start_time: non_neg_integer() | nil,
  status: status() | nil
}
```

# `target`

```elixir
@type target() :: pid() | struct()
```

# `clear`

```elixir
@spec clear(target()) :: :ok | {:error, term()}
```

Deletes every stored call record.

# `count`

```elixir
@spec count(target()) :: non_neg_integer()
```

Counts stored call records.

# `default_max_records`

```elixir
@spec default_max_records() :: pos_integer()
```

Default bound on stored call records, used when no configuration overrides
`config.calls.max_records`.

# `delete_by_peer`

```elixir
@spec delete_by_peer(target(), String.t() | ExWapp.JID.t(), boolean() | nil) ::
  :ok | {:error, term()}
```

Deletes stored 1:1 call records for one peer.

Mirrors the app-state `DeleteIndividualCallLogAction`: `is_incoming` limits
the deletion to one direction, `nil` removes both directions.

# `from_call_log_record`

```elixir
@spec from_call_log_record(WAWebProtobufSyncAction.CallLogRecord.t() | nil, keyword()) ::
  t() | nil
```

Converts a `CallLogRecord` protobuf into a call struct.

Returns `nil` when the record has no call ID. `:source` defaults to
`:app_state`; the history-sync ingester passes `source: :history_sync`.

# `get`

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

Gets one stored call by call ID.

# `list`

```elixir
@spec list(target(), keyword()) :: [t()]
```

Lists stored calls, newest first by default.

## Options

- `:limit` — max records returned, `:all` for everything retained (default `50`)
- `:offset` — records skipped before taking `:limit` (default `0`)
- `:order` — `:newest_first` (default) or `:oldest_first`
- `:status` — a `t:status/0` atom or list of them
- `:jid` — only calls belonging to this chat/group JID
- `:since` / `:until` — unix-second bounds on `start_time`

# `merge_from_sync`

```elixir
@spec merge_from_sync(
  target(),
  WAWebProtobufSyncAction.CallLogRecord.t() | nil,
  keyword()
) ::
  :ok | :ignored
```

Applies one `CallLogRecord` coming from app-state or history sync.

Returns `:ok` when the record was stored, `:ignored` when it carries no
usable call ID.

# `stream`

```elixir
@spec stream(target(), keyword()) :: Enumerable.t()
```

Builds a deferred stream over stored calls.

Defaults to oldest first with no limit so a wrapper can export the log in
chronological order. The retained call-log snapshot is loaded and sorted on
first enumeration, not when the stream is created; it is bounded by
`config.calls.max_records`. Accepts the same options as `list/2`.

# `upsert`

```elixir
@spec upsert(target(), t(), keyword()) :: {:ok, t()} | {:error, term()}
```

Inserts or merges one call record by ID and prunes the log to the bound.

Field-level merge: incoming non-nil fields from an equally or more
authoritative source win. App-state and history-sync records are
authoritative over live signalling; a later live stanza may fill a missing
field but cannot replace a synchronized value or outcome. Transient status
progression is monotonic (`:ringing` then `:ongoing`) and never overwrites a
final outcome. When both `accepted_at` and `ended_at` are known and
`duration` is not, the duration is derived from their difference.

## Options

- `:max_records` — retention bound (default `500`)

---

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