# `MishkaCmsCore.Activity.Behaviours.Source`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/activity/behaviours/source.ex#L5)

Behaviour for activity-feed data sources. Each adapter answers four
questions:

  * `topics/1`         — which PubSub topics the parent LiveView must
    subscribe to so the feed receives live updates.
  * `handles?/1`       — whether a given broadcast belongs to this source.
  * `to_entry/1`       — turn a live broadcast into a feed entry.
  * `initial_fetch/2`  — fetch the most recent entries for first render.
  * `in_scope?/2`      — (optional) whether a live broadcast belongs in a
    resource-scoped feed; only needed by sources on a global topic.

Adapters live under `MishkaCmsCore.Activity.Sources.*` (e.g.
`PaperTrail`, `Oban`). The `ActivityFeed` LiveComponent composes any
list of sources without knowing what each one does.

# `entry`

```elixir
@type entry() :: %{
  :id =&gt; String.t(),
  :resource_label =&gt; String.t(),
  :action =&gt; atom(),
  :severity =&gt; :info | :warn | :error,
  :tone =&gt; :created | :updated | :destroyed | nil,
  :badge_label =&gt; String.t() | nil,
  :badge_tone =&gt; atom() | nil,
  :inserted_at =&gt; DateTime.t(),
  :source_id =&gt; String.t() | nil,
  :site_id =&gt; String.t() | nil,
  :user_id =&gt; String.t() | nil,
  :message =&gt; String.t(),
  optional(atom()) =&gt; any()
}
```

One activity entry. Every key here is one a renderer reads, so a source that omits one hands a
template a map `entry.that_key` raises on — `MishkaCmsCore.Activity.Helpers.complete_entry/1` fills
the gap at both seams, and `entry_contract_test.exs` fails the source that left it. The optional
tail is the pipeline's own: `:actor_label` and `:site_label` are resolved downstream by
`MishkaCmsCore.Activity.ActivityEntry`, `:host` by the dashboard's Recent activity widget.

# `scope`

```elixir
@type scope() :: %{
  optional(:current_user) =&gt; map() | nil,
  optional(:resources) =&gt; [module()] | nil
}
```

# `handles?`

```elixir
@callback handles?(Phoenix.Socket.Broadcast.t()) :: boolean()
```

# `in_scope?`
*optional* 

```elixir
@callback in_scope?(Phoenix.Socket.Broadcast.t(), scope()) :: boolean()
```

Whether a live broadcast belongs in a feed of the given `scope`. Sources whose
`topics/1` already encode the scope (per-resource topics, e.g. `PaperTrail` /
`Oban`) can omit it — the subscription alone filters them. A source on a GLOBAL
topic (e.g. `EventLog`'s `"event_log:created"`) implements it so a
resource-scoped feed drops out-of-scope broadcasts. A missing callback is
treated as always in scope.

# `initial_fetch`

```elixir
@callback initial_fetch(scope(), limit :: pos_integer()) :: [entry()]
```

# `to_entry`

```elixir
@callback to_entry(Phoenix.Socket.Broadcast.t()) :: entry() | nil
```

# `topics`

```elixir
@callback topics(scope()) :: [String.t()]
```

---

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