@@ -0,0 +1,42 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.EmbedController do | |||
use Pleroma.Web, :controller | |||
alias Pleroma.Activity | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
plug(:put_layout, :embed) | |||
def show(conn, %{"id" => id}) do | |||
with %Activity{local: true} = activity <- | |||
Activity.get_by_id_with_object(id), | |||
true <- Visibility.is_public?(activity.object) do | |||
{:ok, author} = User.get_or_fetch(activity.object.data["actor"]) | |||
conn | |||
|> delete_resp_header("x-frame-options") | |||
|> delete_resp_header("content-security-policy") | |||
|> render("show.html", | |||
activity: activity, | |||
author: User.sanitize_html(author), | |||
counts: get_counts(activity) | |||
) | |||
end | |||
end | |||
defp get_counts(%Activity{} = activity) do | |||
%Object{data: data} = Object.normalize(activity) | |||
%{ | |||
likes: Map.get(data, "like_count", 0), | |||
replies: Map.get(data, "repliesCount", 0), | |||
announces: Map.get(data, "announcement_count", 0) | |||
} | |||
end | |||
end |
@@ -35,7 +35,7 @@ defmodule Pleroma.Web.Endpoint do | |||
at: "/", | |||
from: :pleroma, | |||
only: | |||
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc), | |||
~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css), | |||
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength | |||
gzip: true, | |||
cache_control_for_etags: @static_cache_control, | |||
@@ -637,6 +637,8 @@ defmodule Pleroma.Web.Router do | |||
post("/auth/password", MastodonAPI.AuthController, :password_reset) | |||
get("/web/*path", MastoFEController, :index) | |||
get("/embed/:id", EmbedController, :show) | |||
end | |||
pipeline :remote_media do | |||
@@ -0,0 +1,8 @@ | |||
<%= case @mediaType do %> | |||
<% "audio" -> %> | |||
<audio src="<%= @url %>" controls="controls"></audio> | |||
<% "video" -> %> | |||
<video src="<%= @url %>" controls="controls"></video> | |||
<% _ -> %> | |||
<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>"> | |||
<% end %> |
@@ -0,0 +1,76 @@ | |||
<div> | |||
<div class="p-author h-card"> | |||
<a class="u-url" rel="author noopener" href="<%= User.profile_url(@author) %>"> | |||
<div class="avatar"> | |||
<img src="<%= User.avatar_url(@author) |> MediaProxy.url %>" width="48" height="48" alt=""> | |||
</div> | |||
<span class="display-name" style="padding-left: 0.5em;"> | |||
<bdi><%= raw (@author.name |> Formatter.emojify(emoji_for_user(@author))) %></bdi> | |||
<span class="nickname"><%= full_nickname(@author) %></span> | |||
</span> | |||
</a> | |||
</div> | |||
<div class="activity-content" > | |||
<%= if status_title(@activity) != "" do %> | |||
<details <%= if open_content?() do %>open<% end %>> | |||
<summary><%= raw status_title(@activity) %></summary> | |||
<div><%= activity_content(@activity) %></div> | |||
</details> | |||
<% else %> | |||
<div><%= activity_content(@activity) %></div> | |||
<% end %> | |||
<%= for %{"name" => name, "url" => [url | _]} <- attachments(@activity) do %> | |||
<div class="attachment"> | |||
<%= if sensitive?(@activity) do %> | |||
<details class="nsfw"> | |||
<summary onClick="updateHeight()"><%= Gettext.gettext("sensitive media") %></summary> | |||
<div class="nsfw-content"> | |||
<%= render("_attachment.html", %{name: name, url: url["href"], | |||
mediaType: fetch_media_type(url)}) %> | |||
</div> | |||
</details> | |||
<% else %> | |||
<%= render("_attachment.html", %{name: name, url: url["href"], | |||
mediaType: fetch_media_type(url)}) %> | |||
<% end %> | |||
</div> | |||
<% end %> | |||
</div> | |||
<dl class="counts pull-right"> | |||
<dt><%= Gettext.gettext("replies") %></dt><dd><%= @counts.replies %></dd> | |||
<dt><%= Gettext.gettext("announces") %></dt><dd><%= @counts.announces %></dd> | |||
<dt><%= Gettext.gettext("likes") %></dt><dd><%= @counts.likes %></dd> | |||
</dl> | |||
<p class="date pull-left"> | |||
<%= link published(@activity), to: activity_url(@author, @activity) %> | |||
</p> | |||
</div> | |||
<script> | |||
function updateHeight() { | |||
window.requestAnimationFrame(function(){ | |||
var height = document.getElementsByTagName('html')[0].scrollHeight; | |||
window.parent.postMessage({ | |||
type: 'setHeightPleromaEmbed', | |||
id: window.parentId, | |||
height: height, | |||
}, '*'); | |||
}) | |||
} | |||
window.addEventListener('message', function(e){ | |||
var data = e.data || {}; | |||
if (!window.parent || data.type !== 'setHeightPleromaEmbed') { | |||
return; | |||
} | |||
window.parentId = data.id | |||
updateHeight() | |||
}); | |||
</script> |
@@ -0,0 +1,14 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="utf-8" /> | |||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> | |||
<title><%= Pleroma.Config.get([:instance, :name]) %></title> | |||
<meta content='noindex' name='robots'> | |||
<%= Phoenix.HTML.raw(assigns[:meta] || "") %> | |||
<link rel="stylesheet" href="/embed.css"> | |||
</head> | |||
<body> | |||
<%= render @view_module, @view_template, assigns %> | |||
</body> | |||
</html> |
@@ -0,0 +1,83 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.EmbedView do | |||
use Pleroma.Web, :view | |||
alias Calendar.Strftime | |||
alias Pleroma.Activity | |||
alias Pleroma.Emoji.Formatter | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
alias Pleroma.Web.Gettext | |||
alias Pleroma.Web.MediaProxy | |||
alias Pleroma.Web.Metadata.Utils | |||
alias Pleroma.Web.Router.Helpers | |||
use Phoenix.HTML | |||
@media_types ["image", "audio", "video"] | |||
defp emoji_for_user(%User{} = user) do | |||
user.source_data | |||
|> Map.get("tag", []) | |||
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) | |||
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> | |||
{String.trim(name, ":"), url} | |||
end) | |||
end | |||
defp fetch_media_type(%{"mediaType" => mediaType}) do | |||
Utils.fetch_media_type(@media_types, mediaType) | |||
end | |||
defp open_content? do | |||
Pleroma.Config.get( | |||
[:frontend_configurations, :collapse_message_with_subjects], | |||
true | |||
) | |||
end | |||
defp full_nickname(user) do | |||
%{host: host} = URI.parse(user.ap_id) | |||
"@" <> user.nickname <> "@" <> host | |||
end | |||
defp status_title(%Activity{object: %Object{data: %{"name" => name}}}) when is_binary(name), | |||
do: name | |||
defp status_title(%Activity{object: %Object{data: %{"summary" => summary}}}) | |||
when is_binary(summary), | |||
do: summary | |||
defp status_title(_), do: nil | |||
defp activity_content(%Activity{object: %Object{data: %{"content" => content}}}) do | |||
content |> Pleroma.HTML.filter_tags() |> raw() | |||
end | |||
defp activity_content(_), do: nil | |||
defp activity_url(%User{local: true}, activity) do | |||
Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) | |||
end | |||
defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) do | |||
data["url"] || data["external_url"] || data["id"] | |||
end | |||
defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do | |||
attachments | |||
end | |||
defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do | |||
sensitive | |||
end | |||
defp published(%Activity{object: %Object{data: %{"published" => published}}}) do | |||
published | |||
|> NaiveDateTime.from_iso8601!() | |||
|> Strftime.strftime!("%B %d, %Y, %l:%M %p") | |||
end | |||
end |
@@ -0,0 +1,115 @@ | |||
body { | |||
background-color: #282c37; | |||
font-family: sans-serif; | |||
color: white; | |||
margin: 0; | |||
padding: 1em; | |||
padding-bottom: 0; | |||
} | |||
.avatar { | |||
cursor: pointer; | |||
} | |||
.avatar img { | |||
float: left; | |||
border-radius: 4px; | |||
margin-right: 4px; | |||
} | |||
.activity-content { | |||
padding-top: 1em; | |||
} | |||
.attachment { | |||
margin-top: 1em; | |||
} | |||
.attachment img { | |||
max-width: 100%; | |||
} | |||
.date a { | |||
text-decoration: none; | |||
} | |||
.date a:hover { | |||
text-decoration: underline; | |||
} | |||
.date a, | |||
.counts { | |||
color: #666; | |||
font-size: 0.9em; | |||
} | |||
.counts dt, | |||
.counts dd { | |||
float: left; | |||
margin-left: 1em; | |||
} | |||
a { | |||
color: white; | |||
} | |||
.h-card { | |||
min-height: 48px; | |||
margin-bottom: 8px; | |||
} | |||
.h-card a { | |||
text-decoration: none; | |||
} | |||
.h-card a:hover { | |||
text-decoration: underline; | |||
} | |||
.display-name { | |||
padding-top: 4px; | |||
display: block; | |||
text-overflow: ellipsis; | |||
overflow: hidden; | |||
color: white; | |||
} | |||
/* keep emoji from being hilariously huge */ | |||
.display-name img { | |||
max-height: 1em; | |||
} | |||
.display-name .nickname { | |||
padding-top: 4px; | |||
display: block; | |||
} | |||
.nickname:hover { | |||
text-decoration: none; | |||
} | |||
.pull-right { | |||
float: right; | |||
} | |||
.collapse { | |||
margin: 0; | |||
width: auto; | |||
} | |||
a.button { | |||
box-sizing: border-box; | |||
display: inline-block; | |||
color: white; | |||
background-color: #419bdd; | |||
border-radius: 4px; | |||
border: none; | |||
padding: 10px; | |||
font-weight: 500; | |||
font-size: 0.9em; | |||
} | |||
a.button:hover { | |||
text-decoration: none; | |||
background-color: #61a6d9; | |||
} |
@@ -0,0 +1,43 @@ | |||
(function () { | |||
'use strict' | |||
var ready = function (loaded) { | |||
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) { | |||
loaded() | |||
} else { | |||
document.addEventListener('DOMContentLoaded', loaded) | |||
} | |||
} | |||
ready(function () { | |||
var iframes = [] | |||
window.addEventListener('message', function (e) { | |||
var data = e.data || {} | |||
if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) { | |||
return | |||
} | |||
iframes[data.id].height = data.height | |||
}); | |||
[].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) { | |||
iframe.scrolling = 'no' | |||
iframe.style.overflow = 'hidden' | |||
iframes.push(iframe) | |||
var id = iframes.length - 1 | |||
iframe.onload = function () { | |||
iframe.contentWindow.postMessage({ | |||
type: 'setHeightPleromaEmbed', | |||
id: id | |||
}, '*') | |||
} | |||
iframe.onload() | |||
}) | |||
}) | |||
})() |