8000 ✨ Discord Get Channel Acitivy Lens by sojinmm · Pull Request #248 · Spectral-Finance/lux · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

✨ Discord Get Channel Acitivy Lens #248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions lux/lib/lux/lenses/discord/analytics/get_channel_activity.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
defmodule Lux.Lenses.Discord.Analytics.GetChannelActivity do
@moduledoc """
A lens for retrieving activity metrics for a Discord channel.
This lens provides detailed analytics about channel activity including:
- Message count over time
- Active users count
- Peak activity periods
- Message type distribution

## Examples
iex> GetChannelActivity.focus(%{
...> channel_id: "123456789012345678",
...> time_range: "24h" # 24h, 7d, 30d
...> }, %{})
{:ok, %{
message_count: 150,
active_users: 25,
peak_hour: "18:00",
message_types: %{
text: 120,
image: 20,
link: 10
},
activity_timeline: [
%{hour: "00:00", count: 5},
%{hour: "01:00", count: 3},
# ... more hourly data
]
}}
"""

alias Lux.Integrations.Discord

use Lux.Lens,
name: "Get Discord Channel Activity",
description: "Retrieves detailed activity metrics for a Discord channel",
url: "https://discord.com/api/v10/channels/:channel_id/messages",
method: :get,
headers: Discord.headers(),
auth: Discord.auth(),
schema: %{
type: :object,
properties: %{
channel_id: %{
type: :string,
description: "The ID of the channel to analyze",
pattern: "^[0-9]{17,20}$"
},
time_range: %{
type: :string,
description: "Time range for analysis (24h, 7d, or 30d)",
enum: ["24h", "7d", "30d"],
default: "24h"
},
limit: %{
type: :integer,
description: "Maximum number of messages to analyze",
minimum: 1,
maximum: 100,
default: 100
}
},
required: ["channel_id"]
}

@doc """
Transforms the Discord API response into activity metrics.
Analyzes message data to generate activity statistics.
"""
@impl true
def after_focus(messages) when is_list(messages) do
metrics = %{
message_count: length(messages),
active_users: count_active_users(messages),
peak_hour: find_peak_hour(messages),
message_types: analyze_message_types(messages),
activity_timeline: generate_timeline(messages)
}

{:ok, metrics}
end

def after_focus(%{"message" => message}) do
{:error, %{"message" => message}}
end

# Private helper functions

defp count_active_users(messages) do
messages
|> Enum.map(& &1["author"]["id"])
|> Enum.uniq()
|> length()
end

defp find_peak_hour([]) do
"00:00"
end

defp find_peak_hour(messages) do
messages
|> Enum.group_by(fn message ->
{:ok, timestamp, _} = DateTime.from_iso8601(message["timestamp"])
"#{String.pad_leading("#{timestamp.hour}", 2, "0")}:00"
end)
|> Enum.max_by(fn {_hour, msgs} -> length(msgs) end)
|> elem(0)
end

defp analyze_message_types(messages) do
messages
|> Enum.reduce(%{text: 0, image: 0, link: 0}, fn message, acc ->
cond do
message["attachments"] != [] -> Map.update!(acc, :image, &(&1 + 1))
String.contains?(message["content"] || "", "http") -> Map.update!(acc, :link, &(&1 + 1))
true -> Map.update!(acc, :text, &(&1 + 1))
end
end)
end

defp generate_timeline(messages) do
messages
|> Enum.group_by(fn message ->
{:ok, timestamp, _} = DateTime.from_iso8601(message["timestamp"])
"#{String.pad_leading("#{timestamp.hour}", 2, "0")}:00"
end)
|> Enum.map(fn {hour, msgs} -> %{hour: hour, count: length(msgs)} end)
|> Enum.sort_by(& &1.hour)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
defmodule Lux.Lenses.Discord.Analytics.GetChannelActivityTest do
@moduledoc """
Test suite for the GetChannelActivity module.
These tests verify the lens's ability to:
- Retrieve activity metrics for a Discord channel
- Process message data into meaningful statistics
- Handle Discord API errors appropriately
- Validate input parameters
"""

