Merge remote-tracking branch 'origin/develop' into sixohsix/pleroma-post_expiration

This commit is contained in:
lain 2019-08-24 15:48:33 +02:00
commit cc6c0b4ba6
276 changed files with 7847 additions and 1792 deletions

View file

@ -23,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
@ -64,12 +65,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
false <- user.info.deactivated do
:ok
true
else
_e -> :reject
_e -> false
end
else
:ok
true
end
end
@ -118,10 +119,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(_create_data), do: :noop
def insert(map, local \\ true, fake \\ false) when is_map(map) do
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
:ok <- check_actor_is_active(map["actor"]),
true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
@ -207,8 +208,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def stream_out_participations(_, _), do: :noop
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
object = Object.normalize(activity)
# Do not stream out poll replies
@ -216,7 +215,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do
if get_visibility(activity) == "public" do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
@ -238,13 +237,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
else
# TODO: Write test, replace with visibility test
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_cached_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
if get_visibility(activity) == "direct",
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
@ -273,6 +267,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
else
{:fake, true, activity} ->
{:ok, activity}
{:error, message} ->
{:error, message}
end
end
@ -391,7 +388,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
:ok <- maybe_federate(activity),
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity}
end
end
@ -413,7 +411,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true),
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
@ -514,13 +512,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp fetch_activities_for_context_query(context, opts) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
public = [Pleroma.Constants.as_public()]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
@ -534,6 +534,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
)
|> exclude_poll_votes(opts)
|> exclude_id(opts)
|> order_by([activity], desc: activity.id)
end
@ -555,7 +556,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q = fetch_activities_query([Pleroma.Constants.as_public()], opts)
q
|> restrict_unlisted()
@ -626,6 +627,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
|> Map.put("pinned_activity_ids", user.info.pinned_activities)
@ -646,10 +648,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[reading_user.ap_id | reading_user.following]
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
[Pleroma.Constants.as_public()]
end
end
@ -753,8 +754,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
activity in query,
where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data)
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
)
end
@ -790,14 +791,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
mutes = info.mutes
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
unless opts["skip_preload"] do
from([thread_mute: tm] in query, where: is_nil(tm))
else
query
end
end
defp restrict_muted(query, _), do: query
@ -834,7 +841,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^["https://www.w3.org/ns/activitystreams#Public"]
^[Pleroma.Constants.as_public()]
)
)
end
@ -874,6 +881,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
@ -892,7 +905,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts["user"])
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
@ -971,7 +984,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
"https://www.w3.org/ns/activitystreams#Public" in activity.recipients)
^Pleroma.Constants.as_public() in activity.recipients)
)
end
@ -1010,16 +1023,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"url" => [%{"href" => data["image"]["url"]}]
}
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
ap_id: data["id"],
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner,
"locked" => locked
ap_enabled: true,
source_data: data,
banner: banner,
fields: fields,
locked: locked
},
avatar: avatar,
name: data["name"],
@ -1043,6 +1063,71 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, user_data}
end
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
following_count when is_integer(following_count) <- following_data["totalItems"],
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
followers_count when is_integer(followers_count) <- followers_data["totalItems"],
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
hide_follows: hide_follows,
follower_count: followers_count,
following_count: following_count,
hide_followers: hide_followers
}}
else
{:error, _} = e ->
e
e ->
{:error, e}
end
end
defp maybe_update_follow_information(data) do
with {:enabled, true} <-
{:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do
info = Map.merge(data.info, info)
Map.put(data, :info, info)
else
{:enabled, false} ->
data
e ->
Logger.error(
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
)
data
end
end
defp collection_private(data) do
if is_map(data["first"]) and
data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
{:ok, false}
else
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
{:ok, false}
else
{:error, {:ok, %{status: code}}} when code in [401, 403] ->
{:ok, true}
{:error, _} = e ->
e
e ->
{:error, e}
end
end
end
def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do
@ -1054,7 +1139,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data) do
{:ok, data} <- user_data_from_user_object(data),
data <- maybe_update_follow_information(data) do
{:ok, data}
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")

View file

@ -28,11 +28,43 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@spec subdomains_regex([String.t()]) :: [Regex.t()]
def subdomains_regex(domains) when is_list(domains) do
for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)
for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i
end
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
def subdomain_match?(domains, host) do
Enum.any?(domains, fn domain -> Regex.match?(domain, host) end)
end
@callback describe() :: {:ok | :error, Map.t()}
def describe(policies) do
{:ok, policy_configs} =
policies
|> Enum.reduce({:ok, %{}}, fn
policy, {:ok, data} ->
{:ok, policy_data} = policy.describe()
{:ok, Map.merge(data, policy_data)}
_, error ->
error
end)
mrf_policies =
get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
base =
%{
mrf_policies: mrf_policies,
exclusions: length(exclusions) > 0
}
|> Map.merge(policy_configs)
{:ok, base}
end
def describe, do: get_policies() |> describe()
end

View file

@ -62,4 +62,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -5,6 +5,8 @@
defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
alias Pleroma.User
@behaviour Pleroma.Web.ActivityPub.MRF
require Logger
# has the user successfully posted before?
@ -22,6 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
defp contains_links?(_), do: false
@impl true
def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do
with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
@ -45,4 +48,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
# in all other cases, pass through
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -12,4 +12,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
Logger.info("REJECTING #{inspect(object)}")
{:reject, object}
end
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -39,4 +39,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
end
def filter(object), do: {:ok, object}
def describe, do: {:ok, %{}}
end

View file

@ -4,6 +4,9 @@
defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
alias Pleroma.User
require Pleroma.Constants
@moduledoc "Block messages with too much mentions (configurable)"
@behaviour Pleroma.Web.ActivityPub.MRF
@ -19,12 +22,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
when follower_collection? and recipients > threshold ->
message
|> Map.put("to", [follower_collection])
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [Pleroma.Constants.as_public()])
{:public, recipients} when recipients > threshold ->
message
|> Map.put("to", [])
|> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [Pleroma.Constants.as_public()])
_ ->
message
@ -51,10 +54,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
recipients = (message["to"] || []) ++ (message["cc"] || [])
follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address
if Enum.member?(recipients, "https://www.w3.org/ns/activitystreams#Public") do
if Enum.member?(recipients, Pleroma.Constants.as_public()) do
recipients =
recipients
|> List.delete("https://www.w3.org/ns/activitystreams#Public")
|> List.delete(Pleroma.Constants.as_public())
|> List.delete(follower_collection)
{:public, length(recipients)}
@ -87,4 +90,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe,
do: {:ok, %{mrf_hellthread: Pleroma.Config.get(:mrf_hellthread) |> Enum.into(%{})}}
end

View file

@ -3,6 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
require Pleroma.Constants
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF
@ -31,12 +33,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
defp check_ftl_removal(
%{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message
) do
if "https://www.w3.org/ns/activitystreams#Public" in to and
if Pleroma.Constants.as_public() in to and
Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern ->
string_matches?(content, pattern) or string_matches?(summary, pattern)
end) do
to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public")
cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []]
to = List.delete(to, Pleroma.Constants.as_public())
cc = [Pleroma.Constants.as_public() | message["cc"] || []]
message =
message
@ -94,4 +96,36 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe do
# This horror is needed to convert regex sigils to strings
mrf_keyword =
Pleroma.Config.get(:mrf_keyword, [])
|> Enum.map(fn {key, value} ->
{key,
Enum.map(value, fn
{pattern, replacement} ->
%{
"pattern" =>
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end,
"replacement" => replacement
}
pattern ->
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end
end)}
end)
|> Enum.into(%{})
{:ok, %{mrf_keyword: mrf_keyword}}
end
end

View file

@ -53,4 +53,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -21,4 +21,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -19,4 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -10,4 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
def filter(object) do
{:ok, object}
end
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -21,4 +21,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
end
def filter(object), do: {:ok, object}
def describe, do: {:ok, %{}}
end

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@behaviour Pleroma.Web.ActivityPub.MRF
@public "https://www.w3.org/ns/activitystreams#Public"
require Pleroma.Constants
@impl true
def filter(%{"type" => "Create"} = object) do
@ -19,8 +19,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
# Determine visibility
visibility =
cond do
@public in object["to"] -> "public"
@public in object["cc"] -> "unlisted"
Pleroma.Constants.as_public() in object["to"] -> "public"
Pleroma.Constants.as_public() in object["cc"] -> "unlisted"
user.follower_address in object["to"] -> "followers"
true -> "direct"
end
@ -44,4 +44,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe,
do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
end

View file

@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
@moduledoc "Filter activities depending on their origin instance"
@behaviour MRF
require Pleroma.Constants
defp check_accept(%{host: actor_host} = _actor_info, object) do
accepts =
Pleroma.Config.get([:mrf_simple, :accept])
@ -89,14 +91,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
object =
with true <- MRF.subdomain_match?(timeline_removal, actor_host),
user <- User.get_cached_by_ap_id(object["actor"]),
true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do
to =
List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++
[user.follower_address]
true <- Pleroma.Constants.as_public() in object["to"] do
to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address]
cc =
List.delete(object["cc"], user.follower_address) ++
["https://www.w3.org/ns/activitystreams#Public"]
cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()]
object
|> Map.put("to", to)
@ -179,4 +177,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
end
def filter(object), do: {:ok, object}
@impl true
def describe do
exclusions = Pleroma.Config.get([:instance, :mrf_transparency_exclusions])
mrf_simple =
Pleroma.Config.get(:mrf_simple)
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|> Enum.into(%{})
{:ok, %{mrf_simple: mrf_simple}}
end
end

