Packages
indieweb
0.0.60
0.0.66
0.0.65
0.0.64
0.0.63
0.0.62
0.0.61
0.0.60
0.0.59
0.0.58
0.0.56
0.0.55
0.0.54
0.0.53
0.0.51
0.0.50
0.0.49
0.0.48
0.0.47
0.0.46
0.0.42
0.0.41
0.0.40
0.0.39
0.0.38
0.0.37
0.0.36
0.0.35
0.0.34
0.0.33
0.0.32
0.0.31
0.0.30
0.0.29
0.0.28
0.0.27
0.0.26
0.0.25
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.3
0.0.2
0.0.1
Collection of common IndieWeb utilites like authorship resolution, Webmention, post type discovery and IndieAuth.
Current section
Files
Jump to
Current section
Files
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