use UnitAPICase, async: true
alias Lux.Lenses.Discord.Analytics.GetChannelActivity

@channel_id "123456789012345678"
@mock_messages [
%{
"id" => "111111111111111111",
"content" => "Hello world",
"author" => %{"id" => "222222222222222222", "username" => "User 1"},
"timestamp" => "2024-03-28T10:00:00Z",
"attachments" => []
},
%{
"id" => "333333333333333333",
"content" => "Check this link https://example.com",
"author" => %{"id" => "444444444444444444", "username" => "User 2"},
"timestamp" => "2024-03-28T10:30:00Z",
"attachments" => []
},
%{
"id" => "555555555555555555",
"content" => "Look at this image",
"author" => %{"id" => "222222222222222222", "username" => "User 1"},
"timestamp" => "2024-03-28T10:00:00Z",
"attachments" => [%{"url" => "https://example.com/image.jpg"}]
}
]

setup do
Req.Test.verify_on_exit!()
:ok
end

describe "focus/2" do
test "successfully retrieves channel activity metrics" do
Req.Test.expect(Lux.Lens, fn conn ->
assert conn.method == "GET"
assert conn.request_path == "/api/v10/channels/:channel_id/messages"
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bot test-discord-token"]

conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, Jason.encode!(@mock_messages))
end)

assert {:ok, metrics} = GetChannelActivity.focus(%{
"channel_id" => @channel_id
}, %{})

# Verify basic metrics
assert metrics.message_count == 3
assert metrics.active_users == 2
assert metrics.peak_hour == "10:00"

# Verify message type distribution
assert metrics.message_types.text == 1
assert metrics.message_types.link == 1
assert metrics.message_types.image == 1

# Verify timeline
timeline = Enum.find(metrics.activity_timeline, &(&1.hour == "10:00"))
assert timeline.count == 3
end

test "handles empty channel" do
Req.Test.expect(Lux.Lens, fn conn ->
assert conn.method == "GET"
assert conn.request_path == "/api/v10/channels/:channel_id/messages"
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bot test-discord-token"]

conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, Jason.encode!([]))
end)

assert {:ok, metrics} = GetChannelActivity.focus(%{
"channel_id" => @channel_id
}, %{})

assert metrics.message_count == 0
assert metrics.active_users == 0
assert metrics.message_types == %{text: 0, image: 0, link: 0}
assert metrics.activity_timeline == []
assert metrics.peak_hour == "00:00"
end

test "handles Discord API error" do
Req.Test.expect(Lux.Lens, fn conn ->
assert conn.method == "GET"
assert conn.request_path == "/api/v10/channels/:channel_id/messages"
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bot test-discord-token"]

conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(403, Jason.encode!(%{
"message" => "Missing Permissions"
}))
end)

assert {:error, %{"message" => "Missing Permissions"}} = GetChannelActivity.focus(%{
"channel_id" => @channel_id
}, %{})
end
end

describe "schema validation" do
test "validates required fields" do
lens = GetChannelActivity.view()
assert lens.schema.required == ["channel_id"]
end

test "validates channel ID format" do
lens = GetChannelActivity.view()
channel_id = lens.schema.properties.channel_id
assert channel_id.type == :string
assert channel_id.pattern == "^[0-9]{17,20}$"
end

test "validates time range enum" do
lens = GetChannelActivity.view()
time_range = lens.schema.properties.time_range
assert time_range.type == :string
assert time_range.enum == ["24h", "7d", "30d"]
assert time_range.default == "24h"
end

test "validates limit parameter" do
lens = GetChannelActivity.view()
limit = lens.schema.properties.limit
assert limit.type == :integer
assert limit.minimum == 1
assert limit.maximum == 100
assert limit.default == 100
end
end
end
0