View file

@ -37,4 +37,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
- `mrf_tag:disable-any-subscription`: Reject any follow requests
"""
@public "https://www.w3.org/ns/activitystreams#Public"
require Pleroma.Constants
defp get_tags(%User{tags: tags}) when is_list(tags), do: tags
defp get_tags(_), do: []
@ -70,9 +70,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
) do
user = User.get_cached_by_ap_id(actor)
if Enum.member?(to, @public) do
to = List.delete(to, @public) ++ [user.follower_address]
cc = List.delete(cc, user.follower_address) ++ [@public]
if Enum.member?(to, Pleroma.Constants.as_public()) do
to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(cc, user.follower_address) ++ [Pleroma.Constants.as_public()]
object =
object
@ -103,9 +103,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
) do
user = User.get_cached_by_ap_id(actor)
if Enum.member?(to, @public) or Enum.member?(cc, @public) do
to = List.delete(to, @public) ++ [user.follower_address]
cc = List.delete(cc, @public)
if Enum.member?(to, Pleroma.Constants.as_public()) or
Enum.member?(cc, Pleroma.Constants.as_public()) do
to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address]
cc = List.delete(cc, Pleroma.Constants.as_public())
object =
object
@ -164,4 +165,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
@impl true
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
end

View file

@ -32,4 +32,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
end
def filter(object), do: {:ok, object}
@impl true
def describe do
mrf_user_allowlist =
Config.get([:mrf_user_allowlist], [])
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
{:ok, %{mrf_user_allowlist: mrf_user_allowlist}}
end
end

View file

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@moduledoc "Filter messages which belong to certain activity vocabularies"
@behaviour Pleroma.Web.ActivityPub.MRF
def filter(%{"type" => "Undo", "object" => child_message} = message) do
with {:ok, _} <- filter(child_message) do
{:ok, message}
else
{:reject, nil} ->
{:reject, nil}
end
end
def filter(%{"type" => message_type} = message) do
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
true <-
length(accepted_vocabulary) == 0 || Enum.member?(accepted_vocabulary, message_type),
false <-
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type),
{:ok, _} <- filter(message["object"]) do
{:ok, message}
else
_ -> {:reject, nil}
end
end
def filter(message), do: {:ok, message}
def describe,
do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
end

View file

@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
@behaviour Pleroma.Web.Federator.Publisher
@ -44,7 +46,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
%{host: host, path: path} = URI.parse(inbox)
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
@ -54,6 +56,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
signature =
Pleroma.Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: host,
"content-length": byte_size(json),
digest: digest,
@ -117,8 +120,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Enum.map(& &1.ap_id)
end
@as_public "https://www.w3.org/ns/activitystreams#Public"
defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
@ -145,7 +146,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
type == "Delete" ->
maybe_use_sharedinbox(user)
@as_public in to || @as_public in cc ->
Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
maybe_use_sharedinbox(user)
length(to) + length(cc) > 1 ->

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
|> User.get_or_create_service_actor_by_ap_id()
end
@spec follow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
def follow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
@ -21,12 +22,17 @@ defmodule Pleroma.Web.ActivityPub.Relay do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
else
{:error, _} = error ->
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end
end
@spec unfollow(String.t()) :: {:ok, Activity.t()} | {:error, any()}
def unfollow(target_instance) do
with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
@ -34,20 +40,27 @@ defmodule Pleroma.Web.ActivityPub.Relay do
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
else
{:error, _} = error ->
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end
end
@spec publish(any()) :: {:ok, Activity.t(), Object.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
%Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false)
else
e -> Logger.error("error: #{inspect(e)}")
e ->
Logger.error("error: #{inspect(e)}")
{:error, inspect(e)}
end
end
def publish(_), do: nil
def publish(_), do: {:error, "Not implemented"}
end

View file

@ -19,12 +19,14 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
import Ecto.Query
require Logger
require Pleroma.Constants
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object, options \\ []) do
object
|> strip_internal_fields
|> fix_actor
|> fix_url
|> fix_attachments
@ -33,7 +35,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_emoji
|> fix_tag
|> fix_content_map
|> fix_likes
|> fix_addressing
|> fix_summary
|> fix_type(options)
@ -102,8 +103,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
explicit_mentions =
explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
@ -115,11 +115,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
if followers_collection not in recipients do
cond do
"https://www.w3.org/ns/activitystreams#Public" in cc ->
Pleroma.Constants.as_public() in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
"https://www.w3.org/ns/activitystreams#Public" in to ->
Pleroma.Constants.as_public() in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
@ -151,20 +151,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
end
# Check for standardisation
# This is what Peertube does
# curl -H 'Accept: application/activity+json' $likes | jq .totalItems
# Prismo returns only an integer (count) as "likes"
def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
object
|> Map.put("likes", [])
|> Map.put("like_count", 0)
end
def fix_likes(object) do
object
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
@ -347,13 +333,15 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
reply =
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
Object.normalize(reply_id, true)
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
{:ok, object} <- get_obj_helper(reply_id, options) do
object
end
if reply && (reply.data["type"] == "Question" and object["name"]) do
if reply && reply.data["type"] == "Question" do
Map.put(object, "type", "Answer")
else
object
@ -480,8 +468,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <-
{:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)},
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
@ -609,16 +596,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
locked = new_user_data[:info]["locked"] || false
banner = new_user_data[:info][:banner]
locked = new_user_data[:info][:locked] || false
attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
fields =
attachment
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, %{"banner" => banner, "locked" => locked})
|> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
actor
|> User.upgrade_changeset(update_data)
|> User.upgrade_changeset(update_data, true)
|> User.update_and_set_cache()
ActivityPub.update(%{
@ -656,20 +649,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
{:ok, followers} = User.get_followers(user)
Enum.each(followers, fn follower ->
User.unfollow(follower, user)
end)
{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn followed ->
User.unfollow(user, followed)
end)
User.invalidate_cache(user)
Repo.delete(user)
User.delete(user)
nil ->
:error
@ -727,8 +707,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
} = _data,
_options
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
@ -742,8 +721,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
@ -798,7 +776,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> add_likes
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
@ -985,22 +962,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("attributedTo", attributed_to)
end
def add_likes(%{"id" => id, "like_count" => likes} = object) do
likes = %{
"id" => "#{id}/likes",
"first" => "#{id}/likes?page=1",
"type" => "OrderedCollection",
"totalItems" => likes
}
object
|> Map.put("likes", likes)
end
def add_likes(object) do
object
end
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
@ -1016,6 +977,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
defp strip_internal_fields(object) do
object
|> Map.drop([
"likes",
"like_count",
"announcements",
"announcement_count",
@ -1090,10 +1052,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
end
if Pleroma.Config.get([:instance, :external_user_synchronization]) do
update_following_followers_counters(user)
end
{:ok, user}
else
%User{} = user -> {:ok, user}
@ -1126,27 +1084,4 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
data
|> maybe_fix_user_url
end
def update_following_followers_counters(user) do
info = %{}
following = fetch_counter(user.following_address)
info = if following, do: Map.put(info, :following_count, following), else: info
followers = fetch_counter(user.follower_address)
info = if followers, do: Map.put(info, :follower_count, followers), else: info
User.set_info_cache(user, info)
end
defp fetch_counter(url) do
with {:ok, %{body: body, status: code}} when code in 200..299 <-
Pleroma.HTTP.get(
url,
[{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body) do
data["totalItems"]
end
end
end

View file

@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
import Ecto.Query
require Logger
require Pleroma.Constants
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
@supported_report_states ~w(open closed resolved)
@ -250,20 +251,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def insert_full_object(map), do: {:ok, map, nil}
def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
# Update activities that already had this. Could be done in a seperate process.
# Alternatively, just don't do this and fetch the current object each time. Most
# could probably be taken from cache.
relevant_activities = Activity.get_all_create_by_object_ap_id(id)
Enum.map(relevant_activities, fn activity ->
new_activity_data = activity.data |> Map.put("object", object.data)
changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset)
end)
end
#### Like-related helpers
@doc """
@ -346,8 +333,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data),
{:ok, object} <- Object.update_and_set_cache(changeset),
_ <- update_object_in_activities(object) do
{:ok, object} <- Object.update_and_set_cache(changeset) do
{:ok, object}
end
end
@ -388,6 +374,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
[state, actor, object]
)
User.set_follow_state_cache(actor, object, state)
activity = Activity.get_by_id(activity.id)
{:ok, activity}
rescue
@ -396,12 +383,16 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
end
def update_follow_state(%Activity{} = activity, state) do
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
with new_data <-
activity.data
|> Map.put("state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity} <- Repo.update(changeset),
_ <- User.set_follow_state_cache(actor, object, state) do
{:ok, activity}
end
end
@ -418,7 +409,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [Pleroma.Constants.as_public()],
"object" => followed_id,
"state" => "pending"
}
@ -510,7 +501,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
@ -530,7 +521,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
@ -547,7 +538,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
@ -556,7 +547,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def add_announce_to_object(
%Activity{
data: %{"actor" => actor, "cc" => ["https://www.w3.org/ns/activitystreams#Public"]}
data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}
},
object
) do
@ -765,7 +756,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = "https://www.w3.org/ns/activitystreams#Public"
public = Pleroma.Constants.as_public()
case visibility do
"public" ->

