Browse Source

feat(hfeed): Add basis.

tags/v0.1.2^2
jackyalcine 3 months ago
parent
commit
0d40106d98
Signed by: me <yo@jacky.wtf> GPG Key ID: 537A4F904B15268D
12 changed files with 274 additions and 25 deletions
  1. +6
    -1
      lib/feed.ex
  2. +212
    -2
      lib/feed/hfeed.ex
  3. +1
    -1
      lib/indieweb/mf2.ex
  4. +5
    -4
      lib/indieweb/websub.ex
  5. +2
    -2
      lib/web.ex
  6. +4
    -4
      test/integration/controllers/entry_controller_test.exs
  7. +5
    -2
      test/support/factory.ex
  8. +32
    -0
      test/unit/feed/hfeed_test.exs
  9. +1
    -1
      web/controllers/entry_controller.ex
  10. +4
    -1
      web/controllers/feed_controller.ex
  11. +0
    -5
      web/router.ex
  12. +2
    -2
      web/templates/settings/_view-metadata.html.eex

+ 6
- 1
lib/feed.ex View File

@@ -21,7 +21,11 @@ defmodule Koype.Feed do
@semantic_types ~w(all responses content notifications category)a

def formats() do
[%{"name" => "JSON Feed", "type" => "json"}, %{"name" => "ATOM 1.0", "type" => "atom"}]
[
%{"name" => "JSON Feed", "type" => "json"},
%{"name" => "ATOM 1.0", "type" => "atom"},
%{"name" => "hFeed", "type" => "hfeed"}
]
end

def items(options \\ []) do
@@ -161,6 +165,7 @@ defmodule Koype.Feed do
defp do_get_module_for_type(type)
defp do_get_module_for_type(:json), do: Koype.Feed.Json
defp do_get_module_for_type(:atom), do: Koype.Feed.Atom
defp do_get_module_for_type(:hfeed), do: Koype.Feed.HFeed
defp do_get_module_for_type(_), do: :not_supported

defp do_extract_semantic_types(type, options) when type in @semantic_types,


+ 212
- 2
lib/feed/hfeed.ex View File

@@ -17,7 +17,217 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
defmodule Koype.Feed.HFeed do
def generate(_items, _options) do
# TODO: Generate h-feed HTML using Floki
require Logger

def default_options() do
[
title: Koype.host(),
description: "A feed available at #{Koype.host()}",
uri: [
icon: Koype.Profile.photo_uri(),
favicon: Koype.Profile.photo_uri()
]
]
end

def generate(items, options) do
uri = Keyword.merge(default_options()[:uri], Keyword.get(options, :uri, []))
final_options = Keyword.merge(default_options(), options) |> Keyword.put(:uri, uri)

result = %{
"title" => final_options[:title],
"home_page_url" => final_options[:uri][:self],
"feed_url" => final_options[:uri][:feed],
"description" => final_options[:description],
"next_url" => final_options[:uri][:next],
"previous_url" => final_options[:uri][:previous],
"last_url" => final_options[:uri][:last],
"first_url" => final_options[:uri][:first],
"icon" => final_options[:uri][:icon],
"favicon" => final_options[:uri][:favicon],
"items" => items.entries,
"hubs" => [
%{
"url" => final_options[:uri][:hub],
"type" => "websub"
}
],
"author" => %{
"name" => Koype.Profile.displayed_name(),
"url" => Koype.host(),
"avatar" => final_options[:uri][:icon]
},
"_koype" => %{
"about" => "https://koype.net",
"version" => Koype.version()
}
}

Enum.join(
[do_generate_header(result), do_generate_contents(result), do_generate_footer(result)],
"\n"
)
end

defp do_generate_header(result) do
hub_strings =
Enum.map(result["hubs"], fn hub -> '<link rel="#{hub["type"]}" href="#{hub["url"]}" />' end)
|> Enum.join("\n")

