feat (api): update character activity and api to allow date range (#299)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions

* feat (api): update character activity and api to allow date range
This commit is contained in:
guarzo
2025-03-21 11:05:48 -06:00
committed by GitHub
parent 999a702291
commit 06fef2296f
3 changed files with 99 additions and 21 deletions

View File

@@ -527,20 +527,22 @@ defmodule WandererApp.Map do
@doc """ @doc """
Returns the raw activity data that can be processed by WandererApp.Character.Activity. Returns the raw activity data that can be processed by WandererApp.Character.Activity.
Only includes characters that are on the map's ACL. Only includes characters that are on the map's ACL.
If days parameter is provided, filters activity to that time period.
""" """
def get_character_activity(map_id) do def get_character_activity(map_id, days \\ nil) do
{:ok, map} = WandererApp.Api.Map.by_id(map_id) {:ok, map} = WandererApp.Api.Map.by_id(map_id)
_map_with_acls = Ash.load!(map, :acls) _map_with_acls = Ash.load!(map, :acls)
{:ok, jumps} = WandererApp.Api.MapChainPassages.by_map_id(%{map_id: map_id}) # Calculate cutoff date if days is provided
thirty_days_ago = DateTime.utc_now() |> DateTime.add(-30 * 24 * 3600, :second) cutoff_date = if days, do: DateTime.utc_now() |> DateTime.add(-days * 24 * 3600, :second), else: nil
# Get activity data # Get activity data
connections_activity = get_connections_activity(map_id, thirty_days_ago) passages_activity = get_passages_activity(map_id, cutoff_date)
signatures_activity = get_signatures_activity(map_id, thirty_days_ago) connections_activity = get_connections_activity(map_id, cutoff_date)
signatures_activity = get_signatures_activity(map_id, cutoff_date)
# Return raw activity data # Return activity data
jumps passages_activity
|> Enum.map(fn passage -> |> Enum.map(fn passage ->
%{ %{
character: passage.character, character: passage.character,
@@ -554,14 +556,40 @@ defmodule WandererApp.Map do
end) end)
end end
defp get_connections_activity(map_id, thirty_days_ago) do defp get_passages_activity(map_id, nil) do
# Query all map chain passages without time filter
from(p in WandererApp.Api.MapChainPassages,
join: c in assoc(p, :character),
where: p.map_id == ^map_id,
group_by: [c.id],
select: {c, count(p.id)}
)
|> WandererApp.Repo.all()
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
end
defp get_passages_activity(map_id, cutoff_date) do
# Query map chain passages with time filter
from(p in WandererApp.Api.MapChainPassages,
join: c in assoc(p, :character),
where:
p.map_id == ^map_id and
p.inserted_at > ^cutoff_date,
group_by: [c.id],
select: {c, count(p.id)}
)
|> WandererApp.Repo.all()
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
end
defp get_connections_activity(map_id, nil) do
# Query all connection activity without time filter
from(ua in WandererApp.Api.UserActivity, from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character), join: c in assoc(ua, :character),
where: where:
ua.entity_id == ^map_id and ua.entity_id == ^map_id and
ua.entity_type == :map and ua.entity_type == :map and
ua.event_type == :map_connection_added and ua.event_type == :map_connection_added,
ua.inserted_at > ^thirty_days_ago,
group_by: [c.id], group_by: [c.id],
select: {c.id, count(ua.id)} select: {c.id, count(ua.id)}
) )
@@ -569,14 +597,43 @@ defmodule WandererApp.Map do
|> Map.new() |> Map.new()
end end
defp get_signatures_activity(map_id, thirty_days_ago) do defp get_connections_activity(map_id, cutoff_date) do
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :map_connection_added and
ua.inserted_at > ^cutoff_date,
group_by: [c.id],
select: {c.id, count(ua.id)}
)
|> WandererApp.Repo.all()
|> Map.new()
end
defp get_signatures_activity(map_id, nil) do
# Query all signature activity without time filter
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :signatures_added,
select: {ua.character_id, ua.event_data}
)
|> WandererApp.Repo.all()
|> process_signatures_data()
end
defp get_signatures_activity(map_id, cutoff_date) do
from(ua in WandererApp.Api.UserActivity, from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character), join: c in assoc(ua, :character),
where: where:
ua.entity_id == ^map_id and ua.entity_id == ^map_id and
ua.entity_type == :map and ua.entity_type == :map and
ua.event_type == :signatures_added and ua.event_type == :signatures_added and
ua.inserted_at > ^thirty_days_ago, ua.inserted_at > ^cutoff_date,
select: {ua.character_id, ua.event_data} select: {ua.character_id, ua.event_data}
) )
|> WandererApp.Repo.all() |> WandererApp.Repo.all()