View file

@ -66,8 +66,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
"orderedItems" => items
}
if offset < total do
if offset + length(items) < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
end

View file

@ -65,7 +65,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
do: render("service.json", %{user: user})
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user})
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
@ -80,6 +80,17 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
fields =
user.info
|> User.Info.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
"type" => "Person",
@ -98,6 +109,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ user_tags
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))

View file

@ -8,14 +8,14 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
alias Pleroma.Repo
alias Pleroma.User
@public "https://www.w3.org/ns/activitystreams#Public"
require Pleroma.Constants
@spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
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: @public in (data["to"] ++ (data["cc"] || []))
def is_public?(data), do: Pleroma.Constants.as_public() in (data["to"] ++ (data["cc"] || []))
def is_private?(activity) do
with false <- is_public?(activity),
@ -73,10 +73,10 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
cc = object.data["cc"] || []
cond do
@public in to ->
Pleroma.Constants.as_public() in to ->
"public"
@public in cc ->
Pleroma.Constants.as_public() in cc ->
"unlisted"
# this should use the sql for the object's activity

View file

@ -379,6 +379,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
def migrate_to_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
json(conn, %{})
end
def migrate_from_db(conn, _params) do
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
json(conn, %{})
end
def config_show(conn, _params) do
configs = Pleroma.Repo.all(Config)
@ -392,9 +402,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
if Pleroma.Config.get([:instance, :dynamic_configuration]) do
updated =
Enum.map(configs, fn
%{"group" => group, "key" => key, "delete" => "true"} ->
{:ok, _} = Config.delete(%{group: group, key: key})
nil
%{"group" => group, "key" => key, "delete" => "true"} = params ->
{:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
config
%{"group" => group, "key" => key, "value" => value} ->
{:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})

View file

@ -55,8 +55,19 @@ defmodule Pleroma.Web.AdminAPI.Config do
@spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()}
def delete(params) do
with %Config{} = config <- Config.get_by_params(params) do
Repo.delete(config)
with %Config{} = config <- Config.get_by_params(Map.delete(params, :subkeys)) do
if params[:subkeys] do
updated_value =
Keyword.drop(
:erlang.binary_to_term(config.value),
Enum.map(params[:subkeys], &do_transform_string(&1))
)
Config.update(config, %{value: updated_value})
else
Repo.delete(config)
{:ok, nil}
end
else
nil ->
err =

View file

@ -21,8 +21,7 @@ defmodule Pleroma.Web.Auth.Authenticator do
def create_from_registration(plug, registration),
do: implementation().create_from_registration(plug, registration)
@callback get_registration(Plug.Conn.t()) ::
{:ok, Registration.t()} | {:error, any()}
@callback get_registration(Plug.Conn.t()) :: {:ok, Registration.t()} | {:error, any()}
def get_registration(plug), do: implementation().get_registration(plug)
@callback handle_error(Plug.Conn.t(), any()) :: any()

View file

