@@ -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: "/", | at: "/", | ||||
from: :pleroma, | from: :pleroma, | ||||
only: | 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 | # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength | ||||
gzip: true, | gzip: true, | ||||
cache_control_for_etags: @static_cache_control, | cache_control_for_etags: @static_cache_control, | ||||
@@ -637,6 +637,8 @@ defmodule Pleroma.Web.Router do | |||||
post("/auth/password", MastodonAPI.AuthController, :password_reset) | post("/auth/password", MastodonAPI.AuthController, :password_reset) | ||||
get("/web/*path", MastoFEController, :index) | get("/web/*path", MastoFEController, :index) | ||||
get("/embed/:id", EmbedController, :show) | |||||
end | end | ||||
pipeline :remote_media do | 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() | |||||
}) | |||||
}) | |||||
})() |