Current section

Files

Jump to
indieweb lib indieweb post.ex
Raw

lib/indieweb/post.ex

defmodule IndieWeb.Post do
@moduledoc """
Post-specific logic for the IndieWeb.
This module provides helper methods for parsing [MF2](http://microformats.org/wiki/microformats2-parsing)
data in a handy and simple fashion. The structure of the properties you'd use would be similar
to that of a <abbr tittle="Micformats2 JSON">MF2+JSON</abbr> object:
```json
{
"items": [
{
"type": [
"h-feed"
],
"properties": {
"name": [
"Updates"
],
"uid": [
"https://v2.jacky.wtf/stream"
],
"url": [
"https://v2.jacky.wtf/stream"
]
},
"lang": "en",
"children": [
{
"type": [
"h-entry"
],
"properties": {
"summary": [
"One big step for Koype is going to be bridging the IndieWeb into the IPFS landscape. I have concerns over it not being privacy-centric since there\u2019s no sense of private data (everything is publishable outwards). IPFS provides pubsub logic so I thi..."
],
"url": [
"https://v2.jacky.wtf/post/4fbdcd28-b558-42bc-99a9-802e33d01810",
"https://v2.jacky.wtf/tag/7c1e5ef9-aa31-40e6-b26a-ecdba728b4a1",
"https://v2.jacky.wtf/tag/bc945dde-95bb-468e-9127-620f1c35cd70"
],
"uid": [
"https://v2.jacky.wtf/post/4fbdcd28-b558-42bc-99a9-802e33d01810"
],
"category": [
"https://v2.jacky.wtf/tag/7c1e5ef9-aa31-40e6-b26a-ecdba728b4a1",
"https://v2.jacky.wtf/tag/bc945dde-95bb-468e-9127-620f1c35cd70"
],
"published": [
"2019-02-15T13:29:51.60956-08:00"
]
},
"lang": "en"
}
]
},
{
"type": [
"h-card"
],
"properties": {
"name": [
"Jacky Alcine"
],
"tz": [
"America/Los_Angeles"
],
"note": [
"I had a dream I could buy my way into heaven. When I woke up, I spent that on a m4.large from EvilCorp. Wait until I get my money right!"
],
"url": [
"https://v2.jacky.wtf"
],
"photo": [
"https://v2.jacky.wtf/media/image/floating/PhotoJacky%20n%203J5430.png?v=original"
]
},
"lang": "en"
}
]
}
```
"""
require Logger
@doc "Returns a list of atoms representing response post types."
@spec response_types() :: list(atom)
def response_types, do: ~w(reply like bookmark repost read rsvp)a
@doc """
Determines if the provided type is a response type.
## Examples
iex> IndieWeb.Post.is_response_type?(:note)
false
iex> IndieWeb.Post.is_response_type?(:rsvp)
true
"""
@spec is_response_type?(atom()) :: boolean()
def is_response_type?(type) do
Enum.member?(response_types(), type)
end
@property_to_kind %{
"checkin" => :checkin,
"audio" => :audio,
"in-reply-to" => :reply,
"in_reply_to" => :reply,
"like-of" => :like,
"like_of" => :like,
"bookmark-of" => :bookmark,
"bookmark_of" => :bookmark,
"photo" => :photo,
"repost-of" => :repost,
"repost_of" => :repost,
"rsvp" => :rsvp,
"video" => :video,
"start" => :event,
"name" => :article,
"workout" => :workout,
"listen-of" => :listen,
"listen_of" => :listen,
"read_of" => :read,
"read-of" => :read,
"play-of" => :gameplay,
"play_of" => :gameplay
}
@doc """
Determines the type of a post from a set of types and its MF2 properties.
This aims to apply the [Post Type Discovery](http://ptd.spec.indieweb.org/) algorithm for discovering
what kind of post these properties result in.
## Extra Types
* *listen* posts can be detected with `listen-of`.
* *workout* posts can be detected with `workout`.
## Examples
iex> IndieWeb.Post.determine_type(%{"content" => ["Foo."], "name" => ["Foo."]}, ~w(note article)a)
:note
iex> IndieWeb.Post.determine_type(%{"content" => %{"value" => ["Foo."]}, "name" => ["On Bar"]}, ~w(note article)a)
:article
iex> IndieWeb.Post.determine_type(%{"content" => %{"value" => ["Foo."]}, "photo" => ["https://magic/jpeg"]}, ~w(note photo)a)
:photo
"""
@doc since: "http://ptd.spec.indieweb.org/#changes-from-28-october-2016-wd-to-1-march-2017-wd"
@spec determine_type(map(), list()) :: atom()
def determine_type(properties, types) do
cond do
:gameplay in types ->
:gameplay
:read in types ->
:read
:listen in types ->
:listen
:workout in types ->
:workout
:event in types ->
:event
:rsvp in types ->
:rsvp
:reply in types ->
:reply
:checkin in types ->
:checkin
:repost in types ->
:repost
:bookmark in types ->
:bookmark
:like in types ->
:like
:photo in types ->
:photo
:video in types ->
:video
do_detect_note(properties) == true ->
:note
do_detect_article(properties) == true ->
:article
true ->
:note
end
end
@doc """
Determines the potential types exposed by the set of provided properties.
The provided properties are scanned and checked to determine if a particular
post type can be determined. The matching is a direct property to type mapping.
You should use `determine_type/2` to resolve the _actual_ post type.
## Examples
iex> IndieWeb.Post.extract_types(%{"photo" => ["https://magic/jpeg"]})
[:photo]
iex> IndieWeb.Post.extract_types(%{"content" => %{"value" => ["Just a note."]}})
[:note]
iex> IndieWeb.Post.extract_types(%{"content" => %{"value" => ["A whole blog post."]}, "name" => ["Magic."]})
[:article]
iex> IndieWeb.Post.extract_types(%{"properties" => %{"content" => %{"value" => ["A whole blog post."]}, "name" => ["Magic."]}})
[:article]
"""
@spec extract_types(map()) :: list()
def extract_types(properties)
def extract_types(%{"properties" => properties}) do
extract_types(properties)
end
def extract_types(properties) do
property_names =
properties
|> Map.keys()
|> Enum.map(fn
key when is_binary(key) -> key
key -> to_string(key)
end)
types =
property_names
|> Enum.map(&Map.get(@property_to_kind, &1, nil))
|> Enum.reject(&is_nil/1)
if types == [] do
[:note]
else
types
end
end
@spec get_response_property_names_to_types() :: map()
def get_response_property_names_to_types() do
Enum.filter(@property_to_kind, fn {_key, type} ->
Enum.member?(response_types(), type)
end)
end
defp do_detect_note(properties) do
name = do_extract_name(properties)
content = do_extract_plain_text_content(properties)
String.starts_with?(content || name, name) || String.contains?(content, name)
end
defp do_extract_name(properties) do
properties
|> Map.get("name", "")
|> List.wrap()
|> Enum.filter(fn
str when is_binary(str) -> true
_ -> false
end)
|> Enum.join(" ")
|> String.trim()
end
defp do_detect_article(properties) do
content = do_extract_plain_text_content(properties)
name = do_extract_name(properties)
cond do
String.starts_with?(content, name) == false -> true
name == "" -> false
true -> false
end
end
defp do_extract_plain_text_content(properties)
defp do_extract_plain_text_content(%{"properties" => properties}) do
do_extract_plain_text_content(properties)
end
defp do_extract_plain_text_content(%{"content" => content}) do
content
|> List.wrap()
|> Enum.reduce([], fn
content_map, acc when is_map(content_map) ->
cond do
Map.has_key?(content_map, "text") == true ->
content_text = content_map |> Map.get("text") |> List.wrap() |> Enum.join(" ")
acc ++ [content_text]
Map.has_key?(content_map, "value") == true ->
content_value = content_map |> Map.get("text") |> List.wrap() |> Enum.join(" ")
acc ++ [content_value]
true ->
acc
end
content_string, acc when is_binary(content_string) ->
acc ++ [content_string]
end)
|> List.flatten()
|> Enum.join(" ")
end
def extract_uris(type, properties)
def extract_uris(type, properties) when is_binary(type) do
extract_uris(String.to_atom(type), properties)
end
def extract_uris(:rsvp, properties) do
extract_uris(:reply, properties)
end
def extract_uris(type, %{"properties" => properties}) do
extract_uris(type, properties)
end
def extract_uris(type, properties) do
fields =
@property_to_kind
|> Enum.filter(fn {_field, potential_type} -> type == potential_type end)
|> Enum.map(fn {field, _} -> field end)
|> Enum.reject(&is_nil/1)
|> Enum.reject(&(&1 in ~w(name photo video audio)))
extracted_reply_urls =
properties
|> Map.take(fields)
|> Map.values()
|> List.flatten()
|> extract_from_reply_value
extracted_reply_urls ++ extract_uris_from_content(Map.get(properties, "content", %{}))
end
defp extract_uris_from_content(content) do
html =
content
|> List.wrap()
|> List.first()
|> Map.get("html", "")
|> List.wrap()
|> Enum.join("\n")
# TODO: Ignore any links with rel=nofollow
case Floki.parse_fragment(html) do
{:ok, dom} ->
Floki.find(dom, "a[href]")
|> Floki.attribute("href")
|> Enum.reject(&is_nil/1)
_ ->
[]
end
end
def extract_from_reply_value(values) do
values
|> List.wrap()
|> Enum.map(fn
value when is_binary(value) -> value
%{"url" => url} -> List.wrap(url)
%{"properties" => %{"url" => url}} -> List.wrap(url)
_ -> nil
end)
|> List.flatten()
|> Enum.reject(&is_nil/1)
end
end