View File

@@ -648,15 +648,17 @@ defmodule WandererAppWeb.MapAPIController do
Returns character activity data for a map. Returns character activity data for a map.
Requires either `?map_id=<UUID>` or `?slug=<map-slug>`. Requires either `?map_id=<UUID>` or `?slug=<map-slug>`.
Optional `days` parameter to filter activity to a specific time period.
Example: Example:
GET /api/map/character_activity?map_id=<uuid> GET /api/map/character_activity?map_id=<uuid>
GET /api/map/character_activity?slug=<map-slug> GET /api/map/character_activity?slug=<map-slug>
GET /api/map/character_activity?map_id=<uuid>&days=7
""" """
@spec character_activity(Plug.Conn.t(), map()) :: Plug.Conn.t() @spec character_activity(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :character_activity, operation :character_activity,
summary: "Get Character Activity", summary: "Get Character Activity",
description: "Returns character activity data for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.", description: "Returns character activity data for a map. If days parameter is provided, filters activity to that time period, otherwise returns all activity. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [ parameters: [
map_id: [ map_id: [
in: :query, in: :query,
@@ -671,6 +673,13 @@ defmodule WandererAppWeb.MapAPIController do
type: :string, type: :string,
required: false, required: false,
example: "map-name" example: "map-name"
],
days: [
in: :query,
description: "Optional: Number of days to look back for activity data. If not provided, returns all activity history.",
type: :integer,
required: false,
example: "7"
] ]
], ],
responses: [ responses: [
@@ -691,9 +700,10 @@ defmodule WandererAppWeb.MapAPIController do
}} }}
] ]
def character_activity(conn, params) do def character_activity(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params) do with {:ok, map_id} <- Util.fetch_map_id(params),
# Get raw activity data directly from the Map module instead of the Activity processor {:ok, days} <- parse_days(params["days"]) do
raw_activity = WandererApp.Map.get_character_activity(map_id) # Get raw activity data (filtered by days if provided, otherwise all activity)
raw_activity = WandererApp.Map.get_character_activity(map_id, days)
# Group activities by user_id and summarize # Group activities by user_id and summarize
summarized_result = summarized_result =
@@ -744,6 +754,15 @@ defmodule WandererAppWeb.MapAPIController do
end end
end end
# Parse days parameter, return nil if not provided to show all activity
defp parse_days(nil), do: {:ok, nil}
defp parse_days(days_str) do
case Integer.parse(days_str) do
{days, ""} when days > 0 -> {:ok, days}
_ -> {:ok, nil} # Return nil if invalid to show all activity
end
end
# If hours_str is present and valid, parse it. Otherwise return nil (no filter). # If hours_str is present and valid, parse it. Otherwise return nil (no filter).
defp parse_hours_ago(nil), do: nil defp parse_hours_ago(nil), do: nil
defp parse_hours_ago(hours_str) do defp parse_hours_ago(hours_str) do

View File

@@ -338,6 +338,7 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
```bash ```bash
GET /api/map/character-activity?map_id=<UUID> GET /api/map/character-activity?map_id=<UUID>
GET /api/map/character-activity?slug=<map-slug> GET /api/map/character-activity?slug=<map-slug>
GET /api/map/character-activity?map_id=<UUID>&days=7
``` ```
- **Description:** Retrieves character activity data for a map, including passages, connections, and signatures. - **Description:** Retrieves character activity data for a map, including passages, connections, and signatures.
@@ -345,12 +346,13 @@ GET /api/map/character-activity?slug=<map-slug>
- **Parameters:** - **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map. - `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map. - `slug` (optional if `map_id` is provided) — the slug identifier of the map.
- `days` (optional) — if provided, filters activity data to only include records from the specified number of days. If not provided, returns all activity history.
#### Example Request #### Example Request
```bash ```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/character-activity?slug=some-slug" "https://wanderer.example.com/api/map/character-activity?slug=some-slug&days=7"
``` ```
#### Example Response #### Example Response
@@ -859,7 +861,7 @@ curl -X DELETE \
{ "ok": true } { "ok": true }
``` ```
--- ----
## Conclusion ## Conclusion
@@ -876,9 +878,9 @@ For the most up-to-date and interactive documentation, we recommend using the Sw
If you have any questions or need assistance with the API, please reach out to the Wanderer Team. If you have any questions or need assistance with the API, please reach out to the Wanderer Team.
--- ----
Fly safe, Fly safe,
**The Wanderer Team** **The Wanderer Team**
--- ----