@ -33,9 +33,11 @@ defmodule Pleroma.Web.ChatChannel do
end
defmodule Pleroma.Web.ChatChannel.ChatChannelState do
use Agent
@max_messages 20
def start_link do
def start_link(_) do
Agent.start_link(fn -> %{max_id: 1, messages: []} end, name: __MODULE__)
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
@ -172,21 +173,25 @@ defmodule Pleroma.Web.CommonAPI do
end)
end
def get_visibility(%{"visibility" => visibility}, in_reply_to)
def get_visibility(_, _, %Participation{}) do
{"direct", "direct"}
end
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
end
def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_replied_to_visibility(nil), do: nil
@ -212,7 +217,9 @@ defmodule Pleroma.Web.CommonAPI do
with status <- String.trim(status),
attachments <- attachments_from_ids(data),
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
{visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
{visibility, in_reply_to_visibility} <-
get_visibility(data, in_reply_to, in_reply_to_conversation),
{_, false} <-
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
{content_html, mentions, tags} <-
@ -225,8 +232,9 @@ defmodule Pleroma.Web.CommonAPI do
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
context <- make_context(in_reply_to),
{to, cc} <-
get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
context <- make_context(in_reply_to, in_reply_to_conversation),
cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
{:ok, expires_at} <- check_expiry_date(data["expires_at"]),
@ -321,8 +329,7 @@ defmodule Pleroma.Web.CommonAPI do
}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
%{valid?: true} = info_changeset <-
User.Info.add_pinnned_activity(user.info, activity),
%{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@ -19,11 +20,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Web.MediaProxy
require Logger
require Pleroma.Constants
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id)
with true <- Pleroma.FlakeId.is_flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity
else
_ -> Activity.get_create_by_object_ap_id_with_object(id)
end
activity &&
if activity.data["type"] == "Create" do
@ -41,32 +48,61 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def get_replied_to_activity(_), do: nil
def attachments_from_ids(data) do
if Map.has_key?(data, "descriptions") do
attachments_from_ids_descs(data["media_ids"], data["descriptions"])
else
attachments_from_ids_no_descs(data["media_ids"])
end
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end
def attachments_from_ids_no_descs(ids) do
Enum.map(ids || [], fn media_id ->
Repo.get(Object, media_id).data
end)
def attachments_from_ids(%{"media_ids" => ids} = _) do
attachments_from_ids_no_descs(ids)
end
def attachments_from_ids(_), do: []
def attachments_from_ids_no_descs([]), do: []
def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
%Object{data: data} = _ -> data
_ -> nil
end
end)
|> Enum.filter(& &1)
end
def attachments_from_ids_descs([], _), do: []
def attachments_from_ids_descs(ids, descs_str) do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids || [], fn media_id ->
Map.put(Repo.get(Object, media_id).data, "name", descs[media_id])
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
%Object{data: data} = _ ->
Map.put(data, "name", descs[media_id])
_ ->
nil
end
end)
|> Enum.filter(& &1)
end
@spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
@spec get_to_and_cc(
User.t(),
list(String.t()),
Activity.t() | nil,
String.t(),
Participation.t() | nil
) ::
{list(String.t()), list(String.t())}
def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
to = [Pleroma.Constants.as_public() | mentioned_users]
cc = [user.follower_address]
if inReplyTo do
@ -76,9 +112,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
to = [user.follower_address | mentioned_users]
cc = ["https://www.w3.org/ns/activitystreams#Public"]
cc = [Pleroma.Constants.as_public()]
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
@ -87,12 +123,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
{[user.follower_address | to], cc}
end
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else
@ -100,7 +136,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
@ -230,8 +266,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
defp maybe_add_nsfw_tag(data, _), do: data
def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id()
def make_context(_, %Participation{} = participation) do
Repo.preload(participation, :conversation).conversation.ap_id
end
def make_context(%Activity{data: %{"context" => context}}, _), do: context
def make_context(_, _), do: Utils.generate_context_id()
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
@ -241,20 +281,18 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def add_attachments(text, attachments) do
attachment_text =
Enum.map(attachments, fn
%{"url" => [%{"href" => href} | _]} = attachment ->
name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
_ ->
""
end)
attachment_text = Enum.map(attachments, &build_attachment_link/1)
Enum.join([text | attachment_text], "<br>")
end
defp build_attachment_link(%{"url" => [%{"href" => href} | _]} = attachment) do
name = attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
end
defp build_attachment_link(_), do: ""
def format_input(text, format, options \\ [])
@doc """
@ -314,7 +352,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
sensitive \\ false,
merge \\ %{}
) do
object = %{
%{
"type" => "Note",
"to" => to,
"cc" => cc,
@ -324,18 +362,20 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
"tag" => Keyword.values(tags) |> Enum.uniq()
}
|> add_in_reply_to(in_reply_to)
|> Map.merge(merge)
end
object =
with false <- is_nil(in_reply_to),
%Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
defp add_in_reply_to(object, nil), do: object
Map.merge(object, merge)
defp add_in_reply_to(object, in_reply_to) do
with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
end
def format_naive_asctime(date) do
@ -367,17 +407,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
end
def to_masto_date(date) do
try do
date
|> NaiveDateTime.from_iso8601!()
|> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
rescue
_e -> ""
def to_masto_date(date) when is_binary(date) do
with {:ok, date} <- NaiveDateTime.from_iso8601(date) do
to_masto_date(date)
else
_ -> ""
end
end
def to_masto_date(_), do: ""
defp shortname(name) do
if String.length(name) < 30 do
name
@ -422,7 +461,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
object_data =
cond do
!is_nil(object) ->
not is_nil(object) ->
object.data
is_map(data["object"]) ->
@ -466,9 +505,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
|> Enum.uniq()
end
def maybe_extract_mentions(_), do: []

View file

@ -33,4 +33,80 @@ defmodule Pleroma.Web.ControllerHelper do
end
defp param_to_integer(_, default), do: default
def add_link_headers(
conn,
method,
activities,
param \\ nil,
params \\ %{},
func3 \\ nil,
func4 \\ nil
) do
params =
conn.params
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)
last = List.last(activities)
func3 = func3 || (&mastodon_api_url/3)
func4 = func4 || (&mastodon_api_url/4)
if last do
max_id = last.id
limit =
params
|> Map.get("limit", "20")
|> String.to_integer()
min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end
{next_url, prev_url} =
if param do
{
func4.(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: max_id})
),
func4.(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{min_id: min_id})
)
}
else
{
func3.(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: max_id})
),
func3.(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{min_id: min_id})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
end

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.User
alias Pleroma.Web.Metadata
def api_not_implemented(conn, _params) do
conn
|> put_status(404)
|> json(%{error: "Not implemented"})
end
def redirector(conn, _params, code \\ 200)
# redirect to admin section
# /pleroma/admin -> /pleroma/admin/
#
def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do
redirect(conn, to: "/pleroma/admin/")
end
def redirector(conn, _params, code) do
conn
|> put_resp_content_type("text/html")
|> send_file(code, index_file_path())
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
redirector_with_meta(conn, %{user: user})
else
nil ->
redirector(conn, params)
end
end
def redirector_with_meta(conn, params) do
{:ok, index_content} = File.read(index_file_path())
tags =
try do
Metadata.build_tags(params)
rescue
e ->
Logger.error(
"Metadata rendering for #{conn.request_path} failed.\n" <>
Exception.format(:error, e, __STACKTRACE__)
)
""
end
response = String.replace(index_content, "<!--server-generated-meta-->", tags)
conn
|> put_resp_content_type("text/html")
|> send_resp(200, response)
end
def index_file_path do
Pleroma.Plugs.InstanceStatic.file_path("index.html")
end
def registration_page(conn, params) do
redirector(conn, params)
end
def empty(conn, _params) do
conn
|> put_status(204)
|> text("")
end
end

View file

@ -13,7 +13,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do
{:ok, %{args | queue_table: queue_table, running_jobs: :sets.new()}}
end
def start_link do
def start_link(_) do
enabled =
if Pleroma.Config.get(:env) == :test,
do: true,

View file

@ -0,0 +1,20 @@
defmodule Pleroma.Web.Mailer.SubscriptionController do
use Pleroma.Web, :controller
alias Pleroma.JWT
alias Pleroma.Repo
alias Pleroma.User
def unsubscribe(conn, %{"token" => encoded_token}) do
with {:ok, token} <- Base.decode64(encoded_token),
{:ok, claims} <- JWT.verify_and_validate(token),
%{"act" => %{"unsubscribe" => type}, "sub" => uid} <- claims,
%User{} = user <- Repo.get(User, uid),
{:ok, _user} <- User.switch_email_notifications(user, type, false) do
render(conn, "unsubscribe_success.html", email: user.email)
else
_err ->
render(conn, "unsubscribe_failure.html")
end
end
end

View file

@ -13,10 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@spec follow(User.t(), User.t(), map) :: {:ok, User.t()} | {:error, String.t()}
def follow(follower, followed, params \\ %{}) do
options = cast_params(params)
reblogs = options[:reblogs]
result =
if not User.following?(follower, followed) do
CommonAPI.follow(follower, followed)
@ -24,19 +22,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
{:ok, follower, followed, nil}
end
with {:ok, follower, followed, _} <- result do
reblogs
|> case do
false -> CommonAPI.hide_reblogs(follower, followed)
_ -> CommonAPI.show_reblogs(follower, followed)
end
|> case do
with {:ok, follower, _followed, _} <- result do
options = cast_params(params)
case reblogs_visibility(options[:reblogs], result) do
{:ok, follower} -> {:ok, follower}
_ -> {:ok, follower}
end
end
end
defp reblogs_visibility(false, {:ok, follower, followed, _}) do
CommonAPI.hide_reblogs(follower, followed)
end
defp reblogs_visibility(_, {:ok, follower, followed, _}) do
CommonAPI.show_reblogs(follower, followed)
end
@spec get_followers(User.t(), map()) :: list(User.t())
def get_followers(user, params \\ %{}) do
user
|> User.get_followers_query()

View file

@ -4,6 +4,10 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Bookmark
@ -46,6 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
import Ecto.Query
require Logger
require Pleroma.Constants
@rate_limited_relations_actions ~w(follow unfollow)a
@ -74,6 +79,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
plug(RateLimiter, :app_account_creation when action == :account_register)
plug(RateLimiter, :search when action in [:search, :search2, :account_search])
plug(RateLimiter, :password_reset when action == :password_reset)
plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
@local_mastodon_name "Mastodon-Local"
@ -132,7 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
user_info_emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
user.info
|> Map.get(:emoji, [])
|> Enum.concat(Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
info_params =
@ -151,6 +159,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "fields", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields}
end)
|> add_if_present(params, "fields", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
end)
@ -337,71 +351,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, mastodon_emoji)
end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)
last = List.last(activities)
if last do
max_id = last.id
limit =
params
|> Map.get("limit", "20")
|> String.to_integer()
min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end
{next_url, prev_url} =
if param do
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{min_id: min_id})
)
}
else
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{min_id: min_id})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
def home_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
@ -430,6 +379,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
@ -491,12 +441,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user
"user" => user,
"exclude_id" => activity.id
}),
activities <-
activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
activities <-
activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
result = %{
ancestors:
@ -531,8 +478,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> put_view(StatusView)
|> try_render("poll.json", %{object: object, for: user})
else
nil -> render_error(conn, :not_found, "Record not found")
false -> render_error(conn, :not_found, "Record not found")
error when is_nil(error) or error == false ->
render_error(conn, :not_found, "Record not found")
end
end
@ -880,8 +827,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"likes" => likes}} <- Object.normalize(object) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
q = from(u in User, where: u.ap_id in ^likes)
users =
@ -897,8 +844,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
q = from(u in User, where: u.ap_id in ^announces)
users =
@ -939,6 +886,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
@ -1220,10 +1168,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
recipients =
if for_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[for_user.ap_id | for_user.following]
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
[Pleroma.Constants.as_public()]
end
activities =
@ -1346,6 +1293,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("muting_user", user)
# we must filter the following list for the user to avoid leaking statuses the user
@ -1686,45 +1634,35 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
HTTP.get(
url,
[],
adapter: [
recv_timeout: timeout,
pool: :default
]
),
HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
{:ok, data} <- Jason.decode(body) do
data =
data
|> Enum.slice(0, limit)
|> Enum.map(fn x ->
Map.put(
x,
"id",
case User.get_or_fetch(x["acct"]) do
{:ok, %User{id: id}} -> id
_ -> 0
end
)
end)
|> Enum.map(fn x ->
Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
end)
|> Enum.map(fn x ->
Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
x
|> Map.put("id", fetch_suggestion_id(x))
|> Map.put("avatar", MediaProxy.url(x["avatar"]))
|> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
end)
conn
|> json(data)
json(conn, data)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
e ->
Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
else
json(conn, [])
end
end
defp fetch_suggestion_id(attrs) do
case User.get_or_fetch(attrs["acct"]) do
{:ok, %User{id: id}} -> id
_ -> 0
end
end
def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
@ -1803,7 +1741,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conversations =
Enum.map(participations, fn participation ->
ConversationView.render("participation.json", %{participation: participation, user: user})
ConversationView.render("participation.json", %{participation: participation, for: user})
end)
conn
@ -1816,7 +1754,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
participation_view =
ConversationView.render("participation.json", %{participation: participation, user: user})
ConversationView.render("participation.json", %{participation: participation, for: user})
conn
|> json(participation_view)
@ -1839,6 +1777,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
def account_confirmation_resend(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
conn
|> json_response(:no_content, "")
end
end
def try_render(conn, target, params)
when is_binary(target) do
case render(conn, target, params) do

View file

@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(user.id),
acct: user.nickname,
username: username_from_nickname(user.nickname),
url: user.ap_id
url: User.profile_url(user)
}
end
@ -37,11 +37,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
follow_state = User.get_cached_follow_state(user, target)
requested =
if follow_activity && !User.following?(target, user) do
follow_activity.data["state"] == "pending"
if follow_state && !User.following?(user, target) do
follow_state == "pending"
else
false
end
@ -50,13 +50,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(target.id),
following: User.following?(user, target),
followed_by: User.following?(target, user),
blocking: User.blocks?(user, target),
blocked_by: User.blocks?(target, user),
blocking: User.blocks_ap_id?(user, target),
blocked_by: User.blocks_ap_id?(target, user),
muting: User.mutes?(user, target),
muting_notifications: User.muted_notifications?(user, target),
subscribing: User.subscribed_to?(user, target),
requested: requested,
domain_blocking: false,
domain_blocking: User.blocks_domain?(user, target),
showing_reblogs: User.showing_reblogs?(user, target),
endorsed: false
}
@ -72,6 +72,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.get_cached_user_info(user)
following_count =
((!user.info.hide_follows or opts[:for] == user) && user_info.following_count) || 0
followers_count =
((!user.info.hide_followers or opts[:for] == user) && user_info.follower_count) || 0
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis =
@ -87,12 +94,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end)
fields =
(user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
user.info
|> User.Info.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
raw_fields = Map.get(user.info, :raw_fields, [])
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
%{
@ -102,11 +115,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
display_name: display_name,
locked: user_info.locked,
created_at: Utils.to_masto_date(user.inserted_at),
followers_count: user_info.follower_count,
following_count: user_info.following_count,
followers_count: followers_count,
following_count: following_count,
statuses_count: user_info.note_count,
note: bio || "",
url: user.ap_id,
url: User.profile_url(user),
avatar: image,
avatar_static: image,
header: header,
@ -117,6 +130,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
source: %{
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false,
fields: raw_fields,
pleroma: %{}
},

View file

@ -11,8 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
def render("participation.json", %{participation: participation, user: user}) do
participation = Repo.preload(participation, conversation: :users)
def render("participation.json", %{participation: participation, for: user}) do
participation = Repo.preload(participation, conversation: [], recipients: [])
last_activity_id =
with nil <- participation.last_activity_id do
@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
# Conversations return all users except the current user.
users =
participation.conversation.users
participation.recipients
|> Enum.reject(&(&1.id == user.id))
accounts =

View file

@ -5,8 +5,12 @@
defmodule Pleroma.Web.MastodonAPI.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
@ -25,19 +29,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create", "object" => object}} ->
object = Object.normalize(object)
object.data["inReplyTo"] != "" && object.data["inReplyTo"]
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id()
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity)
Map.put(acc, object.data["id"], activity)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
@ -69,12 +73,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true
opts.activities
|> safe_render_many(
StatusView,
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
Map.put(opts, :replied_to_activities, replied_to_activities),
parallel
)
end
@ -89,6 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
reblogged_activity =
Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
@ -143,6 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
@ -158,7 +166,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
mentions =
(object.data["to"] ++ tag_mentions)
|> Enum.uniq()
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.map(fn
Pleroma.Constants.as_public() -> nil
^user_follower_address -> nil
ap_id -> User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
@ -178,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
thread_muted? =
case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
nil -> CommonAPI.thread_muted?(user, activity)
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
end
attachment_data = object.data["attachment"] || []
@ -232,7 +244,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object.data["external_url"] || object.data["id"]
object.data["url"] || object.data["external_url"] || object.data["id"]
end
direct_conversation_id =
with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]},
%{data: %{"context" => context}} when is_binary(context) <- activity,
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
%Participation{id: participation_id} <-
Participation.for_user_and_conversation(for_user, conversation) do
participation_id
else
_e ->
nil
end
%{
@ -273,7 +298,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext},
expires_at: expires_at
expires_at: expires_at,
direct_conversation_id: direct_conversation_id
}
}
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MediaProxy do
alias Pleroma.Config
alias Pleroma.Upload
alias Pleroma.Web
@base64_opts [padding: false]
@ -26,7 +27,18 @@ defmodule Pleroma.Web.MediaProxy do
defp whitelisted?(url) do
%{host: domain} = URI.parse(url)
Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern ->
mediaproxy_whitelist = Config.get([:media_proxy, :whitelist])
upload_base_url_domain =
if !is_nil(Config.get([Upload, :base_url])) do
[URI.parse(Config.get([Upload, :base_url])).host]
else
[]
end
whitelist = mediaproxy_whitelist ++ upload_base_url_domain
Enum.any?(whitelist, fn pattern ->
String.equivalent?(domain, pattern)
end)
end

View file

@ -34,64 +34,18 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
def raw_nodeinfo do
stats = Stats.get_stats()
exclusions = Config.get([:instance, :mrf_transparency_exclusions])
mrf_simple =
Config.get(:mrf_simple)
|> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end)
|> Enum.into(%{})
# This horror is needed to convert regex sigils to strings
mrf_keyword =
Config.get(:mrf_keyword, [])
|> Enum.map(fn {key, value} ->
{key,
Enum.map(value, fn
{pattern, replacement} ->
%{
"pattern" =>
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end,
"replacement" => replacement
}
pattern ->
if not is_binary(pattern) do
inspect(pattern)
else
pattern
end
end)}
end)
|> Enum.into(%{})
mrf_policies =
MRF.get_policies()
|> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end)
quarantined = Config.get([:instance, :quarantined_instances], [])
staff_accounts =
User.all_superusers()
|> Enum.map(fn u -> u.ap_id end)
mrf_user_allowlist =
Config.get([:mrf_user_allowlist], [])
|> Enum.into(%{}, fn {k, v} -> {k, length(v)} end)
federation_response =
if Config.get([:instance, :mrf_transparency]) do
%{
mrf_policies: mrf_policies,
mrf_simple: mrf_simple,
mrf_keyword: mrf_keyword,
mrf_user_allowlist: mrf_user_allowlist,
quarantined_instances: quarantined,
exclusions: length(exclusions) > 0
}
{:ok, data} = MRF.describe()
data
|> Map.merge(%{quarantined_instances: quarantined})
else
%{}
end
@ -165,6 +119,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
},
accountActivationRequired: Config.get([:instance, :account_activation_required], false),
invitesEnabled: Config.get([:instance, :invites_enabled], false),
mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
features: features,
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)

View file

@ -365,8 +365,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do
with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn),
%Registration{} = registration <- Repo.get(Registration, registration_id),
{_, {:ok, auth}} <-
{:create_authorization, do_create_authorization(conn, params)},
{_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)},
%User{} = user <- Repo.preload(auth, :user).user,
{:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do
conn

View file

@ -44,8 +44,7 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.find_resource()
end
@spec exchange_token(App.t(), Authorization.t()) ::
{:ok, Token.t()} | {:error, Changeset.t()}
@spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do

View file

@ -6,36 +6,30 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do
@moduledoc """
The module represents functions to clean an expired oauth tokens.
"""
use GenServer
@ten_seconds 10_000
@one_day 86_400_000
# 10 seconds
@start_interval 10_000
@interval Pleroma.Config.get(
# 24 hours
[:oauth2, :clean_expired_tokens_interval],
86_400_000
@one_day
)
@queue :background
alias Pleroma.Web.OAuth.Token
def start_link, do: GenServer.start_link(__MODULE__, nil)
def start_link(_), do: GenServer.start_link(__MODULE__, %{})
def init(_) do
if Pleroma.Config.get([:oauth2, :clean_expired_tokens], false) do
Process.send_after(self(), :perform, @start_interval)
{:ok, nil}
else
:ignore
end
Process.send_after(self(), :perform, @ten_seconds)
{:ok, nil}
end
@doc false
def handle_info(:perform, state) do
Token.delete_expired_tokens()
Process.send_after(self(), :perform, @interval)
PleromaJobQueue.enqueue(@queue, __MODULE__, [:clean])
{:noreply, state}
end
# Job Worker Callbacks
def perform(:clean), do: Token.delete_expired_tokens()
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
alias Pleroma.Web.OStatus.UserRepresenter
require Logger
require Pleroma.Constants
defp get_href(id) do
with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do
@ -34,7 +35,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
Enum.map(to, fn id ->
cond do
# Special handling for the AP/Ostatus public collections
"https://www.w3.org/ns/activitystreams#Public" == id ->
Pleroma.Constants.as_public() == id ->
{:link,
[
rel: "mentioned",
@ -182,6 +183,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
retweeted_object = Object.normalize(retweeted_activity)
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
@ -196,7 +198,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
{:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} repeated a notice']},
{:content, [type: 'html'], ['RT #{retweeted_activity.data["object"]["content"]}']},
{:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])],

View file

@ -9,14 +9,18 @@ defmodule Pleroma.Web.OStatus.FollowHandler do
alias Pleroma.Web.XML
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
{:locked, false} <- {:locked, followed.info.locked},
{:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do
User.follow(actor, followed)
{:ok, activity}
else
{:locked, true} ->
{:error, "It's not possible to follow locked accounts over OStatus"}
end
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.OStatus.NoteHandler do
require Logger
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Object
@ -49,7 +50,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
def get_collection_mentions(entry) do
transmogrify = fn
"http://activityschema.org/collection/public" ->
"https://www.w3.org/ns/activitystreams#Public"
Pleroma.Constants.as_public()
group ->
group
@ -110,7 +111,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
[author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author),
{:ok, actor} <- OStatus.find_make_or_update_actor(author),
content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry),
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
@ -126,7 +127,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
to <- make_to_list(actor, mentions),
date <- XML.string_from_xpath("//published", entry),
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []),
cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []),
note <-
CommonAPI.Utils.make_note_data(
actor.ap_id,

View file

@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.UnfollowHandler do
alias Pleroma.Web.XML
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),

View file

@ -56,7 +56,7 @@ defmodule Pleroma.Web.OStatus do
def handle_incoming(xml_string, options \\ []) do
with doc when doc != :error <- parse_document(xml_string) do
with {:ok, actor_user} <- find_make_or_update_user(doc),
with {:ok, actor_user} <- find_make_or_update_actor(doc),
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
entries = :xmerl_xpath.string('//entry', doc)
@ -120,7 +120,7 @@ defmodule Pleroma.Web.OStatus do
end
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
with {:ok, actor} <- find_make_or_update_actor(doc),
%Object{} = object <- Object.normalize(retweeted_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
@ -138,7 +138,7 @@ defmodule Pleroma.Web.OStatus do
end
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
with {:ok, actor} <- find_make_or_update_actor(doc),
%Object{} = object <- Object.normalize(favorited_activity),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
@ -264,11 +264,18 @@ defmodule Pleroma.Web.OStatus do
end
end
def find_make_or_update_user(doc) do
def find_make_or_update_actor(doc) do
uri = string_from_xpath("//author/uri[1]", doc)
with {:ok, user} <- find_or_make_user(uri) do
with {:ok, %User{} = user} <- find_or_make_user(uri),
{:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do
maybe_update(doc, user)
else
{:ap_enabled, true} ->
{:error, :invalid_protocol}
_ ->
{:error, :unknown_user}
end
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.OStatus.OStatusController do
use Pleroma.Web, :controller
alias Fallback.RedirectController
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
@ -12,42 +13,49 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.Metadata.PlayerView
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Router
alias Pleroma.Web.XML
plug(
Pleroma.Plugs.RateLimiter,
{:ap_routes, params: ["uuid"]} when action in [:object, :activity]
)
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
plug(
Pleroma.Plugs.SetFormatPlug
when action in [:feed_redirect, :object, :activity, :notice]
)
action_fallback(:errors)
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <-
{:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
RedirectController.redirector_with_meta(conn, %{user: user})
end
end
def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :user)
end
def feed_redirect(conn, %{"nickname" => nickname}) do
case get_format(conn) do
"html" ->
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
Fallback.RedirectController.redirector_with_meta(conn, %{user: user})
else
nil -> {:error, :not_found}
end
"activity+json" ->
ActivityPubController.call(conn, :user)
"json" ->
ActivityPubController.call(conn, :user)
_ ->
with %User{} = user <- User.get_cached_by_nickname(nickname) do
redirect(conn, external: OStatus.feed_path(user))
else
nil -> {:error, :not_found}
end
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
redirect(conn, external: OStatus.feed_path(user))
end
end
def feed(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
query_params =
Map.take(params, ["max_id"])
|> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
@ -65,8 +73,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
else
nil -> {:error, :not_found}
end
end
@ -97,93 +103,82 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|> send_resp(200, "")
end
def object(conn, %{"uuid" => uuid}) do
if get_format(conn) in ["activity+json", "json"] do
ActivityPubController.call(conn, :object)
else
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, 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}")
_ -> represent_activity(conn, nil, activity, user)
end
else
{:public?, false} ->
{:error, :not_found}
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
end
{:activity, nil} ->
{:error, :not_found}
e ->
e
def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, nil, activity, user)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def activity(conn, %{"uuid" => uuid}) do
if get_format(conn) in ["activity+json", "json"] do
ActivityPubController.call(conn, :activity)
else
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, 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}")
_ -> represent_activity(conn, format, activity, user)
end
else
{:public?, false} ->
{:error, :not_found}
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :activity)
end
{:activity, nil} ->
{:error, :not_found}
e ->
e
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, format, activity, user)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def notice(conn, %{"id" => id}) do
def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, 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" ->
if activity.data["type"] == "Create" do
%Object{} = object = Object.normalize(activity)
cond do
format == "html" && activity.data["type"] == "Create" ->
%Object{} = object = Object.normalize(activity)
Fallback.RedirectController.redirector_with_meta(conn, %{
RedirectController.redirector_with_meta(
conn,
%{
activity_id: activity.id,
object: object,
url:
Pleroma.Web.Router.Helpers.o_status_url(
Pleroma.Web.Endpoint,
:notice,
activity.id
),
url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id),
user: user
})
else
Fallback.RedirectController.redirector(conn, nil)
end
}
)
_ ->
format == "html" ->
RedirectController.redirector(conn, nil)
true ->
represent_activity(conn, format, activity, user)
end
else
{:public?, false} ->
reason when reason in [{:public?, false}, {:activity, nil}] ->
conn
|> put_status(404)
|> Fallback.RedirectController.redirector(nil, 404)
{:activity, nil} ->
conn
|> Fallback.RedirectController.redirector(nil, 404)
|> RedirectController.redirector(nil, 404)
e ->
e
@ -204,13 +199,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
"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)
|> put_view(PlayerView)
|> render("player.html", url)
else
_error ->
conn
|> put_status(404)
|> Fallback.RedirectController.redirector(nil, 404)
|> RedirectController.redirector(nil, 404)
end
end
@ -248,6 +243,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do
render_error(conn, :not_found, "Not found")
end
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end

View file

@ -0,0 +1,73 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
alias Pleroma.Conversation.Participation
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.StatusView
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
def conversation_statuses(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id} = params
) do
params =
params
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
participation =
participation_id
|> Participation.get(preload: [:conversation])
if user.id == participation.user_id do
activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context(params)
|> Enum.reverse()
conn
|> add_link_headers(
:conversation_statuses,
activities,
participation_id,
params,
nil,
&pleroma_api_url/4
)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end
def update_conversation(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients}
) do
participation =
participation_id
|> Participation.get()
with true <- user.id == participation.user_id,
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
end

View file

@ -19,8 +19,7 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
defp is_aws_signed_url(image) when is_binary(image) do
%URI{host: host, query: query} = URI.parse(image)
if String.contains?(host, "amazonaws.com") and
String.contains?(query, "X-Amz-Expires") do
if String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") do
image
else
nil

View file

@ -3,13 +3,20 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do
alias Pleroma.Web.RichMedia.Parsers.MetaTagsParser
@spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def parse(html, data) do
Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse(
html,
data,
"twitter",
"No twitter card metadata found",
"name"
)
data
|> parse_name_attrs(html)
|> parse_property_attrs(html)
end
defp parse_name_attrs(data, html) do
MetaTagsParser.parse(html, data, "twitter", %{}, "name")
end
defp parse_property_attrs({_, data}, html) do
MetaTagsParser.parse(html, data, "twitter", "No twitter card metadata found", "property")
end
end

View file

@ -196,6 +196,8 @@ defmodule Pleroma.Web.Router do
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/migrate_to_db", AdminAPIController, :migrate_to_db)
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
end
scope "/", Pleroma.Web.TwitterAPI do
@ -257,6 +259,21 @@ defmodule Pleroma.Web.Router do
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
scope [] do
pipe_through(:oauth_read)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
end
scope [] do
pipe_through(:oauth_write)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
@ -412,6 +429,12 @@ defmodule Pleroma.Web.Router do
get("/accounts/search", SearchController, :account_search)
post(
"/pleroma/accounts/confirmation_resend",
MastodonAPIController,
:account_confirmation_resend
)
scope [] do
pipe_through(:oauth_read_or_public)
@ -600,6 +623,8 @@ defmodule Pleroma.Web.Router do
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
pipeline :activitypub do
@ -692,7 +717,7 @@ defmodule Pleroma.Web.Router do
post("/auth/password", MastodonAPIController, :password_reset)
scope [] do
pipe_through(:oauth_read_or_public)
pipe_through(:oauth_read)
get("/web/*path", MastodonAPIController, :index)
end
end
@ -729,68 +754,3 @@ defmodule Pleroma.Web.Router do
options("/*path", RedirectController, :empty)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.User
alias Pleroma.Web.Metadata
def api_not_implemented(conn, _params) do
conn
|> put_status(404)
|> json(%{error: "Not implemented"})
end
def redirector(conn, _params, code \\ 200) do
conn
|> put_resp_content_type("text/html")
|> send_file(code, index_file_path())
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
redirector_with_meta(conn, %{user: user})
else
nil ->
redirector(conn, params)
end
end
def redirector_with_meta(conn, params) do
{:ok, index_content} = File.read(index_file_path())
tags =
try do
Metadata.build_tags(params)
rescue
e ->
Logger.error(
"Metadata rendering for #{conn.request_path} failed.\n" <>
Exception.format(:error, e, __STACKTRACE__)
)
""
end
response = String.replace(index_content, "<!--server-generated-meta-->", tags)
conn
|> put_resp_content_type("text/html")
|> send_resp(200, response)
end
def index_file_path do
Pleroma.Plugs.InstanceStatic.file_path("index.html")
end
def registration_page(conn, params) do
redirector(conn, params)
end
def empty(conn, _params) do
conn
|> put_status(204)
|> text("")
end
end

View file

@ -18,7 +18,7 @@ defmodule Pleroma.Web.Streamer do
@keepalive_interval :timer.seconds(30)
def start_link do
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@ -35,28 +35,21 @@ defmodule Pleroma.Web.Streamer do
end
def init(args) do
spawn(fn ->
# 30 seconds
Process.sleep(@keepalive_interval)
GenServer.cast(__MODULE__, %{action: :ping})
end)
Process.send_after(self(), %{action: :ping}, @keepalive_interval)
{:ok, args}
end
def handle_cast(%{action: :ping}, topics) do
Map.values(topics)
def handle_info(%{action: :ping}, topics) do
topics
|> Map.values()
|> List.flatten()
|> Enum.each(fn socket ->
Logger.debug("Sending keepalive ping")
send(socket.transport_pid, {:text, ""})
end)
spawn(fn ->
# 30 seconds
Process.sleep(@keepalive_interval)
GenServer.cast(__MODULE__, %{action: :ping})
end)
Process.send_after(self(), %{action: :ping}, @keepalive_interval)
{:noreply, topics}
end
@ -120,8 +113,7 @@ defmodule Pleroma.Web.Streamer do
|> Map.get("#{topic}:#{item.user_id}", [])
|> Enum.each(fn socket ->
with %User{} = user <- User.get_cached_by_ap_id(socket.assigns[:user].ap_id),
true <- should_send?(user, item),
false <- CommonAPI.thread_muted?(user, item.activity) do
true <- should_send?(user, item) do
send(
socket.transport_pid,
{:text, represent_notification(socket.assigns[:user], item)}
@ -209,7 +201,7 @@ defmodule Pleroma.Web.Streamer do
payload:
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
participation: participation,
user: participation.user
for: participation.user
})
|> Jason.encode!()
}
@ -234,11 +226,17 @@ defmodule Pleroma.Web.Streamer do
blocks = user.info.blocks || []
mutes = user.info.mutes || []
reblog_mutes = user.info.muted_reblogs || []
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
with parent when not is_nil(parent) <- Object.normalize(item),
true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)),
true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)),
true <- thread_containment(item, user) do
%{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
false <- CommonAPI.thread_muted?(user, item) do
true
else
_ -> false

View file

@ -0,0 +1,568 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<!--[if gte mso 9]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<!--[if !mso]><!-->
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<!--<![endif]-->
<title><%= @email.subject %><</title>
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
body {
margin: 0;
padding: 0;
}
a {
color: <%= @styling.link_color %>;
text-decoration: none;
}
table,
td,
tr {
vertical-align: top;
border-collapse: collapse;
}
* {
line-height: inherit;
}
a[x-apple-data-detectors=true] {
color: inherit !important;
text-decoration: none !important;
}
</style>
<style id="media-query" type="text/css">
@media (max-width: 610px) {
.block-grid,
.col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.block-grid {
width: 100% !important;
}
.col {
width: 100% !important;
}
.col>div {
margin: 0 auto;
}
.no-stack .col {
min-width: 0 !important;
display: table-cell !important;
}
.no-stack.two-up .col {
width: 50% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num8 {
width: 66% !important;
}
.no-stack .col.num4 {
width: 33% !important;
}
.no-stack .col.num3 {
width: 25% !important;
}
.no-stack .col.num6 {
width: 50% !important;
}
.no-stack .col.num9 {
width: 75% !important;
}
}
</style>
</head>
<body class="clean-body" style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: <%= @styling.background_color %>;">
<!--[if IE]><div class="ie-browser"><![endif]-->
<table bgcolor="<%= @styling.background_color %>" cellpadding="0" cellspacing="0" class="nl-container" role="presentation"
style="table-layout: fixed; vertical-align: top; min-width: 320px; Margin: 0 auto; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: <%= @styling.background_color %>; width: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td style="word-break: break-word; vertical-align: top;" valign="top">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color:<%= @styling.background_color %>"><![endif]-->
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<div align="center" class="img-container center"
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="center"><![endif]--><img
align="center" alt="Image" border="0" class="center" src="cid:logo.png"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: 80px; width: auto; max-height: 80px; display: block;"
title="Image" height="80" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height: 14px; color: <%= @styling.header_color %>;">
<p style="line-height: 36px; text-align: center; margin: 0;"><span
style="font-size: 30px; color: <%= @styling.header_color %>;">Hey <%= @user.nickname %>, here is what you've missed!</span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 24px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 20px;">Mentions</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%= for %{data: mention, object: object, from: from} <- @mentions do %>
<%# mention START %>
<%# user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num3"
style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
<!--<![endif]-->
<div align="left" class="img-container left "
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
title="<%= from.name %>" width="76" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num9"
style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: mention.activity.actor %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# user card END %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<span style="font-size: 16px; line-height: 19px;"><%= raw object.data["content"] %></span></div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 15px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_muted_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:15px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_muted_color %>;">
<p style="font-size: 14px; line-height: 16px; margin: 0;"><%= format_date object.data["published"] %></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# mention END %>
<% end %>
<%= if @followers != [] do %>
<%# new followers header START %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 15px; padding-left: 15px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 15px; padding-left: 15px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 12px; line-height: 24px; text-align: center; margin: 0;"><span
style="font-size: 20px;"><%= length(@followers) %> New Followers</span><span
style="font-size: 20px; line-height: 24px;"></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# new followers header END %>
<%= for %{data: follow, from: from} <- @followers do %>
<%# user card START %>
<div style="background-color:transparent;">
<div class="block-grid mixed-two-up no-stack"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="147" style="background-color:<%= @styling.content_background_color%>;width:76px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 20px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num3"
style="display: table-cell; vertical-align: top; max-width: 320px; min-width: 76px; width: 76px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 20px;">
<!--<![endif]-->
<div align="left" class="img-container left "
style="padding-right: 0px;padding-left: 0px;">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr style="line-height:0px"><td style="padding-right: 0px;padding-left: 0px;" align="left"><![endif]--><img
alt="<%= from.name %>" border="0" class="left " src="<%= avatar_url(from) %>"
style="text-decoration: none; -ms-interpolation-mode: bicubic; border: 0; height: auto; width: 100%; max-width: 76px; display: block;"
title="<%= from.name %>" width="76" />
<!--[if mso]></td></tr></table><![endif]-->
</div>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td><td align="center" width="442" style="background-color:<%= @styling.content_background_color%>;width:442px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num9"
style="display: table-cell; vertical-align: top; min-width: 320px; max-width: 441px; width: 442px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<div
style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: <%= @styling.text_color %>;">
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px; color: <%= @styling.text_color %>;"><%= from.name %></span></p>
<p style="font-size: 14px; line-height: 19px; margin: 0;"><span
style="font-size: 16px;"><%= link "@" <> from.nickname, style: "color: #{@styling.link_color};text-decoration: none;", to: follow.activity.actor %></span></p>
</div>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# user card END %>
<% end %>
<% end %>
<%# divider start %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" class="divider" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td class="divider_inner"
style="word-break: break-word; vertical-align: top; min-width: 100%; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 10px;"
valign="top">
<table align="center" border="0" cellpadding="0" cellspacing="0" class="divider_content"
height="0" role="presentation"
style="table-layout: fixed; vertical-align: top; border-spacing: 0; border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-top: 1px solid <%= @styling.text_color %>; height: 0px;"
valign="top" width="100%">
<tbody>
<tr style="vertical-align: top;" valign="top">
<td height="0"
style="word-break: break-word; vertical-align: top; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;"
valign="top"><span></span></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<%# divider end %>
<div style="background-color:transparent;">
<div class="block-grid"
style="Margin: 0 auto; min-width: 320px; max-width: 590px; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; background-color: <%= @styling.content_background_color%>;">
<div style="border-collapse: collapse;display: table;width: 100%;background-color:<%= @styling.content_background_color%>;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:transparent;"><tr><td align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:590px"><tr class="layout-full-width" style="background-color:<%= @styling.content_background_color%>"><![endif]-->
<!--[if (mso)|(IE)]><td align="center" width="590" style="background-color:<%= @styling.content_background_color%>;width:590px; border-top: 0px solid transparent; border-left: 0px solid transparent; border-bottom: 0px solid transparent; border-right: 0px solid transparent;" valign="top"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 0px; padding-left: 0px; padding-top:5px; padding-bottom:5px;"><![endif]-->
<div class="col num12"
style="min-width: 320px; max-width: 590px; display: table-cell; vertical-align: top; width: 590px;">
<div style="width:100% !important;">
<!--[if (!mso)&(!IE)]><!-->
<div
style="border-top:0px solid transparent; border-left:0px solid transparent; border-bottom:0px solid transparent; border-right:0px solid transparent; padding-top:5px; padding-bottom:5px; padding-right: 0px; padding-left: 0px;">
<!--<![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding-right: 10px; padding-left: 10px; padding-top: 10px; padding-bottom: 10px; font-family: Arial, sans-serif"><![endif]-->
<div
style="color:<%= @styling.text_color %>;font-family:Arial, 'Helvetica Neue', Helvetica, sans-serif;line-height:120%;padding-top:10px;padding-right:10px;padding-bottom:10px;padding-left:10px;">
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">You have received this email because you have signed up to receive digest emails from <b><%= @instance %></b> Pleroma instance.</span></p>
<p
style="font-size: 12px; line-height: 14px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
 </p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">The email address you are subscribed as is <a href="mailto:<%= @user.email %>" style="color: <%= @styling.link_color %>;text-decoration: none;"><%= @user.email %></a>. </span></p>
<p
style="font-size: 12px; line-height: 16px; text-align: center; color: <%= @styling.text_color %>; font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; margin: 0;">
<span style="font-size: 14px;">To unsubscribe, please go <%= link "here", style: "color: #{@styling.link_color};text-decoration: none;", to: @unsubscribe_link %>.</span></p>
</div>
<!--[if mso]></td></tr></table><![endif]-->
<!--[if (!mso)&(!IE)]><!-->
</div>
<!--<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
<!--[if (mso)|(IE)]></td></tr></table></td></tr></table><![endif]-->
</div>
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if (IE)]></div><![endif]-->
</body>
</html>

View file

@ -36,6 +36,11 @@
margin-bottom: 20px;
}
a {
color: color: #d8a070;
text-decoration: none;
}
form {
width: 100%;
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= @email.subject %></title>
</head>
<body>
<%= render @view_module, @view_template, assigns %>
</body>
</html>

View file

@ -0,0 +1 @@
<h1>UNSUBSCRIBE FAILURE</h1>

View file

@ -0,0 +1 @@
<h1>UNSUBSCRIBE SUCCESSFUL</h1>

View file

@ -15,11 +15,11 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version])
def help_test(conn, _params) do
json(conn, "ok")
end
@ -60,27 +60,25 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
%Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"])
redirect(conn, to: "/notice/#{activity_id}")
else
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
with {:ok, followee} <- User.get_or_fetch(acct) do
conn
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
else
conn
|> render("follow_login.html", %{
|> render(follow_template(user), %{
error: false,
acct: acct,
avatar: avatar,
name: name,
id: id
avatar: User.avatar_url(followee),
name: followee.nickname,
id: followee.id
})
else
{:error, _reason} ->
render(conn, follow_template(user), %{error: :error})
end
end
end
defp follow_template(%User{} = _user), do: "follow.html"
defp follow_template(_), do: "follow_login.html"
defp is_status?(acct) do
case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] ->
@ -94,50 +92,53 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
followee = User.get_cached_by_id(id)
avatar = User.avatar_url(followee)
name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username),
true <- AuthenticationPlug.checkpw(password, user.password_hash),
%User{} = _followed <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
with %User{} = followee <- User.get_cached_by_id(id),
{_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee},
{_, true, _} <- {
:auth,
AuthenticationPlug.checkpw(password, user.password_hash),
followee
},
{:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
render(conn, "followed.html", %{error: "Error following account"})
_e ->
{:auth, _, followee} ->
conn
|> render("follow_login.html", %{
error: "Wrong username or password",
id: id,
name: name,
avatar: avatar
name: followee.nickname,
avatar: User.avatar_url(followee)
})
e ->
Logger.debug("Remote follow failed with error #{inspect(e)}")
render(conn, "followed.html", %{error: "Something went wrong."})
end
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
render(conn, "followed.html", %{error: "Error following account"})
{:fetch_user, error} ->
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Could not find user"})
e ->
Logger.debug("Remote follow failed with error #{inspect(e)}")
conn
|> render("followed.html", %{error: inspect(e)})
render(conn, "followed.html", %{error: "Something went wrong."})
end
end
@ -152,67 +153,70 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
def config(%{assigns: %{format: "xml"}} = conn, _params) do
instance = Pleroma.Config.get(:instance)
response = """
<config>
<site>
<name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(instance, :limit)}</textlimit>
<closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site>
</config>
"""
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
end
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
case get_format(conn) do
"xml" ->
response = """
<config>
<site>
<name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(instance, :limit)}</textlimit>
<closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site>
</config>
"""
vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
uploadlimit = %{
uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
}
_ ->
vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
data = %{
name: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
server: Web.base_url(),
textlimit: to_string(Keyword.get(instance, :limit)),
uploadlimit: uploadlimit,
closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"),
private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"),
vapidPublicKey: vapid_public_key,
accountActivationRequired:
bool_to_val(Keyword.get(instance, :account_activation_required, false)),
invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)),
safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions]))
}
uploadlimit = %{
uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
}
data = %{
name: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
server: Web.base_url(),
textlimit: to_string(Keyword.get(instance, :limit)),
uploadlimit: uploadlimit,
closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
vapidPublicKey: vapid_public_key,
accountActivationRequired:
if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"),
safeDMMentionsEnabled:
if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0")
}
managed_config = Keyword.get(instance, :managed_config)
data =
if managed_config do
pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
Map.put(data, "pleromafe", pleroma_fe)
else
data
end
managed_config = Keyword.get(instance, :managed_config)
data =
if managed_config do
data |> Map.put("pleromafe", pleroma_fe)
else
data
end
json(conn, %{site: data})
end
json(conn, %{site: data})
end
defp bool_to_val(true), do: "1"
defp bool_to_val(_), do: "0"
defp bool_to_val(true, val, _), do: val
defp bool_to_val(_, _, val), do: val
def frontend_configurations(conn, _params) do
config =
Pleroma.Config.get(:frontend_configurations, %{})
@ -221,20 +225,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
json(conn, config)
end
def version(conn, _params) do
def version(%{assigns: %{format: "xml"}} = conn, _params) do
version = Pleroma.Application.named_version()
case get_format(conn) do
"xml" ->
response = "<version>#{version}</version>"
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, "<version>#{version}</version>")
end
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
_ ->
json(conn, version)
end
def version(conn, _params) do
json(conn, Pleroma.Application.named_version())
end
def emoji(conn, _params) do

View file

@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
import Ecto.Query
require Pleroma.Constants
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
end
@ -286,7 +288,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
from(
[a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where: ^Pleroma.Constants.as_public() in a.recipients,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
import Ecto.Query
require Logger
require Pleroma.Constants
defp query_context_ids([]), do: []
@ -91,7 +92,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
String.ends_with?(ap_id, "/followers") ->
nil
ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
ap_id == Pleroma.Constants.as_public() ->
nil
user = User.get_cached_by_ap_id(ap_id) ->

View file

@ -10,6 +10,8 @@ defmodule Pleroma.Web.TwitterAPI.NotificationView do
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.UserView
require Pleroma.Constants
defp get_user(ap_id, opts) do
cond do
user = opts[:users][ap_id] ->
@ -18,7 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.NotificationView do
String.ends_with?(ap_id, "/followers") ->
nil
ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
ap_id == Pleroma.Constants.as_public() ->
nil
true ->

View file

@ -74,12 +74,15 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|> HTML.filter_tags(User.html_filter_policy(for_user))
|> Formatter.emojify(emoji)
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields =
(user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
user.info
|> User.Info.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
data =
%{

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Web.EmailView do
use Pleroma.Web, :view
import Phoenix.HTML
import Phoenix.HTML.Link
def avatar_url(user) do
Pleroma.User.avatar_url(user)
end
def format_date(date) when is_binary(date) do
date
|> Timex.parse!("{ISO:Extended:Z}")
|> Timex.format!("{Mshort} {D}, {YYYY} {h24}:{m}")
end
end

View file

@ -0,0 +1,3 @@
defmodule Pleroma.Web.Mailer.SubscriptionView do
use Pleroma.Web, :view
end

View file

@ -58,17 +58,31 @@ defmodule Pleroma.Web do
rescue
error ->
Logger.error(
"#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}"
"#{__MODULE__} failed to render #{inspect({view, template})}\n" <>
Exception.format(:error, error, __STACKTRACE__)
)
Logger.error(inspect(__STACKTRACE__))
nil
end
@doc """
Same as `render_many/4` but wrapped in rescue block.
Same as `render_many/4` but wrapped in rescue block and parallelized (unless disabled by passing false as a fifth argument).
"""
def safe_render_many(collection, view, template, assigns \\ %{}) do
def safe_render_many(collection, view, template, assigns \\ %{}, parallel \\ true)
def safe_render_many(collection, view, template, assigns, true) do
Enum.map(collection, fn resource ->
Task.async(fn ->
as = Map.get(assigns, :as) || view.__resource__
assigns = Map.put(assigns, as, resource)
safe_render(view, template, assigns)
end)
end)
|> Enum.map(&Task.await(&1, :infinity))
|> Enum.filter(& &1)
end
def safe_render_many(collection, view, template, assigns, false) do
Enum.map(collection, fn resource ->
as = Map.get(assigns, :as) || view.__resource__
assigns = Map.put(assigns, as, resource)

View file

@ -86,11 +86,17 @@ defmodule Pleroma.Web.WebFinger do
|> XmlBuilder.to_doc()
end
defp get_magic_key(magic_key) do
"data:application/magic-public-key," <> magic_key = magic_key
defp get_magic_key("data:application/magic-public-key," <> magic_key) do
{:ok, magic_key}
rescue
MatchError -> {:error, "Missing magic key data."}
end
defp get_magic_key(nil) do
Logger.debug("Undefined magic key.")
{:ok, nil}
end
defp get_magic_key(_) do
{:error, "Missing magic key data."}
end
defp webfinger_from_xml(doc) do
@ -187,6 +193,7 @@ defmodule Pleroma.Web.WebFinger do
end
end
@spec finger(String.t()) :: {:ok, map()} | {:error, any()}
def finger(account) do
account = String.trim_leading(account, "@")
@ -220,8 +227,6 @@ defmodule Pleroma.Web.WebFinger do
else
with {:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else
{:error, e} -> e
end
end
else

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
alias Pleroma.Web.WebFinger
plug(Pleroma.Plugs.SetFormatPlug)
plug(Pleroma.Web.FederatingPlug)
def host_meta(conn, _params) do
@ -17,30 +18,28 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
|> send_resp(200, xml)
end
def webfinger(conn, %{"resource" => resource}) do
case get_format(conn) do
n when n in ["xml", "xrd+xml"] ->
with {:ok, response} <- WebFinger.webfinger(resource, "XML") do
conn
|> put_resp_content_type("application/xrd+xml")
|> send_resp(200, response)
else
_e -> send_resp(conn, 404, "Couldn't find user")
end
n when n in ["json", "jrd+json"] ->
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response)
else
_e -> send_resp(conn, 404, "Couldn't find user")
end
_ ->
send_resp(conn, 404, "Unsupported format")
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
when format in ["xml", "xrd+xml"] do
with {:ok, response} <- WebFinger.webfinger(resource, "XML") do
conn
|> put_resp_content_type("application/xrd+xml")
|> send_resp(200, response)
else
_e -> send_resp(conn, 404, "Couldn't find user")
end
end
def webfinger(conn, _params) do
send_resp(conn, 400, "Bad Request")
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
when format in ["json", "jrd+json"] do
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response)
else
_e ->
conn
|> put_status(404)
|> json("Couldn't find user")
end
end
def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request")
end