diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex new file mode 100644 index 000000000..f6b8a5ee1 --- /dev/null +++ b/lib/pleroma/web/embed_controller.ex @@ -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 diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 72cb3ee27..4f665db12 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -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, diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 3f36f6c1a..eef0a8023 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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 diff --git a/lib/pleroma/web/templates/embed/_attachment.html.eex b/lib/pleroma/web/templates/embed/_attachment.html.eex new file mode 100644 index 000000000..7e04e9550 --- /dev/null +++ b/lib/pleroma/web/templates/embed/_attachment.html.eex @@ -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 %> diff --git a/lib/pleroma/web/templates/embed/show.html.eex b/lib/pleroma/web/templates/embed/show.html.eex new file mode 100644 index 000000000..6bf8fac29 --- /dev/null +++ b/lib/pleroma/web/templates/embed/show.html.eex @@ -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> diff --git a/lib/pleroma/web/templates/layout/embed.html.eex b/lib/pleroma/web/templates/layout/embed.html.eex new file mode 100644 index 000000000..57ae4f802 --- /dev/null +++ b/lib/pleroma/web/templates/layout/embed.html.eex @@ -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> diff --git a/lib/pleroma/web/views/embed_view.ex b/lib/pleroma/web/views/embed_view.ex new file mode 100644 index 000000000..77536835b --- /dev/null +++ b/lib/pleroma/web/views/embed_view.ex @@ -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 diff --git a/priv/static/embed.css b/priv/static/embed.css new file mode 100644 index 000000000..cc79ee7ab --- /dev/null +++ b/priv/static/embed.css @@ -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; +} diff --git a/priv/static/embed.js b/priv/static/embed.js new file mode 100644 index 000000000..f675f6417 --- /dev/null +++ b/priv/static/embed.js @@ -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() + }) + }) +})()