"""
<html>
<head>
<title>#{result["title"]}</title>
#{hub_strings}
<link rel="shortcut icon" href="#{result["favicon"]}" />
<link rel="self" href="#{result["feed_url"]}" />
<link rel="next" href="#{result["next_url"]}" />
<link rel="first" href="#{result["first_url"]}" />
<link rel="last" href="#{result["last_url"]}" />
<link rel="previous" href="#{result["previous_url"]}" />
</head>
<body class="h-feed">
<h1 class="p-name"><a href="#{result["feed_url"]}">#{result["title"]}</a></h1>
<p class="e-note">#{result["description"]}</p>
<p>
Generated for <a href="#{result["author"]["url"]}" class="u-author p-name">#{
result["author"]["name"]
}</a>.
</p>
"""
end

defp do_generate_footer(_) do
"""
</body>
</html>
"""
end

defp do_generate_contents(result) do
result["items"]
|> Enum.map(fn item ->
Task.async(fn ->
do_convert_item_to_html(item) |> Apex.ap()
end)
end)
|> Task.yield_many(20_000)
|> Enum.map(fn
{task, nil} -> Task.shutdown(task, :brutual_kill)
{_, {:ok, res}} when not is_nil(res) -> res
_ -> nil
end)
|> Enum.reject(&is_nil/1)
|> Enum.join()
end

defp do_convert_item_to_html(item)

defp do_convert_item_to_html(%Koype.Repo.Webmention{} = item) do
with(
{:ok, webmention_json} <- Koype.Repo.Webmention.json_find(item),
{:ok, _entry} <- IndieWeb.Webmention.resolve_target_from_url(item.target)
) do
Apex.ap(webmention_json)
wm_mf2 = webmention_json["mf2"]

"""
<div class="h-entry">
<a class="u-url" href="#{item.source}"></a>
#{
Enum.join(
[
do_generate_author(webmention_json["author"]),
do_extract_reply_links(wm_mf2),
do_extract_media(wm_mf2),
do_extract_content(wm_mf2)
]
|> Enum.reject(&is_nil/1),
"\n"
)
}
</div>
"""
else
_ -> ""
end
end

def do_generate_author(author) do
'<a class="u-author p-name" href="#{author["url"]}">#{author["name"]}</a>'
end

def do_get_url_of_reply(mf2)
def do_get_url_of_reply(mf2) when is_binary(mf2), do: mf2
def do_get_url_of_reply(mf2) when is_map(mf2), do: mf2["url"]

def do_extract_reply_links(wm_mf2) do
props = wm_mf2["properties"]
resolved_types = IndieWeb.Post.extract_types(props)
dominant_type = IndieWeb.Post.determine_type(props, resolved_types)

case dominant_type do
:like ->
'<a class="u-like-of" href="#{do_get_url_of_reply(props["like-of"])}"></a>'

:repost ->
'<a class="u-repost-of" href="#{do_get_url_of_reply(props["repost-of"])}"></a>'

:bookmark ->
'<a class="u-bookmark-of" href="#{do_get_url_of_reply(props["bookmark-of"])}"></a>'

:reply ->
'<a class="u-in-reply-to" href="#{do_get_url_of_reply(props["in-reply-to"])}"></a>'

:rsvp ->
'<a class="u-in-reply-to" href="#{do_get_url_of_reply(props["in-reply-to"])}"></a>'

_ ->
""
end
end

def do_extract_media(wm_mf2) do
wm_mf2["properties"]
|> Map.take(~w(audio video photo))
|> Enum.map(fn
{"photo", media} ->
media
|> Enum.map(fn entry -> '<img src="#{entry}" class="u-photo" />' end)
|> Enum.join("\n")

{"video", media} ->
media
|> Enum.map(fn entry ->
'<video controls><source src="#{entry}" class="u-video" /></video>'
end)
|> Enum.join("\n")

{"audio", media} ->
media
|> Enum.map(fn entry ->
'<audio controls><source src="#{entry}" class="u-audio" /></audio>'
end)
|> Enum.join("\n")

_ ->
nil
end)
|> Enum.reject(&is_nil/1)
|> Enum.join("\n")
end

def do_extract_content(wm_mf2) do
content_mf2 = wm_mf2["properties"]["content"]
is_html = Map.has_key?(content_mf2, "html")

