@@ -1,6 +1,7 @@ | |||
# default Apache site config for Pleroma | |||
# | |||
# needed modules: define headers proxy proxy_http proxy_wstunnel rewrite ssl | |||
# optional modules: cache cache_disk | |||
# | |||
# Simple installation instructions: | |||
# 1. Install your TLS certificate, possibly using Let's Encrypt. | |||
@@ -8,6 +9,14 @@ | |||
# 3. This assumes a Debian style Apache config. Copy this file to | |||
# /etc/apache2/sites-available/ and then add a symlink to it in | |||
# /etc/apache2/sites-enabled/ by running 'a2ensite pleroma-apache.conf', then restart Apache. | |||
# | |||
# Optional: enable disk-based caching for the media proxy | |||
# For details, see https://git.pleroma.social/pleroma/pleroma/wikis/How%20to%20activate%20mediaproxy | |||
# | |||
# 1. Create the directory listed below as the CacheRoot, and make sure | |||
# the Apache user can write to it. | |||
# 2. Configure Apache's htcacheclean to clean the directory periodically. | |||
# 3. Run 'a2enmod cache cache_disk' and restart Apache. | |||
Define servername example.tld | |||
@@ -34,6 +43,15 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined | |||
SSLCompression off | |||
SSLSessionTickets off | |||
# uncomment the following to enable mediaproxy caching on disk | |||
# <IfModule mod_cache_disk.c> | |||
# CacheRoot /var/cache/apache2/mod_cache_disk | |||
# CacheDirLevels 1 | |||
# CacheDirLength 2 | |||
# CacheEnable disk /proxy | |||
# CacheLock on | |||
# </IfModule> | |||
RewriteEngine On | |||
RewriteCond %{HTTP:Connection} Upgrade [NC] | |||
RewriteCond %{HTTP:Upgrade} websocket [NC] | |||
@@ -11,7 +11,7 @@ proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cac | |||
server { | |||
server_name example.tld; | |||
listen 80; | |||
listen [::]:80; | |||
return 301 https://$server_name$request_uri; | |||
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure | |||
@@ -29,7 +29,7 @@ server { | |||
ssl_session_cache shared:ssl_session_cache:10m; | |||
server { | |||
listen 443 ssl http2; | |||
listen [::]:443 ssl http2; | |||
ssl_session_timeout 5m; | |||
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; | |||
@@ -37,6 +37,7 @@ end | |||
defmodule Pleroma.Gopher.Server.ProtocolHandler do | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Activity | |||
alias Pleroma.HTML | |||
alias Pleroma.User | |||
@@ -110,7 +111,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do | |||
def response("/notices/" <> id) do | |||
with %Activity{} = activity <- Repo.get(Activity, id), | |||
true <- ActivityPub.is_public?(activity) do | |||
true <- Visibility.is_public?(activity) do | |||
activities = | |||
ActivityPub.fetch_activities_for_context(activity.data["context"]) | |||
|> render_activities | |||
@@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
import Ecto.Query | |||
import Pleroma.Web.ActivityPub.Utils | |||
import Pleroma.Web.ActivityPub.Visibility | |||
require Logger | |||
@@ -912,57 +913,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
end | |||
end | |||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false | |||
def is_public?(%Object{data: data}), do: is_public?(data) | |||
def is_public?(%Activity{data: data}), do: is_public?(data) | |||
def is_public?(%{"directMessage" => true}), do: false | |||
def is_public?(data) do | |||
"https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || [])) | |||
end | |||
def is_private?(activity) do | |||
unless is_public?(activity) do | |||
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address | |||
Enum.any?(activity.data["to"], &(&1 == follower_address)) | |||
else | |||
false | |||
end | |||
end | |||
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true | |||
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true | |||
def is_direct?(activity) do | |||
!is_public?(activity) && !is_private?(activity) | |||
end | |||
def visible_for_user?(activity, nil) do | |||
is_public?(activity) | |||
end | |||
def visible_for_user?(activity, user) do | |||
x = [user.ap_id | user.following] | |||
y = activity.data["to"] ++ (activity.data["cc"] || []) | |||
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) | |||
end | |||
# guard | |||
def entire_thread_visible_for_user?(nil, _user), do: false | |||
# child | |||
def entire_thread_visible_for_user?( | |||
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, | |||
user | |||
) | |||
when is_binary(parent_id) do | |||
parent = Activity.get_in_reply_to_activity(tail) | |||
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) | |||
end | |||
# root | |||
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user) | |||
# filter out broken threads | |||
def contain_broken_threads(%Activity{} = activity, %User{} = user) do | |||
entire_thread_visible_for_user?(activity, user) | |||
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
alias Pleroma.Web.ActivityPub.ObjectView | |||
alias Pleroma.Web.ActivityPub.UserView | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Web.ActivityPub.Relay | |||
alias Pleroma.Web.ActivityPub.Transmogrifier | |||
alias Pleroma.Web.ActivityPub.Utils | |||
@@ -49,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
def object(conn, %{"uuid" => uuid}) do | |||
with ap_id <- o_status_url(conn, :object, uuid), | |||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), | |||
{_, true} <- {:public?, ActivityPub.is_public?(object)} do | |||
{_, true} <- {:public?, Visibility.is_public?(object)} do | |||
conn | |||
|> put_resp_header("content-type", "application/activity+json") | |||
|> json(ObjectView.render("object.json", %{object: object})) | |||
@@ -62,7 +63,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
def object_likes(conn, %{"uuid" => uuid, "page" => page}) do | |||
with ap_id <- o_status_url(conn, :object, uuid), | |||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), | |||
{_, true} <- {:public?, ActivityPub.is_public?(object)}, | |||
{_, true} <- {:public?, Visibility.is_public?(object)}, | |||
likes <- Utils.get_object_likes(object) do | |||
{page, _} = Integer.parse(page) | |||
@@ -78,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
def object_likes(conn, %{"uuid" => uuid}) do | |||
with ap_id <- o_status_url(conn, :object, uuid), | |||
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), | |||
{_, true} <- {:public?, ActivityPub.is_public?(object)}, | |||
{_, true} <- {:public?, Visibility.is_public?(object)}, | |||
likes <- Utils.get_object_likes(object) do | |||
conn | |||
|> put_resp_header("content-type", "application/activity+json") | |||
@@ -92,7 +93,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do | |||
def activity(conn, %{"uuid" => uuid}) do | |||
with ap_id <- o_status_url(conn, :activity, uuid), | |||
%Activity{} = activity <- Activity.normalize(ap_id), | |||
{_, true} <- {:public?, ActivityPub.is_public?(activity)} do | |||
{_, true} <- {:public?, Visibility.is_public?(activity)} do | |||
conn | |||
|> put_resp_header("content-type", "application/activity+json") | |||
|> json(ObjectView.render("object.json", %{object: activity})) | |||
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||
alias Pleroma.Repo | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
import Ecto.Query | |||
@@ -489,7 +490,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do | |||
with actor <- get_actor(data), | |||
%User{} = actor <- User.get_or_fetch_by_ap_id(actor), | |||
{:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id), | |||
public <- ActivityPub.is_public?(data), | |||
public <- Visibility.is_public?(data), | |||
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do | |||
{:ok, activity} | |||
else | |||
@@ -188,14 +188,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do | |||
end | |||
activities = ActivityPub.fetch_user_activities(user, nil, params) | |||
min_id = Enum.at(Enum.reverse(activities), 0).id | |||
max_id = Enum.at(activities, 0).id | |||
collection = | |||
Enum.map(activities, fn act -> | |||
{:ok, data} = Transmogrifier.prepare_outgoing(act.data) | |||
data | |||
end) | |||
{max_id, min_id, collection} = | |||
if length(activities) > 0 do | |||
{ | |||
Enum.at(Enum.reverse(activities), 0).id, | |||
Enum.at(activities, 0).id, | |||
Enum.map(activities, fn act -> | |||
{:ok, data} = Transmogrifier.prepare_outgoing(act.data) | |||
data | |||
end) | |||
} | |||
else | |||
{ | |||
0, | |||
0, | |||
[] | |||
} | |||
end | |||
iri = "#{user.ap_id}/outbox" | |||
@@ -0,0 +1,56 @@ | |||
defmodule Pleroma.Web.ActivityPub.Visibility do | |||
alias Pleroma.Activity | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false | |||
def is_public?(%Object{data: data}), do: is_public?(data) | |||
def is_public?(%Activity{data: data}), do: is_public?(data) | |||
def is_public?(%{"directMessage" => true}), do: false | |||
def is_public?(data) do | |||
"https://www.w3.org/ns/activitystreams#Public" in (data["to"] ++ (data["cc"] || [])) | |||
end | |||
def is_private?(activity) do | |||
unless is_public?(activity) do | |||
follower_address = User.get_cached_by_ap_id(activity.data["actor"]).follower_address | |||
Enum.any?(activity.data["to"], &(&1 == follower_address)) | |||
else | |||
false | |||
end | |||
end | |||
def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true | |||
def is_direct?(%Object{data: %{"directMessage" => true}}), do: true | |||
def is_direct?(activity) do | |||
!is_public?(activity) && !is_private?(activity) | |||
end | |||
def visible_for_user?(activity, nil) do | |||
is_public?(activity) | |||
end | |||
def visible_for_user?(activity, user) do | |||
x = [user.ap_id | user.following] | |||
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) | |||
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) | |||
end | |||
# guard | |||
def entire_thread_visible_for_user?(nil, _user), do: false | |||
# child | |||
def entire_thread_visible_for_user?( | |||
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail, | |||
user | |||
) | |||
when is_binary(parent_id) do | |||
parent = Activity.get_in_reply_to_activity(tail) | |||
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user) | |||
end | |||
# root | |||
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user) | |||
end |
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.Federator do | |||
alias Pleroma.Web.Websub | |||
alias Pleroma.Web.Salmon | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Web.ActivityPub.Relay | |||
alias Pleroma.Web.ActivityPub.Transmogrifier | |||
alias Pleroma.Web.ActivityPub.Utils | |||
@@ -94,7 +95,7 @@ defmodule Pleroma.Web.Federator do | |||
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do | |||
{:ok, actor} = WebFinger.ensure_keys_present(actor) | |||
if ActivityPub.is_public?(activity) do | |||
if Visibility.is_public?(activity) do | |||
if OStatus.is_representable?(activity) do | |||
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) | |||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) | |||
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
alias Pleroma.Web.MastodonAPI.ReportView | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Web.OAuth.App | |||
alias Pleroma.Web.OAuth.Authorization | |||
alias Pleroma.Web.OAuth.Token | |||
@@ -307,7 +308,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{} = activity <- Repo.get(Activity, id), | |||
true <- ActivityPub.visible_for_user?(activity, user) do | |||
true <- Visibility.visible_for_user?(activity, user) do | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user}) | |||
@@ -449,7 +450,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{} = activity <- Repo.get(Activity, id), | |||
%User{} = user <- User.get_by_nickname(user.nickname), | |||
true <- ActivityPub.visible_for_user?(activity, user), | |||
true <- Visibility.visible_for_user?(activity, user), | |||
{:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do | |||
conn | |||
|> put_view(StatusView) | |||
@@ -460,7 +461,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{} = activity <- Repo.get(Activity, id), | |||
%User{} = user <- User.get_by_nickname(user.nickname), | |||
true <- ActivityPub.visible_for_user?(activity, user), | |||
true <- Visibility.visible_for_user?(activity, user), | |||
{:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do | |||
conn | |||
|> put_view(StatusView) | |||
@@ -867,7 +868,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
if Regex.match?(~r/https?:/, query) do | |||
with {:ok, object} <- ActivityPub.fetch_object_from_id(query), | |||
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), | |||
true <- ActivityPub.visible_for_user?(activity, user) do | |||
true <- Visibility.visible_for_user?(activity, user) do | |||
[activity] | |||
else | |||
_e -> [] | |||
@@ -1518,9 +1519,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
end | |||
def status_card(conn, %{"id" => status_id}) do | |||
def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do | |||
with %Activity{} = activity <- Repo.get(Activity, status_id), | |||
true <- ActivityPub.is_public?(activity) do | |||
true <- Visibility.visible_for_user?(activity, user) do | |||
data = | |||
StatusView.render( | |||
"card.json", | |||
@@ -3,12 +3,10 @@ | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
alias Pleroma.HTML | |||
alias Pleroma.Formatter | |||
alias Pleroma.User | |||
alias Pleroma.Web.Metadata | |||
alias Pleroma.Web.MediaProxy | |||
alias Pleroma.Web.Metadata.Providers.Provider | |||
alias Pleroma.Web.Metadata.Utils | |||
@behaviour Provider | |||
@@ -19,7 +17,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
user: user | |||
}) do | |||
attachments = build_attachments(object) | |||
scrubbed_content = scrub_html_and_truncate(object) | |||
scrubbed_content = Utils.scrub_html_and_truncate(object) | |||
# Zero width space | |||
content = | |||
if scrubbed_content != "" and scrubbed_content != "\u200B" do | |||
@@ -44,13 +42,14 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
{:meta, | |||
[ | |||
property: "og:description", | |||
content: "#{user_name_string(user)}" <> content | |||
content: "#{Utils.user_name_string(user)}" <> content | |||
], []}, | |||
{:meta, [property: "og:type", content: "website"], []} | |||
] ++ | |||
if attachments == [] or Metadata.activity_nsfw?(object) do | |||
[ | |||
{:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, | |||
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], | |||
[]}, | |||
{:meta, [property: "og:image:width", content: 150], []}, | |||
{:meta, [property: "og:image:height", content: 150], []} | |||
] | |||
@@ -61,17 +60,17 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
@impl Provider | |||
def build_tags(%{user: user}) do | |||
with truncated_bio = scrub_html_and_truncate(user.bio || "") do | |||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do | |||
[ | |||
{:meta, | |||
[ | |||
property: "og:title", | |||
content: user_name_string(user) | |||
content: Utils.user_name_string(user) | |||
], []}, | |||
{:meta, [property: "og:url", content: User.profile_url(user)], []}, | |||
{:meta, [property: "og:description", content: truncated_bio], []}, | |||
{:meta, [property: "og:type", content: "website"], []}, | |||
{:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []}, | |||
{:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []}, | |||
{:meta, [property: "og:image:width", content: 150], []}, | |||
{:meta, [property: "og:image:height", content: 150], []} | |||
] | |||
@@ -93,14 +92,15 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
case media_type do | |||
"audio" -> | |||
[ | |||
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} | |||
{:meta, | |||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} | |||
| acc | |||
] | |||
"image" -> | |||
[ | |||
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], | |||
[]}, | |||
{:meta, | |||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, | |||
{:meta, [property: "og:image:width", content: 150], []}, | |||
{:meta, [property: "og:image:height", content: 150], []} | |||
| acc | |||
@@ -108,7 +108,8 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
"video" -> | |||
[ | |||
{:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []} | |||
{:meta, | |||
[property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} | |||
| acc | |||
] | |||
@@ -120,37 +121,4 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do | |||
acc ++ rendered_tags | |||
end) | |||
end | |||
defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do | |||
content | |||
# html content comes from DB already encoded, decode first and scrub after | |||
|> HtmlEntities.decode() | |||
|> String.replace(~r/<br\s?\/?>/, " ") | |||
|> HTML.get_cached_stripped_html_for_object(object, __MODULE__) | |||
|> Formatter.demojify() | |||
|> Formatter.truncate() | |||
end | |||
defp scrub_html_and_truncate(content) when is_binary(content) do | |||
content | |||
# html content comes from DB already encoded, decode first and scrub after | |||
|> HtmlEntities.decode() | |||
|> String.replace(~r/<br\s?\/?>/, " ") | |||
|> HTML.strip_tags() | |||
|> Formatter.demojify() | |||
|> Formatter.truncate() | |||
end | |||
defp attachment_url(url) do | |||
MediaProxy.url(url) | |||
end | |||
defp user_name_string(user) do | |||
"#{user.name} " <> | |||
if user.local do | |||
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" | |||
else | |||
"(@#{user.nickname})" | |||
end | |||
end | |||
end |
@@ -0,0 +1,21 @@ | |||
defmodule Pleroma.Web.Metadata.PlayerView do | |||
use Pleroma.Web, :view | |||
import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2] | |||
def render("player.html", %{"mediaType" => type, "href" => href}) do | |||
{tag_type, tag_attrs} = | |||
case type do | |||
"audio" <> _ -> {:audio, []} | |||
"video" <> _ -> {:video, [loop: true]} | |||
end | |||
content_tag( | |||
tag_type, | |||
[ | |||
tag(:source, src: href, type: type), | |||
"Your browser does not support #{type} playback." | |||
], | |||
[controls: true] ++ tag_attrs | |||
) | |||
end | |||
end |
@@ -3,44 +3,120 @@ | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.Metadata.Providers.TwitterCard do | |||
alias Pleroma.Web.Metadata.Providers.Provider | |||
alias Pleroma.User | |||
alias Pleroma.Web.Metadata | |||
alias Pleroma.Web.Metadata.Providers.Provider | |||
alias Pleroma.Web.Metadata.Utils | |||
@behaviour Provider | |||
@impl Provider | |||
def build_tags(%{object: object}) do | |||
if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do | |||
build_tags(nil) | |||
else | |||
case find_first_acceptable_media_type(object) do | |||
"image" -> | |||
[{:meta, [property: "twitter:card", content: "summary_large_image"], []}] | |||
"audio" -> | |||
[{:meta, [property: "twitter:card", content: "player"], []}] | |||
"video" -> | |||
[{:meta, [property: "twitter:card", content: "player"], []}] | |||
_ -> | |||
build_tags(nil) | |||
def build_tags(%{ | |||
activity_id: id, | |||
object: object, | |||
user: user | |||
}) do | |||
attachments = build_attachments(id, object) | |||
scrubbed_content = Utils.scrub_html_and_truncate(object) | |||
# Zero width space | |||
content = | |||
if scrubbed_content != "" and scrubbed_content != "\u200B" do | |||
"“" <> scrubbed_content <> "”" | |||
else | |||
"" | |||
end | |||
[ | |||
{:meta, | |||
[ | |||
property: "twitter:title", | |||
content: Utils.user_name_string(user) | |||
], []}, | |||
{:meta, | |||
[ | |||
property: "twitter:description", | |||
content: content | |||
], []} | |||
] ++ | |||
if attachments == [] or Metadata.activity_nsfw?(object) do | |||
[ | |||
{:meta, | |||
[property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, | |||
{:meta, [property: "twitter:card", content: "summary_large_image"], []} | |||
] | |||
else | |||
attachments | |||
end | |||
end | |||
end | |||
@impl Provider | |||
def build_tags(_) do | |||
[{:meta, [property: "twitter:card", content: "summary"], []}] | |||
def build_tags(%{user: user}) do | |||
with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do | |||
[ | |||
{:meta, | |||
[ | |||
property: "twitter:title", | |||
content: Utils.user_name_string(user) | |||
], []}, | |||
{:meta, [property: "twitter:description", content: truncated_bio], []}, | |||
{:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], | |||
[]}, | |||
{:meta, [property: "twitter:card", content: "summary"], []} | |||
] | |||
end | |||
end | |||
def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do | |||
Enum.find_value(attachment, fn attachment -> | |||
Enum.find_value(attachment["url"], fn url -> | |||
Enum.find(["image", "audio", "video"], fn media_type -> | |||
String.starts_with?(url["mediaType"], media_type) | |||
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do | |||
Enum.reduce(attachments, [], fn attachment, acc -> | |||
rendered_tags = | |||
Enum.reduce(attachment["url"], [], fn url, acc -> | |||
media_type = | |||
Enum.find(["image", "audio", "video"], fn media_type -> | |||
String.starts_with?(url["mediaType"], media_type) | |||
end) | |||
# TODO: Add additional properties to objects when we have the data available. | |||
case media_type do | |||
"audio" -> | |||
[ | |||
{:meta, [property: "twitter:card", content: "player"], []}, | |||
{:meta, [property: "twitter:player:width", content: "480"], []}, | |||
{:meta, [property: "twitter:player:height", content: "80"], []}, | |||
{:meta, [property: "twitter:player", content: player_url(id)], []} | |||
| acc | |||
] | |||
"image" -> | |||
[ | |||
{:meta, [property: "twitter:card", content: "summary_large_image"], []}, | |||
{:meta, | |||
[ | |||
property: "twitter:player", | |||
content: Utils.attachment_url(url["href"]) | |||
], []} | |||
| acc | |||
] | |||
# TODO: Need the true width and height values here or Twitter renders an iFrame with a bad aspect ratio | |||
"video" -> | |||
[ | |||
{:meta, [property: "twitter:card", content: "player"], []}, | |||
{:meta, [property: "twitter:player", content: player_url(id)], []}, | |||
{:meta, [property: "twitter:player:width", content: "480"], []}, | |||
{:meta, [property: "twitter:player:height", content: "480"], []} | |||
| acc | |||
] | |||
_ -> | |||
acc | |||
end | |||
end) | |||
end) | |||
acc ++ rendered_tags | |||
end) | |||
end | |||
defp player_url(id) do | |||
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) | |||
end | |||
end |
@@ -0,0 +1,42 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright \xc2\xa9 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# SPDX-License-Identifier: AGPL-3.0-only | |||
defmodule Pleroma.Web.Metadata.Utils do | |||
alias Pleroma.HTML | |||
alias Pleroma.Formatter | |||
alias Pleroma.Web.MediaProxy | |||
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do | |||
content | |||
# html content comes from DB already encoded, decode first and scrub after | |||
|> HtmlEntities.decode() | |||
|> String.replace(~r/<br\s?\/?>/, " ") | |||
|> HTML.get_cached_stripped_html_for_object(object, __MODULE__) | |||
|> Formatter.demojify() | |||
|> Formatter.truncate() | |||
end | |||
def scrub_html_and_truncate(content) when is_binary(content) do | |||
content | |||
# html content comes from DB already encoded, decode first and scrub after | |||
|> HtmlEntities.decode() | |||
|> String.replace(~r/<br\s?\/?>/, " ") | |||
|> HTML.strip_tags() | |||
|> Formatter.demojify() | |||
|> Formatter.truncate() | |||
end | |||
def attachment_url(url) do | |||
MediaProxy.url(url) | |||
end | |||
def user_name_string(user) do | |||
"#{user.name} " <> | |||
if user.local do | |||
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" | |||
else | |||
"(@#{user.nickname})" | |||
end | |||
end | |||
end |
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
alias Pleroma.Object | |||
alias Pleroma.User | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Web.ActivityPub.ActivityPubController | |||
alias Pleroma.Web.ActivityPub.ObjectView | |||
alias Pleroma.Web.OStatus.ActivityRepresenter | |||
@@ -102,7 +103,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
else | |||
with id <- o_status_url(conn, :object, uuid), | |||
{_, %Activity{} = activity} <- {:activity, Activity.get_create_by_object_ap_id(id)}, | |||
{_, true} <- {:public?, ActivityPub.is_public?(activity)}, | |||
{_, true} <- {:public?, Visibility.is_public?(activity)}, | |||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do | |||
case get_format(conn) do | |||
"html" -> redirect(conn, to: "/notice/#{activity.id}") | |||
@@ -127,7 +128,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
else | |||
with id <- o_status_url(conn, :activity, uuid), | |||
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, | |||
{_, true} <- {:public?, ActivityPub.is_public?(activity)}, | |||
{_, true} <- {:public?, Visibility.is_public?(activity)}, | |||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do | |||
case format = get_format(conn) do | |||
"html" -> redirect(conn, to: "/notice/#{activity.id}") | |||
@@ -148,7 +149,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
def notice(conn, %{"id" => id}) do | |||
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id(id)}, | |||
{_, true} <- {:public?, ActivityPub.is_public?(activity)}, | |||
{_, true} <- {:public?, Visibility.is_public?(activity)}, | |||
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do | |||
case format = get_format(conn) do | |||
"html" -> | |||
@@ -156,6 +157,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
%Object{} = object = Object.normalize(activity.data["object"]) | |||
Fallback.RedirectController.redirector_with_meta(conn, %{ | |||
activity_id: activity.id, | |||
object: object, | |||
url: | |||
Pleroma.Web.Router.Helpers.o_status_url( | |||
@@ -187,6 +189,30 @@ defmodule Pleroma.Web.OStatus.OStatusController do | |||
end | |||
end | |||
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes. | |||
def notice_player(conn, %{"id" => id}) do | |||
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), | |||
true <- Visibility.is_public?(activity), | |||
%Object{} = object <- Object.normalize(activity.data["object"]), | |||
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, | |||
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do | |||
conn | |||
|> put_layout(:metadata_player) | |||
|> put_resp_header("x-frame-options", "ALLOW") | |||
|> put_resp_header( | |||
"content-security-policy", | |||
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" | |||
) | |||
|> put_view(Pleroma.Web.Metadata.PlayerView) | |||
|> render("player.html", url) | |||
else | |||
_error -> | |||
conn | |||
|> put_status(404) | |||
|> Fallback.RedirectController.redirector(nil, 404) | |||
end | |||
end | |||
defp represent_activity( | |||
conn, | |||
"activity+json", | |||
@@ -505,6 +505,7 @@ defmodule Pleroma.Web.Router do | |||
get("/objects/:uuid", OStatus.OStatusController, :object) | |||
get("/activities/:uuid", OStatus.OStatusController, :activity) | |||
get("/notice/:id", OStatus.OStatusController, :notice) | |||
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) | |||
get("/users/:nickname/feed", OStatus.OStatusController, :feed) | |||
get("/users/:nickname", OStatus.OStatusController, :feed_redirect) | |||
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.Streamer do | |||
alias Pleroma.Activity | |||
alias Pleroma.Object | |||
alias Pleroma.Repo | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
@keepalive_interval :timer.seconds(30) | |||
@@ -73,7 +73,7 @@ defmodule Pleroma.Web.Streamer do | |||
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do | |||
# filter the recipient list if the activity is not public, see #270. | |||
recipient_lists = | |||
case ActivityPub.is_public?(item) do | |||
case Visibility.is_public?(item) do | |||
true -> | |||
Pleroma.List.get_lists_from_activity(item) | |||
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.Streamer do | |||
|> Enum.filter(fn list -> | |||
owner = Repo.get(User, list.user_id) | |||
ActivityPub.visible_for_user?(item, owner) | |||
Visibility.visible_for_user?(item, owner) | |||
end) | |||
end | |||
@@ -0,0 +1,16 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<body> | |||
<style type="text/css"> | |||
video, audio { | |||
width:100%; | |||
max-width:600px; | |||
height: auto; | |||
} | |||
</style> | |||
<%= render @view_module, @view_template, assigns %> | |||
</body> | |||
</html> |
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||
alias Pleroma.{Repo, Activity, Object, User, Notification} | |||
alias Pleroma.Web.OAuth.Token | |||
alias Pleroma.Web.ActivityPub.ActivityPub | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
alias Pleroma.Web.ActivityPub.Utils | |||
alias Pleroma.Web.CommonAPI | |||
alias Pleroma.Web.TwitterAPI.ActivityView | |||
@@ -268,7 +269,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{} = activity <- Repo.get(Activity, id), | |||
true <- ActivityPub.visible_for_user?(activity, user) do | |||
true <- Visibility.visible_for_user?(activity, user) do | |||
conn | |||
|> put_view(ActivityView) | |||
|> render("activity.json", %{activity: activity, for: user}) | |||
@@ -34,7 +34,7 @@ | |||
"jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, | |||
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, | |||
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, | |||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, | |||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, | |||
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, | |||
@@ -0,0 +1,11 @@ | |||
defmodule Pleroma.Repo.Migrations.DataMigrationNormalizeScopes do | |||
use Ecto.Migration | |||
def up do | |||
for t <- [:apps, :oauth_authorizations, :oauth_tokens] do | |||
execute "UPDATE #{t} SET scopes = string_to_array(array_to_string(scopes, ' '), ' ');" | |||
end | |||
end | |||
def down, do: :noop | |||
end |
@@ -304,6 +304,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do | |||
end | |||
describe "/users/:nickname/outbox" do | |||
test "it will not bomb when there is no activity", %{conn: conn} do | |||
user = insert(:user) | |||
conn = | |||
conn | |||
|> put_req_header("accept", "application/activity+json") | |||
|> get("/users/#{user.nickname}/outbox") | |||
result = json_response(conn, 200) | |||
assert user.ap_id <> "/outbox" == result["id"] | |||
end | |||
test "it returns a note activity in a collection", %{conn: conn} do | |||
note_activity = insert(:note_activity) | |||
user = User.get_cached_by_ap_id(note_activity.data["actor"]) | |||
@@ -0,0 +1,98 @@ | |||
defmodule Pleroma.Web.ActivityPub.VisibilityTest do | |||
use Pleroma.DataCase | |||
alias Pleroma.Web.CommonAPI | |||
alias Pleroma.Web.ActivityPub.Visibility | |||
import Pleroma.Factory | |||
setup do | |||
user = insert(:user) | |||
mentioned = insert(:user) | |||
following = insert(:user) | |||
unrelated = insert(:user) | |||
{:ok, following} = Pleroma.User.follow(following, user) | |||
{:ok, public} = | |||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"}) | |||
{:ok, private} = | |||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "private"}) | |||
{:ok, direct} = | |||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "direct"}) | |||
{:ok, unlisted} = | |||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"}) | |||
%{ | |||
public: public, | |||
private: private, | |||
direct: direct, | |||
unlisted: unlisted, | |||
user: user, | |||
mentioned: mentioned, | |||
following: following, | |||
unrelated: unrelated | |||
} | |||
end | |||
test "is_direct?", %{public: public, private: private, direct: direct, unlisted: unlisted} do | |||
assert Visibility.is_direct?(direct) | |||
refute Visibility.is_direct?(public) | |||
refute Visibility.is_direct?(private) | |||
refute Visibility.is_direct?(unlisted) | |||
end | |||
test "is_public?", %{public: public, private: private, direct: direct, unlisted: unlisted} do | |||
refute Visibility.is_public?(direct) | |||
assert Visibility.is_public?(public) | |||
refute Visibility.is_public?(private) | |||
assert Visibility.is_public?(unlisted) | |||
end | |||
test "is_private?", %{public: public, private: private, direct: direct, unlisted: unlisted} do | |||
refute Visibility.is_private?(direct) | |||
refute Visibility.is_private?(public) | |||
assert Visibility.is_private?(private) | |||
refute Visibility.is_private?(unlisted) | |||
end | |||
test "visible_for_user?", %{ | |||
public: public, | |||
private: private, | |||
direct: direct, | |||
unlisted: unlisted, | |||
user: user, | |||
mentioned: mentioned, | |||
following: following, | |||
unrelated: unrelated | |||
} do | |||
# All visible to author | |||
assert Visibility.visible_for_user?(public, user) | |||
assert Visibility.visible_for_user?(private, user) | |||
assert Visibility.visible_for_user?(unlisted, user) | |||
assert Visibility.visible_for_user?(direct, user) | |||
# All visible to a mentioned user | |||
assert Visibility.visible_for_user?(public, mentioned) | |||
assert Visibility.visible_for_user?(private, mentioned) | |||
assert Visibility.visible_for_user?(unlisted, mentioned) | |||
assert Visibility.visible_for_user?(direct, mentioned) | |||
# DM not visible for just follower | |||
assert Visibility.visible_for_user?(public, following) | |||
assert Visibility.visible_for_user?(private, following) | |||
assert Visibility.visible_for_user?(unlisted, following) | |||
refute Visibility.visible_for_user?(direct, following) | |||
# Public and unlisted visible for unrelated user | |||
assert Visibility.visible_for_user?(public, unrelated) | |||
assert Visibility.visible_for_user?(unlisted, unrelated) | |||
refute Visibility.visible_for_user?(private, unrelated) | |||
refute Visibility.visible_for_user?(direct, unrelated) | |||
end | |||
end |
@@ -1744,6 +1744,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do | |||
} | |||
} | |||
# works with private posts | |||
{:ok, activity} = | |||
CommonAPI.post(user, %{"status" => "http://example.com/ogp", "visibility" => "direct"}) | |||
response_two = | |||
conn | |||
|> assign(:user, user) | |||
|> get("/api/v1/statuses/#{activity.id}/card") | |||
|> json_response(200) | |||
assert response_two == response | |||
Pleroma.Config.put([:rich_media, :enabled], false) | |||
end | |||
end | |||