defmodule Omni.Tools.WebFetch.Strategy.Reddit do
@moduledoc false
@behaviour Omni.Tools.WebFetch.Strategy
@max_comments 20
@max_depth 2
@max_replies 5
@impl true
def match?(uri, _opts) do
is_binary(uri.host) and
(uri.host == "reddit.com" or String.ends_with?(uri.host, ".reddit.com"))
end
@impl true
def request(req, _opts) do
uri = req.url
path =
(uri.path || "/")
|> String.trim_trailing("/")
|> then(fn p -> if String.ends_with?(p, ".json"), do: p, else: p <> ".json" end)
query =
case uri.query do
nil -> "raw_json=1"
existing -> existing <> "&raw_json=1"
end
url = URI.to_string(%{uri | path: path, query: query})
Req.merge(req, url: url, headers: [{"user-agent", "Omni.Tools.WebFetch/0.1"}])
end
@impl true
def extract(%{body: body}, _opts) when body in [nil, ""] do
"(empty response)"
end
def extract(response, _opts) do
case Jason.decode(response.body) do
{:ok, [post_listing, comments_listing]} ->
format_post_page(post_listing, comments_listing)
{:ok, %{"kind" => "Listing"} = listing} ->
format_subreddit_listing(listing)
{:ok, _other} ->
pretty_json(response.body)
{:error, _} ->
response.body
end
end
# ── Post page ────────────────────────────────────────────────────
defp format_post_page(post_listing, comments_listing) do
post = get_in(post_listing, ["data", "children", Access.at(0), "data"])
comments = get_in(comments_listing, ["data", "children"]) || []
post_section = format_post(post)
comment_section =
comments
|> Enum.filter(&(&1["kind"] == "t1"))
|> Enum.take(@max_comments)
|> Enum.map(&format_comment(&1["data"], 0))
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n\n")
case comment_section do
"" -> post_section
section -> post_section <> "\n\n---\n\n## Comments\n\n" <> section
end
end
defp format_post(nil), do: "(post data unavailable)"
defp format_post(data) do
title = data["title"] || "(untitled)"
author = data["author"] || "[deleted]"
subreddit = data["subreddit"] || "?"
score = data["score"] || 0
selftext =
case data["selftext"] do
text when is_binary(text) and text != "" -> text
_ -> nil
end
link =
if not data["is_self"] and is_binary(data["url"]) do
"Link: #{data["url"]}"
end
body =
[selftext, link]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n\n")
header = "# #{title}\n\n**r/#{subreddit}** | #{score} points | u/#{author}"
case body do
"" -> header
text -> header <> "\n\n" <> text
end
end
# ── Comments ─────────────────────────────────────────────────────
defp format_comment(nil, _depth), do: ""
defp format_comment(_data, depth) when depth > @max_depth, do: ""
defp format_comment(data, depth) do
author = data["author"] || "[deleted]"
body = data["body"] || ""
if skip_comment?(author, body), do: "", else: do_format_comment(data, author, body, depth)
end
defp do_format_comment(data, author, body, depth) do
score = data["score"] || 0
prefix = String.duplicate("> ", depth)
header = "#{prefix}**u/#{author}** (#{score} points)"
body_lines =
body
|> String.split("\n")
|> Enum.map_join("\n", &"#{prefix}#{&1}")
result = header <> "\n" <> body_lines
replies = format_replies(data["replies"], depth)
case replies do
"" -> result
text -> result <> "\n\n" <> text
end
end
defp format_replies(%{"data" => %{"children" => children}}, depth) when is_list(children) do
children
|> Enum.filter(&(&1["kind"] == "t1"))
|> Enum.take(@max_replies)
|> Enum.map(&format_comment(&1["data"], depth + 1))
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n\n")
end
defp format_replies(_other, _depth), do: ""
defp skip_comment?(author, body) do
author == "[deleted]" and body in ["[removed]", "[deleted]", ""]
end
# ── Subreddit listing ───────────────────────────────────────────
defp format_subreddit_listing(listing) do
children = get_in(listing, ["data", "children"]) || []
posts =
children
|> Enum.filter(&(&1["kind"] == "t3"))
|> Enum.map(fn %{"data" => data} ->
title = data["title"] || "(untitled)"
score = data["score"] || 0
permalink = data["permalink"] || ""
"- **#{score}** [#{title}](https://reddit.com#{permalink})"
end)
subreddit =
case get_in(children, [Access.at(0), "data", "subreddit"]) do
nil -> "Reddit"
sub -> "r/#{sub}"
end
["# #{subreddit}" | posts]
|> Enum.join("\n")
end
defp pretty_json(body) do
case Jason.decode(body) do
{:ok, decoded} -> Jason.encode!(decoded, pretty: true)
{:error, _} -> body
end
end
end