content =
if is_html do
content_mf2["html"]
else
content_mf2["text"]
end

'<div class="#{if is_html, do: "e-content", else: "p-content"}">#{content}</div>'
end
end

+ 1
- 1
lib/indieweb/mf2.ex View File

@@ -42,7 +42,7 @@ defmodule IndieWeb.MF2 do
def parse_item(item, format) do
types = Map.get(item, "type", [])

if Enum.member?(types, "h-#{format}") do
if Enum.member?(types |> Apex.ap(), "h-#{format}") |> Apex.ap() do
[item]
else
Logger.info(


+ 5
- 4
lib/indieweb/websub.ex View File

@@ -11,11 +11,12 @@ defmodule IndieWeb.WebSub do

def notify!(%Koype.Repo.Entry{} = entry), do: notify!(feeds_for(entry))

def notify!(urls) when is_binary(urls) do
def notify!(urls) when is_list(urls) do
payload = %{"hub.mode" => "publish", "hub.url" => urls} |> Plug.Conn.Query.encode()
endpoint = hub_endpoint()

if hub_endpoint() != nil do
case Koype.Http.post(hub_endpoint(),
if endpoint != nil do
case Koype.Http.post(endpoint,
body: payload,
headers: %{"Content-Type" => "application/x-www-form-urlencoded"}
) do
@@ -34,7 +35,7 @@ defmodule IndieWeb.WebSub do
error
end
else
Logger.warn("No WebSub endpoint was defined so no publish action will occur.", urls: urls)
Logger.info("No WebSub endpoint was defined so no publish action will occur.", urls: urls)
end
end



+ 2
- 2
lib/web.ex View File

@@ -81,13 +81,13 @@ defmodule Koype.Web do

{:error, %Plug.Conn{} = error_conn} ->
error_conn
|> put_resp_content_type("text/plain")
|> put_resp_content_type("text/plain", nil)

{:error, error} ->
Logger.warn("Failed to render the page.", error: inspect(error))

conn
|> put_resp_content_type("text/html")
|> put_resp_content_type("text/html", nil)
|> Explode.internal_server_error("Failed to render the page.")
end
end


+ 4
- 4
test/integration/controllers/entry_controller_test.exs View File

@@ -37,8 +37,8 @@ defmodule Koype.Web.EntryControllerTest do
resp =
build_conn() |> put_req_header("accept", "application/json") |> get(Model.get_path(entry))

assert response(resp, :bad_request)
# assert resp.resp_headers |> Map.new() |> Map.get("content-type", "") =~ "mf2+json"
assert response(resp, :ok)
assert resp.resp_headers |> Map.new() |> Map.get("content-type", "") =~ "mf2+json"
end

test "200 when visiting public post as MF2+JSON" do
@@ -51,8 +51,8 @@ defmodule Koype.Web.EntryControllerTest do
|> put_req_header("accept", "application/mf2+json")
|> get(Model.get_path(entry))

assert response(mf2_resp, :bad_request)
# assert mf2_resp.resp_headers |> Map.new() |> Map.get("content-type", "") =~ "mf2+json"
assert response(mf2_resp, :ok)
assert mf2_resp.resp_headers |> Map.new() |> Map.get("content-type", "") =~ "mf2+json"
end

@tag skip: true


+ 5
- 2
test/support/factory.ex View File

@@ -503,8 +503,11 @@ defmodule Koype.Factory do
def key_for_post_type(:bookmark), do: "bookmark-of"
def key_for_post_type(:repost), do: "repost-of"

def with_mf2(%Koype.Repo.Webmention{} = model) do
mf2 = %{"properties" => entry_json_factory() |> with_html_content, "type" => ["h-entry"]}
def with_mf2(%Koype.Repo.Webmention{} = model, mf2 \\ %{}) do
mf2 = %{
"properties" => entry_json_factory() |> with_html_content |> Map.merge(mf2),
"type" => ["h-entry"]
}

{:ok, _} =
Koype.Repo.Webmention.json_persist(model, %{


+ 32
- 0
test/unit/feed/hfeed_test.exs View File

@@ -0,0 +1,32 @@
defmodule Koype.Feed.HFeedTest do
use Koype.DataCase
alias Koype.Feed.HFeed, as: Subject

describe ".generate/2" do
test "renders a empty h-feed with no items" do
entries = insert_list(5, :webmention, source: "https://jacky.wtf")
Enum.each(entries, fn wm -> {:ok, _} = wm |> with_mf2 end)

hfeed_html =
Subject.generate(
%{
entries: entries,
uri: %{
self: "/self",
feed: "/the_feed",
previous: Faker.Internet.url(),
next: Faker.Internet.url()
}
},
[]
)

hfeed_mf2 =
hfeed_html
|> Microformats2.parse("https://jacky.wtf")

assert hfeed_mf2
assert IndieWeb.MF2.get_format(hfeed_mf2, :feed)
end
end
end

+ 1
- 1
web/controllers/entry_controller.ex View File

@@ -198,7 +198,7 @@ defmodule Koype.Web.EntryController do
format: format
)

do_entry_content_generation(conn, format, template_data: template_data, entry: entry)
do_entry_content_generation(conn, "html", template_data: template_data, entry: entry)
else
{:ok, %Koype.Repo.Entry{deleted_at: date}} when not is_nil(date) ->
conn


+ 4
- 1
web/controllers/feed_controller.ex View File

@@ -23,6 +23,7 @@ defmodule Koype.Web.FeedController do
defp do_get_feed(format, options)
defp do_get_feed("atom" <> _, options), do: Koype.Feed.generate(:atom, options)
defp do_get_feed("json", options), do: Koype.Feed.generate(:json, options)
defp do_get_feed("hfeed", options), do: Koype.Feed.generate(:hfeed, options)

defp do_get_title(kind)
defp do_get_title("mentions"), do: "Mentions received for #{Koype.Profile.displayed_name()}."
@@ -42,6 +43,7 @@ defmodule Koype.Web.FeedController do

defp do_get_content_type_for_feed("atom"), do: "application/atom+xml"
defp do_get_content_type_for_feed("json"), do: "application/json"
defp do_get_content_type_for_feed("hfeed"), do: "text/html"

defp do_get_feed_self_path(conn, assigns) do
case Map.get(assigns, "type", "content") do
@@ -105,7 +107,8 @@ defmodule Koype.Web.FeedController do
def render(conn, assigns)

@doc false
def render(conn, %{"format" => format, "type" => type} = assigns) when format in ~w(atom json) do
def render(conn, %{"format" => format, "type" => type} = assigns)
when format in ~w(atom json hfeed) do
options = [
description: do_get_title(type),
uri: [


+ 0
- 5
web/router.ex View File

@@ -176,11 +176,6 @@ defmodule Koype.Web.Router do
get("/schema/:version", NodeInfoController, :version)
end

scope "/", Koype.Web do
pipe_through([:browser])
get("/*path", FallbackController, :catch_all)
end

def swagger_info do
%{
info: %{


+ 2
- 2
web/templates/settings/_view-metadata.html.eex View File

@@ -5,7 +5,7 @@
<dl>
<dt class="lh-copy pv2">
<div class="pretty p-success p-fill p-switch center">
<input type="checkbox" name="silo[]" value="facebook" <%= if Koype.Setting.get("metadata:facebook", false), do: "checked" %> />
<input type="checkbox" name="silo[]" value="facebook" <%= if Koype.Setting.get("metadata:facebook", "no") == "yes", do: "checked" %> />
<div class="state">
<label>Facebook</label>
</div>
@@ -13,7 +13,7 @@
</dt>
<dt class="lh-copy pv2">
<div class="pretty p-success p-fill p-switch center">
<input type="checkbox" name="silo[]" value="twitter" <%= if Koype.Setting.get("metadata:twitter", false), do: "checked" %> />
<input type="checkbox" name="silo[]" value="twitter" <%= if Koype.Setting.get("metadata:twitter", "no") == "yes", do: "checked" %> />
<div class="state">
<label>Twitter</label>
</div>


Loading…
Cancel
Save